consumer模块完成90%,前端完成supabase对接
This commit is contained in:
@@ -168,7 +168,7 @@
|
||||
<script setup lang="uts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { supabaseService, type CartItem as SupabaseCartItem } from '@/utils/supabaseService.uts'
|
||||
import { supabaseService, type CartItem as SupabaseCartItem, type Product } from '@/utils/supabaseService.uts'
|
||||
|
||||
// 响应式数据
|
||||
const cartItems = ref<any[]>([])
|
||||
@@ -176,78 +176,22 @@ const recommendProducts = ref<any[]>([])
|
||||
const loading = ref<boolean>(false)
|
||||
const statusBarHeight = ref(0)
|
||||
const isManageMode = ref(false)
|
||||
|
||||
const mockRecommendProducts = [
|
||||
{
|
||||
id: 'rec_001',
|
||||
shopId: 'shop_rec_1',
|
||||
shopName: '潮流运动旗舰店',
|
||||
name: '运动保温杯',
|
||||
price: 59,
|
||||
image: 'https://picsum.photos/100/100?random=11',
|
||||
specification: '颜色:星空黑 | 容量:500ml | 材质:304不锈钢',
|
||||
specDetails: {
|
||||
color: '星空黑',
|
||||
capacity: '500ml',
|
||||
material: '304不锈钢'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'rec_002',
|
||||
shopId: 'shop_rec_2',
|
||||
shopName: '智能家居生活馆',
|
||||
name: '声波电动牙刷',
|
||||
price: 129,
|
||||
image: 'https://picsum.photos/100/100?random=12',
|
||||
specification: '颜色:珍珠白 | 刷头:敏感型×2 | 续航:30天',
|
||||
specDetails: {
|
||||
color: '珍珠白',
|
||||
brushHead: '敏感型×2',
|
||||
batteryLife: '30天'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'rec_003',
|
||||
shopId: 'shop_rec_3',
|
||||
shopName: '健康防护专家店',
|
||||
name: '医用护理口罩',
|
||||
price: 29.9,
|
||||
image: 'https://picsum.photos/100/100?random=13',
|
||||
specification: '规格:三层防护 | 数量:50只独立装 | 执行标准:YY0469',
|
||||
specDetails: {
|
||||
layers: '三层防护',
|
||||
quantity: '50只',
|
||||
standard: 'YY0469'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'rec_004',
|
||||
shopId: 'shop_rec_4',
|
||||
shopName: '户外运动装备店',
|
||||
name: '专业护膝',
|
||||
price: 45,
|
||||
image: 'https://picsum.photos/100/100?random=14',
|
||||
specification: '尺码:L码 | 材质:记忆棉+弹力布 | 适用:篮球/跑步',
|
||||
specDetails: {
|
||||
size: 'L码',
|
||||
material: '记忆棉+弹力布',
|
||||
suitableFor: '篮球/跑步'
|
||||
}
|
||||
}
|
||||
]
|
||||
const updatingItems = ref<Set<string>>(new Set()) // Track items being updated to prevent race conditions
|
||||
|
||||
// 计算属性
|
||||
const cartGroups = computed(() => {
|
||||
const groups = new Map<string, any>()
|
||||
cartItems.value.forEach(item => {
|
||||
if (!groups.has(item.shopId)) {
|
||||
groups.set(item.shopId, {
|
||||
// Build a unique key for the shop
|
||||
const shopKey = item.shopId || 'unknown'
|
||||
if (!groups.has(shopKey)) {
|
||||
groups.set(shopKey, {
|
||||
shopId: item.shopId,
|
||||
shopName: item.shopName,
|
||||
shopName: item.shopName || '商城优选', // Better default name
|
||||
items: [] as any[]
|
||||
})
|
||||
}
|
||||
const group = groups.get(item.shopId)
|
||||
const group = groups.get(shopKey)
|
||||
if (group) {
|
||||
group.items.push(item)
|
||||
}
|
||||
@@ -304,35 +248,59 @@ const loadCartData = async () => {
|
||||
const supabaseCartItems = await supabaseService.getCartItems()
|
||||
|
||||
// 转换数据格式以匹配前端界面
|
||||
const transformedItems = supabaseCartItems.map((item: SupabaseCartItem) => ({
|
||||
id: item.id,
|
||||
shopId: item.shop_id || 'unknown_shop',
|
||||
shopName: item.shop_name || '未知店铺',
|
||||
name: item.product_name || '商品',
|
||||
price: item.product_price || 0,
|
||||
image: item.product_image || '/static/product1.jpg',
|
||||
spec: item.product_specification || '默认规格',
|
||||
quantity: item.quantity || 1,
|
||||
selected: item.selected || false,
|
||||
productId: item.product_id // 保留productId用于后续操作
|
||||
}))
|
||||
const transformedItems = supabaseCartItems.map((item: SupabaseCartItem) => {
|
||||
// 调试日志:打印每条商品数据的关键字段
|
||||
console.log(`CartItem raw: id=${item.id}, shop_id=${item.shop_id}, shop_name=${item.shop_name}, name=${item.product_name}, price=${item.product_price}`);
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
// 关键修复:确保shopId有值,如果后端返回null/undefined,使用'default_shop'作为分组键
|
||||
shopId: (item.shop_id != null && item.shop_id !== '') ? item.shop_id : 'default_shop',
|
||||
// 关键修复:确保shopName有值
|
||||
shopName: (item.shop_name != null && item.shop_name !== '') ? item.shop_name : '商城优选',
|
||||
name: item.product_name || '未知商品',
|
||||
price: item.product_price != null ? item.product_price : 0,
|
||||
image: item.product_image || '/static/images/default-product.png',
|
||||
spec: item.product_specification || '标准规格',
|
||||
quantity: item.quantity || 1,
|
||||
selected: item.selected || false,
|
||||
productId: item.product_id,
|
||||
skuId: item.sku_id,
|
||||
merchantId: item.merchant_id
|
||||
}
|
||||
})
|
||||
|
||||
console.log('Transformed items count:', transformedItems.length);
|
||||
cartItems.value = transformedItems
|
||||
|
||||
// 加载推荐商品(暂时保持Mock数据)
|
||||
recommendProducts.value = [...mockRecommendProducts]
|
||||
// 加载推荐商品(优先获取推荐位商品,如果没有则通过搜索获取热销商品)
|
||||
let recommends = await supabaseService.getRecommendedProducts(6)
|
||||
|
||||
// 如果没有设置推荐商品,则获取热销商品作为补充
|
||||
if (recommends.length === 0) {
|
||||
const hotResp = await supabaseService.searchProducts('', 1, 6, 'sales')
|
||||
recommends = hotResp.data
|
||||
}
|
||||
|
||||
if (recommends.length > 0) {
|
||||
recommendProducts.value = recommends.map((p: Product) => {
|
||||
return {
|
||||
id: p.id,
|
||||
shopId: p.merchant_id || 'unknown',
|
||||
shopName: p.shop_name || '商城推荐',
|
||||
name: p.name,
|
||||
price: p.base_price,
|
||||
image: p.main_image_url || '/static/images/default-product.png',
|
||||
specification: '', // 推荐列表不显示详细规格
|
||||
specDetails: {}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
recommendProducts.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载购物车数据失败:', error)
|
||||
// 如果API调用失败,尝试从本地存储加载
|
||||
const cartData = uni.getStorageSync('cart')
|
||||
if (cartData) {
|
||||
try {
|
||||
cartItems.value = JSON.parse(cartData as string) as any[]
|
||||
} catch (e) {
|
||||
console.error('解析购物车数据失败', e)
|
||||
cartItems.value = []
|
||||
}
|
||||
}
|
||||
cartItems.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -340,6 +308,7 @@ const loadCartData = async () => {
|
||||
|
||||
// 商品操作 - 更新选中状态到Supabase
|
||||
const toggleSelect = async (itemId: string) => {
|
||||
// 乐观更新
|
||||
const index = cartItems.value.findIndex(item => item.id === itemId)
|
||||
if (index !== -1) {
|
||||
const newSelected = !cartItems.value[index].selected
|
||||
@@ -353,15 +322,17 @@ const toggleSelect = async (itemId: string) => {
|
||||
// 恢复状态
|
||||
cartItems.value[index].selected = !newSelected
|
||||
cartItems.value = [...cartItems.value]
|
||||
uni.showToast({ title: '网络异常,请重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toggleShopSelect = async (shopId: string) => {
|
||||
// 查找该组是否已存在,并判断目标状态
|
||||
const group = cartGroups.value.find((g: any) => g.shopId === shopId)
|
||||
if (!group) return
|
||||
|
||||
// 检查当前是否全选
|
||||
// 检查当前是否全选: 如果所有都选中,则目标是全不选(false);否则全选(true)
|
||||
const isAllShopSelected = (group.items as any[]).every((item: any) => item.selected)
|
||||
const newState = !isAllShopSelected
|
||||
|
||||
@@ -370,19 +341,28 @@ const toggleShopSelect = async (shopId: string) => {
|
||||
.filter(item => item.shopId === shopId)
|
||||
.map(item => item.id)
|
||||
|
||||
// 乐观更新本地状态
|
||||
const oldStates = new Map<string, boolean>()
|
||||
cartItems.value.forEach(item => {
|
||||
if (item.shopId === shopId) {
|
||||
oldStates.set(item.id, item.selected)
|
||||
item.selected = newState
|
||||
}
|
||||
})
|
||||
cartItems.value = [...cartItems.value]
|
||||
|
||||
// 批量更新到Supabase
|
||||
const success = await supabaseService.batchUpdateCartItemSelection(shopItemIds, newState)
|
||||
|
||||
if (success) {
|
||||
// 更新本地状态
|
||||
cartItems.value.forEach(item => {
|
||||
if (item.shopId === shopId) {
|
||||
item.selected = newState
|
||||
}
|
||||
})
|
||||
cartItems.value = [...cartItems.value]
|
||||
} else {
|
||||
if (!success) {
|
||||
console.error('批量更新店铺商品选中状态失败')
|
||||
// 回滚
|
||||
cartItems.value.forEach(item => {
|
||||
if (item.shopId === shopId && oldStates.has(item.id)) {
|
||||
item.selected = oldStates.get(item.id)!
|
||||
}
|
||||
})
|
||||
cartItems.value = [...cartItems.value]
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
@@ -391,20 +371,26 @@ const toggleShopSelect = async (shopId: string) => {
|
||||
}
|
||||
|
||||
const toggleSelectAll = async () => {
|
||||
// 目标状态:如果当前全选,则取消全选;否则全选
|
||||
const newSelectedState = !allSelected.value
|
||||
|
||||
// 乐观更新
|
||||
const oldItems = JSON.parse(JSON.stringify(cartItems.value))
|
||||
const selectedItems = cartItems.value.map(item => ({
|
||||
...item,
|
||||
selected: newSelectedState
|
||||
}))
|
||||
cartItems.value = selectedItems
|
||||
|
||||
// 更新到Supabase
|
||||
const itemIds = cartItems.value.map(item => item.id)
|
||||
if (itemIds.length === 0) return
|
||||
|
||||
const success = await supabaseService.batchUpdateCartItemSelection(itemIds, newSelectedState)
|
||||
|
||||
if (success) {
|
||||
cartItems.value = selectedItems
|
||||
} else {
|
||||
if (!success) {
|
||||
console.error('批量更新选中状态失败')
|
||||
cartItems.value = oldItems
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
@@ -413,38 +399,50 @@ const toggleSelectAll = async () => {
|
||||
}
|
||||
|
||||
const increaseQuantity = async (itemId: string) => {
|
||||
if (updatingItems.value.has(itemId)) return
|
||||
|
||||
const index = cartItems.value.findIndex(item => item.id === itemId)
|
||||
if (index !== -1) {
|
||||
updatingItems.value.add(itemId)
|
||||
const newQuantity = cartItems.value[index].quantity + 1
|
||||
cartItems.value[index].quantity = newQuantity
|
||||
cartItems.value = [...cartItems.value]
|
||||
|
||||
// 更新到Supabase
|
||||
const success = await supabaseService.updateCartItemQuantity(itemId, newQuantity)
|
||||
updatingItems.value.delete(itemId)
|
||||
|
||||
if (!success) {
|
||||
console.error('更新商品数量失败')
|
||||
// 恢复状态
|
||||
cartItems.value[index].quantity = newQuantity - 1
|
||||
cartItems.value = [...cartItems.value]
|
||||
uni.showToast({ title: '更新失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const decreaseQuantity = async (itemId: string) => {
|
||||
if (updatingItems.value.has(itemId)) return
|
||||
|
||||
const index = cartItems.value.findIndex(item => item.id === itemId)
|
||||
if (index !== -1) {
|
||||
if (cartItems.value[index].quantity > 1) {
|
||||
updatingItems.value.add(itemId)
|
||||
const newQuantity = cartItems.value[index].quantity - 1
|
||||
cartItems.value[index].quantity = newQuantity
|
||||
cartItems.value = [...cartItems.value]
|
||||
|
||||
// 更新到Supabase
|
||||
const success = await supabaseService.updateCartItemQuantity(itemId, newQuantity)
|
||||
updatingItems.value.delete(itemId)
|
||||
|
||||
if (!success) {
|
||||
console.error('更新商品数量失败')
|
||||
// 恢复状态
|
||||
cartItems.value[index].quantity = newQuantity + 1
|
||||
cartItems.value = [...cartItems.value]
|
||||
uni.showToast({ title: '更新失败', icon: 'none' })
|
||||
}
|
||||
} else {
|
||||
// 数量为1时,询问是否删除
|
||||
@@ -583,29 +581,39 @@ const goToCheckout = () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取选中的商品 (直接过滤cartItems,不依赖cartGroups)
|
||||
// 获取选中的商品 (直接过滤cartItems,不依赖cartGroups,确保扁平化传递)
|
||||
const selectedItems = cartItems.value
|
||||
.filter(item => item.selected)
|
||||
.map(item => ({
|
||||
id: item.id,
|
||||
product_id: item.id, // 使用商品ID作为product_id
|
||||
sku_id: item.id, // 使用商品ID作为sku_id
|
||||
product_id: item.productId || item.id,
|
||||
sku_id: item.skuId || item.id,
|
||||
product_name: item.name,
|
||||
shop_id: item.shopId, // 关键:保留shopId用于分组
|
||||
shop_name: item.shopName, // 关键:保留shopName
|
||||
merchant_id: item.merchantId,
|
||||
product_image: item.image,
|
||||
sku_specifications: item.spec,
|
||||
price: Number(item.price), // 确保是数字
|
||||
quantity: Number(item.quantity) // 确保是数字
|
||||
}))
|
||||
|
||||
|
||||
// 关键修复:将结算数据写入 Storage,确保 checkout 页面能稳定获取
|
||||
uni.setStorageSync('checkout_type', 'cart')
|
||||
uni.setStorageSync('checkout_items', JSON.stringify(selectedItems))
|
||||
// 使用纯JSON序列化防止复杂对象引发的问题
|
||||
try {
|
||||
uni.setStorageSync('checkout_items', JSON.stringify(selectedItems))
|
||||
} catch (e) {
|
||||
console.error('存储结算数据失败', e)
|
||||
uni.showToast({ title: '系统异常,请重试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 跳转到结算页面并传递数据
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/checkout',
|
||||
success: (res) => {
|
||||
// 通过eventChannel传递数据
|
||||
// 通过eventChannel传递数据 (作为备份)
|
||||
res.eventChannel.emit('acceptData', {
|
||||
selectedItems: selectedItems
|
||||
})
|
||||
@@ -1186,50 +1194,59 @@ const goToCheckout = () => {
|
||||
background-color: white;
|
||||
margin: 10px;
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
padding: 10px 15px; /* 减小内边距 */
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.action-bar-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: row; /* 强制横向 */
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-left, .action-right {
|
||||
.action-left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-right {
|
||||
display: flex;
|
||||
flex-direction: row; /* 强制横向 */
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
min-width: 0; /* 防止溢出 */
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 合计信息区域 - 自适应横向排列 */
|
||||
/* 合计信息区域 */
|
||||
.total-info {
|
||||
display: flex;
|
||||
flex-direction: row; /* 强制横向 */
|
||||
align-items: center;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 1; /* 允许压缩 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.total-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-right: 5px;
|
||||
margin-right: 2px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
color: #ff5000;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 结算按钮 */
|
||||
@@ -1238,20 +1255,22 @@ const goToCheckout = () => {
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
padding: 8px 20px;
|
||||
padding: 6px 16px; /* 减小按钮内边距 */
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
margin: 0; /* 移除可能的margin */
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background-color: #ff3b30; /* 红色删除按钮 */
|
||||
padding: 8px 25px;
|
||||
background-color: #ff3b30;
|
||||
padding: 6px 20px;
|
||||
}
|
||||
|
||||
/* 全选区域 */
|
||||
.select-all {
|
||||
display: flex;
|
||||
flex-direction: row; /* 强制横向 */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -1265,25 +1284,27 @@ const goToCheckout = () => {
|
||||
/* 响应式调整 */
|
||||
/* 手机端小屏幕优化 */
|
||||
@media screen and (max-width: 375px) {
|
||||
.action-bar-content {
|
||||
gap: 8px;
|
||||
.cart-action-bar {
|
||||
padding: 10px;
|
||||
margin: 10px 5px; /* 减小外边距增加可用宽度 */
|
||||
}
|
||||
|
||||
.total-text {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.checkout-btn, .delete-btn {
|
||||
padding: 8px 15px;
|
||||
font-size: 13px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.select-all-text {
|
||||
font-size: 13px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user