feat: improve catalog responsive design, cart layout and card component
- Redesign Cart component with improved alignments, gaps, and responsive padding - Refactor Card component with mobile-first layout and consistent spacing - Update catalog layout to 60/40 split for products/cart on desktop (lg) - Add PaginationControls component (previously untracked) - Clean up obsolete CSS styles from catalog page
This commit is contained in:
@@ -1,26 +1,37 @@
|
||||
<template>
|
||||
<v-card class="product-card">
|
||||
<v-row no-gutters align="center" class="w-100 py-3 px-2">
|
||||
<v-col cols="12" md="3" class="d-flex justify-center">
|
||||
<v-row align="start" class="product-card-row">
|
||||
<!-- Columna de Imagen -->
|
||||
<v-col cols="12" md="4" class="image-column">
|
||||
<v-img :src="product.img" class="product-img" contain></v-img>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="5">
|
||||
<v-card-title class="product-name">{{ product.name }}</v-card-title>
|
||||
<v-card-subtitle class="product-description">{{ product.description }}</v-card-subtitle>
|
||||
<div class="prices mt-3">
|
||||
<div class="price-unit">
|
||||
<span class="price-label">Precio unitario</span>
|
||||
<span class="price-value">{{ currency(product.price) }}</span>
|
||||
</div>
|
||||
<div class="price-total">
|
||||
<span class="price-label">Precio total</span>
|
||||
<span class="price-value total">{{ currency(product.price * product.quantity) }}</span>
|
||||
<!-- Columna de Detalles -->
|
||||
<v-col cols="12" md="5" class="details-column">
|
||||
<div class="product-details-content">
|
||||
<v-card-title class="product-name">{{ product.name }}</v-card-title>
|
||||
<v-card-subtitle class="product-description">{{
|
||||
product.description
|
||||
}}</v-card-subtitle>
|
||||
|
||||
<div class="prices">
|
||||
<div class="price-unit">
|
||||
<span class="price-label">Precio unitario</span>
|
||||
<span class="price-value">{{ currency(product.price) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="price-total">
|
||||
<span class="price-label">Precio total</span>
|
||||
<span class="price-value total"
|
||||
>{{ currency(product.price * product.quantity) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="4" class="d-flex align-center justify-md-end justify-center mt-3 mt-md-0">
|
||||
<!-- Columna de Controles -->
|
||||
<v-col cols="12" md="3" class="controls-column">
|
||||
<div class="quantity-controls">
|
||||
<v-btn icon small class="qty-btn" @click="decrease(product)">
|
||||
<v-icon small>mdi-minus</v-icon>
|
||||
@@ -50,153 +61,431 @@ export default {
|
||||
props: {
|
||||
product: {
|
||||
type: Object,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
increase: {
|
||||
type: Function,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
decrease: {
|
||||
type: Function,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
currency: {
|
||||
type: Function,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
updateQuantity: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleIncrease() {
|
||||
this.increase(this.product);
|
||||
this.$emit('add-to-cart', this.product);
|
||||
this.$emit("add-to-cart", this.product);
|
||||
},
|
||||
handleQuantityChange(value) {
|
||||
this.updateQuantity(this.product);
|
||||
if (this.product.quantity > 0) {
|
||||
this.$emit('add-to-cart', this.product);
|
||||
this.$emit("add-to-cart", this.product);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ===== ESTILOS BASE - MOBILE FIRST (< 560px) ===== */
|
||||
|
||||
/* Card Container */
|
||||
.product-card {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-2px);
|
||||
border-color: #bdbdbd;
|
||||
}
|
||||
|
||||
/* Row Container */
|
||||
.product-card-row {
|
||||
padding: 12px 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* === COLUMNA DE IMAGEN === */
|
||||
.image-column {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.product-img {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 10px;
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
border-radius: 12px;
|
||||
object-fit: cover;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.product-card:hover .product-img {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
/* === COLUMNA DE DETALLES === */
|
||||
.details-column {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.product-details-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 1.1rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
line-height: 1.3;
|
||||
padding: 0 0 4px 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.product-description {
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.8rem;
|
||||
color: #7f8c8d;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* === PRECIOS === */
|
||||
.prices {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.price-unit, .price-total {
|
||||
|
||||
.price-unit,
|
||||
.price-total {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.price-label {
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
color: #95a5a6;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-size: 0.95rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.price-value.total {
|
||||
color: #27ae60;
|
||||
font-size: 1.1rem;
|
||||
background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
|
||||
color: white;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.95rem;
|
||||
box-shadow: 0 2px 8px rgba(39, 174, 96, 0.3);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* === COLUMNA DE CONTROLES === */
|
||||
.controls-column {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px 0 0 0;
|
||||
}
|
||||
|
||||
.quantity-controls {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 25px;
|
||||
padding: 4px;
|
||||
padding: 6px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.quantity-input {
|
||||
max-width: 70px;
|
||||
max-width: 60px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.quantity-input input {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.qty-btn {
|
||||
min-width: 32px !important;
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
border-radius: 50% !important;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.qty-btn:hover {
|
||||
background: #e0e0e0;
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.qty-btn-add {
|
||||
background: #27ae60;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.qty-btn-add:hover {
|
||||
background: #219a52 !important;
|
||||
}
|
||||
@media (max-width: 960px) {
|
||||
|
||||
/* ===== RESOLUCIÓN 375-559px (Mobile Standard) ===== */
|
||||
@media (min-width: 375px) and (max-width: 559px) {
|
||||
.product-img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.product-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.product-img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
|
||||
.product-card-row {
|
||||
padding: 14px 10px;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.product-description {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.quantity-input {
|
||||
max-width: 65px;
|
||||
min-width: 65px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== RESOLUCIÓN 560-959px (Tablet) ===== */
|
||||
@media (min-width: 560px) and (max-width: 959px) {
|
||||
.product-card-row {
|
||||
padding: 16px 12px;
|
||||
}
|
||||
|
||||
.image-column {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.product-img {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.product-details-content {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.product-description {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.prices {
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.price-label {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.price-value.total {
|
||||
font-size: 1rem;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
.controls-column {
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.quantity-input {
|
||||
max-width: 70px;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.qty-btn {
|
||||
min-width: 34px !important;
|
||||
width: 34px !important;
|
||||
height: 34px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== RESOLUCIÓN ≥960px (Desktop) ===== */
|
||||
@media (min-width: 960px) {
|
||||
.product-card {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.product-card:hover .product-img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.product-card-row {
|
||||
padding: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.image-column {
|
||||
padding: 0 24px 0 0;
|
||||
margin-bottom: 0;
|
||||
max-width: 380px;
|
||||
}
|
||||
|
||||
.product-img {
|
||||
width: 350px;
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
.details-column {
|
||||
padding: 0 24px 0 0;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
flex: 1;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.product-details-content {
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 1.15rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.product-description {
|
||||
font-size: 0.9rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.prices {
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.price-unit,
|
||||
.price-total {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.price-label {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.price-value.total {
|
||||
font-size: 1.05rem;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
.controls-column {
|
||||
justify-content: flex-end;
|
||||
margin-top: 0;
|
||||
padding: 0 0 0 16px;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.quantity-controls {
|
||||
gap: 8px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.quantity-input {
|
||||
max-width: 70px;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.qty-btn {
|
||||
min-width: 36px !important;
|
||||
width: 36px !important;
|
||||
height: 36px !important;
|
||||
}
|
||||
|
||||
.qty-btn:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== RESOLUCIÓN ≥1280px (Desktop Large) ===== */
|
||||
@media (min-width: 1280px) {
|
||||
.product-card-row {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.image-column {
|
||||
padding-right: 28px;
|
||||
}
|
||||
|
||||
.details-column {
|
||||
padding-right: 28px;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.product-description {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.controls-column {
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,78 +1,142 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-card :class="{
|
||||
'cart-mobile-expanded': isMobile && !isCollapsed,
|
||||
'cart-collapsed-mobile': isMobile && isCollapsed,
|
||||
'cart-desktop-collapsed': !isMobile && isCollapsed
|
||||
}">
|
||||
<v-card-title
|
||||
class="d-flex align-center cart-title"
|
||||
:class="{ 'cart-header-mobile': isMobile, 'cart-header-desktop': !isMobile }"
|
||||
>
|
||||
<!-- Icono del carrito - SIEMPRE VISIBLE -->
|
||||
<v-icon class="mr-2">mdi-cart</v-icon>
|
||||
Carrito
|
||||
<v-chip v-if="cartCount" color="primary" class="ml-2">{{ cartCount }}</v-chip>
|
||||
|
||||
<!-- Texto "Carrito" - SIEMPRE VISIBLE -->
|
||||
<span class="cart-title-text">Carrito</span>
|
||||
|
||||
<!-- Cantidad de productos - SIEMPRE VISIBLE -->
|
||||
<v-chip
|
||||
v-if="cartCount > 0"
|
||||
color="primary"
|
||||
class="ml-2"
|
||||
size="small"
|
||||
>
|
||||
{{ cartCount }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-else
|
||||
color="grey"
|
||||
class="ml-2"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
>
|
||||
0
|
||||
</v-chip>
|
||||
|
||||
<!-- Total visible cuando está colapsado (mobile o desktop) -->
|
||||
<span
|
||||
v-if="isCollapsed && cartItems.length > 0"
|
||||
class="ml-auto text-subtitle-1 font-weight-bold mr-2"
|
||||
>
|
||||
{{ currency(cartTotal) }}
|
||||
</span>
|
||||
<v-spacer v-else></v-spacer>
|
||||
|
||||
<!-- Botón toggle SIEMPRE visible (mobile y desktop) -->
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="$emit('toggle-collapse')"
|
||||
:title="isCollapsed ? 'Expandir carrito' : 'Contraer carrito'"
|
||||
>
|
||||
<v-icon>{{ isCollapsed ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text v-if="cartItems.length === 0" class="text-center grey--text">
|
||||
El carrito está vacío
|
||||
</v-card-text>
|
||||
|
||||
<v-list v-else density="compact" max-height="300" class="overflow-y-auto">
|
||||
<v-list-item v-for="item in cartItems" :key="item.id">
|
||||
<template v-slot:prepend>
|
||||
<v-avatar size="40" rounded>
|
||||
<v-img :src="item.img" cover></v-img>
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle class="d-flex align-center">
|
||||
<div class="quantity-controls">
|
||||
<v-btn small text class="qty-btn" @click="decreaseQuantity(item.id)"
|
||||
><v-icon small>mdi-minus</v-icon></v-btn
|
||||
>
|
||||
<v-text-field
|
||||
:model-value="item.quantity"
|
||||
@update:model-value="updateQuantity(item.id, $event)"
|
||||
type="number"
|
||||
min="1"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="qty-input"
|
||||
/>
|
||||
<v-btn small text class="qty-btn" @click="increaseQuantity(item.id)"
|
||||
><v-icon small>mdi-plus</v-icon></v-btn
|
||||
>
|
||||
<div v-show="!isCollapsed">
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text v-if="cartItems.length === 0" class="text-center grey--text">
|
||||
El carrito está vacío
|
||||
</v-card-text>
|
||||
|
||||
<v-list v-else density="compact" :max-height="listMaxHeight" class="overflow-y-auto cart-list">
|
||||
<v-list-item v-for="(item, index) in cartItems" :key="item.id" class="cart-list-item">
|
||||
<template v-slot:prepend>
|
||||
<div class="prepend-wrapper">
|
||||
<!-- Mostrar imagen solo si NO es extra small -->
|
||||
<v-avatar v-if="!isExtraSmall" size="40" rounded>
|
||||
<v-img :src="item.img" cover></v-img>
|
||||
</v-avatar>
|
||||
<!-- Mostrar número cuando es extra small -->
|
||||
<div v-else class="item-number">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Contenido reorganizado -->
|
||||
<div class="cart-item-content">
|
||||
<!-- Línea 1: Nombre del producto -->
|
||||
<div class="product-name">{{ item.name }}</div>
|
||||
|
||||
<!-- Línea 2: Controles + Precio unitario -->
|
||||
<div class="controls-row">
|
||||
<div class="quantity-controls">
|
||||
<v-btn small text class="qty-btn" @click="decreaseQuantity(item.id)">
|
||||
<v-icon small>mdi-minus</v-icon>
|
||||
</v-btn>
|
||||
<v-text-field
|
||||
:model-value="item.quantity"
|
||||
@update:model-value="updateQuantity(item.id, $event)"
|
||||
type="number"
|
||||
min="1"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="qty-input"
|
||||
/>
|
||||
<v-btn small text class="qty-btn" @click="increaseQuantity(item.id)">
|
||||
<v-icon small>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<span class="unit-price">x {{ currency(item.price) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="ml-2">x {{ currency(item.price) }}</span>
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template v-slot:append>
|
||||
<div class="d-flex align-center">
|
||||
<strong>{{ currency(item.price * item.quantity) }}</strong>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="red"
|
||||
class="ml-2"
|
||||
@click="$emit('remove', item.id)"
|
||||
></v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<v-divider v-if="cartItems.length > 0"></v-divider>
|
||||
|
||||
<v-card-text v-if="cartItems.length > 0">
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<strong>Total:</strong>
|
||||
<strong class="text-h6">{{ currency(cartTotal) }}</strong>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions v-if="cartItems.length > 0">
|
||||
<v-btn color="primary" block @click="$emit('checkout')">
|
||||
Finalizar Compra
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
<!-- Append: Total + Delete -->
|
||||
<template v-slot:append>
|
||||
<div class="item-actions">
|
||||
<strong class="total-price">{{ currency(item.price * item.quantity) }}</strong>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
class="delete-btn"
|
||||
@click="$emit('remove', item.id)"
|
||||
></v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<v-divider v-if="cartItems.length > 0"></v-divider>
|
||||
|
||||
<v-card-text v-if="cartItems.length > 0" class="cart-total-section">
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<strong>Total:</strong>
|
||||
<strong class="text-h6">{{ currency(cartTotal) }}</strong>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions v-if="cartItems.length > 0" class="cart-checkout-section">
|
||||
<v-btn color="primary" block @click="$emit('checkout')">
|
||||
Finalizar Compra
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
@@ -87,15 +151,39 @@ export default {
|
||||
currency: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
isCollapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isMobile: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
windowWidth: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
emits: ['remove', 'checkout', 'update-quantity'],
|
||||
emits: ['remove', 'checkout', 'update-quantity', 'toggle-collapse'],
|
||||
computed: {
|
||||
cartCount() {
|
||||
return this.cartItems.reduce((sum, item) => sum + item.quantity, 0);
|
||||
},
|
||||
cartTotal() {
|
||||
return this.cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||
},
|
||||
listMaxHeight() {
|
||||
// En desktop, permitir más altura para scroll
|
||||
if (!this.isMobile) {
|
||||
return '500px';
|
||||
}
|
||||
// En mobile, altura limitada
|
||||
return '300px';
|
||||
},
|
||||
isExtraSmall() {
|
||||
// Detectar si la resolución es menor a 560px
|
||||
return this.windowWidth < 560;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -122,20 +210,393 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.qty-input {
|
||||
width: 70px;
|
||||
/* === ESTILOS BASE PARA CART ITEMS (Mobile First) === */
|
||||
|
||||
/* Contenedor principal del item */
|
||||
.cart-item-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
min-width: 0; /* Permite que el flex funcione con overflow */
|
||||
}
|
||||
.qty-input input {
|
||||
text-align: center;
|
||||
|
||||
/* Nombre del producto */
|
||||
.product-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.3;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2; /* Máximo 2 líneas */
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Fila de controles + precio unitario */
|
||||
.controls-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Controles de cantidad */
|
||||
.quantity-controls {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Input de cantidad */
|
||||
.qty-input {
|
||||
width: 55px;
|
||||
min-width: 55px;
|
||||
}
|
||||
|
||||
.qty-input input {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Botones de cantidad */
|
||||
.qty-btn {
|
||||
min-width: 28px !important;
|
||||
height: 28px !important;
|
||||
border-radius: 14px !important;
|
||||
min-width: 24px !important;
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
border-radius: 12px !important;
|
||||
padding: 0 !important;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
.qty-btn .v-icon {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
/* Precio unitario */
|
||||
.unit-price {
|
||||
font-size: 0.8rem;
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Contenedor de acciones (total + delete) */
|
||||
.item-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Precio total */
|
||||
.total-price {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: #2e7d32;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Botón de eliminar */
|
||||
.delete-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Footer: Total y checkout */
|
||||
.cart-total-section {
|
||||
padding: 12px 16px !important;
|
||||
}
|
||||
|
||||
.cart-checkout-section {
|
||||
padding: 4px 16px 12px !important;
|
||||
}
|
||||
|
||||
/* Ajustes al list-item */
|
||||
.cart-list-item {
|
||||
padding: 10px 8px !important;
|
||||
min-height: auto !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
/* Wrapper del prepend para controlar gap con content */
|
||||
.prepend-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* Estilos para mobile expandido */
|
||||
.cart-mobile-expanded {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
border-radius: 16px 16px 0 0 !important;
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
/* Estilos para mobile colapsado */
|
||||
.cart-collapsed-mobile {
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
border-radius: 16px 16px 0 0 !important;
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
/* Estilos para desktop colapsado */
|
||||
.cart-desktop-collapsed {
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Header del carrito */
|
||||
.cart-title {
|
||||
padding: 8px 12px !important;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.cart-title-text {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Header interactivo en mobile */
|
||||
.cart-header-mobile {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Header interactivo en desktop */
|
||||
.cart-header-desktop {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.cart-header-desktop:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* Lista de items con scroll */
|
||||
.cart-list {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
||||
}
|
||||
|
||||
.cart-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.cart-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.cart-list::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.cart-list::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Transiciones suaves */
|
||||
.v-card {
|
||||
transition: max-height 0.35s ease-in-out, box-shadow 0.3s ease-in-out, border-radius 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* === MEDIA QUERIES RESPONSIVE === */
|
||||
|
||||
/* Resolución 560-959px (Mobile/Tablet con imagen) */
|
||||
@media (min-width: 560px) and (max-width: 959px) {
|
||||
.cart-title {
|
||||
padding: 10px 16px !important;
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
.cart-title-text {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.cart-list-item {
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
.prepend-wrapper {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.cart-item-content {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.controls-row {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.quantity-controls {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.qty-input {
|
||||
width: 65px !important;
|
||||
min-width: 65px !important;
|
||||
}
|
||||
|
||||
.qty-btn {
|
||||
min-width: 28px !important;
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
border-radius: 14px !important;
|
||||
}
|
||||
|
||||
.qty-btn .v-icon {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.unit-price {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.cart-total-section {
|
||||
padding: 12px 16px !important;
|
||||
}
|
||||
|
||||
.cart-checkout-section {
|
||||
padding: 4px 16px 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Resolución ≥960px (Desktop) */
|
||||
@media (min-width: 960px) {
|
||||
.v-card {
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
.cart-title {
|
||||
padding: 12px 16px !important;
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
.cart-title-text {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.cart-list-item {
|
||||
padding: 12px 16px !important;
|
||||
}
|
||||
|
||||
.prepend-wrapper {
|
||||
margin-right: 14px;
|
||||
}
|
||||
|
||||
.cart-item-content {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.controls-row {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.quantity-controls {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.qty-input {
|
||||
width: 70px !important;
|
||||
min-width: 70px !important;
|
||||
}
|
||||
|
||||
.qty-btn {
|
||||
min-width: 32px !important;
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
border-radius: 16px !important;
|
||||
}
|
||||
|
||||
.qty-btn .v-icon {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
.unit-price {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Item-actions en fila en desktop */
|
||||
.item-actions {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cart-desktop-collapsed {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cart-total-section {
|
||||
padding: 14px 16px !important;
|
||||
}
|
||||
|
||||
.cart-checkout-section {
|
||||
padding: 4px 16px 14px !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Ajustes para la lista de items en mobile */
|
||||
@media (max-width: 680px) {
|
||||
.v-list {
|
||||
max-height: calc(70vh - 200px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ajustes para resoluciones extra pequeñas (<560px) */
|
||||
@media (max-width: 559px) {
|
||||
/* Número del item cuando no hay imagen */
|
||||
.item-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
min-width: 32px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 50%;
|
||||
font-weight: 700;
|
||||
font-size: 0.875rem;
|
||||
color: white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.quantity-controls {
|
||||
gap: 3px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
269
src/components/catalog/PaginationControls.vue
Normal file
269
src/components/catalog/PaginationControls.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<v-row class="pagination-container my-3" align="center">
|
||||
<!-- Fila 1: Navegador de páginas (centrado, ancho completo) -->
|
||||
<v-col cols="12" class="d-flex justify-center mb-2">
|
||||
<v-pagination
|
||||
:model-value="currentPage"
|
||||
@update:model-value="$emit('page-change', $event)"
|
||||
:length="totalPages"
|
||||
:total-visible="computedTotalVisible"
|
||||
:show-first-last-page="showFirstLastButtons"
|
||||
rounded="circle"
|
||||
color="primary"
|
||||
:size="paginationSize"
|
||||
></v-pagination>
|
||||
</v-col>
|
||||
|
||||
<!-- Fila 2: Info + Selector (en línea) - Solo Desktop -->
|
||||
<v-col
|
||||
cols="12"
|
||||
class="d-none d-md-flex justify-center align-center pagination-info-row"
|
||||
>
|
||||
<!-- Información de resultados -->
|
||||
<span class="pagination-info-text">
|
||||
Mostrando {{ paginationInfo.start }}-{{ paginationInfo.end }}
|
||||
de {{ paginationInfo.total }} productos
|
||||
</span>
|
||||
|
||||
<!-- Separador visual -->
|
||||
<v-divider vertical class="mx-4" style="height: 24px;"></v-divider>
|
||||
|
||||
<!-- Selector de items por página -->
|
||||
<span class="mr-2 text-body-1 font-weight-medium">
|
||||
Productos por página:
|
||||
</span>
|
||||
<v-select
|
||||
:model-value="itemsPerPage"
|
||||
@update:model-value="$emit('items-per-page-change', $event)"
|
||||
:items="itemsPerPageOptions"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="items-per-page-selector"
|
||||
></v-select>
|
||||
</v-col>
|
||||
|
||||
<!-- Fila 2 Mobile: Info de resultados - Solo Mobile -->
|
||||
<v-col cols="12" class="d-flex d-md-none justify-center">
|
||||
<span class="pagination-info-text">
|
||||
Mostrando {{ paginationInfo.start }}-{{ paginationInfo.end }}
|
||||
de {{ paginationInfo.total }} productos
|
||||
</span>
|
||||
</v-col>
|
||||
|
||||
<!-- Fila 3 Mobile: Selector - Solo Mobile -->
|
||||
<v-col
|
||||
cols="12"
|
||||
class="d-flex d-md-none justify-center align-center"
|
||||
>
|
||||
<span class="mr-2 text-body-1 font-weight-medium">
|
||||
Productos por página:
|
||||
</span>
|
||||
<v-select
|
||||
:model-value="itemsPerPage"
|
||||
@update:model-value="$emit('items-per-page-change', $event)"
|
||||
:items="itemsPerPageOptions"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="items-per-page-selector"
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, ref, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
export default {
|
||||
name: 'PaginationControls',
|
||||
props: {
|
||||
currentPage: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
totalPages: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
itemsPerPage: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
itemsPerPageOptions: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
paginationInfo: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
position: {
|
||||
type: String,
|
||||
default: 'top',
|
||||
validator: (value) => ['top', 'bottom'].includes(value)
|
||||
},
|
||||
totalVisiblePages: {
|
||||
type: Number,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
emits: ['page-change', 'items-per-page-change'],
|
||||
setup(props) {
|
||||
const windowWidth = ref(window.innerWidth);
|
||||
|
||||
// Actualizar ancho de ventana en resize
|
||||
const updateWidth = () => {
|
||||
windowWidth.value = window.innerWidth;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', updateWidth);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateWidth);
|
||||
});
|
||||
|
||||
const isMobile = computed(() => windowWidth.value < 680);
|
||||
|
||||
// Computed para tamaño de paginación: más pequeño en tablet para ahorrar espacio
|
||||
const paginationSize = computed(() => {
|
||||
const width = windowWidth.value;
|
||||
|
||||
// En pantallas pequeñas/medianas, usar tamaño default para ahorrar espacio
|
||||
if (width < 960) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
// En desktop, usar tamaño large
|
||||
return 'large';
|
||||
});
|
||||
|
||||
// Computed para mostrar botones first/last: solo en pantallas >= 680px
|
||||
const showFirstLastButtons = computed(() => {
|
||||
const width = windowWidth.value;
|
||||
|
||||
// Mostrar first/last solo en tablet y desktop (>= 680px)
|
||||
// En mobile (<680px), solo prev/next para ahorrar espacio
|
||||
return width >= 680;
|
||||
});
|
||||
|
||||
// Computed property para total-visible: prioriza prop recibida, sino calcula localmente
|
||||
const computedTotalVisible = computed(() => {
|
||||
// Si se recibe totalVisiblePages desde el padre, usarlo (SINCRONIZACIÓN)
|
||||
if (props.totalVisiblePages !== null) {
|
||||
return props.totalVisiblePages;
|
||||
}
|
||||
|
||||
// Fallback: cálculo local (por compatibilidad)
|
||||
const width = windowWidth.value;
|
||||
const totalPages = props.totalPages;
|
||||
|
||||
// Si hay pocas páginas, mostrarlas todas
|
||||
if (totalPages <= 7) {
|
||||
return totalPages;
|
||||
}
|
||||
|
||||
// Breakpoints responsivos
|
||||
if (width < 400) {
|
||||
return 3;
|
||||
} else if (width < 680) {
|
||||
return 5;
|
||||
} else if (width < 960) {
|
||||
return 7;
|
||||
} else if (width < 1280) {
|
||||
return 9;
|
||||
} else {
|
||||
return 11;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
isMobile,
|
||||
computedTotalVisible,
|
||||
paginationSize,
|
||||
showFirstLastButtons
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pagination-container {
|
||||
padding: 12px 0;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pagination-info-text {
|
||||
font-size: 1.05rem;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Nueva clase para la fila de info en desktop */
|
||||
.pagination-info-row {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.items-per-page-selector {
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 680px) {
|
||||
.pagination-container {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.items-per-page-selector {
|
||||
max-width: 80px;
|
||||
}
|
||||
|
||||
.text-body-1 {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ajustes visuales */
|
||||
.v-pagination {
|
||||
margin: 0 auto;
|
||||
min-width: 400px; /* Garantizar espacio mínimo para iconos + páginas */
|
||||
}
|
||||
|
||||
/* En móviles muy pequeños, reducir min-width */
|
||||
@media (max-width: 480px) {
|
||||
.v-pagination {
|
||||
min-width: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
/* En pantallas medianas problemáticas (680-960px), asegurar espacio suficiente */
|
||||
@media (min-width: 680px) and (max-width: 960px) {
|
||||
.v-pagination {
|
||||
min-width: 450px; /* Más espacio para evitar que desaparezcan los iconos */
|
||||
}
|
||||
}
|
||||
|
||||
/* Separador vertical */
|
||||
.v-divider--vertical {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Aumentar tamaño del icono del dropdown en v-select */
|
||||
.items-per-page-selector :deep(.v-icon) {
|
||||
font-size: 1.5rem !important;
|
||||
}
|
||||
|
||||
/* Aumentar tamaño del texto dentro del select */
|
||||
.items-per-page-selector :deep(.v-field__input) {
|
||||
font-size: 1rem !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Aumentar tamaño de los items del menú dropdown */
|
||||
.items-per-page-selector :deep(.v-list-item-title) {
|
||||
font-size: 1rem !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -1,23 +1,88 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<!-- Backdrop para mobile cuando el carrito está expandido -->
|
||||
<div
|
||||
v-if="isMobile && !cartCollapsed"
|
||||
class="cart-backdrop"
|
||||
@click="cartCollapsed = true"
|
||||
></div>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="9">
|
||||
<v-col cols="12" md="9" lg="7" :class="{ 'pb-mobile-cart': isMobile }">
|
||||
<v-card-title>
|
||||
<span class="headline">Catálogo</span>
|
||||
</v-card-title>
|
||||
<v-list-item v-for="item in items" :key="item.id" class="catalog-item">
|
||||
<Card :product="item" :increase="increase" :decrease="decrease" :currency="currency" :updateQuantity="updateQuantity" @add-to-cart="addToCart"/>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="3">
|
||||
<div class="cart-sidebar">
|
||||
<Cart
|
||||
:cart-items="cartItems"
|
||||
|
||||
<!-- Controles de paginación superiores -->
|
||||
<PaginationControls
|
||||
v-if="items.length > 0"
|
||||
:current-page="currentPage"
|
||||
:total-pages="totalPages"
|
||||
:items-per-page="itemsPerPage"
|
||||
:items-per-page-options="itemsPerPageOptions"
|
||||
:pagination-info="paginationInfo"
|
||||
:total-visible-pages="totalVisiblePages"
|
||||
@page-change="handlePageChange"
|
||||
@items-per-page-change="handleItemsPerPageChange"
|
||||
position="top"
|
||||
/>
|
||||
|
||||
<!-- Lista de productos paginados -->
|
||||
<v-list-item
|
||||
v-for="item in paginatedItems"
|
||||
:key="item.id"
|
||||
class="catalog-item"
|
||||
>
|
||||
<Card
|
||||
:product="item"
|
||||
:increase="increase"
|
||||
:decrease="decrease"
|
||||
:currency="currency"
|
||||
:updateQuantity="updateQuantity"
|
||||
@add-to-cart="addToCart"
|
||||
/>
|
||||
</v-list-item>
|
||||
|
||||
<!-- Mensaje cuando no hay productos -->
|
||||
<v-alert
|
||||
v-if="items.length === 0"
|
||||
type="info"
|
||||
class="my-4"
|
||||
variant="tonal"
|
||||
>
|
||||
No hay productos disponibles en el catálogo
|
||||
</v-alert>
|
||||
|
||||
<!-- Controles de paginación inferiores -->
|
||||
<PaginationControls
|
||||
v-if="items.length > 0"
|
||||
:current-page="currentPage"
|
||||
:total-pages="totalPages"
|
||||
:items-per-page="itemsPerPage"
|
||||
:items-per-page-options="itemsPerPageOptions"
|
||||
:pagination-info="paginationInfo"
|
||||
:total-visible-pages="totalVisiblePages"
|
||||
@page-change="handlePageChange"
|
||||
@items-per-page-change="handleItemsPerPageChange"
|
||||
position="bottom"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="3" lg="5">
|
||||
<div
|
||||
class="cart-sidebar"
|
||||
:class="{ collapsed: cartCollapsed && isMobile }"
|
||||
>
|
||||
<Cart
|
||||
:cart-items="cartItems"
|
||||
:currency="currency"
|
||||
:is-collapsed="cartCollapsed"
|
||||
:is-mobile="isMobile"
|
||||
:window-width="windowWidth"
|
||||
@remove="removeFromCart"
|
||||
@checkout="goToCheckout"
|
||||
@update-quantity="updateCartQuantity"
|
||||
@toggle-collapse="toggleCart"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
@@ -26,24 +91,52 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Card from '@/components/catalog/Card.vue';
|
||||
import Cart from '@/components/catalog/Cart.vue';
|
||||
import { useCartStore } from '@/stores/cart';
|
||||
import { inject } from 'vue';
|
||||
import Card from "@/components/catalog/Card.vue";
|
||||
import Cart from "@/components/catalog/Cart.vue";
|
||||
import PaginationControls from "@/components/catalog/PaginationControls.vue";
|
||||
import { useCartStore } from "@/stores/cart";
|
||||
import { inject, ref, computed, onMounted, onUnmounted } from "vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Card,
|
||||
Cart
|
||||
Cart,
|
||||
PaginationControls,
|
||||
},
|
||||
setup() {
|
||||
const cartStore = useCartStore();
|
||||
return { cartStore };
|
||||
const cartCollapsed = ref(false); // Cambiado a false para que inicie expandido en desktop
|
||||
const windowWidth = ref(window.innerWidth);
|
||||
|
||||
const isMobile = computed(() => windowWidth.value < 960); // Cambiado de 680 a 960
|
||||
|
||||
const updateWindowWidth = () => {
|
||||
windowWidth.value = window.innerWidth;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("resize", updateWindowWidth);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("resize", updateWindowWidth);
|
||||
});
|
||||
|
||||
return {
|
||||
cartStore,
|
||||
cartCollapsed,
|
||||
isMobile,
|
||||
windowWidth,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
api: inject('api'),
|
||||
api: inject("api"),
|
||||
items: [],
|
||||
// Paginación
|
||||
currentPage: 1,
|
||||
itemsPerPage: 20,
|
||||
itemsPerPageOptions: [10, 20, 50, 100],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -53,26 +146,74 @@ export default {
|
||||
},
|
||||
set(value) {
|
||||
this.cartStore.items = value;
|
||||
}
|
||||
},
|
||||
},
|
||||
cartCount() {
|
||||
return this.cartStore.cartCount;
|
||||
}
|
||||
},
|
||||
// Paginación
|
||||
paginatedItems() {
|
||||
const start = (this.currentPage - 1) * this.itemsPerPage;
|
||||
const end = start + this.itemsPerPage;
|
||||
return this.items.slice(start, end);
|
||||
},
|
||||
totalPages() {
|
||||
return Math.ceil(this.items.length / this.itemsPerPage);
|
||||
},
|
||||
paginationInfo() {
|
||||
const start = (this.currentPage - 1) * this.itemsPerPage + 1;
|
||||
const end = Math.min(
|
||||
this.currentPage * this.itemsPerPage,
|
||||
this.items.length,
|
||||
);
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
total: this.items.length,
|
||||
};
|
||||
},
|
||||
// Computed para total-visible dinámico y responsive (usado por ambos PaginationControls)
|
||||
totalVisiblePages() {
|
||||
// Si hay pocas páginas, mostrarlas todas (IMPORTANTE para mostrar iconos de navegación)
|
||||
if (this.totalPages <= 7) {
|
||||
return this.totalPages;
|
||||
}
|
||||
|
||||
// Breakpoints responsivos basados en windowWidth
|
||||
// OPTIMIZADO: Reducidos para evitar que desaparezcan los iconos de navegación
|
||||
const width = this.windowWidth;
|
||||
|
||||
if (width < 400) {
|
||||
return 3; // Extra small mobile
|
||||
} else if (width < 680) {
|
||||
return 5; // Mobile
|
||||
} else if (width < 960) {
|
||||
return 5; // Tablet (REDUCIDO de 7 → 5 para evitar overflow)
|
||||
} else if (width < 1280) {
|
||||
return 7; // Desktop small (REDUCIDO de 9 → 7)
|
||||
} else {
|
||||
return 9; // Desktop large (REDUCIDO de 11 → 9)
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadItemsPerPagePreference();
|
||||
this.fetchProducts();
|
||||
},
|
||||
methods: {
|
||||
fetchProducts() {
|
||||
this.api.getProducts()
|
||||
.then(data => {
|
||||
this.items = data.map(product => ({
|
||||
this.api
|
||||
.getProducts()
|
||||
.then((data) => {
|
||||
this.items = data.map((product) => ({
|
||||
...product,
|
||||
quantity: 0,
|
||||
img: product.img || `https://picsum.photos/300/200?random=${product.id}`
|
||||
img:
|
||||
product.img ||
|
||||
`https://picsum.photos/300/200?random=${product.id}`,
|
||||
}));
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
@@ -101,24 +242,62 @@ export default {
|
||||
},
|
||||
removeFromCart(itemId) {
|
||||
this.cartStore.removeItem(itemId);
|
||||
const item = this.items.find(i => i.id === itemId);
|
||||
const item = this.items.find((i) => i.id === itemId);
|
||||
if (item) {
|
||||
item.quantity = 0;
|
||||
}
|
||||
},
|
||||
updateCartQuantity({ itemId, quantity }) {
|
||||
this.cartStore.updateQuantity({ itemId, quantity });
|
||||
const productItem = this.items.find(i => i.id === itemId);
|
||||
const productItem = this.items.find((i) => i.id === itemId);
|
||||
if (productItem) {
|
||||
productItem.quantity = quantity;
|
||||
}
|
||||
},
|
||||
goToCheckout() {
|
||||
this.$router.push('/comprar');
|
||||
this.$router.push("/comprar");
|
||||
},
|
||||
toggleCart() {
|
||||
this.cartCollapsed = !this.cartCollapsed;
|
||||
},
|
||||
// Paginación
|
||||
handlePageChange(newPage) {
|
||||
this.currentPage = newPage;
|
||||
this.scrollToTop();
|
||||
},
|
||||
handleItemsPerPageChange(newValue) {
|
||||
this.itemsPerPage = newValue;
|
||||
this.currentPage = 1;
|
||||
this.saveItemsPerPagePreference(newValue);
|
||||
this.scrollToTop();
|
||||
},
|
||||
saveItemsPerPagePreference(value) {
|
||||
localStorage.setItem("catalog_items_per_page", value);
|
||||
},
|
||||
loadItemsPerPagePreference() {
|
||||
const saved = localStorage.getItem("catalog_items_per_page");
|
||||
if (saved && this.itemsPerPageOptions.includes(parseInt(saved))) {
|
||||
this.itemsPerPage = parseInt(saved);
|
||||
}
|
||||
},
|
||||
scrollToTop() {
|
||||
this.$nextTick(() => {
|
||||
const catalogHeader = this.$el?.querySelector(".headline");
|
||||
if (catalogHeader) {
|
||||
catalogHeader.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
currency(val) {
|
||||
if (val == null) return '-';
|
||||
return new Intl.NumberFormat('es-CO', { style: 'currency', currency: 'COP', minimumFractionDigits: 0 }).format(val);
|
||||
if (val == null) return "-";
|
||||
return new Intl.NumberFormat("es-CO", {
|
||||
style: "currency",
|
||||
currency: "COP",
|
||||
minimumFractionDigits: 0,
|
||||
}).format(val);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -127,55 +306,94 @@ export default {
|
||||
<style scoped>
|
||||
.headline {
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
/* === LISTADO DE PRODUCTOS === */
|
||||
.catalog-item {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
padding: 0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.catalog-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Mobile First: Estilos base (< 960px) */
|
||||
.cart-sidebar {
|
||||
position: sticky;
|
||||
top: 80px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
.product-img {
|
||||
width: 150px;
|
||||
height: 100px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
|
||||
.cart-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
.quantity-controls {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.prices .price-line {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.quantity-input {
|
||||
max-width: 90px;
|
||||
}
|
||||
.quantity-input input {
|
||||
text-align: center;
|
||||
}
|
||||
.qty-btn {
|
||||
min-width: 36px !important;
|
||||
height: 36px !important;
|
||||
border-radius: 18px !important;
|
||||
}
|
||||
@media (max-width: 960px) {
|
||||
.cart-sidebar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: auto;
|
||||
z-index: 100;
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.product-img {
|
||||
width: 120px;
|
||||
height: 80px;
|
||||
|
||||
/* Padding para el contenido del catálogo en mobile */
|
||||
.pb-mobile-cart {
|
||||
padding-bottom: 76px !important; /* 60px sidebar + 16px margen */
|
||||
}
|
||||
|
||||
/* Desktop (≥ 960px) - Sidebar sticky con scroll */
|
||||
/* Layout: 960-1023px = md breakpoint (75% productos / 25% cart) */
|
||||
/* Layout: ≥1024px = lg breakpoint (60% productos / 40% cart) */
|
||||
@media (min-width: 960px) {
|
||||
.cart-sidebar {
|
||||
position: sticky;
|
||||
top: 80px;
|
||||
z-index: auto;
|
||||
max-height: calc(100vh - 100px);
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
.cart-backdrop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pb-mobile-cart {
|
||||
padding-bottom: 16px !important;
|
||||
}
|
||||
|
||||
.catalog-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Resolución <560px - Mobile extra small */
|
||||
@media (max-width: 559px) {
|
||||
.headline {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.catalog-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Resolución 560-959px - Tablet */
|
||||
@media (min-width: 560px) and (max-width: 959px) {
|
||||
.catalog-item {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user