shop-wx/src/pages/index/components/product-container.vue

447 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts" setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useProductStore } from '@/pinia/stores/product'
import { useCartStore } from '@/pinia/stores/cart'
import { useCurrentProductStore } from '@/pinia/stores/currentProduct'
import { storeToRefs } from 'pinia'
import Cart from './cart.vue'
import { toHttpsUrl } from '@/utils'
definePage({
style: {
navigationBarTitleText: '商品列表',
},
})
// 定义Emit事件
const emit = defineEmits<{
(e: 'backToShopList'): void
(e: 'checkout'): void
}>()
// 状态管理
const productStore = useProductStore()
const { labels, categories } = storeToRefs(productStore)
const cartStore = useCartStore()
const { cartItems, totalPrice, totalQuantity } = storeToRefs(cartStore)
const currentProductStore = useCurrentProductStore()
// 从props接收的数据
const activeCategory = ref<number>(0)
// 商品详情弹窗控制
// const showDetailPopup = ref<boolean>(false)
// 当前查看的商品ID
const currentCellId = ref<number>()
// 当前商品详情计算属性
// const currentProduct = computed(() =>
// categories.value.find(p => p.cellId === currentCellId.value)
// )
// 购物车弹窗控制
const showCartPopup = ref<boolean>(false)
const searchQuery = ref<string>('')
/**
* 处理分类导航点击事件
* @param index - 被点击的分类索引
*/
function handleCategoryClick(index: number) {
activeCategory.value = index
}
/**
* 打开商品详情弹窗
* @param cellId - 要显示详情的格口ID
*/
function showProductDetail(cellId: number) {
currentCellId.value = cellId
// 获取当前选中的商品
const product = categories.value.find(p => p.cellId === cellId)
if (product) {
// 设置当前商品到store
currentProductStore.setCurrentProduct(product)
// 跳转到详情页面
uni.navigateTo({
url: '/pages/index/detail'
})
}
}
/**
* 处理添加商品到购物车
* @param product - 要添加到购物车的商品对象
*/
function handleAddToCart(product: any) {
cartStore.addToCart(product)
}
/**
* 处理从购物车移除商品
* @param cellId - 要移除的商品格子ID
*/
function handleRemoveFromCart(cellId: number) {
cartStore.removeFromCart(cellId)
}
/**
* 获取购物车中指定商品的数量
* @param cellId - 商品的格子ID
* @returns 购物车中该商品的数量如果没有则返回0
*/
function getCartItemCount(cellId: number) {
const item = cartItems.value.find(item => item.product.cellId === cellId)
return item ? item.quantity : 0
}
/**
* 计算当前显示的商品列表
* 根据选中的分类和搜索查询条件过滤商品
*/
const currentProducts = computed(() => {
// 先按分类过滤
const filteredByCategory = categories.value.filter(c => c.label === labels.value[activeCategory.value]?.id);
// 再按搜索查询过滤
if (!searchQuery.value) return filteredByCategory;
return filteredByCategory.filter(p =>
p.name.toLowerCase().includes(searchQuery.value.trim().toLowerCase())
);
})
/**
* 处理结算操作
* 触发checkout事件通知父组件进行结算
*/
function handleCheckout() {
emit('checkout')
}
</script>
<template>
<view class="product-container">
<!-- 左侧分类导航 -->
<view class="left-nav">
<view class="back-btn" @click="emit('backToShopList')">
<wd-icon name="arrow-left" size="16px"></wd-icon>
<text style="margin-left: 4px">重选地址</text>
</view>
<scroll-view class="category-nav" scroll-y>
<view
v-for="(label, index) in labels"
:key="label.id"
:class="['category-item', { active: activeCategory === index }]"
@click="handleCategoryClick(index)"
>
{{ label.name }}
</view>
</scroll-view>
</view>
<!-- 右侧商品列表 -->
<scroll-view class="product-list" scroll-y>
<view class="search-box">
<wd-search v-model="searchQuery" placeholder="搜索商品名称" hide-cancel></wd-search>
</view>
<view class="category-section">
<view v-for="product in currentProducts" :key="product.cellId" class="product-item">
<view class="product-content">
<view class="product-image-wrapper" @click.stop="showProductDetail(product.cellId)">
<wd-img
:src="toHttpsUrl(product.image)"
width="80"
height="80"
border-radius="4"
>
<template #error>
<view class="custom-error">图片加载失败</view>
</template>
</wd-img>
<view v-if="product.stock === 0" class="sold-out-overlay">
<text class="sold-out-text">已售罄</text>
</view>
</view>
<view class="product-info">
<view class="product-name" @click.stop="showProductDetail(product.cellId)">
{{ product.name }}
</view>
<view class="product-price" @click.stop="showProductDetail(product.cellId)">
¥{{ product.price.toFixed(2) }}
</view>
<view class="action-row">
<text v-if="product.stock > 0" class="stock-count">
还剩{{ product.stock }}份
</text>
<view class="cart-actions">
<view
v-if="getCartItemCount(product.cellId)"
class="cart-btn-minus"
@click.stop="handleRemoveFromCart(product.cellId)"
>
<wd-icon name="decrease" size="12px" color="#000"></wd-icon>
</view>
<text v-if="getCartItemCount(product.cellId)" class="cart-count">
{{ getCartItemCount(product.cellId) }}
</text>
<view
class="cart-btn-plus"
:style="{ opacity: product.stock === 0 ? 0.6 : 1 }"
@click.stop="handleAddToCart(product)"
>
<wd-icon name="add" size="12px" color="#fff"></wd-icon>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 底部购物车栏 -->
<view v-if="cartItems.length" class="shopping-cart-bar">
<view class="cart-info" @click.stop="showCartPopup = true">
<wd-badge :model-value="totalQuantity" right="0" top="0">
<wd-icon name="cart" size="24px"></wd-icon>
</wd-badge>
<text class="total-price">
合计:¥{{ totalPrice.toFixed(2) }}
</text>
</view>
<view class="checkout-btn" @click="handleCheckout">
去结算
</view>
</view>
<!-- 购物车弹窗 -->
<wd-popup v-model="showCartPopup" position="bottom" :z-index="999">
<view class="detail-container">
<Cart @cart-close="showCartPopup = false" />
</view>
</wd-popup>
</view>
</template>
<style scoped lang="scss">
.product-container {
display: flex;
// height: 100%;
height: calc(100vh - 150px);
background: #f7f8fa;
position: relative;
overflow: hidden;
}
.left-nav {
width: 100px;
flex-shrink: 0;
background: #fff;
border-right: 1px solid #e0e0e0;
}
.back-btn {
display: flex;
align-items: center;
padding: 12px;
background: #fff;
border-bottom: 1px solid #e0e0e0;
font-size: 14px;
color: #666;
}
.category-nav {
flex: 1;
height: calc(100% - 50px);
}
.category-item {
padding: 16px 8px;
text-align: center;
font-size: 14px;
color: #666;
border-left: 3px solid transparent;
background: #fff;
&.active {
background: #f7f8fa;
color: #F56C6C;
border-left-color: #F56C6C;
}
}
.product-list {
flex: 1;
height: calc(100vh - 150px);
padding: 10px;
background: #ffffff;
}
.search-box {
padding: 0px;
background: #fff;
}
.category-section {
padding-bottom: 55px;
}
.product-item {
margin-bottom: 10px;
background: #fff;
border-radius: 8px;
padding: 12px;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
.product-content {
display: flex;
gap: 12px;
}
.product-image-wrapper {
position: relative;
width: 80px;
height: 80px;
flex-shrink: 0;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 80px;
}
.product-name {
font-size: 14px;
color: #333;
line-height: 1.4;
}
.product-price {
font-size: 16px;
color: #F56C6C;
font-weight: bold;
}
.action-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.stock-count {
font-size: 11px;
color: #bbbbbb;
}
.cart-actions {
display: flex;
align-items: center;
gap: 4px;
}
.cart-btn-minus,
.cart-btn-plus {
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: #F56C6C;
}
.cart-btn-minus {
background: #fff;
border: 1px solid #F56C6C;
}
.cart-count {
font-size: 12px;
min-width: 20px;
text-align: center;
color: #333;
}
.sold-out-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
}
.sold-out-text {
color: #999;
font-size: 14px;
transform: rotate(-15deg);
border: 1px solid #eee;
padding: 2px 8px;
border-radius: 4px;
}
.custom-error {
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
color: #969799;
font-size: 12px;
background: #f7f8fa;
}
.shopping-cart-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 50px;
background: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
z-index: 100;
}
.cart-info {
display: flex;
align-items: center;
gap: 12px;
}
.total-price {
font-size: 14px;
color: #333;
font-weight: bold;
}
.checkout-btn {
background-color: #F56C6C;
border: none;
border-radius: 16px;
padding: 0 24px;
height: 36px;
line-height: 36px;
color: #fff;
font-size: 14px;
}
.detail-container {
height: 92vh;
}
</style>