consumer模块完成90%,前端完成supabase对接

This commit is contained in:
2026-02-03 17:11:50 +08:00
parent b6200cda28
commit 8a535e3f38
69 changed files with 5020 additions and 33273 deletions

View File

@@ -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;
}
}