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

File diff suppressed because one or more lines are too long

View File

@@ -21,7 +21,7 @@
<text class="action-icon">📝</text>
</view>
<view class="action-item" @click.stop="deleteAddress(item.id)">
<text class="action-icon"><EFBFBD></text>
<text class="action-icon">🗑</text>
</view>
</view>
</view>

View File

@@ -57,7 +57,7 @@
<script setup lang="uts">
import { ref, onMounted } from 'vue'
// import supa from '@/components/supadb/aksupainstance.uts'
import { supabaseService } from '@/utils/supabaseService.uts'
const orderId = ref('')
const orderItemId = ref('') // Optional, if refunding specific item
@@ -92,19 +92,22 @@ onMounted(() => {
const loadOrderInfo = async () => {
try {
const { data, error } = await supa
.from('orders')
.select('actual_amount, delivery_fee')
.eq('id', orderId.value)
.single()
const orderData = await supabaseService.getOrderDetail(orderId.value)
if (error == null && data != null) {
maxAmount.value = data['actual_amount'] as number
deliveryFee.value = data['delivery_fee'] as number
if (orderData != null) {
const total = Number(orderData['total_amount'] ?? 0)
const shipping = Number(orderData['shipping_fee'] ?? 0)
maxAmount.value = total
deliveryFee.value = shipping
refundAmount.value = maxAmount.value.toString()
}
} catch (err) {
console.error('加载订单信息失败', err)
uni.showToast({
title: '加载订单失败',
icon: 'none'
})
}
}
@@ -132,53 +135,29 @@ const submitRefund = async () => {
submitting.value = true
try {
const userStore = uni.getStorageSync('userInfo')
const userId = userStore?.id
// 1. Create Refund Record
/* const { data, error } = await supa
.from('refunds')
.insert({
user_id: userId,
order_id: orderId.value,
refund_no: 'REF' + Date.now(),
refund_type: refundType.value,
refund_reason: refundReason.value,
refund_amount: amount,
description: description.value,
status: 1, // 待处理
status_history: [{
status: 1,
remark: '用户提交申请',
created_at: new Date().toISOString()
}]
})
if (error != null) throw error */
// MOCK SUBMIT
await new Promise(resolve => setTimeout(resolve, 1000))
// 2. Update Order Status (Optional, e.g. to "After-sales")
// Assuming status 6 is "After-sales/Refund"
/*
await supa
.from('orders')
.update({ status: 6 })
.eq('id', orderId.value)
*/
uni.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => {
uni.redirectTo({
url: '/pages/mall/consumer/refund'
})
}, 1500)
const result = await supabaseService.createRefund({
order_id: orderId.value,
refund_type: refundType.value,
refund_reason: refundReason.value,
refund_amount: amount,
description: description.value
})
if (result.success) {
uni.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => {
// Go back to orders listing focused on refund type?
// or stay here? User probably wants to see list.
// Since profile redirects "Refunds" to orders list, let's go there.
uni.navigateBack()
}, 1500)
} else {
uni.showToast({ title: result.message, icon: 'none' })
}
} catch (err) {
console.error('提交退款失败', err)
uni.showToast({ title: '提交失败,请重试', icon: 'none' })
uni.showToast({ title: '提交异常', icon: 'none' })
} finally {
submitting.value = false
}

View File

@@ -1,810 +0,0 @@
<!-- 购物车页面 -->
<template>
<view class="cart-page">
<!-- 顶部栏 -->
<view class="cart-header">
<view class="header-title">
<text class="title-text">购物车</text>
</view>
<view v-if="selectedCount > 0" class="edit-btn" @click="toggleEditMode">
<text class="edit-text">{{ isEditMode ? '完成' : '编辑' }}</text>
</view>
</view>
<!-- 购物车为空 -->
<view v-if="cartItems.length === 0" class="empty-cart">
<text class="empty-icon">🛒</text>
<text class="empty-text">购物车还是空的</text>
<text class="empty-subtext">快去挑选心仪的商品吧</text>
<button class="go-shopping-btn" @click="goShopping">去逛逛</button>
</view>
<!-- 购物车列表 -->
<scroll-view v-else direction="vertical" class="cart-content">
<!-- 商品列表 -->
<view v-for="(item, index) in cartItems" :key="item.id" class="cart-item">
<view class="item-selector" @click="toggleSelectItem(item)">
<view :class="['select-icon', { selected: item.selected }]">
<text v-if="item.selected" class="icon-text">✓</text>
</view>
</view>
<image class="item-image" :src="item.product_image || '/static/default-product.png'" />
<view class="item-info">
<text class="item-name">{{ item.product_name }}</text>
<text v-if="item.sku_specifications" class="item-spec">{{ getSpecText(item.sku_specifications) }}</text>
<view class="item-price-row">
<text class="item-price">¥{{ item.price }}</text>
<view class="quantity-control">
<view class="quantity-btn minus" @click="decreaseQuantity(item)">-</view>
<text class="quantity-text">{{ item.quantity }}</text>
<view class="quantity-btn plus" @click="increaseQuantity(item)">+</view>
</view>
</view>
</view>
<view v-if="isEditMode" class="delete-btn" @click="removeItem(item, index)">
<text class="delete-text">删除</text>
</view>
</view>
<!-- 推荐商品 -->
<view v-if="recommendProducts.length > 0" class="recommend-section">
<view class="section-header">
<text class="section-title">猜你喜欢</text>
</view>
<view class="recommend-grid">
<view v-for="product in recommendProducts" :key="product.id" class="recommend-item" @click="viewProduct(product)">
<image class="recommend-image" :src="getProductFirstImage(product)" />
<text class="recommend-name">{{ product.name }}</text>
<text class="recommend-price">¥{{ product.price }}</text>
</view>
</view>
</view>
</scroll-view>
<!-- 底部结算栏 -->
<view v-if="cartItems.length > 0" class="bottom-bar">
<view class="select-all" @click="toggleSelectAll">
<view :class="['all-select-icon', { selected: isAllSelected }]">
<text v-if="isAllSelected" class="icon-text">✓</text>
</view>
<text class="select-all-text">全选</text>
</view>
<view v-if="!isEditMode" class="settlement-info">
<view class="total-price">
<text class="total-label">合计:</text>
<text class="total-value">¥{{ totalPrice.toFixed(2) }}</text>
</view>
<text class="total-desc">已选{{ selectedCount }}件</text>
</view>
<view v-if="isEditMode" class="edit-actions">
<view class="delete-all-btn" @click="removeSelected">
<text class="delete-all-text">删除({{ selectedCount }})</text>
</view>
</view>
<view v-else class="settle-btn" :class="{ disabled: selectedCount === 0 }" @click="goToCheckout">
<text class="settle-text">去结算</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, computed, onMounted } from 'vue'
import type { ProductType } from '@/types/mall-types.uts'
import supa from '@/components/supadb/aksupainstance.uts'
type CartItemType = {
id: string
user_id: string
product_id: string
sku_id: string
product_name: string
product_image: string
sku_specifications: any
price: number
quantity: number
selected: boolean
}
const cartItems = ref<Array<CartItemType>>([])
const recommendProducts = ref<Array<ProductType>>([])
const isEditMode = ref<boolean>(false)
const isLoading = ref<boolean>(false)
// 计算属性
const selectedCount = computed(() => {
return cartItems.value.filter(item => item.selected).length
})
const totalPrice = computed(() => {
return cartItems.value
.filter(item => item.selected)
.reduce((total, item) => total + (item.price * item.quantity), 0)
})
const isAllSelected = computed(() => {
return cartItems.value.length > 0 && cartItems.value.every(item => item.selected)
})
// 生命周期
onMounted(() => {
loadCartItems()
loadRecommendProducts()
})
// 加载购物车商品
const loadCartItems = async () => {
const userId = getCurrentUserId()
if (!userId) {
uni.showToast({
title: '请先登录',
icon: 'none'
})
uni.navigateTo({
url: '/pages/user/login'
})
return
}
try {
const { data, error } = await supa
.from('shopping_cart')
.select(`
id,
user_id,
product_id,
sku_id,
quantity,
products (
name,
price,
images
),
product_skus (
specifications,
price as sku_price
)
`)
.eq('user_id', userId)
if (error !== null) {
console.error('加载购物车失败:', error)
return
}
const items: CartItemType[] = []
const cartData = data ?? []
for (let i = 0; i < cartData.length; i++) {
const item = cartData[i]
const product = item.products as any
const sku = item.product_skus as any
items.push({
id: item.id,
user_id: item.user_id,
product_id: item.product_id,
sku_id: item.sku_id,
product_name: product?.name || '未知商品',
product_image: product?.images?.[0] || '/static/default-product.png',
sku_specifications: sku?.specifications,
price: sku?.sku_price || product?.price || 0,
quantity: item.quantity,
selected: false
})
}
cartItems.value = items
} catch (err) {
console.error('加载购物车异常:', err)
}
}
// 加载推荐商品
const loadRecommendProducts = async () => {
try {
const { data, error } = await supa
.from('products')
.select('*')
.eq('status', 1)
.order('sales', { ascending: false })
.limit(6)
if (error !== null) {
console.error('加载推荐商品失败:', error)
return
}
recommendProducts.value = data ?? []
} catch (err) {
console.error('加载推荐商品异常:', err)
}
}
// 获取当前用户ID
const getCurrentUserId = (): string | null => {
// 这里应该从全局状态或storage中获取
const userStore = uni.getStorageSync('userInfo')
return userStore?.id || null
}
// 获取商品第一张图片
const getProductFirstImage = (product: ProductType): string => {
return product.images?.[0] || '/static/default-product.png'
}
// 获取规格文本
const getSpecText = (specs: any): string => {
if (!specs) return ''
if (typeof specs === 'object') {
return Object.keys(specs)
.map(key => `${key}: ${specs[key]}`)
.join('; ')
}
return String(specs)
}
// 切换编辑模式
const toggleEditMode = () => {
isEditMode.value = !isEditMode.value
}
// 切换商品选择
const toggleSelectItem = (item: CartItemType) => {
item.selected = !item.selected
cartItems.value = [...cartItems.value]
}
// 全选/取消全选
const toggleSelectAll = () => {
const newSelectedState = !isAllSelected.value
cartItems.value.forEach(item => {
item.selected = newSelectedState
})
cartItems.value = [...cartItems.value]
}
// 增加数量
const increaseQuantity = async (item: CartItemType) => {
if (isLoading.value) return
isLoading.value = true
try {
const newQuantity = item.quantity + 1
const { error } = await supa
.from('shopping_cart')
.update({ quantity: newQuantity })
.eq('id', item.id)
if (error !== null) {
console.error('更新数量失败:', error)
uni.showToast({
title: '更新失败',
icon: 'none'
})
return
}
item.quantity = newQuantity
cartItems.value = [...cartItems.value]
} catch (err) {
console.error('更新数量异常:', err)
} finally {
isLoading.value = false
}
}
// 减少数量
const decreaseQuantity = async (item: CartItemType) => {
if (item.quantity <= 1) {
removeItem(item, cartItems.value.indexOf(item))
return
}
if (isLoading.value) return
isLoading.value = true
try {
const newQuantity = item.quantity - 1
const { error } = await supa
.from('shopping_cart')
.update({ quantity: newQuantity })
.eq('id', item.id)
if (error !== null) {
console.error('更新数量失败:', error)
uni.showToast({
title: '更新失败',
icon: 'none'
})
return
}
item.quantity = newQuantity
cartItems.value = [...cartItems.value]
} catch (err) {
console.error('更新数量异常:', err)
} finally {
isLoading.value = false
}
}
// 移除单个商品
const removeItem = async (item: CartItemType, index: number) => {
uni.showModal({
title: '确认删除',
content: '确定要删除这个商品吗?',
success: async (res) => {
if (res.confirm) {
try {
const { error } = await supa
.from('shopping_cart')
.delete()
.eq('id', item.id)
if (error !== null) {
console.error('删除商品失败:', error)
uni.showToast({
title: '删除失败',
icon: 'none'
})
return
}
cartItems.value.splice(index, 1)
cartItems.value = [...cartItems.value]
uni.showToast({
title: '删除成功',
icon: 'success'
})
} catch (err) {
console.error('删除商品异常:', err)
}
}
}
})
}
// 移除选中商品
const removeSelected = () => {
const selectedItems = cartItems.value.filter(item => item.selected)
if (selectedItems.length === 0) {
uni.showToast({
title: '请选择要删除的商品',
icon: 'none'
})
return
}
uni.showModal({
title: '批量删除',
content: `确定要删除选中的${selectedItems.length}件商品吗?`,
success: async (res) => {
if (res.confirm) {
const deletePromises = selectedItems.map(item =>
supa
.from('shopping_cart')
.delete()
.eq('id', item.id)
)
try {
await Promise.all(deletePromises)
cartItems.value = cartItems.value.filter(item => !item.selected)
uni.showToast({
title: '删除成功',
icon: 'success'
})
} catch (err) {
console.error('批量删除异常:', err)
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
}
}
})
}
// 查看商品详情
const viewProduct = (product: ProductType) => {
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?id=${product.id}`
})
}
// 去逛逛
const goShopping = () => {
uni.switchTab({
url: '/pages/mall/consumer/index'
})
}
// 去结算
const goToCheckout = () => {
if (selectedCount.value === 0) {
uni.showToast({
title: '请选择要结算的商品',
icon: 'none'
})
return
}
const selectedItems = cartItems.value.filter(item => item.selected)
const productIds = selectedItems.map(item => ({
product_id: item.product_id,
sku_id: item.sku_id,
quantity: item.quantity
}))
uni.navigateTo({
url: '/pages/mall/consumer/checkout',
success: (res) => {
res.eventChannel.emit('acceptData', {
selectedItems: productIds,
totalAmount: totalPrice.value
})
}
})
}
</script>
<style scoped>
.cart-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.cart-header {
background-color: #ffffff;
padding: 15px;
border-bottom: 1px solid #e5e5e5;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
flex: 1;
}
.title-text {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.edit-btn {
padding: 5px 10px;
}
.edit-text {
color: #007aff;
font-size: 14px;
}
.empty-cart {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 50px 20px;
}
.empty-icon {
font-size: 80px;
margin-bottom: 20px;
}
.empty-text {
font-size: 16px;
color: #666666;
margin-bottom: 10px;
}
.empty-subtext {
font-size: 14px;
color: #999999;
margin-bottom: 30px;
}
.go-shopping-btn {
background-color: #007aff;
color: #ffffff;
padding: 10px 40px;
border-radius: 25px;
font-size: 14px;
border: none;
}
.cart-content {
flex: 1;
}
.cart-item {
background-color: #ffffff;
margin-bottom: 10px;
padding: 15px;
display: flex;
align-items: center;
position: relative;
}
.item-selector {
margin-right: 10px;
}
.select-icon {
width: 20px;
height: 20px;
border: 1px solid #cccccc;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.select-icon.selected {
background-color: #007aff;
border-color: #007aff;
}
.icon-text {
color: #ffffff;
font-size: 12px;
}
.item-image {
width: 80px;
height: 80px;
border-radius: 5px;
margin-right: 10px;
}
.item-info {
flex: 1;
min-height: 80px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.item-name {
font-size: 14px;
color: #333333;
line-height: 1.4;
margin-bottom: 5px;
}
.item-spec {
font-size: 12px;
color: #999999;
margin-bottom: 10px;
}
.item-price-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.item-price {
font-size: 16px;
color: #ff4757;
font-weight: bold;
}
.quantity-control {
display: flex;
align-items: center;
border: 1px solid #e5e5e5;
border-radius: 15px;
overflow: hidden;
}
.quantity-btn {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #666666;
background-color: #f8f8f8;
}
.quantity-btn.minus {
border-right: 1px solid #e5e5e5;
}
.quantity-btn.plus {
border-left: 1px solid #e5e5e5;
}
.quantity-text {
width: 40px;
text-align: center;
font-size: 14px;
color: #333333;
}
.delete-btn {
position: absolute;
right: 15px;
bottom: 15px;
padding: 5px 10px;
background-color: #ff4757;
border-radius: 12px;
}
.delete-text {
color: #ffffff;
font-size: 12px;
}
.recommend-section {
background-color: #ffffff;
margin-top: 10px;
padding: 15px;
}
.section-header {
margin-bottom: 15px;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #333333;
}
.recommend-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.recommend-item {
width: 48%;
margin-bottom: 15px;
}
.recommend-image {
width: 100%;
height: 120px;
border-radius: 5px;
margin-bottom: 8px;
}
.recommend-name {
font-size: 13px;
color: #333333;
line-height: 1.4;
margin-bottom: 5px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.recommend-price {
font-size: 14px;
color: #ff4757;
font-weight: bold;
}
.bottom-bar {
background-color: #ffffff;
border-top: 1px solid #e5e5e5;
padding: 15px;
display: flex;
align-items: center;
justify-content: space-between;
}
.select-all {
display: flex;
align-items: center;
}
.all-select-icon {
width: 20px;
height: 20px;
border: 1px solid #cccccc;
border-radius: 10px;
margin-right: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.all-select-icon.selected {
background-color: #007aff;
border-color: #007aff;
}
.select-all-text {
font-size: 14px;
color: #333333;
}
.settlement-info {
flex: 1;
margin-left: 20px;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.total-price {
display: flex;
align-items: baseline;
margin-bottom: 5px;
}
.total-label {
font-size: 14px;
color: #333333;
}
.total-value {
font-size: 18px;
color: #ff4757;
font-weight: bold;
}
.total-desc {
font-size: 12px;
color: #999999;
}
.edit-actions {
flex: 1;
margin-left: 20px;
display: flex;
justify-content: flex-end;
}
.delete-all-btn {
background-color: #ff4757;
padding: 8px 20px;
border-radius: 15px;
}
.delete-all-text {
color: #ffffff;
font-size: 14px;
}
.settle-btn {
background-color: #007aff;
padding: 10px 30px;
border-radius: 20px;
margin-left: 20px;
}
.settle-btn.disabled {
background-color: #cccccc;
opacity: 0.6;
}
.settle-text {
color: #ffffff;
font-size: 14px;
font-weight: bold;
}
</style>

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

View File

@@ -48,7 +48,12 @@
</scroll-view>
<!-- 右侧商品列表 -->
<scroll-view scroll-y class="product-content">
<scroll-view
scroll-y
class="product-content"
@scrolltolower="loadMore"
:lower-threshold="50"
>
<!-- 分类标题 -->
<view class="category-header">
<text class="category-title">{{ currentCategoryName }}</text>
@@ -63,30 +68,29 @@
class="product-card"
@click="navigateToProduct(product)"
>
<view class="product-badge" v-if="product.badge">{{ product.badge }}</view>
<view class="product-badge" v-if="product.is_hot">热销</view>
<image
class="product-image"
:src="product.image"
:src="product.main_image_url"
mode="aspectFill"
/>
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<text class="product-spec">{{ product.specification }}</text>
<view class="price-section">
<view class="current-price">
<text class="price-symbol">¥</text>
<text class="price-value">{{ product.price }}</text>
<text class="price-value">{{ product.base_price }}</text>
</view>
<text class="original-price" v-if="product.originalPrice > product.price">
¥{{ product.originalPrice }}
<text class="original-price" v-if="product.market_price != null && product.market_price! > product.base_price">
¥{{ product.market_price }}
</text>
</view>
<view class="product-meta">
<text class="manufacturer">{{ product.manufacturer }}</text>
<text class="manufacturer">{{ product.brand_name || product.shop_name || '自营' }}</text>
<view class="sales-info">
<text class="sales-count">已售{{ product.sales }}</text>
<text class="sales-count">已售{{ product.sale_count }}</text>
</view>
</view>
</view>
@@ -123,6 +127,8 @@ const activePrimary = ref<string>('')
const cartCount = ref(3)
const hasMore = ref(true)
const hasLoadedFromParams = ref(false) // 标记是否已通过参数加载
const currentPage = ref(1)
const loading = ref(false)
// 获取当前分类信息
const currentCategoryName = ref('')
@@ -166,17 +172,29 @@ const loadCategories = async () => {
// 加载商品数据
const loadProducts = async () => {
if (loading.value) return
if (!activePrimary.value) {
console.warn('activePrimary为空无法加载商品')
return
}
loading.value = true
try {
if (activePrimary.value) {
console.log('开始加载商品分类ID:', activePrimary.value)
const response = await supabaseService.getProductsByCategory(activePrimary.value)
console.log('开始加载商品分类ID:', activePrimary.value, '页码:', currentPage.value)
const response = await supabaseService.getProductsByCategory(activePrimary.value, currentPage.value)
console.log('商品加载结果:', {
dataCount: response.data.length,
total: response.total,
hasmore: response.hasmore
hasmore: response.hasmore,
page: currentPage.value
})
productList.value = response.data
if (currentPage.value === 1) {
productList.value = response.data
} else {
productList.value.push(...response.data)
}
hasMore.value = response.hasmore
// 更新当前分类信息
@@ -184,21 +202,27 @@ const loadProducts = async () => {
if (category) {
currentCategoryName.value = category.name
currentCategoryDesc.value = category.description || ''
console.log('当前分类信息:', category.name, '描述:', category.description)
} else {
console.warn('未找到对应的分类信息分类ID:', activePrimary.value)
}
console.log('商品列表加载完成,数量:', productList.value.length)
} else {
console.warn('activePrimary为空无法加载商品')
}
console.log('商品列表加载完成,当前总数量:', productList.value.length)
} catch (error) {
console.error('加载商品数据失败:', error)
productList.value = []
if (currentPage.value === 1) {
productList.value = []
}
} finally {
loading.value = false
}
}
// 加载更多
const loadMore = () => {
if (hasMore.value && !loading.value) {
currentPage.value++
loadProducts()
}
}
// 页面加载时处理参数 - 这是处理分类切换的主要入口
onLoad((options: any) => {
console.log('=== category页面onLoad被调用 ===')
@@ -382,15 +406,15 @@ const selectPrimaryCategory = async (categoryId: string) => {
console.log('准备加载商品数据...')
// 加载对应商品 - 使用 Supabase 服务
const response = await supabaseService.getProductsByCategory(categoryId)
productList.value = response.data
hasMore.value = response.hasmore
// 重置分页并加载
currentPage.value = 1
hasMore.value = true
await loadProducts()
console.log('✅ 加载商品数据成功')
console.log('分类:', categoryId)
console.log('商品数量:', response.data.length)
console.log('商品列表:', response.data)
console.log('商品数量:', productList.value.length)
console.log('商品列表:', productList.value)
// 验证数据是否已正确更新
console.log('数据更新验证:')
@@ -403,55 +427,42 @@ const selectPrimaryCategory = async (categoryId: string) => {
}
// 添加到购物车
const addToCart = (product: any) => {
// 获取现有购物车数据
const cartData = uni.getStorageSync('cart')
let cartItems: any[] = []
if (cartData) {
try {
cartItems = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
const addToCart = async (product: any) => {
uni.showLoading({ title: '添加中...' })
try {
const success = await supabaseService.addToCart(product.id, 1)
if (success) {
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
cartCount.value++
} else {
uni.showToast({
title: '添加失败,请先登录',
icon: 'none'
})
}
} catch (e) {
console.error('添加到购物车异常', e)
uni.showToast({ title: '操作失败', icon: 'none' })
} finally {
uni.hideLoading()
}
// 检查商品是否已存在
const existingItem = cartItems.find((item: any) => item.id === product.id)
if (existingItem) {
existingItem.quantity++
} else {
// 添加新商品
cartItems.push({
id: product.id,
shopId: product.shopId || 'shop_default',
shopName: product.shopName || product.manufacturer || '自营店铺',
name: product.name,
price: product.price,
image: product.image,
spec: product.specification || '默认规格',
quantity: 1,
selected: true
})
}
// 保存回存储
uni.setStorageSync('cart', JSON.stringify(cartItems))
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
cartCount.value++
}
// 导航函数
const navigateToSearch = () => uni.navigateTo({ url: '/pages/mall/consumer/search' })
const navigateToCart = () => uni.navigateTo({ url: '/pages/medicine/cart' })
const navigateToCart = () => uni.navigateTo({ url: '/pages/mall/consumer/cart' })
const navigateToProduct = (product: any) => {
const id = product.id
const price = (product.base_price || 0).toString()
const originalPrice = (product.market_price || '').toString()
const name = encodeURIComponent(product.name || '')
const image = encodeURIComponent(product.main_image_url || '')
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${product.id}&price=${product.price}&originalPrice=${product.originalPrice || ''}&name=${encodeURIComponent(product.name)}&image=${encodeURIComponent(product.image || '')}`
url: `/pages/mall/consumer/product-detail?id=${id}&productId=${id}&price=${price}&originalPrice=${originalPrice}&name=${name}&image=${image}`
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -120,6 +120,7 @@
<script setup lang="uts">
import { ref, reactive, onMounted, nextTick } from 'vue'
import { supabaseService, type ChatMessage } from '@/utils/supabaseService.uts'
// 响应式数据
const messages = ref<any[]>([])
@@ -168,16 +169,25 @@ const mockMessages = [
// 生命周期
onMounted(() => {
loadChatHistory()
// 模拟客服自动回复
setTimeout(() => {
addReceivedMessage('查询到您的订单正在打包中,预计今天下午发货')
}, 3000)
})
// 加载聊天记录
const loadChatHistory = () => {
messages.value = [...mockMessages]
const loadChatHistory = async () => {
const rawMsgs = await supabaseService.getUserChatMessages()
messages.value = rawMsgs.reverse().map((m: ChatMessage) => {
const date = new Date(m.created_at || new Date().toISOString())
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
// Use explicit 'as' casting to avoid type errors if needed, though map handles it
const msg : any = {
id: m.id,
type: m.is_from_user ? 'sent' : 'received',
content: m.content,
time: timeStr
}
return msg
})
// 滚动到底部
setTimeout(() => {
@@ -186,7 +196,7 @@ const loadChatHistory = () => {
}
// 发送消息
const sendMessage = () => {
const sendMessage = async () => {
const content = inputMessage.value.trim()
if (!content) return
@@ -203,6 +213,9 @@ const sendMessage = () => {
// 滚动到底部
scrollToBottom()
// Backend Save
await supabaseService.sendChatMessage(content)
// 模拟客服回复2秒后
setTimeout(() => {
@@ -211,7 +224,7 @@ const sendMessage = () => {
}
// 模拟客服回复
const simulateCustomerReply = () => {
const simulateCustomerReply = async () => {
const replies = [
'好的,已为您记录',
'这个问题需要进一步核实',
@@ -221,6 +234,9 @@ const simulateCustomerReply = () => {
]
const randomReply = replies[Math.floor(Math.random() * replies.length)]
await supabaseService.simulateServiceReply(randomReply)
addReceivedMessage(randomReply)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -20,24 +20,42 @@
</view>
</view>
<!-- 商品列表 -->
<!-- 商品列表 (按店铺分组) -->
<view class="products-section">
<!-- 调试信息 -->
<!-- 调试信息 -->
<view v-if="checkoutItems.length > 0" class="debug-info">
<text class="debug-text">调试:共{{ checkoutItems.length }}件商品,总价计算:{{ totalAmount }}</text>
<text class="debug-text">共 {{ checkoutItems.length }} 件商品</text>
</view>
<view v-if="checkoutItems.length > 0">
<view v-for="item in checkoutItems" :key="item.id" class="product-item">
<image class="product-image" :src="item.product_image" />
<view class="product-info">
<text class="product-name">{{ item.product_name }}</text>
<text v-if="item.sku_specifications" class="product-spec">{{ getSpecText(item.sku_specifications) }}</text>
<view class="product-bottom">
<text class="product-price">¥{{ item.price }}</text>
<text class="product-quantity">×{{ item.quantity }}</text>
</view>
</view>
</view>
<view v-if="shopGroups.length > 0">
<view v-for="group in shopGroups" :key="group.shopId" class="shop-group">
<view class="shop-header">
<text class="shop-icon">🏪</text>
<text class="shop-name">{{ group.shopName }}</text>
</view>
<view v-for="item in group.items" :key="item.id" class="product-item">
<image class="product-image" :src="item.product_image" mode="aspectFill" />
<view class="product-info">
<text class="product-name">{{ item.product_name }}</text>
<text v-if="item.sku_specifications" class="product-spec">{{ getSpecText(item.sku_specifications) }}</text>
<view class="product-bottom">
<text class="product-price">¥{{ item.price }}</text>
<text class="product-quantity">×{{ item.quantity }}</text>
</view>
</view>
</view>
<!-- 店铺小计 -->
<view class="shop-subtotal">
<text class="subtotal-label">配送方式</text>
<text class="subtotal-value">快递 免邮</text>
</view>
<view class="shop-subtotal">
<text class="subtotal-text">小计: </text>
<text class="subtotal-price">¥{{ getGroupTotal(group) }}</text>
</view>
</view>
</view>
<view v-else class="no-products">
<text class="no-products-text">暂无商品信息</text>
@@ -286,6 +304,8 @@ type CheckoutItemType = {
sku_specifications: any
price: number
quantity: number
shop_id?: string
shop_name?: string
}
type DeliveryOptionType = {
@@ -329,6 +349,32 @@ const showSaveConfirm = ref<boolean>(false)
const smartAddressInput = ref<string>('')
// 计算属性 - 修复价格同步问题
// 按店铺分组商品
const shopGroups = computed(() => {
const groups = new Map<string, any>()
checkoutItems.value.forEach(item => {
// 使用类型断言访问可能的额外属性
const rawItem = item as any
const shopId = rawItem.shop_id || 'unknown'
if (!groups.has(shopId)) {
groups.set(shopId, {
shopId: shopId,
shopName: rawItem.shop_name || '商城优选',
merchant_id: rawItem.merchant_id || rawItem.shop_id,
items: [] as any[]
})
}
groups.get(shopId).items.push(item)
})
return Array.from(groups.values())
})
const getGroupTotal = (group: any) => {
return group.items.reduce((sum: number, item: any) => {
return sum + (Number(item.price) * Number(item.quantity))
}, 0).toFixed(2)
}
const totalAmount = computed(() => {
console.log('计算商品总价checkoutItems:', checkoutItems.value)
if (!checkoutItems.value || checkoutItems.value.length === 0) {
@@ -405,34 +451,38 @@ watch(checkoutItems, (newItems) => {
// 页面加载时监听eventChannel
onLoad(() => {
// 优先检查Storage中是否有"立即购买"的数据
let dataLoaded = false
// 优先检查Storage中是否有结算数据 (支持 buy_now 和 cart 两种模式)
const checkoutType = uni.getStorageSync('checkout_type')
if (checkoutType === 'buy_now') {
console.log('检测到立即购买模式从Storage加载数据')
if (checkoutType === 'buy_now' || checkoutType === 'cart') {
console.log(`检测到结算模式(${checkoutType})从Storage加载数据`)
const itemsStr = uni.getStorageSync('checkout_items')
if (itemsStr) {
try {
const items = JSON.parse(itemsStr as string)
console.log('从Storage加载的商品数据:', items)
processCheckoutItems(items)
// 清除Storage避免污染下次进入刷新页面时可能需要保留暂时不清除或者在离开页面时清除
// uni.removeStorageSync('checkout_type')
// uni.removeStorageSync('checkout_items')
loadDefaultAddress()
return // 成功加载,直接返回
if (items && Array.isArray(items) && items.length > 0) {
processCheckoutItems(items)
dataLoaded = true
}
} catch (e) {
console.error('解析立即购买数据失败', e)
console.error('解析结算数据失败', e)
}
}
}
// 如果没有从checkout_items加载到数据则尝试从通用购物车Storage加载 (回退方案)
if (!dataLoaded) {
console.log('未找到预结算数据,尝试从购物车本地存储加载')
loadFromLocalStorage()
} else {
// 如果已经加载了数据还需要单独加载地址因为loadFromLocalStorage通常会附带加载地址
loadDefaultAddress()
}
// 从上一页获取数据
const eventChannel = uni.getEventChannel ? uni.getEventChannel() : null
// 默认先尝试从本地存储加载(确保有数据)
loadFromLocalStorage()
if (eventChannel) {
eventChannel.on('acceptData', (data: any) => {
console.log('接收到商品数据:', data)
@@ -539,9 +589,6 @@ const loadFromLocalStorage = () => {
if (selectedCartItems.length > 0) {
// 转换为CheckoutItemType格式
const convertedItems: CheckoutItemType[] = selectedCartItems.map(item => {
// 确保价格和数量是数字
let price = typeof item.price === 'string' ? parseFloat(item.price) : Number(item.price)
if (isNaN(price)) price = 0
let quantity = typeof item.quantity === 'string' ? parseInt(item.quantity) : Number(item.quantity)
if (isNaN(quantity) || quantity < 1) quantity = 1
@@ -553,7 +600,7 @@ const loadFromLocalStorage = () => {
product_name: item.name || '',
product_image: item.image || '',
sku_specifications: item.spec ? { spec: item.spec } : {},
price: price,
price: Number(item.price) || 0,
quantity: quantity
}
})
@@ -1247,8 +1294,7 @@ const submitOrder = async () => {
uni.showLoading({ title: '提交中...' })
try {
const userId = getCurrentUserId()
// 确保使用当前登录用户ID (如果本地存储为空,可能需要处理)
const userId = supabaseService.getCurrentUserId()
if (!userId) {
uni.hideLoading()
uni.showToast({
@@ -1258,59 +1304,77 @@ const submitOrder = async () => {
return
}
// 准备订单项数据
// 注意:需根据 checkoutItems 的实际结构转换为 createOrder 需要的 CartItem 结构
// 假设 checkoutItems 已经包含了 product_id, quantity, price, name, image 等字段
const orderItems = checkoutItems.value.map((item: any): any => ({
id: item.id || '', // 这是一个临时ID或者购物车IDcreateOrder 中会使用 product_id
product_id: item.product_id || item.id, // 确保有 product_id
quantity: item.quantity,
price: item.price,
product_name: item.name,
product_image: item.image,
spec: item.spec,
checked: true
}))
// 准备按店铺分组数据
const groups = shopGroups.value.map((group: any): any => {
return {
merchant_id: group.merchant_id || group.shopId,
shopName: group.shopName,
items: group.items.map((item: any): any => ({
id: item.id, // 用于清理购物车
product_id: item.product_id,
sku_id: item.sku_id,
quantity: item.quantity,
price: item.price,
product_name: item.product_name,
product_image: item.product_image,
specifications: item.sku_specifications // 保持原始对象createOrder 会处理序列化
}))
}
})
// 调用 Supabase 服务创建订单
const result = await supabaseService.createOrder(
userId,
selectedAddress.value!.id, // 地址ID
actualAmount.value, // 实付金额
orderItems
)
// 调用 Supabase 服务创建多店铺订单
const result = await supabaseService.createOrdersByShop({
shipping_address: selectedAddress.value,
shopGroups: groups,
deliveryFee: deliveryFee.value,
discountAmount: discountAmount.value
})
uni.hideLoading()
if (result.success) {
// 清除购买的商品 (如果来自购物车,应该在 createOrder 成功后清除,或者这里手动清除本地存储)
// 这里我们假设购物车清理逻辑可能在 createOrder 后端处理,或者需要在这里清除本地
// 清除结算商品
try {
uni.removeStorageSync('checkout_items')
uni.removeStorageSync('checkout_type')
} catch(e) {
console.error('清除结算商品失败', e)
}
const activeOrderId = result.data as string
const orderIds = result.orderIds
// 跳转支付页面
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${activeOrderId}&amount=${actualAmount.value}&productAmount=${totalAmount.value}&deliveryFee=${deliveryFee.value}&discountAmount=${discountAmount.value}`
})
if (orderIds.length === 1) {
// 单个订单跳转支付
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${orderIds[0]}&amount=${actualAmount.value}`
})
} else {
// 多个订单跳转到订单列表
uni.showToast({
title: `成功创建${orderIds.length}个订单`,
icon: 'success'
})
setTimeout(() => {
uni.redirectTo({
url: '/pages/mall/consumer/orders'
})
}, 1500)
}
} else {
throw new Error(result.error)
throw new Error(result.error || '创建订单失败')
}
} catch (err: any) {
uni.hideLoading()
console.error('创建订单失败:', err)
console.error('提交订单错误:', err)
uni.showToast({
title: err.message || '订单创建失败',
title: err.message || '提交订单失败',
icon: 'none'
})
}
}
// 生成订单号
const generateOrderNo = (): string => {
const date = new Date()
@@ -1463,6 +1527,62 @@ const goBack = () => {
text-align: center;
}
.shop-group {
background-color: #fff;
margin: 10px 0;
border-radius: 12px;
padding: 10px;
}
.shop-header {
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: 10px;
border-bottom: 1px solid #f0f0f0;
}
.shop-icon {
font-size: 18px;
margin-right: 8px;
}
.shop-name {
font-size: 16px;
font-weight: bold;
color: #333;
}
.shop-subtotal {
display: flex;
justify-content: flex-end; /* 右对齐 */
align-items: center;
padding-top: 10px;
margin-top: 5px;
border-top: 1px dashed #f0f0f0;
font-size: 14px;
}
.subtotal-label {
color: #666;
margin-right: 10px;
}
.subtotal-value {
color: #333;
}
.subtotal-text {
color: #333;
margin-right: 5px;
}
.subtotal-price {
color: #ff4757;
font-weight: bold;
font-size: 16px;
}
.product-item {
display: flex;
padding: 15px 0;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
# 功能与页面状态详解
本文档详细记录了 Consumer App 各个功能模块的实现状态与业务逻辑。
> **状态图例**: ✅ 正常 (已对接真实 DB) | 🚧 开发中 | ❌ 未开始
## 🛍️ 核心购物流程
| 功能模块 | 页面路径 | 状态 | 备注 |
| :--- | :--- | :--- | :--- |
| **首页** | `pages/mall/consumer/index.uvue` | ✅ 正常 | 金刚区、Banner、推荐商品流。 |
| **分类** | `pages/mall/consumer/category.uvue` | ✅ 正常 | 一级/二级分类联动,跳转搜索结果。 |
| **商品详情** | `pages/mall/consumer/product-detail.uvue` | ✅ 正常 | SKU 选择、加入购物车、立即购买。 |
| **购物车** | `pages/mall/consumer/cart.uvue` | ✅ 正常 | 数量增减、勾选计算、结算校验。 |
| **结算页** | `pages/mall/consumer/checkout.uvue` | ✅ 正常 | 选择地址、运费计算、创建订单。 |
| **收银台** | `pages/mall/consumer/payment.uvue` | ✅ 正常 | 模拟支付流程,更新订单为“待发货”。 |
## 👤 个人中心 (Profile)
**文件**: `pages/mall/consumer/profile.uvue`
* **资产看板**: 实时加载积分、余额、优惠券数量。
* **订单看板**: 待支付、待发货、待收货、退款/售后(跳转至 `refund.uvue`)。
* **服务矩阵**:
* **地址管理**: `address-list.uvue` (CRUD 正常)
* **我的收藏**: `favorites.uvue` (商品/店铺收藏 正常)
* **浏览足迹**: `footprint.uvue` (按日期分组 正常)
* **在线客服**: 跳转至 `chat.uvue`
* **消息通知**: 跳转至 `messages.uvue`
## 📦 订单管理体系
**列表页**: `pages/mall/consumer/orders.uvue`
* **状态筛选**: 全部 / 待支付 / 待发货 / 待收货 / 已完成。
* **核心操作**:
* **去支付**: 跳转收银台。
* **确认收货**: 变更状态为已完成。
* **申请售后**: 跳转 `apply-refund.uvue` (带入订单信息)。
**详情页**: `pages/mall/consumer/order-detail.uvue`
* **信息展示**: 完整的地址、商品规格、金额明细、时间线。
* **业务状态**: 根据 `order_status` 动态展示可操作按钮。
## 🔄 售后服务体系 (Refunds)
**申请页**: `pages/mall/consumer/apply-refund.uvue`
* **功能**: 支持仅退款/退货退款。
* **逻辑**: 自动获取订单最大可退金额,防止超额申请。
* **提交**: 数据写入 `ml_refunds` 表。
**记录页**: `pages/mall/consumer/refund.uvue`
* **列表**: 展示所有历史售后申请及其当前状态。
* **进度**: 可视化展示审核进度 (目前模拟进度条)。
## 💬 社交与互动
| 功能模块 | 页面路径 | 状态 | 说明 |
| :--- | :--- | :--- | :--- |
| **在线客服** | `pages/mall/consumer/chat.uvue` | ✅ 正常 | 支持文本/表情发送,历史记录持久化。 |
| **消息中心** | `pages/mall/consumer/messages.uvue` | ✅ 正常 | 聚合系统通知订单消息与客服消息。 |
| **商品评价** | `pages/mall/consumer/review.uvue` | ✅ 正常 | 支持星级评分与文本评价。 |

View File

@@ -0,0 +1,44 @@
# 测试数据生成指南 (Mock Data Guide)
为了有效测试消费者端前端功能,我们编写了 SQL 脚本来向数据库填充真实的模拟数据。
## 📂 脚本位置
所有脚本均位于 `doc_mall/consumer/sql/` 目录下。
## 🛠 使用说明
### 1. 修复现有数据问题 (优先级最高)
**脚本**: `fix_order_items_data.sql`
* **适用场景**: 如果您的订单列表中,商品显示为空白图片或缺失名称。
* **功能**:
*`ml_product_skus``ml_products` 表自动回填缺失的 `image_url`
* 修正占位符形式的 `product_name`
* 补充缺失的 `specifications` (如:规格参数)。
### 2. 生成新的测试订单
**脚本**: `add_mock_orders_corrected.sql`
* **适用场景**: 为测试用户 (`test@mall.com`) 创建一批全新的订单数据。
* **生成数据包含**:
* 1x **待支付** 订单
* 1x **待发货** 订单 (已支付)
* 1x **待收货** 订单 (已发货)
* 1x **已完成** 订单
* **注意**: 该脚本会随机选取数据库中现有的真实商品,确保数据关联正确无误。
### 3. 生成评价测试数据
**脚本**: `add_mock_reviews_for_test_user.sql`
* **适用场景**: 测试“我的评价”列表或商品详情页的评价展示。
* **功能**: 创建已完成的订单,并自动为其添加一条带图片的 5 星好评。
## 🧪 建议测试流程
1. **运行** `add_mock_orders_corrected.sql`
2. **打开 App** > 个人中心 (Profile) > 我的订单。
3. **验证**:
* 各状态标签页下是否有对应的订单。
* 商品图片和名称是否显示正常。
* 在“待收货”或“已完成”订单上点击**申请售后**,验证是否跳转正确。

View File

@@ -0,0 +1,62 @@
# 商城消费者端 (Consumer) 开发文档
本文档包含了商城消费者前端模块的详细开发指南和状态说明。
## 📂 项目结构
消费者端模块位于 `pages/mall/consumer/` 目录下,基于 UniApp x (UTS/UVUE) 开发。
### 核心页面清单
| 页面文件 | 描述 | 当前状态 |
|-----------|-------------|--------|
| `index.uvue` | 首页 (商城门面) | ✅ 正常 |
| `category.uvue` | 商品分类浏览 | ✅ 正常 |
| `cart.uvue` | 购物车 | ✅ 正常 |
| `profile.uvue` | 个人中心 (用户主页) | ✅ 正常 |
| `orders.uvue` | 订单列表管理 | ✅ 正常 |
| `order-detail.uvue` | 订单详情页 | ✅ 正常 |
| `apply-refund.uvue` | 申请售后 (退款/退货) | ✅ 正常 |
| `refund.uvue` | 售后记录列表 | ✅ 正常 |
| `chat.uvue` | 在线客服 | ✅ 正常 |
| `messages.uvue` | 消息通知中心 | ✅ 正常 |
### 关键功能实现
1. **用户个人中心**:
* 可视化展示用户状态(积分、余额)。
* 订单状态快捷入口(待支付、待发货、待收货等)。
* **我的服务**: 优惠券、地址、收藏夹。
* **新功能**: 已集成“评价”入口(跳转至待评价订单)和“退款/售后”入口(跳转至售后记录页)。
2. **订单管理**:
* 多状态标签页切换(全部、待支付、待收货、已完成)。
* **操作**: 支付、取消、提醒发货、确认收货、评价、**申请售后**。
*`supabaseService` 后端服务实时交互。
3. **售后系统**:
* 独立页面 `apply-refund.uvue`:支持仅退款/退货退款,关联原订单金额。
* 售后列表 `refund.uvue`:查看历史退款记录及进度。
4. **客服与消息**:
* 在线客服 `chat.uvue`:支持文本/表情发送,消息持久化存储。
* 消息中心 `messages.uvue`:聚合系统通知与客服消息。
## 🛠 技术栈
* **框架**: UniApp x (Vue 3 + UTS)
* **后端**: Supabase (PostgreSQL)
* **语言**: UTS (TypeScript 方言)
* **样式**: SCSS / UVUE Styles
## 🚀 快速开始
1. **环境准备**: 确保已安装 HBuilderX 并配置好 UniApp x 插件。
2. **数据库**: 运行 `doc_mall/consumer/sql/` 下的 SQL 脚本初始测试数据。
3. **运行**: 在 HBuilderX 中打开项目,运行到 Web 浏览器或 App 模拟器。
## 📚 文档索引
* [功能与页面状态详解](./FEATURES_&_PAGES.md)
* [Supabase 集成与数据库架构](./SUPABASE_INTEGRATION.md)
* [测试数据生成指南](./MOCK_DATA_GUIDE.md)

View File

@@ -0,0 +1,121 @@
# Supabase 集成与数据库架构 (Consumer App)
本文档详细描述了消费者端 (Consumer App) 涉及的所有数据库集成点、核心表结构以及 `supabaseService.uts` 提供的 API 服务。
> **更新时间**: 2026-02-03
> **状态**: 已完成核心业务闭环 (订单、支付、售后、客服、足迹等)
## 🗄️ 核心数据架构
消费者端业务依赖以下核心数据库表:
### 1. 交易与订单 (Orders & Transactions)
| 表名 | 描述 | 关键字段 |
| :--- | :--- | :--- |
| `ml_orders` | 订单主表 | `id`, `user_id`, `merchant_id`, `order_status` (1:待付, 2:待发, 3:待收, 4:完成, 5:取消), `total_amount` |
| `ml_order_items` | 订单商品明细 | `order_id`, `product_id`, `image_url` (快照), `specifications` (快照) |
| `ml_refunds` | **[新增]** 售后/退款申请 | `order_id`, `reason_category`, `refund_amount`, `status` (0:待审, 1:同意, 2:拒绝), `refund_type` (1:仅退款, 2:退货退款) |
### 2. 互动与消息 (Interaction & Communication)
| 表名 | 描述 | 关键字段 |
| :--- | :--- | :--- |
| `ml_chat_messages` | **[新增]** 客服聊天记录 | `session_id`, `sender_id`, `receiver_id`, `content`, `msg_type`, `is_from_user` |
| `ml_notifications` | 消息通知 | `type` (system/order/promotion), `title`, `is_read` |
| `ml_product_reviews` | 商品评价 | `order_id`, `product_id`, `rating`, `content`, `images` |
### 3. 用户行为 (User Behavior)
| 表名 | 描述 | 关键字段 |
| :--- | :--- | :--- |
| `ml_browsing_history` | 足迹/浏览记录 | `user_id`, `product_id`, `view_time` |
| `ml_favorites` | 收藏夹 | `user_id`, `target_id`, `type` (1:商品, 2:店铺) |
| `ml_user_addresses` | 收货地址 | `user_id`, `receiver_name`, `phone`, `province`... |
---
## 🔌 API 服务层 (`utils/supabaseService.uts`)
所有后端交互通过单例 `supabaseService` 进行,主要模块如下:
### 1. 售后/退款服务 (Refunds)
> **状态**: ✅ 已集成 (apply-refund.uvue)
```typescript
// 创建退款/售后申请
async createRefund(data: {
order_id: string,
refund_type: number,
refund_amount: number,
reason_category: string,
description: string,
images: string[]
}): Promise<boolean>
```
### 2. 在线客服/消息服务 (Chat & Messages)
> **状态**: ✅ 已集成 (chat.uvue, messages.uvue)
```typescript
// 获取当前用户的聊天记录
async getUserChatMessages(): Promise<ChatMessage[]>
// 发送聊天消息 (持久化到 ml_chat_messages)
async sendChatMessage(content: string, type: string = 'text'): Promise<boolean>
// (测试用) 模拟客服自动回复
async simulateServiceReply(content: string): Promise<boolean>
```
### 3. 订单与支付 (Orders & Payment)
> **状态**: ✅ 已集成 (checkout.uvue, payment.uvue, orders.uvue)
```typescript
// 创建订单 (由购物车或直接购买触发)
async createOrder(orderData: any): Promise<string | null>
// 获取订单详情 (包含商品明细)
async getOrderDetail(orderId: string): Promise<any | null>
// 支付订单 (模拟支付,更新订单状态 1->2记录支付时间)
async payOrder(orderId: string, paymentMethod: string, amount: number): Promise<boolean>
// 确认收货 (3->4)
async confirmReceipt(orderId: string): Promise<Result>
```
### 4. 商品与搜索 (Products)
> **状态**: ✅ 已集成 (search.uvue, product-detail.uvue)
```typescript
// 搜索商品 (支持关键词、分类、价格排序、销量排序)
async searchProducts(keyword: string, page: number, pageSize: number, sort: string, asc: boolean): Promise<PaginatedResponse<Product>>
// 获取足迹
async getFootprints(): Promise<any[]>
```
---
## 📊 页面集成状态一览表
| 页面模块 | 文件路径 | 数据源状态 | 说明 |
| :--- | :--- | :--- | :--- |
| **首页** | `pages/mall/consumer/index.uvue` | ✅ Real DB | 金刚区、推荐商品已接入 |
| **搜索** | `pages/mall/consumer/search.uvue` | ✅ Real DB | 关键词搜索、排序、分页正常 |
| **购物车** | `pages/mall/consumer/cart.uvue` | ✅ Real DB | 加减购、结算校验正常 |
| **结算台** | `pages/mall/consumer/checkout.uvue` | ✅ Real DB | 地址选择、订单创建正常 |
| **收银台** | `pages/mall/consumer/payment.uvue` | ✅ Real DB | 读取待付金额,更新支付状态 |
| **订单列表** | `pages/mall/consumer/orders.uvue` | ✅ Real DB | 状态筛选 (全部/待付/待收/退款) 正常 |
| **订单详情** | `pages/mall/consumer/order-detail.uvue` | ✅ Real DB | 地址、商品、金额展示正常 |
| **申请售后** | `pages/mall/consumer/apply-refund.uvue` | ✅ Real DB | **[本次完成]** 关联订单金额,提交至 `ml_refunds` |
| **在线客服** | `pages/mall/consumer/chat.uvue` | ✅ Real DB | **[本次完成]** 消息收发持久化,支持历史记录 |
| **消息中心** | `pages/mall/consumer/messages.uvue` | ✅ Real DB | 能够统计未读客服消息数 |
| **我的评价** | `pages/mall/consumer/review.uvue` | ✅ Real DB | 提交评价至 `ml_product_reviews` |
## 🛠️ 下一步维护建议
1. **异常处理**: 目前部分接口在网络异常时仅打印 `console.error`,建议增加全局统一的 Toasts 提示。
2. **图片上传**: 目前退款和评价中的图片上传依赖 Mock 或简单路径,需对接真实的 OSS/Supabase Storage 文件上传。
3. **实时消息**: 目前 `chat.uvue` 使用 polling (轮询) 或手动刷新Supabase 支持 Realtime Subscription后续可升级为 WebSocket 实时推送。

View File

@@ -65,26 +65,69 @@ const loadFavorites = async () => {
// Map response
favorites.value = res.map((item: any): Product => {
const prod = item.ml_products
let prod: any = null
if (item instanceof UTSJSONObject) {
prod = item.get('ml_products')
} else {
prod = item['ml_products']
}
let image = '/static/default-product.png'
if (prod) {
if (prod.main_image_url) image = prod.main_image_url
else if (prod.image_url) image = prod.image_url
else if (prod.image_urls) {
// Try parse
try {
const arr = JSON.parse(prod.image_urls)
if (Array.isArray(arr) && arr.length > 0) image = arr[0]
} catch(e) {}
let id = ''
let name = '未知商品'
let price = 0
let sales = 0
if (prod != null) {
if (prod instanceof UTSJSONObject) {
id = prod.getString('id') || ''
name = prod.getString('name') || '未知商品'
price = prod.getNumber('base_price') || 0
image = prod.getString('main_image_url') || image
sales = prod.getNumber('sale_count') || 0
// 如果 main_image_url 为空,尝试解析 image_urls
if (image === '/static/default-product.png') {
const imgUrls = prod.getString('image_urls')
if (imgUrls) {
try {
const arr = JSON.parse(imgUrls)
if (Array.isArray(arr) && arr.length > 0) image = arr[0] as string
} catch(e) {}
}
}
} else {
id = (prod['id'] as string) || ''
name = (prod['name'] as string) || '未知商品'
price = (prod['base_price'] as number) || 0
image = (prod['main_image_url'] as string) || image
sales = (prod['sale_count'] as number) || 0
if (image === '/static/default-product.png') {
const imgUrls = prod['image_urls'] as string
if (imgUrls) {
try {
const arr = JSON.parse(imgUrls)
if (Array.isArray(arr) && arr.length > 0) image = arr[0] as string
} catch(e) {}
}
}
}
} else {
// 如果没取到商品,尝试直接从 item 取 target_id
if (item instanceof UTSJSONObject) {
id = item.getString('target_id') || ''
} else {
id = (item['target_id'] as string) || ''
}
}
return {
id: prod?.id || item.target_id,
name: prod?.name || '未知商品',
price: prod?.price || 0,
id: id,
name: name,
price: price,
image: image,
sales: prod?.sales || 0,
sales: sales,
shopId: '',
shopName: ''
}
@@ -109,8 +152,11 @@ const removeFavorite = async (id: string) => {
content: '确定要取消收藏该商品吗?',
success: async (res) => {
if (res.confirm) {
const success = await supabaseService.toggleFavorite(id) // Toggle removes if exists
if (success) {
// toggleFavorite 返回最新的状态true=已收藏false=未收藏
const isStillFavorite = await supabaseService.toggleFavorite(id)
if (!isStillFavorite) {
// 现在的状态是"未收藏",说明取消成功
// Remove from local list
const index = favorites.value.findIndex(item => item.id === id)
if (index !== -1) {
@@ -120,6 +166,11 @@ const removeFavorite = async (id: string) => {
title: '已取消收藏',
icon: 'none'
})
} else {
uni.showToast({
title: '取消失败',
icon: 'none'
})
}
}
}

View File

@@ -83,6 +83,7 @@
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import { supabaseService } from '@/utils/supabaseService.uts'
type FootprintType = {
id: string
@@ -131,24 +132,101 @@ onMounted(() => {
})
// 加载足迹数据
const loadFootprints = (loadMore: boolean = false) => {
const loadFootprints = async (loadMore: boolean = false) => {
isLoading.value = true
// 从本地存储获取足迹数据
const storedFootprints = uni.getStorageSync('footprints')
if (storedFootprints) {
try {
const data = JSON.parse(storedFootprints as string) as any[]
footprints.value = data.map(item => ({
...item,
selected: false
}))
} catch (e) {
console.error('Failed to parse footprints', e)
footprints.value = []
try {
const remoteData = await supabaseService.getFootprints()
if (remoteData.length > 0) {
console.log('获取到远程足迹数据:', remoteData.length)
// 使用远程数据
footprints.value = remoteData.map((item: any): FootprintType => {
let id = ''
let name = ''
let price = 0
let original_price = 0
let image = ''
let sales = 0
let shopId = ''
let shopName = ''
let viewTime = 0
if (item instanceof UTSJSONObject) {
id = item.getString('id') || ''
name = item.getString('name') || ''
price = item.getNumber('price') || 0
original_price = item.getNumber('original_price') || 0
image = item.getString('image') || ''
sales = item.getNumber('sales') || 0
shopId = item.getString('shopId') || ''
shopName = item.getString('shopName') || ''
viewTime = item.getNumber('viewTime') || 0
} else {
id = (item['id'] as string) || ''
name = (item['name'] as string) || ''
price = (item['price'] as number) || 0
original_price = (item['original_price'] as number) || 0
image = (item['image'] as string) || ''
sales = (item['sales'] as number) || 0
shopId = (item['shopId'] as string) || ''
shopName = (item['shopName'] as string) || ''
viewTime = (item['viewTime'] as number) || 0
}
return {
id: id,
name: name,
price: price,
original_price: original_price,
image: image,
sales: sales,
shopId: shopId,
shopName: shopName,
viewTime: viewTime,
selected: false
} as FootprintType
})
// 更新本地缓存
const dataToSave = footprints.value.map(item => {
const { selected, ...rest } = item
return rest
})
uni.setStorageSync('footprints', JSON.stringify(dataToSave))
} else {
// 如果远程为空,尝试加载本地
const storedFootprints = uni.getStorageSync('footprints')
if (storedFootprints) {
try {
const data = JSON.parse(storedFootprints as string) as any[]
footprints.value = data.map(item => ({
...item,
selected: false
}))
} catch (e) {
console.error('Failed to parse footprints', e)
footprints.value = []
}
} else {
footprints.value = []
}
}
} catch (e) {
console.error('加载足迹失败', e)
// 失败时加载本地
const storedFootprints = uni.getStorageSync('footprints')
if (storedFootprints) {
try {
const data = JSON.parse(storedFootprints as string) as any[]
footprints.value = data.map(item => ({
...item,
selected: false
}))
} catch (err) {
footprints.value = []
}
}
} else {
footprints.value = []
}
isLoading.value = false
@@ -262,9 +340,18 @@ const deleteSelected = () => {
uni.showModal({
title: '确认删除',
content: `确定要删除选中的${selectedItems.length}条记录吗?`,
success: (res) => {
success: async (res) => {
if (res.confirm) {
// 从列表中移除
uni.showLoading({ title: '删除中' })
// 远程删除
for (const item of selectedItems) {
await supabaseService.deleteFootprint(item.id)
}
uni.hideLoading()
// 从列表移除
footprints.value = footprints.value.filter(item => !item.selected)
// 保存回本地存储

View File

@@ -39,52 +39,14 @@
<scroll-view
scroll-y
class="main-scroll"
:style="{ height: scrollHeight + 'px' }"
refresher-enabled
:refresher-triggered="refreshing"
:lower-threshold="50"
@refresherrefresh="onRefresh"
@scrolltolower="loadMore"
@scroll="handleScroll"
>
<!-- 智能健康卡片 -->
<view class="smart-health-card" :style="{ marginTop: (statusBarHeight + 44 + 10) + 'px' }">
<view class="health-content">
<view class="health-header">
<text class="health-title">智能健康助手</text>
<text class="health-subtitle">根据您的健康数据推荐</text>
</view>
<view class="health-tips">
<text class="tip-item">💡 按时用药提醒</text>
<text class="tip-item">📋 健康记录跟踪</text>
<text class="tip-item">🩺 在线问诊咨询</text>
</view>
</view>
</view>
<!-- 智能分类网格 - 完全响应式 -->
<view class="smart-categories">
<view class="section-header">
<text class="section-title">智能分类</text>
<text class="section-desc">快速定位所需药品</text>
</view>
<view class="category-grid">
<view
v-for="category in categories"
:key="category.id"
class="category-card"
@click="switchCategory(category)"
:style="{ '--card-color': category.color }"
>
<view class="card-icon">
<text>{{ category.icon }}</text>
</view>
<text class="card-name">{{ category.name }}</text>
<text class="card-desc">{{ category.desc }}</text>
</view>
</view>
</view>
<!-- 健康资讯轮播 -->
<!-- 健康资讯轮播 (Moved Up) -->
<view class="health-news">
<view class="news-header">
<text class="news-title">健康资讯</text>
@@ -120,8 +82,48 @@
</swiper>
</view>
<!-- 智能服务入口 -->
<view class="smart-services">
<!-- 智能健康卡片 (Hidden) -->
<!-- <view class="smart-health-card" :style="{ marginTop: (statusBarHeight + 44 + 10) + 'px' }">
<view class="health-content">
<view class="health-header">
<text class="health-title">智能健康助手</text>
<text class="health-subtitle">根据您的健康数据推荐</text>
</view>
<view class="health-tips">
<text class="tip-item">💡 按时用药提醒</text>
<text class="tip-item">📋 健康记录跟踪</text>
<text class="tip-item">🩺 在线问诊咨询</text>
</view>
</view>
</view> -->
<!-- 智能分类网格 - 完全响应式 -->
<view class="smart-categories">
<view class="section-header">
<text class="section-title">智能分类</text>
<text class="section-desc">快速定位所需药品</text>
</view>
<view class="category-grid">
<view
v-for="category in categories"
:key="category.id"
class="category-card"
@click="switchCategory(category)"
:style="{ '--card-color': category.color }"
>
<view class="card-icon">
<text>{{ category.icon }}</text>
</view>
<text class="card-name">{{ category.name }}</text>
<text class="card-desc">{{ category.desc }}</text>
</view>
</view>
</view>
<!-- 健康资讯轮播 (Original Position - Removed) -->
<!-- 智能服务入口 (Hidden) -->
<!-- <view class="smart-services">
<view class="services-grid">
<view class="service-card" @click="navigateToConsultation">
<view class="service-icon" style="background: #2196F3;">
@@ -152,14 +154,14 @@
<text class="service-desc">健康管理助手</text>
</view>
</view>
</view>
</view> -->
<!-- 热销药品专区 -->
<view class="hot-products">
<view class="section-header">
<view class="title-section">
<text class="section-icon">🔥</text>
<text class="section-title">热销品</text>
<text class="section-title">热销品</text>
</view>
<view class="sort-tabs">
<text
@@ -180,30 +182,30 @@
class="product-card"
@click="navigateToProduct(product)"
>
<view class="product-badge" v-if="product.badge">{{ product.badge }}</view>
<view class="product-badge" v-if="product.is_hot">热销</view>
<image
class="product-image"
:src="product.image"
:src="product.main_image_url"
mode="aspectFill"
/>
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<text class="product-spec">{{ product.specification }}</text>
<!-- spec is omitted if not available -->
<view class="price-section">
<view class="current-price">
<text class="price-symbol">¥</text>
<text class="price-value">{{ product.price }}</text>
<text class="price-value">{{ product.base_price }}</text>
</view>
<text class="original-price" v-if="product.originalPrice > product.price">
¥{{ product.originalPrice }}
<text class="original-price" v-if="product.market_price != null && product.market_price! > product.base_price">
¥{{ product.market_price }}
</text>
</view>
<view class="product-meta">
<text class="manufacturer">{{ product.manufacturer }}</text>
<text class="manufacturer">{{ product.brand_name || product.shop_name || '自营' }}</text>
<view class="sales-info">
<text class="sales-count">已售{{ product.sales }}</text>
<text class="sales-count">已售{{ product.sale_count }}</text>
</view>
</view>
@@ -216,10 +218,14 @@
</view>
</view>
</view>
<!-- 加载状态提示 -->
<view class="load-more-status" v-if="loading || showLoadMore">
<text class="loading-text">正在加载更多商品...</text>
</view>
</view>
<!-- 家庭常备药 -->
<view class="family-medicine">
<!-- 家庭常备药 (Hidden) -->
<!-- <view class="family-medicine">
<view class="section-header">
<view class="title-section">
<text class="section-icon">🏠</text>
@@ -242,12 +248,12 @@
<text class="family-desc">{{ item.desc }}</text>
</view>
</view>
</view>
</view> -->
<!-- 智能推荐模块已隐藏 -->
<!-- 健康提醒 -->
<view class="health-reminder">
<!-- 健康提醒 (Hidden) -->
<!-- <view class="health-reminder">
<view class="reminder-content">
<text class="reminder-icon">⏰</text>
<view class="reminder-text">
@@ -258,7 +264,7 @@
<text class="action-text">查看</text>
</view>
</view>
</view>
</view> -->
<!-- 底部安全区域 -->
<view class="safe-area"></view>
@@ -280,7 +286,7 @@ const refreshing = ref(false)
const loading = ref(false)
const isFirstShow = ref(true)
const hasMore = ref(true)
const activeSort = ref('sales')
const activeSort = ref('recommend') // 默认展示智能推荐
const activeFilter = ref('recommend')
const currentPage = ref(1)
@@ -290,6 +296,7 @@ const recommendedProducts = ref<Product[]>([])
// 屏幕尺寸检测
const isMobile = ref(false)
const showLoadMore = ref(false)
// 导航栏显示控制
const showNavbar = ref(true)
@@ -358,12 +365,12 @@ const loadCategories = async () => {
}
// 获取热销商品(根据当前排序方式)
const loadHotProducts = async () => {
const loadHotProducts = async (targetLimit: number = 6) => {
try {
let products: Product[] = []
const limit = 6
const limit = targetLimit
console.log('加载热销商品,当前排序方式:', activeSort.value)
console.log('加载热销商品,当前排序方式:', activeSort.value, 'limit:', limit)
switch (activeSort.value) {
case 'sales':
@@ -522,9 +529,8 @@ const initPage = () => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
// 计算滚动区域高度 - 使用整个窗口高度
const windowHeight = systemInfo.windowHeight
scrollHeight.value = windowHeight
// 计算滚动区域高度 - 不再需要手动计算,使用 Flex 布局自动撑开
// scrollHeight.value = windowHeight - 50
// 检测屏幕尺寸
const screenWidth = systemInfo.screenWidth || systemInfo.windowWidth
@@ -609,6 +615,7 @@ const switchCategory = (category: any) => {
// 切换排序
const switchSort = (sortId: string) => {
activeSort.value = sortId
hasMore.value = true // 重置加载更多状态
// 重新加载热销商品,排序由 Supabase 服务处理
loadHotProducts()
}
@@ -641,75 +648,77 @@ const onRefresh = () => {
// 加载更多
const loadMore = async () => {
if (loading.value || !hasMore.value) return
console.log('=== 触发触底事件 ===')
if (loading.value) {
console.log('正在加载中,跳过')
return
}
showLoadMore.value = true
loading.value = true
try {
// 增加限制以加载更多推荐商品
const currentLimit = recommendedProducts.value.length + 6
await loadRecommendedProducts(currentLimit)
// 获取当前热销商品的数量
const currentCount = hotProducts.value.length
const nextLimit = currentCount + 6
// 假设如果返回的商品数量小于请求的限制,则没有更多数据
if (recommendedProducts.value.length < currentLimit) {
console.log('开始加载更多,当前数量:', currentCount, '目标数量:', nextLimit)
// 加载更多热销商品
await loadHotProducts(nextLimit)
// 检查是否还有更多数据
if (hotProducts.value.length === currentCount) {
hasMore.value = false
uni.showToast({
title: '没有更多了',
icon: 'none'
})
} else {
// 还有数据,或者是刚加载了一批
/* uni.showToast({
title: '加载完成',
icon: 'success'
}) */
}
uni.showToast({
title: '加载完成',
icon: 'success'
})
} catch (error) {
console.error('加载更多失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
loading.value = false
// 稍微延迟隐藏加载条,让用户看到
setTimeout(() => {
showLoadMore.value = false
}, 500)
}
}
// 添加到购物车
const addToCart = (product: any) => {
// 获取现有购物车数据
const cartData = uni.getStorageSync('cart')
let cartItems: any[] = []
if (cartData) {
try {
cartItems = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
const addToCart = async (product: any) => {
uni.showLoading({ title: '添加中...' })
try {
// 尝试调用 Supabase 服务添加
const success = await supabaseService.addToCart(product.id, 1)
if (success) {
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
} else {
// 失败(如未登录),回退到本地存储或提示登录
// 这里简单提示失败
uni.showToast({
title: '添加失败,请先登录',
icon: 'none'
})
}
}
// 检查商品是否已存在
const existingItem = cartItems.find((item: any) => item.id === product.id)
if (existingItem) {
existingItem.quantity++
} else {
// 添加新商品
cartItems.push({
id: product.id,
shopId: product.shopId || 'shop_default',
shopName: product.shopName || product.manufacturer || '自营店铺',
name: product.name,
price: product.price,
image: product.image,
spec: product.specification || '默认规格',
quantity: 1,
selected: true
})
}
// 保存回存储
uni.setStorageSync('cart', JSON.stringify(cartItems))
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
} catch (e) {
console.error('添加到购物车异常', e)
uni.showToast({
title: '操作异常',
icon: 'none'
})
} finally {
uni.hideLoading()
}
}
// 导航函数
@@ -718,18 +727,16 @@ const navigateToNews = () => uni.navigateTo({ url: '/pages/news/list' })
const navigateToProduct = (product: any) => {
// 使用productId如果存在作为跳转的商品ID否则使用id
const productId = product.productId || product.id
// 传递完整的参数,确保商品详情页能正确加载
// 移除 URLSearchParams 内部的 encodeURIComponent因为 append 会自动编码
// 或者直接构建 URL 字符串以确保兼容性
const name = product.name || ''
const image = product.image || '/static/product1.jpg'
const price = product.price?.toString() || '0'
const originalPrice = (product.original_price || product.originalPrice || (product.price * 1.2).toFixed(2))?.toString()
// 使用 main_image_url
const image = product.main_image_url || product.image || '/static/product1.jpg'
const price = (product.base_price || product.price || 0).toString()
const originalPrice = (product.market_price || product.original_price || (parseFloat(price) * 1.2).toFixed(2))?.toString()
// 手动构建URL避免双重编码问题
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?id=${productId}&productId=${productId}&price=${price}&originalPrice=${originalPrice}&name=${encodeURIComponent(name)}&image=${encodeURIComponent(image)}`
url: `/pages/mall/consumer/product-detail?id=${productId}&price=${price}&originalPrice=${originalPrice}&name=${encodeURIComponent(name)}&image=${encodeURIComponent(image)}`
})
}
const navigateToCategory = (item: any) => {
@@ -754,7 +761,8 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.medic-home {
width: 100%;
min-height: 100vh;
height: 100vh;
overflow: hidden;
background: #f8fafc;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif;
line-height: 1.5;
@@ -762,6 +770,12 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
flex-direction: column;
}
.main-scroll {
flex: 1;
height: 1px; /* 让 flex 生效并允许滚动 */
width: 100%;
}
/* 智能导航栏 - 重新设计布局 */
.smart-navbar {
position: fixed;
@@ -1222,13 +1236,17 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
/* 产品网格 */
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
display: block;
column-count: 2;
column-gap: 10px;
margin-top: 20px;
min-height: 500px; /* 确保有足够高度触发滚动 */
padding-bottom: 20px;
}
.product-card {
break-inside: avoid;
margin-bottom: 10px;
background: #f8f9fa;
border-radius: 12px;
overflow: hidden;
@@ -1796,6 +1814,19 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
display: none; /* 隐藏描述 */
}
.load-more-status {
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
.loading-text {
color: #888;
font-size: 14px;
}
.products-grid,
.recommend-grid {
grid-template-columns: repeat(2, 1fr); /* 手机端调整为双列显示 */
@@ -1964,7 +1995,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.products-grid {
grid-template-columns: repeat(2, 1fr);
column-count: 2;
}
.recommend-grid {
@@ -2031,7 +2062,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.products-grid {
grid-template-columns: repeat(3, 1fr);
column-count: 3;
}
.recommend-grid {
@@ -2068,7 +2099,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.products-grid {
grid-template-columns: repeat(4, 1fr);
column-count: 4;
}
.recommend-grid {
@@ -2092,7 +2123,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.products-grid {
grid-template-columns: repeat(4, 1fr);
column-count: 4;
}
.recommend-grid {

View File

@@ -180,30 +180,30 @@
class="product-card"
@click="navigateToProduct(product)"
>
<view class="product-badge" v-if="product.badge">{{ product.badge }}</view>
<view class="product-badge" v-if="product.is_hot">热销</view>
<image
class="product-image"
:src="product.image"
:src="product.main_image_url"
mode="aspectFill"
/>
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<text class="product-spec">{{ product.specification }}</text>
<!-- spec is omitted if not available -->
<view class="price-section">
<view class="current-price">
<text class="price-symbol">¥</text>
<text class="price-value">{{ product.price }}</text>
<text class="price-value">{{ product.base_price }}</text>
</view>
<text class="original-price" v-if="product.originalPrice > product.price">
¥{{ product.originalPrice }}
<text class="original-price" v-if="product.market_price != null && product.market_price! > product.base_price">
¥{{ product.market_price }}
</text>
</view>
<view class="product-meta">
<text class="manufacturer">{{ product.manufacturer }}</text>
<text class="manufacturer">{{ product.brand_name || product.shop_name || '自营' }}</text>
<view class="sales-info">
<text class="sales-count">已售{{ product.sales }}</text>
<text class="sales-count">已售{{ product.sale_count }}</text>
</view>
</view>
@@ -271,12 +271,14 @@ import { ref, reactive, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import supabaseService from '@/utils/supabaseService.uts'
import type { Product, Category } from '@/utils/supabaseService.uts'
import { getCurrentUser } from '@/utils/store.uts'
// 响应式数据
const statusBarHeight = ref(0)
const scrollHeight = ref(0)
const refreshing = ref(false)
const loading = ref(false)
const isFirstShow = ref(true)
const hasMore = ref(true)
const activeSort = ref('sales')
const activeFilter = ref('recommend')
@@ -334,13 +336,13 @@ const healthNews = [
const loadCategories = async () => {
try {
const categoriesData = await supabaseService.getCategories()
// 映射字段:将description映射为desc保持与原有结构兼容
// 映射字段:根据ml_categories表结构映射
categories.value = categoriesData.map((cat: any) => ({
id: cat.id,
name: cat.name,
icon: cat.icon || '📦',
desc: cat.description || cat.desc || '',
color: cat.color || '#4CAF50'
icon: cat.icon_url || '📦', // 使用icon_url字段
desc: cat.description || '', // 使用description字段
color: '#4CAF50' // 默认颜色表中可能没有color字段
}))
} catch (error) {
console.error('加载分类数据失败:', error)
@@ -408,6 +410,13 @@ const loadRecommendedProducts = async (limit: number = 6) => {
// 初始化数据
const initData = async () => {
// 首先确保用户资料已加载
try {
await getCurrentUser()
console.log('主页初始化:用户资料加载完成')
} catch (error) {
console.error('加载用户资料失败:', error)
}
await loadCategories()
await loadHotProducts()
await loadRecommendedProducts()
@@ -489,6 +498,22 @@ onShow(() => {
// 让分类页面在成功读取后自行清除
// 这样可以确保分类页面能正确读取到传递的数据
// 每次页面显示时尝试更新用户资料
if (!isFirstShow.value) {
getCurrentUser().then(profile => {
if (profile) {
console.log('主页onShow用户资料更新成功')
} else {
console.log('主页onShow用户资料为空可能未登录')
}
}).catch(error => {
console.error('主页onShow加载用户资料失败:', error)
})
} else {
isFirstShow.value = false
console.log('主页首次显示跳过onShow中的用户资料检查交由initData处理')
}
console.log('=== index页面onShow执行完成 ===')
})
@@ -645,46 +670,33 @@ const loadMore = async () => {
}
// 添加到购物车
const addToCart = (product: any) => {
// 获取现有购物车数据
const cartData = uni.getStorageSync('cart')
let cartItems: any[] = []
if (cartData) {
try {
cartItems = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
const addToCart = async (product: any) => {
uni.showLoading({ title: '添加中...' })
try {
// 尝试调用 Supabase 服务添加
const success = await supabaseService.addToCart(product.id, 1)
if (success) {
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
} else {
// 失败(如未登录),回退到本地存储或提示登录
// 这里简单提示失败
uni.showToast({
title: '添加失败,请先登录',
icon: 'none'
})
}
}
// 检查商品是否已存在
const existingItem = cartItems.find((item: any) => item.id === product.id)
if (existingItem) {
existingItem.quantity++
} else {
// 添加新商品
cartItems.push({
id: product.id,
shopId: product.shopId || 'shop_default',
shopName: product.shopName || product.manufacturer || '自营店铺',
name: product.name,
price: product.price,
image: product.image,
spec: product.specification || '默认规格',
quantity: 1,
selected: true
})
}
// 保存回存储
uni.setStorageSync('cart', JSON.stringify(cartItems))
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
} catch (e) {
console.error('添加到购物车异常', e)
uni.showToast({
title: '操作异常',
icon: 'none'
})
} finally {
uni.hideLoading()
}
}
// 导航函数
@@ -693,18 +705,16 @@ const navigateToNews = () => uni.navigateTo({ url: '/pages/news/list' })
const navigateToProduct = (product: any) => {
// 使用productId如果存在作为跳转的商品ID否则使用id
const productId = product.productId || product.id
// 传递完整的参数,确保商品详情页能正确加载
// 移除 URLSearchParams 内部的 encodeURIComponent因为 append 会自动编码
// 或者直接构建 URL 字符串以确保兼容性
const name = product.name || ''
const image = product.image || '/static/product1.jpg'
const price = product.price?.toString() || '0'
const originalPrice = (product.original_price || product.originalPrice || (product.price * 1.2).toFixed(2))?.toString()
// 使用 main_image_url
const image = product.main_image_url || product.image || '/static/product1.jpg'
const price = (product.base_price || product.price || 0).toString()
const originalPrice = (product.market_price || product.original_price || (parseFloat(price) * 1.2).toFixed(2))?.toString()
// 手动构建URL避免双重编码问题
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?id=${productId}&productId=${productId}&price=${price}&originalPrice=${originalPrice}&name=${encodeURIComponent(name)}&image=${encodeURIComponent(image)}`
url: `/pages/mall/consumer/product-detail?id=${productId}&price=${price}&originalPrice=${originalPrice}&name=${encodeURIComponent(name)}&image=${encodeURIComponent(image)}`
})
}
const navigateToCategory = (item: any) => {

File diff suppressed because it is too large Load Diff

View File

@@ -1,634 +0,0 @@
<!-- pages/mall/consumer/messages.uvue -->
<template>
<view class="messages-page">
<!-- 顶部标题栏 -->
<view class="messages-header">
<text class="header-title">消息</text>
<view class="header-actions">
<text class="action-icon" @click="clearAllUnread">📝</text>
</view>
</view>
<!-- 消息分类标签 -->
<view class="message-tabs">
<view
v-for="tab in messageTabs"
:key="tab.id"
:class="['tab-item', { active: activeTab === tab.id }]"
@click="switchTab(tab.id)"
>
<text class="tab-name">{{ tab.name }}</text>
<text v-if="tab.unread > 0" class="tab-badge">{{ tab.unread }}</text>
</view>
</view>
<!-- 消息列表 -->
<scroll-view
scroll-y
class="messages-content"
refresher-enabled
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
>
<!-- 系统通知 -->
<view v-if="activeTab === 'system'" class="message-section">
<view
v-for="message in systemMessages"
:key="message.id"
:class="['message-item', { unread: !message.read }]"
@click="viewSystemMessage(message)"
>
<view class="message-icon-wrapper">
<text class="message-icon">📢</text>
</view>
<view class="message-content">
<view class="message-header">
<text class="message-title">{{ message.title }}</text>
<text class="message-time">{{ message.time }}</text>
</view>
<text class="message-preview">{{ message.content }}</text>
</view>
</view>
</view>
<!-- 订单消息 -->
<view v-if="activeTab === 'order'" class="message-section">
<view
v-for="message in orderMessages"
:key="message.id"
:class="['message-item', { unread: !message.read }]"
@click="viewOrderMessage(message)"
>
<view class="message-icon-wrapper">
<text class="message-icon">📦</text>
</view>
<view class="message-content">
<view class="message-header">
<text class="message-title">{{ message.title }}</text>
<text class="message-time">{{ message.time }}</text>
</view>
<text class="message-preview">{{ message.content }}</text>
<text class="order-info" v-if="message.order_no">订单号: {{ message.order_no }}</text>
</view>
</view>
</view>
<!-- 客服消息 -->
<view v-if="activeTab === 'service'" class="message-section">
<view
v-for="message in serviceMessages"
:key="message.id"
:class="['message-item', { unread: !message.read }]"
@click="startCustomerService(message)"
>
<view class="message-icon-wrapper">
<image
v-if="message.avatar"
class="message-avatar"
:src="message.avatar"
mode="aspectFill"
/>
<text v-else class="message-icon">💁</text>
</view>
<view class="message-content">
<view class="message-header">
<text class="message-title">{{ message.title }}</text>
<text class="message-time">{{ message.time }}</text>
</view>
<text class="message-preview">{{ message.content }}</text>
</view>
</view>
</view>
<!-- 优惠活动 -->
<view v-if="activeTab === 'promo'" class="message-section">
<view
v-for="message in promoMessages"
:key="message.id"
:class="['message-item', { unread: !message.read }]"
@click="viewPromoMessage(message)"
>
<view class="message-icon-wrapper">
<text class="message-icon">🎁</text>
</view>
<view class="message-content">
<view class="message-header">
<text class="message-title">{{ message.title }}</text>
<text class="message-time">{{ message.time }}</text>
</view>
<text class="message-preview">{{ message.content }}</text>
<view v-if="message.coupon" class="coupon-tag">
<text class="coupon-text">{{ message.coupon }}优惠券</text>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="!loading && currentMessages.length === 0" class="empty-messages">
<text class="empty-icon">💬</text>
<text class="empty-title">暂无消息</text>
<text class="empty-desc">暂时没有新消息</text>
</view>
</scroll-view>
<!-- 底部固定按钮 -->
<view class="floating-action">
<button class="action-button" @click="contactCustomerService">
<text class="button-icon">💁</text>
<text class="button-text">联系客服</text>
</button>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, computed, onMounted } from 'vue'
// 响应式数据
const activeTab = ref<string>('system')
const refreshing = ref<boolean>(false)
const loading = ref<boolean>(false)
const unreadCount = ref<number>(5)
// 消息分类标签
const messageTabs = reactive([
{ id: 'system', name: '系统通知', unread: 3 },
{ id: 'order', name: '订单消息', unread: 2 },
{ id: 'service', name: '客服消息', unread: 0 },
{ id: 'promo', name: '优惠活动', unread: 1 }
])
// Mock 系统通知数据
const systemMessages = reactive([
{
id: 'sys001',
title: '系统维护通知',
content: '平台将于今晚23:00-01:00进行系统维护届时部分功能可能无法使用。',
time: '2023-11-23 15:30',
read: false,
type: 'system'
},
{
id: 'sys002',
title: '隐私政策更新',
content: '我们已更新隐私政策,请查阅相关条款。',
time: '2023-11-22 10:15',
read: true,
type: 'system'
},
{
id: 'sys003',
title: '账户安全提醒',
content: '检测到您的账户在异地登录,如果不是您本人操作,请及时修改密码。',
time: '2023-11-21 18:45',
read: false,
type: 'system'
}
])
// Mock 订单消息数据
const orderMessages = reactive([
{
id: 'order001',
title: '订单发货通知',
content: '您的订单202311230001已发货点击查看物流信息。',
time: '2023-11-23 14:20',
read: false,
type: 'order',
order_no: '202311230001'
},
{
id: 'order002',
title: '订单支付成功',
content: '您的订单202311220001支付成功商家正在备货中。',
time: '2023-11-22 09:30',
read: false,
type: 'order',
order_no: '202311220001'
},
{
id: 'order003',
title: '订单确认收货',
content: '您的订单202311210001已完成期待您的评价。',
time: '2023-11-21 16:15',
read: true,
type: 'order',
order_no: '202311210001'
}
])
// Mock 客服消息数据
const serviceMessages = reactive([
{
id: 'service001',
title: '在线客服',
content: '您好,有什么可以帮助您的吗?',
time: '2023-11-23 10:05',
read: true,
type: 'service',
avatar: 'https://picsum.photos/50/50?random=1'
},
{
id: 'service002',
title: '售后客服',
content: '关于您申请的退款,已处理完成。',
time: '2023-11-22 15:20',
read: true,
type: 'service',
avatar: 'https://picsum.photos/50/50?random=2'
}
])
// Mock 优惠活动数据
const promoMessages = reactive([
{
id: 'promo001',
title: '新人专享券',
content: '您有一张新人专享优惠券已到账有效期3天。',
time: '2023-11-23 08:00',
read: false,
type: 'promo',
coupon: '50元'
},
{
id: 'promo002',
title: '双11大促',
content: '双11狂欢购物节全场满300减50。',
time: '2023-11-22 12:30',
read: true,
type: 'promo',
coupon: '满300减50'
}
])
// 计算当前显示的消息
const currentMessages = computed(() => {
switch (activeTab.value) {
case 'system': return systemMessages
case 'order': return orderMessages
case 'service': return serviceMessages
case 'promo': return promoMessages
default: return []
}
})
// 生命周期
onMounted(() => {
loadMessages()
})
// 加载消息
const loadMessages = () => {
loading.value = true
setTimeout(() => {
// 这里应该调用API获取消息数据
loading.value = false
}, 800)
}
// 切换标签
const switchTab = (tabId: string) => {
activeTab.value = tabId
}
// 查看系统消息
const viewSystemMessage = (message: any) => {
message.read = true
uni.navigateTo({
url: `/pages/mall/consumer/message-detail?id=${message.id}&type=system`
})
}
// 查看订单消息
const viewOrderMessage = (message: any) => {
message.read = true
uni.navigateTo({
url: `/pages/mall/consumer/order-detail?id=${message.order_no}`
})
}
// 联系客服
const startCustomerService = (message: any) => {
uni.navigateTo({
url: '/pages/mall/consumer/chat'
})
}
// 查看优惠活动
const viewPromoMessage = (message: any) => {
message.read = true
uni.navigateTo({
url: `/pages/mall/consumer/coupons`
})
}
// 联系客服
const contactCustomerService = () => {
uni.navigateTo({
url: '/pages/mall/consumer/chat'
})
}
// 清除所有未读
const clearAllUnread = () => {
uni.showModal({
title: '确认操作',
content: '确定要标记所有消息为已读吗?',
success: (res) => {
if (res.confirm) {
// 标记所有消息为已读
systemMessages.forEach(msg => msg.read = true)
orderMessages.forEach(msg => msg.read = true)
serviceMessages.forEach(msg => msg.read = true)
promoMessages.forEach(msg => msg.read = true)
// 更新标签未读数
messageTabs.forEach(tab => tab.unread = 0)
uni.showToast({
title: '已标记所有消息为已读',
icon: 'success'
})
}
}
})
}
// 下拉刷新
const onRefresh = () => {
refreshing.value = true
setTimeout(() => {
loadMessages()
refreshing.value = false
uni.showToast({
title: '刷新成功',
icon: 'success'
})
}, 1000)
}
</script>
<style>
.messages-page {
width: 100%;
height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
/* 头部 */
.messages-header {
background-color: white;
padding: 15px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #eee;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.header-actions .action-icon {
font-size: 20px;
color: #666;
}
/* 消息分类标签 */
.message-tabs {
background-color: white;
display: flex;
padding: 0 15px;
border-bottom: 1px solid #eee;
}
.tab-item {
flex: 1;
padding: 15px 5px;
text-align: center;
position: relative;
border-bottom: 2px solid transparent;
}
.tab-item.active {
color: #ff5000;
border-bottom-color: #ff5000;
font-weight: bold;
}
.tab-name {
font-size: 14px;
}
.tab-badge {
position: absolute;
top: 8px;
right: 8px;
background-color: #ff5000;
color: white;
font-size: 10px;
padding: 2px 6px;
border-radius: 10px;
min-width: 16px;
text-align: center;
}
/* 消息内容区 */
.messages-content {
flex: 1;
padding-bottom: 80px; /* 为底部按钮留出空间 */
}
/* 消息项 */
.message-section {
padding: 10px;
}
.message-item {
background-color: white;
border-radius: 10px;
padding: 15px;
margin-bottom: 10px;
display: flex;
align-items: flex-start;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.message-item.unread {
background-color: #fff8f6;
border-left: 3px solid #ff5000;
}
.message-icon-wrapper {
width: 50px;
height: 50px;
border-radius: 25px;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
flex-shrink: 0;
}
.message-icon {
font-size: 24px;
}
.message-avatar {
width: 50px;
height: 50px;
border-radius: 25px;
}
.message-content {
flex: 1;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.message-title {
font-size: 16px;
color: #333;
font-weight: bold;
flex: 1;
margin-right: 10px;
}
.message-time {
font-size: 12px;
color: #999;
white-space: nowrap;
}
.message-preview {
font-size: 14px;
color: #666;
line-height: 1.4;
margin-bottom: 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.order-info {
font-size: 12px;
color: #ff5000;
background-color: #fff0e8;
padding: 3px 8px;
border-radius: 4px;
display: inline-block;
}
.coupon-tag {
display: inline-block;
background-color: #ff5000;
color: white;
font-size: 12px;
padding: 4px 10px;
border-radius: 12px;
margin-top: 5px;
}
.coupon-text {
font-size: 12px;
}
/* 空状态 */
.empty-messages {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
text-align: center;
}
.empty-icon {
font-size: 80px;
color: #ddd;
margin-bottom: 20px;
}
.empty-title {
font-size: 18px;
color: #666;
margin-bottom: 10px;
}
.empty-desc {
font-size: 14px;
color: #999;
}
/* 底部浮动按钮 */
.floating-action {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 100;
}
.action-button {
background: linear-gradient(135deg, #ff5000, #ff9500);
color: white;
border: none;
border-radius: 25px;
padding: 12px 20px;
display: flex;
align-items: center;
box-shadow: 0 4px 12px rgba(255, 80, 0, 0.3);
}
.button-icon {
font-size: 18px;
margin-right: 8px;
}
.button-text {
font-size: 14px;
font-weight: bold;
}
/* 响应式适配 */
@media screen and (max-width: 320px) {
.tab-name {
font-size: 12px;
}
.message-title {
font-size: 14px;
}
.message-preview {
font-size: 13px;
}
}
@media screen and (min-width: 415px) {
.message-item {
padding: 20px;
}
.message-icon-wrapper {
width: 60px;
height: 60px;
border-radius: 30px;
}
.message-icon {
font-size: 28px;
}
}
</style>

View File

@@ -43,8 +43,8 @@
<!-- 客服消息 -->
<view v-if="activeTab === 'service'" class="message-section">
<!-- 在线客服卡片 -->
<view class="customer-service-info">
<!-- 在线客服卡片 (hidden) -->
<!-- <view class="customer-service-info">
<view class="service-header">
<text class="service-title">康乐医药在线客服</text>
<text class="service-status online">在线</text>
@@ -69,7 +69,7 @@
<text class="category-name">药品配送</text>
</view>
</view>
</view>
</view> -->
<!-- 客服消息列表 -->
<view
@@ -214,6 +214,7 @@
<script setup lang="uts">
import { ref, reactive, computed, onMounted } from 'vue'
import { supabaseService, type Notification, type ChatMessage } from '@/utils/supabaseService.uts'
// 响应式数据
const activeTab = ref<string>('service')
@@ -222,6 +223,7 @@ const loading = ref<boolean>(false)
const unreadCount = ref<number>(12)
const statusBarHeight = ref(0)
const scrollTop = ref(0)
const scrollHeight = ref(0)
// 初始化页面布局数据
const initPage = () => {
@@ -242,182 +244,11 @@ const messageTabs = reactive([
])
// Mock 客服消息数据
const serviceMessages = reactive([
{
id: 'service001',
title: '康乐医药在线客服',
role: '官方客服',
content: '您好,我是康乐医药在线客服,有什么可以帮助您的吗?',
lastMessage: '请问有什么药品需要咨询?',
time: '刚刚',
read: false,
type: 'service',
avatar: 'https://picsum.photos/50/50?random=service1',
online: true,
unreadCount: 3,
tags: ['在线', '专业药师'],
icon: '👨‍⚕️',
color: '#4CAF50'
},
{
id: 'service002',
title: '处方药咨询',
role: '药师',
content: '关于您的处方药咨询,我们已经收到,请提供处方照片。',
lastMessage: '已收到您的处方,正在审核中...',
time: '10:30',
read: true,
type: 'service',
avatar: 'https://picsum.photos/50/50?random=service2',
online: true,
unreadCount: 0,
tags: ['处方药', '审核'],
icon: '💊',
color: '#2196F3'
},
{
id: 'service003',
title: '药品配送服务',
role: '配送客服',
content: '您的订单预计今天下午送达,请保持电话畅通。',
lastMessage: '配送员正在路上预计30分钟内送达',
time: '09:45',
read: false,
type: 'service',
avatar: 'https://picsum.photos/50/50?random=service3',
online: true,
unreadCount: 1,
tags: ['配送中', '今日达'],
icon: '🚚',
color: '#FF9800'
},
{
id: 'service004',
title: '用药指导',
role: '临床药师',
content: '关于您咨询的药品服用方法,建议饭后半小时服用。',
lastMessage: '记得按时服药,如有不适及时联系',
time: '昨天',
read: true,
type: 'service',
avatar: 'https://picsum.photos/50/50?random=service4',
online: false,
unreadCount: 0,
tags: ['用药指导', '专业'],
icon: '📋',
color: '#9C27B0'
},
{
id: 'service005',
title: '售后服务中心',
role: '售后专员',
content: '您申请的药品退换货已受理,我们会尽快处理。',
lastMessage: '退款将在3-5个工作日内退回原账户',
time: '前天',
read: false,
type: 'service',
avatar: 'https://picsum.photos/50/50?random=service5',
online: true,
unreadCount: 2,
tags: ['售后', '退换货'],
icon: '🔄',
color: '#F44336'
}
])
// Mock 系统通知数据
const systemMessages = reactive([
{
id: 'sys001',
title: '系统维护通知',
content: '平台将于今晚23:00-01:00进行系统维护届时部分功能可能无法使用。',
time: '2023-11-23 15:30',
read: false,
type: 'system',
important: true
},
{
id: 'sys002',
title: '隐私政策更新',
content: '我们已更新隐私政策,请查阅相关条款。',
time: '2023-11-22 10:15',
read: true,
type: 'system',
important: false
},
{
id: 'sys003',
title: '账户安全提醒',
content: '检测到您的账户在异地登录,如果不是您本人操作,请及时修改密码。',
time: '2023-11-21 18:45',
read: false,
type: 'system',
important: true
}
])
// Mock 订单消息数据
const orderMessages = reactive([
{
id: 'order001',
title: '订单发货通知',
content: '您的订单202311230001已发货点击查看物流信息。',
time: '2023-11-23 14:20',
read: false,
type: 'order',
order_no: '202311230001',
status: 'shipping',
statusText: '配送中'
},
{
id: 'order002',
title: '订单支付成功',
content: '您的订单202311220001支付成功商家正在备货中。',
time: '2023-11-22 09:30',
read: false,
type: 'order',
order_no: '202311220001',
status: 'processing',
statusText: '处理中'
},
{
id: 'order003',
title: '订单确认收货',
content: '您的订单202311210001已完成期待您的评价。',
time: '2023-11-21 16:15',
read: true,
type: 'order',
order_no: '202311210001',
status: 'completed',
statusText: '已完成'
}
])
const serviceMessages = reactive<any[]>([])
const systemMessages = reactive<any[]>([])
const orderMessages = reactive<any[]>([])
// Mock 优惠活动数据
const promoMessages = reactive([
{
id: 'promo001',
title: '新人专享券',
content: '您有一张新人专享优惠券已到账有效期3天。',
time: '2023-11-23 08:00',
read: false,
type: 'promo',
coupon: '50元',
expiry: '2023-11-26',
claimed: false
},
{
id: 'promo002',
title: '双11大促',
content: '双11狂欢购物节全场满300减50。',
time: '2023-11-22 12:30',
read: true,
type: 'promo',
coupon: '满300减50',
expiry: '2023-11-30',
claimed: false
}
])
const promoMessages = reactive<any[]>([])
// 计算当前显示的消息
const currentMessages = computed(() => {
@@ -432,18 +263,134 @@ const currentMessages = computed(() => {
// 生命周期
onMounted(() => {
console.log('Messages Page Mounted')
initPage()
loadMessages()
})
// 简单的日期格式化
const formatTime = (isoString: string): string => {
if (!isoString) return ''
try {
return isoString.split('T')[0]
} catch(e) {
return isoString
}
}
// 加载消息
const loadMessages = () => {
const loadMessages = async () => {
loading.value = true
setTimeout(() => {
// 模拟加载消息数据
try {
// 清空现有Mock数据
serviceMessages.length = 0
systemMessages.length = 0
orderMessages.length = 0
promoMessages.length = 0
// 1. 获取通知 (系统、订单、优惠)
const notes = await supabaseService.getUserNotifications()
notes.forEach((note: Notification) => {
// 这里使用 any 类型构建对象,以匹配 reactive 数组的结构
const item = {
id: note.id,
title: note.title,
content: note.content,
time: formatTime(note.created_at || ''),
read: note.is_read,
type: note.type, // 'system', 'order', 'promotion' => 'promo'
// 默认填充字段以避免渲染报错
avatar: note.icon_url,
important: note.type === 'system', // 简单逻辑
coupon: '点击查看',
expiry: '',
claimed: false,
order_no: '',
status: '',
statusText: '',
role: '',
lastMessage: '',
online: false,
unreadCount: 0,
tags: [],
icon: '',
color: ''
}
if (note.type === 'system') {
systemMessages.push(item)
} else if (note.type === 'order') {
orderMessages.push(item)
} else if (note.type === 'promotion') {
// map type 'promotion' to 'promo' for tab
item.type = 'promo'
promoMessages.push(item)
}
})
// 2. 获取客服消息 (Chat)
const chats = await supabaseService.getUserChatMessages()
if (chats.length > 0) {
// 简单处理:将最新一条显示为"在线客服"会话
const lastMsg = chats[0]
serviceMessages.push({
id: lastMsg.id,
title: '在线客服',
role: '客服专员',
content: lastMsg.content,
lastMessage: lastMsg.content,
time: formatTime(lastMsg.created_at || ''),
read: lastMsg.is_read,
type: 'service',
avatar: '/static/icons/service-avatar.png',
online: true,
unreadCount: chats.filter((m: ChatMessage) => !m.is_read && !m.is_from_user).length,
tags: ['官方客服'],
icon: '👩‍💼',
color: '#2196F3',
important: false,
coupon: '',
expiry: '',
claimed: false,
order_no: '',
status: '',
statusText: ''
})
} else {
// 如果没有真实数据,保留一个默认客服入口
serviceMessages.push({
id: 'default_service',
title: '在线客服',
role: '智能助手',
content: '有问题请随时联系我们',
lastMessage: '欢迎咨询',
time: '刚刚',
read: true,
type: 'service',
avatar: '/static/icons/service-avatar.png',
online: true,
unreadCount: 0,
tags: ['自动回复'],
icon: '🤖',
color: '#2196F3',
important: false,
coupon: '',
expiry: '',
claimed: false,
order_no: '',
status: '',
statusText: ''
})
}
} catch (e) {
console.error('加载消息失败', e)
} finally {
updateUnreadCount()
loading.value = false
}, 800)
}
}
// 更新未读数量

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -115,15 +115,18 @@
<view v-if="order.status === 2" class="action-buttons">
<button class="action-btn remind" @click="remindShipping(order.id)">提醒发货</button>
<button class="action-btn refund" @click.stop="onApplyRefund(order)">申请售后</button>
</view>
<view v-if="order.status === 3" class="action-buttons">
<button class="action-btn view" @click="viewLogistics(order.id)">查看物流</button>
<button class="action-btn confirm" @click="confirmReceipt(order.id)">确认收货</button>
<button class="action-btn refund" @click.stop="onApplyRefund(order)">申请售后</button>
</view>
<view v-if="order.status === 4" class="action-buttons">
<button class="action-btn review" @click="goReview(order)">评价</button>
<button class="action-btn refund" @click.stop="onApplyRefund(order)">申请售后</button>
<button class="action-btn repurchase" @click="repurchase(order)">再次购买</button>
</view>
@@ -213,6 +216,7 @@ onLoad((options) => {
if (type === 'pending') activeTab.value = 'pending'
else if (type === 'shipped') activeTab.value = 'delivering' // 映射到待收货
else if (type === 'review') activeTab.value = 'completed' // 映射到已完成
else if (type === 'refund') activeTab.value = 'all' // 申请售后默认显示全部
}
})
@@ -241,8 +245,8 @@ const loadOrders = async () => {
id: item.product_id,
name: item.product_name,
price: item.price,
image: item.product_image,
spec: item.spec || '',
image: item.image_url || '/static/default-product.png',
spec: item.specifications ? (typeof item.specifications === 'string' ? item.specifications : Object.values(item.specifications).join(' ')) : '',
quantity: item.quantity
}))
}))
@@ -451,23 +455,49 @@ const viewLogistics = (orderId: string) => {
})
}
const confirmReceipt = (orderId: string) => {
const confirmReceipt = async (orderId: string) => {
uni.showModal({
title: '确认收货',
content: '请确认您已收到商品,且商品无误',
success: (res) => {
success: async (res) => {
if (res.confirm) {
// 这里应该是实际的API调用
uni.showToast({
title: '收货成功',
icon: 'success'
})
// 更新订单状态
const index = orders.value.findIndex(order => order.id === orderId)
if (index !== -1) {
orders.value[index].status = 4
orders.value = [...orders.value]
uni.showLoading({ title: '处理中...' })
try {
const result = await supabaseService.confirmReceipt(orderId)
uni.hideLoading()
if (result.success) {
uni.showToast({
title: '收货成功',
icon: 'success'
})
// 更新本地状态
const index = orders.value.findIndex(order => order.id === orderId)
if (index !== -1) {
orders.value[index].status = 4
orders.value = [...orders.value]
}
// 跳转到评价页面
setTimeout(() => {
const order = orders.value.find(o => o.id === orderId)
if (order) {
goReview(order)
}
}, 1000)
} else {
uni.showToast({
title: result.error || '确认收货失败',
icon: 'none'
})
}
} catch (e) {
uni.hideLoading()
uni.showToast({
title: '系统异常',
icon: 'none'
})
}
}
}
@@ -503,6 +533,12 @@ const viewOrderDetail = (orderId: string) => {
})
}
const onApplyRefund = (order: any) => {
uni.navigateTo({
url: `/pages/mall/consumer/apply-refund?orderId=${order.id}`
})
}
// 导航函数
const navigateToSearch = () => {
uni.navigateTo({ url: '/pages/mall/consumer/search' })

View File

@@ -1,996 +0,0 @@
<!-- pages/mall/consumer/orders.uvue -->
<template>
<view class="orders-page">
<!-- 顶部标题栏 -->
<view class="orders-header">
<text class="header-title">我的订单</text>
<view class="header-actions">
<text class="search-icon" @click="navigateToSearch">🔍</text>
</view>
</view>
<!-- 订单状态筛选 -->
<view class="order-tabs">
<scroll-view scroll-x class="tab-scroll" :show-scrollbar="false">
<view class="tab-container">
<view
v-for="tab in orderTabs"
:key="tab.id"
:class="['tab-item', { active: activeTab === tab.id }]"
@click="switchTab(tab.id)"
>
<text class="tab-name">{{ tab.name }}</text>
<text v-if="tab.count > 0" class="tab-count">{{ tab.count }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 订单列表 -->
<scroll-view
scroll-y
class="orders-content"
refresher-enabled
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="loadMore"
>
<!-- 空状态 -->
<view v-if="!loading && orders.length === 0" class="empty-orders">
<text class="empty-icon">📦</text>
<text class="empty-title">暂无订单</text>
<text class="empty-desc">去逛逛,发现心仪的商品</text>
<button class="go-shopping-btn" @click="goShopping">去逛逛</button>
</view>
<!-- 订单列表 -->
<view v-else class="order-list">
<view
v-for="order in orders"
:key="order.id"
class="order-card"
>
<!-- 订单头部 -->
<view class="order-header">
<text class="order-no">订单号:{{ order.order_no }}</text>
<text :class="['order-status', getStatusClass(order.status)]">
{{ getStatusText(order.status) }}
</text>
</view>
<!-- 订单商品 -->
<view class="order-products">
<view
v-for="product in order.products"
:key="product.id"
class="order-product"
@click="navigateToProduct(product)"
>
<image
class="product-image"
:src="product.image"
mode="aspectFill"
/>
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<text class="product-spec">{{ product.spec }}</text>
<view class="product-footer">
<text class="product-price">¥{{ product.price }}</text>
<text class="product-quantity">×{{ product.quantity }}</text>
</view>
</view>
</view>
</view>
<!-- 订单信息 -->
<view class="order-info">
<view class="info-row">
<text class="info-label">商品合计</text>
<text class="info-value">¥{{ order.product_amount }}</text>
</view>
<view class="info-row">
<text class="info-label">运费</text>
<text class="info-value">¥{{ order.shipping_fee }}</text>
</view>
<view class="info-row total">
<text class="info-label">实付款</text>
<text class="info-value total-price">¥{{ order.total_amount }}</text>
</view>
</view>
<!-- 订单操作 -->
<view class="order-actions">
<view v-if="order.status === 1" class="action-buttons">
<button class="action-btn cancel" @click="cancelOrder(order.id)">取消订单</button>
<button class="action-btn pay" @click="payOrder(order.id)">立即支付</button>
</view>
<view v-if="order.status === 2" class="action-buttons">
<button class="action-btn remind" @click="remindShipping(order.id)">提醒发货</button>
</view>
<view v-if="order.status === 3" class="action-buttons">
<button class="action-btn view" @click="viewLogistics(order.id)">查看物流</button>
<button class="action-btn confirm" @click="confirmReceipt(order.id)">确认收货</button>
</view>
<view v-if="order.status === 4" class="action-buttons">
<button class="action-btn review" @click="goReview(order)">评价</button>
<button class="action-btn repurchase" @click="repurchase(order)">再次购买</button>
</view>
<view v-if="order.status === 5" class="action-buttons">
<button class="action-btn view" @click="viewOrderDetail(order.id)">查看详情</button>
</view>
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="loadingMore" class="loading-more">
<view class="loading-spinner"></view>
<text>加载中...</text>
</view>
<view v-if="!hasMore && orders.length > 0" class="no-more">
<text>没有更多订单了</text>
</view>
<!-- 安全区域 -->
<view class="safe-area"></view>
</scroll-view>
<!-- 底部导航 -->
<view class="tabbar-placeholder"></view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, onMounted, computed } from 'vue'
import { onShow, onLoad } from '@dcloudio/uni-app'
// // import supa from '@/components/supadb/aksupainstance.uts'
// 响应式数据
const orders = ref<any[]>([])
const loading = ref<boolean>(false)
const loadingMore = ref<boolean>(false)
const hasMore = ref<boolean>(true)
const refreshing = ref<boolean>(false)
const page = ref<number>(1)
const activeTab = ref<string>('all')
// 订单标签页
const orderTabs = reactive([
{ id: 'all', name: '全部', count: 12 },
{ id: 'pending', name: '待付款', count: 2 },
{ id: 'shipping', name: '待发货', count: 1 },
{ id: 'delivering', name: '待收货', count: 3 },
{ id: 'completed', name: '已完成', count: 5 },
{ id: 'cancelled', name: '已取消', count: 1 }
])
// Mock 订单数据
const mockOrders = [
{
id: '202311230001',
order_no: '202311230001',
status: 1, // 1:待付款 2:待发货 3:待收货 4:已完成 5:已取消
create_time: '2023-11-23 14:30:22',
product_amount: 378.00,
shipping_fee: 0.00,
total_amount: 378.00,
products: [
{
id: '1001',
name: '无线蓝牙耳机 降噪版',
price: 299.00,
image: 'https://picsum.photos/80/80?random=1',
spec: '白色',
quantity: 1
},
{
id: '1002',
name: '耳机保护套',
price: 29.00,
image: 'https://picsum.photos/80/80?random=2',
spec: '黑色',
quantity: 1
},
{
id: '1003',
name: '数据线',
price: 19.00,
image: 'https://picsum.photos/80/80?random=3',
spec: '1米',
quantity: 2
}
]
},
{
id: '202311220001',
order_no: '202311220001',
status: 2,
create_time: '2023-11-22 10:15:33',
product_amount: 199.00,
shipping_fee: 10.00,
total_amount: 209.00,
products: [
{
id: '2001',
name: '运动T恤 速干面料',
price: 79.00,
image: 'https://picsum.photos/80/80?random=4',
spec: '黑色 L',
quantity: 2
},
{
id: '2002',
name: '运动短裤',
price: 59.00,
image: 'https://picsum.photos/80/80?random=5',
spec: '黑色 M',
quantity: 1
}
]
},
{
id: '202311210001',
order_no: '202311210001',
status: 3,
create_time: '2023-11-21 16:45:12',
product_amount: 299.00,
shipping_fee: 0.00,
total_amount: 299.00,
products: [
{
id: '3001',
name: '智能手环 心率监测',
price: 199.00,
image: 'https://picsum.photos/80/80?random=6',
spec: '黑色',
quantity: 1
},
{
id: '3002',
name: '手环腕带',
price: 29.00,
image: 'https://picsum.photos/80/80?random=7',
spec: '蓝色',
quantity: 2
}
]
},
{
id: '202311200001',
order_no: '202311200001',
status: 4,
create_time: '2023-11-20 09:30:45',
product_amount: 99.00,
shipping_fee: 0.00,
total_amount: 99.00,
products: [
{
id: '4001',
name: '保温杯 500ml',
price: 49.00,
image: 'https://picsum.photos/80/80?random=8',
spec: '白色',
quantity: 2
}
]
},
{
id: '202311190001',
order_no: '202311190001',
status: 5,
create_time: '2023-11-19 14:20:18',
product_amount: 599.00,
shipping_fee: 0.00,
total_amount: 599.00,
products: [
{
id: '5001',
name: '蓝牙音箱 便携式',
price: 199.00,
image: 'https://picsum.photos/80/80?random=9',
spec: '黑色',
quantity: 3
}
]
}
]
// 计算属性:根据当前标签筛选订单
const filteredOrders = computed(() => {
if (activeTab.value === 'all') {
return orders.value
}
const statusMap: Record<string, number> = {
'pending': 1,
'shipping': 2,
'delivering': 3,
'completed': 4,
'cancelled': 5
}
const targetStatus = statusMap[activeTab.value]
return orders.value.filter(order => order.status === targetStatus)
})
// 生命周期
onLoad((options) => {
if (options['status']) {
const status = options['status'] as string
if (['all', 'pending', 'shipping', 'delivering', 'completed', 'cancelled'].includes(status)) {
activeTab.value = status
}
}
if (options['type']) {
const type = options['type'] as string
if (type === 'pending') activeTab.value = 'pending'
else if (type === 'shipped') activeTab.value = 'delivering' // 映射到待收货
else if (type === 'review') activeTab.value = 'completed' // 映射到已完成
}
})
onShow(() => {
loadOrders()
})
// 加载订单数据
const loadOrders = async () => {
loading.value = true
const userStore = uni.getStorageSync('userInfo')
const userId = userStore?.id
if (!userId) {
loading.value = false
return
}
try {
// 从本地存储获取订单
const ordersStr = uni.getStorageSync('orders')
let localOrders: any[] = []
if (ordersStr) {
localOrders = JSON.parse(ordersStr as string) as any[]
}
// 过滤当前用户的订单
// const userOrders = localOrders.filter((o: any) => o.user_id === userId)
// 暂时显示所有订单用于测试
let userOrders = localOrders
// 根据标签页过滤
let filtered = userOrders
const statusMap: Record<string, number> = {
'pending': 1,
'shipping': 2,
'delivering': 3,
'completed': 4,
'cancelled': 5
}
if (activeTab.value !== 'all') {
const targetStatus = statusMap[activeTab.value]
filtered = userOrders.filter((o: any) => o.status === targetStatus)
}
// 按时间倒序
filtered.sort((a: any, b: any) => {
const timeA = new Date(a.created_at || a.create_time).getTime()
const timeB = new Date(b.created_at || b.create_time).getTime()
return timeB - timeA
})
// 处理数据格式以适配当前页面
orders.value = filtered.map((order: any) => ({
id: order.id,
order_no: order.order_no,
status: order.status,
create_time: order.created_at || order.create_time,
product_amount: order.total_amount,
shipping_fee: order.delivery_fee,
total_amount: order.actual_amount,
products: (order.items || order.products || []).map((item: any) => ({
id: item.product_id || item.id,
name: item.product_name || item.name,
price: item.price,
image: item.product_image || item.image || '/static/default-product.png',
spec: item.sku_specifications ? formatSpec(item.sku_specifications) : (item.spec || ''),
quantity: item.quantity
}))
}))
// 更新统计数据
orderTabs[0].count = userOrders.length
orderTabs[1].count = userOrders.filter((o: any) => o.status === 1).length
orderTabs[2].count = userOrders.filter((o: any) => o.status === 2).length
orderTabs[3].count = userOrders.filter((o: any) => o.status === 3).length
orderTabs[4].count = userOrders.filter((o: any) => o.status === 4).length
orderTabs[5].count = userOrders.filter((o: any) => o.status === 5).length
} catch (err) {
console.error('加载订单异常:', err)
} finally {
loading.value = false
}
}
const formatDate = (isoString: string): string => {
if (!isoString) return ''
const date = new Date(isoString)
return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
}
const formatSpec = (specs: any): string => {
if (!specs) return ''
if (typeof specs === 'object') {
return Object.keys(specs).map(key => `${key}:${specs[key]}`).join(' ')
}
return String(specs)
}
// 切换标签
const switchTab = (tabId: string) => {
activeTab.value = tabId
page.value = 1
orders.value = []
loadOrders()
}
// 获取状态文本
const getStatusText = (status: number): string => {
const statusMap: Record<number, string> = {
1: '待付款',
2: '待发货',
3: '待收货',
4: '已完成',
5: '已取消'
}
return statusMap[status] || '未知状态'
}
// 获取状态类名
const getStatusClass = (status: number): string => {
const classMap: Record<number, string> = {
1: 'status-pending',
2: 'status-shipping',
3: 'status-delivering',
4: 'status-completed',
5: 'status-cancelled'
}
return classMap[status] || 'status-unknown'
}
// 下拉刷新
const onRefresh = () => {
refreshing.value = true
setTimeout(() => {
loadOrders()
refreshing.value = false
uni.showToast({
title: '刷新成功',
icon: 'success'
})
}, 1000)
}
// 上拉加载更多
const loadMore = () => {
if (loadingMore.value || !hasMore.value) return
// 暂未实现分页,直接返回
hasMore.value = false
}
// 订单操作函数
const cancelOrder = (orderId: string) => {
uni.showModal({
title: '确认取消',
content: '确定要取消此订单吗?',
success: (res) => {
if (res.confirm) {
// 这里应该是实际的API调用
uni.showToast({
title: '订单已取消',
icon: 'success'
})
// 更新订单状态
const index = orders.value.findIndex(order => order.id === orderId)
if (index !== -1) {
orders.value[index].status = 5
orders.value = [...orders.value]
}
}
}
})
}
const payOrder = (orderId: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${orderId}`
})
}
const remindShipping = (orderId: string) => {
uni.showToast({
title: '已提醒卖家发货',
icon: 'success'
})
}
const viewLogistics = (orderId: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/logistics?orderId=${orderId}`
})
}
const confirmReceipt = (orderId: string) => {
uni.showModal({
title: '确认收货',
content: '请确认您已收到商品,且商品无误',
success: (res) => {
if (res.confirm) {
// 这里应该是实际的API调用
uni.showToast({
title: '收货成功',
icon: 'success'
})
// 更新订单状态
const index = orders.value.findIndex(order => order.id === orderId)
if (index !== -1) {
orders.value[index].status = 4
orders.value = [...orders.value]
}
}
}
})
}
const goReview = (order: any) => {
const productIds = order.products.map((p: any) => p.id).join(',')
uni.navigateTo({
url: `/pages/mall/consumer/review?orderId=${order.id}&productIds=${productIds}`
})
}
const repurchase = (order: any) => {
uni.showModal({
title: '再次购买',
content: '确定要将这些商品加入购物车吗?',
success: (res) => {
if (res.confirm) {
// 这里应该是实际的API调用
uni.showToast({
title: '已加入购物车',
icon: 'success'
})
}
}
})
}
const viewOrderDetail = (orderId: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/order-detail?id=${orderId}`
})
}
// 导航函数
const navigateToSearch = () => {
uni.navigateTo({ url: '/pages/mall/consumer/search' })
}
const navigateToProduct = (product: any) => {
uni.navigateTo({ url: `/pages/mall/consumer/product-detail?id=${product.id}` })
}
const goShopping = () => {
uni.switchTab({ url: '/pages/mall/consumer/index' })
}
</script>
<style>
.orders-page {
width: 100%;
min-height: 100vh;
background-color: #f5f5f5;
}
/* 头部 */
.orders-header {
background-color: white;
padding: 15px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #eee;
position: sticky;
top: 0;
z-index: 10;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.header-actions .search-icon {
font-size: 20px;
color: #666;
}
/* 标签页 */
.order-tabs {
background-color: #ffffff;
border-bottom: 1px solid #e5e5e5;
position: sticky;
top: 50px;
z-index: 10;
}
.tab-scroll {
width: 100%;
white-space: nowrap;
}
.tab-container {
display: flex;
flex-direction: row;
padding: 0 10px;
min-width: 100%;
}
.tab-item {
/* 移除 flex: 1改为自适应宽度或固定最小宽度 */
padding: 15px 15px; /* 增加水平内边距 */
text-align: center;
position: relative;
display: flex;
justify-content: center;
align-items: center;
white-space: nowrap; /* 防止文字换行 */
flex-shrink: 0; /* 防止被压缩 */
}
.tab-item.active {
color: #ff5000;
font-weight: bold;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #ff5000;
}
.tab-name {
font-size: 14px;
}
.tab-count {
margin-left: 4px;
background-color: #ff5000;
color: white;
font-size: 10px;
padding: 1px 4px;
border-radius: 8px;
min-width: 12px;
text-align: center;
}
/* 内容区 */
.orders-content {
height: calc(100vh - 100px);
}
/* 空状态 */
.empty-orders {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
text-align: center;
}
.empty-icon {
font-size: 80px;
color: #ddd;
margin-bottom: 20px;
}
.empty-title {
font-size: 18px;
color: #666;
margin-bottom: 10px;
}
.empty-desc {
font-size: 14px;
color: #999;
margin-bottom: 30px;
}
.go-shopping-btn {
background-color: #ff5000;
color: white;
border: none;
border-radius: 25px;
padding: 10px 40px;
font-size: 16px;
}
/* 订单列表 */
.order-list {
padding: 10px;
}
.order-card {
background-color: white;
border-radius: 10px;
margin-bottom: 10px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
/* 订单头部 */
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-bottom: 1px solid #f5f5f5;
}
.order-no {
font-size: 14px;
color: #666;
}
.order-status {
font-size: 14px;
font-weight: bold;
}
.status-pending {
color: #ff5000;
}
.status-shipping {
color: #ff9500;
}
.status-delivering {
color: #007aff;
}
.status-completed {
color: #34c759;
}
.status-cancelled {
color: #999;
}
/* 订单商品 */
.order-products {
padding: 15px;
}
.order-product {
display: flex;
margin-bottom: 15px;
}
.order-product:last-child {
margin-bottom: 0;
}
.product-image {
width: 80px;
height: 80px;
border-radius: 8px;
margin-right: 15px;
}
.product-info {
flex: 1;
}
.product-name {
font-size: 15px;
color: #333;
margin-bottom: 5px;
display: block;
line-height: 1.4;
}
.product-spec {
font-size: 13px;
color: #999;
margin-bottom: 10px;
display: block;
}
.product-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.product-price {
font-size: 16px;
color: #ff5000;
font-weight: bold;
}
.product-quantity {
font-size: 14px;
color: #666;
}
/* 订单信息 */
.order-info {
padding: 15px;
border-top: 1px solid #f5f5f5;
border-bottom: 1px solid #f5f5f5;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-row.total {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #f5f5f5;
}
.info-label {
font-size: 14px;
color: #666;
}
.info-value {
font-size: 14px;
color: #333;
}
.total-price {
font-size: 18px;
color: #ff5000;
font-weight: bold;
}
/* 订单操作 */
.order-actions {
padding: 15px;
}
.action-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.action-btn {
padding: 6px 15px;
border-radius: 15px;
font-size: 13px;
border: 1px solid;
background: none;
}
.action-btn.cancel {
color: #666;
border-color: #ccc;
}
.action-btn.pay {
color: #ff5000;
border-color: #ff5000;
}
.action-btn.remind {
color: #666;
border-color: #ccc;
}
.action-btn.view {
color: #666;
border-color: #ccc;
}
.action-btn.confirm {
color: #34c759;
border-color: #34c759;
}
.action-btn.review {
color: #ff9500;
border-color: #ff9500;
}
.action-btn.repurchase {
color: #ff5000;
border-color: #ff5000;
}
/* 加载更多 */
.loading-more {
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid #f0f5ff;
border-top-color: #ff5000;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.no-more {
text-align: center;
color: #999;
font-size: 13px;
padding: 20px 0;
}
/* 安全区域 */
.safe-area {
height: 20px;
}
/* 底部导航占位 */
.tabbar-placeholder {
height: 50px;
background-color: #f5f5f5;
}
/* 响应式适配 */
@media screen and (max-width: 320px) {
.tab-item {
padding: 0 10px;
margin-right: 5px;
}
.action-btn {
padding: 6px 10px;
font-size: 12px;
}
}
@media screen and (min-width: 415px) {
.order-card {
margin-bottom: 15px;
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -111,7 +111,7 @@
<script setup lang="uts">
import { ref, onMounted, watch, computed, onUnmounted } from 'vue'
import { onLoad, onBackPress } from '@dcloudio/uni-app'
// import { supabase as supa } from '@/components/supadb/aksupainstance.uts'
import { supabaseService } from '@/utils/supabaseService.uts'
type PaymentMethodType = {
id: string
@@ -212,25 +212,25 @@ const updateOrderInStorage = (status: number) => {
// 加载订单信息
const loadOrderInfo = async () => {
try {
/* const { data, error } = await supa
.from('orders')
.select('order_no, actual_amount')
.eq('id', orderId.value)
.single()
if (!orderId.value) return
if (error !== null) {
console.error('加载订单信息失败:', error)
return
}
if (data) {
orderNo.value = data.order_no
amount.value = data.actual_amount || amount.value
} */
// MOCK DATA
orderNo.value = 'ORD_MOCK_' + Date.now()
// Amount already set from options or default
const order = await supabaseService.getOrderDetail(orderId.value)
if (order) {
orderNo.value = order.order_no
// Only update amount if not passed via options (options is priority for UI flow usually, but DB is source of truth)
// But checking consistency is good
const dbAmount = Number(order.total_amount)
if (dbAmount > 0) {
amount.value = dbAmount
}
if (order.items && order.items.length > 0) {
// Could update product name etc if displayed
}
} else {
// Fallback or error
console.warn('Order not found in DB', orderId.value)
if (!orderNo.value) orderNo.value = 'ORD_PENDING_' + Date.now()
}
} catch (err) {
console.error('加载订单信息异常:', err)
}
@@ -272,28 +272,12 @@ const loadPaymentMethods = () => {
// 加载用户余额
const loadUserBalance = async () => {
const userId = getCurrentUserId()
if (!userId) return
try {
// 这里假设有用户钱包表
/* const { data, error } = await supa
.from('user_wallets')
.select('balance')
.eq('user_id', userId)
.single()
if (error !== null) {
console.error('加载用户余额失败:', error)
return
}
userBalance.value = data?.balance || 0 */
// MOCK BALANCE
userBalance.value = 10000.00
const balance = await supabaseService.getUserBalance()
userBalance.value = balance
} catch (err) {
console.error('加载用户余额异常:', err)
userBalance.value = 0
}
}
@@ -346,44 +330,9 @@ const getPayButtonText = (): string => {
}
// 减少商品库存
const reduceStock = (orderId: string) => {
try {
// 读取订单
const ordersStr = uni.getStorageSync('orders')
if (!ordersStr) return
const orders = JSON.parse(ordersStr as string) as any[]
const order = orders.find((o: any) => o.id === orderId)
if (!order || !order.items) return
// 读取商品库(这里假设商品库也在本地,实际项目中通常在服务器端处理)
// 模拟:如果有本地商品缓存,则更新
/*
const productsStr = uni.getStorageSync('products')
if (productsStr) {
const products = JSON.parse(productsStr as string) as any[]
let hasChange = false
order.items.forEach((item: any) => {
const product = products.find((p: any) => p.id === item.product_id)
if (product && product.stock >= item.quantity) {
product.stock -= item.quantity
hasChange = true
console.log(`商品 ${product.name} 库存减少 ${item.quantity}, 剩余 ${product.stock}`)
}
})
if (hasChange) {
uni.setStorageSync('products', JSON.stringify(products))
}
}
*/
console.log('模拟扣减库存成功', order.items)
} catch (e) {
console.error('扣减库存失败', e)
}
}
// const reduceStock = (orderId: string) => {
// Update should happen on server side during payment processing
// }
// 确认支付
const confirmPayment = async () => {
@@ -416,33 +365,15 @@ const confirmPayment = async () => {
isPaying.value = true
try {
// 模拟支付过程
await new Promise(resolve => setTimeout(resolve, 2000))
// Call Supabase Service to handle payment
const success = await supabaseService.payOrder(orderId.value, selectedMethod.value, amount.value)
if (!success) {
throw new Error('Payment processing failed')
}
// 更新订单状态
updateOrderInStorage(2) // 2: 待发货(已支付)
// 扣减库存
reduceStock(orderId.value)
/* const { error } = await supa
.from('orders')
.update({
status: 2, // 待发货
payment_method: getPaymentMethodCode(selectedMethod.value),
payment_status: 1, // 已支付
updated_at: new Date().toISOString()
})
.eq('id', orderId.value)
if (error !== null) {
throw error
}
// 余额支付需要扣减余额
if (selectedMethod.value === 'balance') {
await updateUserBalance(-amount.value)
} */
// 支付成功
uni.showToast({
@@ -483,57 +414,6 @@ const getPaymentMethodCode = (methodId: string): number => {
return codes[methodId] || 0
}
// 更新用户余额
const updateUserBalance = async (change: number) => {
const userId = getCurrentUserId()
if (!userId) return
try {
/* const { data: wallet, error: walletError } = await supa
.from('user_wallets')
.select('balance')
.eq('user_id', userId)
.single()
if (walletError !== null) {
console.error('查询钱包失败:', walletError)
return
}
const newBalance = (wallet?.balance || 0) + change
const { error: updateError } = await supa
.from('user_wallets')
.update({ balance: newBalance })
.eq('user_id', userId)
if (updateError !== null) {
console.error('更新余额失败:', updateError)
return
}
// 记录余额变动
const { error: recordError } = await supa
.from('balance_records')
.insert({
user_id: userId,
change_amount: change,
current_balance: newBalance,
change_type: 'order_payment',
related_id: orderId.value,
remark: `订单支付: ${orderNo.value}`
})
if (recordError !== null) {
console.error('记录余额变动失败:', recordError)
}
userBalance.value = newBalance */
} catch (err) {
console.error('更新余额异常:', err)
}
}
// 输入密码
const inputPassword = (num: string) => {
if (password.value.length >= 6) return

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -52,7 +52,7 @@
</view>
<!-- 规格选择 -->
<view class="spec-section" @click="showSpecModal">
<view class="spec-section" @click="showSpecModal" v-if="productSkus.length > 0">
<text class="spec-title">规格</text>
<text class="spec-selected">{{ selectedSpec || '请选择规格' }}</text>
<text class="spec-arrow">></text>
@@ -276,6 +276,13 @@ export default {
},
methods: {
saveFootprint(productId: string) {
// 调用后端API记录足迹
supabaseService.addFootprint(productId).then(success => {
if (success) {
console.log('足迹已同步到服务器')
}
})
const footprintData = uni.getStorageSync('footprints')
let footprints: any[] = []
@@ -311,330 +318,158 @@ export default {
},
async loadProductDetail(productId: string, options: any = {}) {
// 尝试从数据库加载
let dbProductRaw = null
uni.showLoading({ title: '加载中...' })
try {
console.log('正在尝试从数据库加载商品详情:', productId)
dbProductRaw = await supabaseService.getProductById(productId)
console.log('数据库返回的商品详情 (原始数据):', dbProductRaw)
// 调试:打印数据库返回的所有字段
if (dbProductRaw) {
console.log('数据库返回字段详情:')
if (Array.isArray(dbProductRaw)) {
console.log('返回数据是数组,长度:', dbProductRaw.length)
if (dbProductRaw.length > 0) {
const firstItem = dbProductRaw[0]
console.log('数组第一个元素:', firstItem)
for (const key in firstItem) {
console.log(` ${key}:`, firstItem[key], typeof firstItem[key])
}
const dbProductResponse = await supabaseService.getProductById(productId)
let dbProduct: any = null
if (Array.isArray(dbProductResponse) && dbProductResponse.length > 0) {
dbProduct = dbProductResponse[0]
} else if (dbProductResponse && !Array.isArray(dbProductResponse)) {
dbProduct = dbProductResponse
}
if (dbProduct) {
// Map DB product to local product
this.product = {
id: dbProduct.id,
merchant_id: dbProduct.merchant_id || dbProduct.shop_id || '',
category_id: dbProduct.category_id || '',
name: dbProduct.name,
description: dbProduct.description || '',
images: [] as string[],
price: dbProduct.base_price || dbProduct.price || 0,
original_price: dbProduct.market_price || dbProduct.original_price || 0,
stock: dbProduct.available_stock || dbProduct.total_stock || dbProduct.stock || 0,
sales: dbProduct.sale_count || dbProduct.sales || 0,
status: dbProduct.status !== undefined ? dbProduct.status : 1,
created_at: dbProduct.created_at || new Date().toISOString(),
// Attributes
specification: dbProduct.specification || null,
usage: dbProduct.usage || null,
side_effects: dbProduct.side_effects || null,
precautions: dbProduct.precautions || null,
expiry_date: dbProduct.expiry_date || null,
storage_conditions: dbProduct.storage_conditions || null,
approval_number: dbProduct.approval_number || null,
tags: [] as string[]
} as ProductType
// Handle Images
if (dbProduct.image_urls) {
try {
const parsed = typeof dbProduct.image_urls === 'string' ? JSON.parse(dbProduct.image_urls) : dbProduct.image_urls
if (Array.isArray(parsed)) {
this.product.images = parsed.map((i: any) => String(i))
}
} catch (e) { console.error('Error parsing image_urls', e) }
}
} else {
console.log('返回数据是对象')
for (const key in dbProductRaw) {
console.log(` ${key}:`, dbProductRaw[key], typeof dbProductRaw[key])
// Fallback to main_image_url if no images found
if (this.product.images.length === 0 && dbProduct.main_image_url) {
this.product.images.push(dbProduct.main_image_url)
}
}
// Fallback to 'image' field (legacy)
if (this.product.images.length === 0 && dbProduct.image) {
this.product.images.push(dbProduct.image)
}
// Final fallback
if (this.product.images.length === 0) {
this.product.images.push('/static/default-product.png')
}
// Handle Tags
if (dbProduct.tags) {
try {
const parsedTags = typeof dbProduct.tags === 'string' ? JSON.parse(dbProduct.tags) : dbProduct.tags
if (Array.isArray(parsedTags)) {
this.product.tags = parsedTags.map((t: any) => String(t))
}
} catch (e) {}
}
// Handle JSON attributes if present
if (dbProduct.attributes && typeof dbProduct.attributes === 'string') {
try {
const attrs = JSON.parse(dbProduct.attributes)
if (attrs) {
// Merge attributes into product if they match keys
if (attrs.specification) this.product.specification = attrs.specification
if (attrs.usage) this.product.usage = attrs.usage
// ... augment as needed
}
} catch(e) {}
}
// Load SKUs
// this.loadProductSkus(productId) // If SKU logic exists
} else {
throw new Error('No product found')
}
} catch (e) {
console.error('Failed to load product from DB', e)
}
// 处理数据库返回数据:可能是数组或对象
let dbProduct = null
if (dbProductRaw) {
if (Array.isArray(dbProductRaw)) {
if (dbProductRaw.length > 0) {
dbProduct = dbProductRaw[0] // 取数组第一个元素
} else {
console.warn('数据库返回空数组')
}
} else {
dbProduct = dbProductRaw // 已经是对象
}
}
if (dbProduct) {
console.log('使用数据库数据渲染页面')
// 调试打印dbProduct的详细结构和类型
console.log('dbProduct类型:', typeof dbProduct)
console.log('dbProduct原型:', Object.getPrototypeOf(dbProduct))
console.log('dbProduct的键:')
for (let key in dbProduct) {
console.log(' ', key, ':', dbProduct[key], '类型:', typeof dbProduct[key])
}
// 验证必要字段,如果关键字段缺失则使用模拟数据
// 注意数据库返回的字段可能与本地ProductType不完全匹配
console.log('验证必要字段dbProduct:', dbProduct)
// 尝试多种方式访问属性
const idValue = dbProduct.id !== undefined ? dbProduct.id : (dbProduct['id'] !== undefined ? dbProduct['id'] : undefined)
const nameValue = dbProduct.name !== undefined ? dbProduct.name : (dbProduct['name'] !== undefined ? dbProduct['name'] : undefined)
// 价格字段兼容性处理:优先查找 price其次查找 base_price
let priceValue = dbProduct.price
if (priceValue === undefined || priceValue === null) {
priceValue = dbProduct.base_price
}
if (priceValue === undefined || priceValue === null) {
priceValue = dbProduct['price']
}
if (priceValue === undefined || priceValue === null) {
priceValue = dbProduct['base_price']
}
const hasId = idValue !== undefined && idValue !== null
const hasName = nameValue !== undefined && nameValue !== null
const hasPrice = priceValue !== undefined && priceValue !== null
const hasRequiredFields = dbProduct && hasId && hasName && hasPrice
console.log('字段检查 - id:', idValue, 'hasId:', hasId, 'name:', nameValue, 'hasName:', hasName, 'price:', priceValue, 'hasPrice:', hasPrice)
console.log('hasRequiredFields:', hasRequiredFields)
if (!hasRequiredFields) {
console.warn('数据库返回数据缺少必要字段,使用模拟数据')
// 继续执行会进入下面的else分支
dbProduct = null
} else {
// 更新dbProduct的字段为实际值确保后续使用正确的属性访问
if (dbProduct.id === undefined && idValue !== undefined) dbProduct.id = idValue
if (dbProduct.name === undefined && nameValue !== undefined) dbProduct.name = nameValue
if (dbProduct.price === undefined && priceValue !== undefined) dbProduct.price = priceValue
// 使用数据库数据 - 处理字段映射
// 数据库Product接口和本地ProductType接口字段可能不同
const images = [] as Array<string>
// 处理图片字段优先使用image_urls字段其次使用main_image_url
console.log('处理数据库图片字段')
// 尝试从数据库的image_urls字段获取图片JSON字符串或对象
if (dbProduct.image_urls) {
let imagesArray: any[] = []
if (typeof dbProduct.image_urls === 'string') {
try {
imagesArray = JSON.parse(dbProduct.image_urls)
} catch (e) {
console.error('解析image_urls字段失败:', e, dbProduct.image_urls)
// 尝试逗号分割
if (dbProduct.image_urls.includes(',')) {
imagesArray = dbProduct.image_urls.split(',').map((img: string) => img.trim())
}
}
} else if (Array.isArray(dbProduct.image_urls)) {
imagesArray = dbProduct.image_urls
}
if (imagesArray.length > 0) {
for (const img of imagesArray) {
if (typeof img === 'string' && img) {
images.push(img)
}
}
}
}
// 如果没有获取到相册图,但有主图,放入相册
if (dbProduct.main_image_url) {
// 如果相册里没有这张图,把它加到第一位
if (!images.includes(dbProduct.main_image_url)) {
images.unshift(dbProduct.main_image_url)
}
}
// 兼容旧字段 image
if (images.length === 0 && dbProduct.image) {
images.push(dbProduct.image)
}
// 如果仍然没有图片,使用传入的图片或默认图片
if (images.length === 0) {
if (options.image) {
images.push(decodeURIComponent(options.image as string))
} else {
images.push('/static/product1.jpg')
}
}
// 补充模拟图片如果图片数量不足3张
const needSupplementCount = 3 - images.length
if (needSupplementCount > 0) {
const supplementalImages = ['/static/product2.jpg', '/static/product3.jpg']
for (let i = 0; i < needSupplementCount && i < supplementalImages.length; i++) {
images.push(supplementalImages[i])
}
}
console.log('最终图片数组:', images)
// 映射字段数据库shop_id对应本地merchant_id
const merchantId = dbProduct.shop_id || dbProduct.merchant_id || 'merchant_001'
// 确保数值字段有效
// 优先使用 price不存在则使用 base_price
let productPrice = 0
if (typeof dbProduct.price === 'number') {
productPrice = dbProduct.price
} else if (typeof dbProduct.base_price === 'number') {
productPrice = dbProduct.base_price
} else if (priceValue !== undefined) {
// 使用上面校验时获取到的 priceValue
productPrice = Number(priceValue)
}
const stock = (dbProduct.stock != null && !isNaN(Number(dbProduct.stock))) ? Math.floor(Number(dbProduct.stock)) : ((dbProduct.total_stock != null && !isNaN(Number(dbProduct.total_stock))) ? Math.floor(Number(dbProduct.total_stock)) : 100)
const sales = (dbProduct.sales != null && !isNaN(Number(dbProduct.sales))) ? Math.floor(Number(dbProduct.sales)) : ((dbProduct.sale_count != null && !isNaN(Number(dbProduct.sale_count))) ? Math.floor(Number(dbProduct.sale_count)) : 50)
// 解析 attributes
let attributes: any = {}
if (dbProduct.attributes) {
try {
if (typeof dbProduct.attributes === 'string') {
attributes = JSON.parse(dbProduct.attributes)
} else {
attributes = dbProduct.attributes
}
} catch (e) {
console.error('解析 attributes 失败', e)
}
}
this.product = {
id: dbProduct.id || productId,
merchant_id: merchantId,
category_id: dbProduct.category_id || 'cat_001',
name: dbProduct.name || '商品名称',
description: dbProduct.description || '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。',
images: images,
price: productPrice,
original_price: (dbProduct.original_price != null && !isNaN(Number(dbProduct.original_price))) ? Number(dbProduct.original_price) : ((dbProduct.market_price != null && !isNaN(Number(dbProduct.market_price))) ? Number(dbProduct.market_price) : null),
stock: stock,
sales: sales,
status: 1,
created_at: dbProduct.created_at || '2024-01-01',
// 药品相关字段
specification: attributes.specification || dbProduct.specification || null,
usage: attributes.usage || dbProduct.usage || null,
side_effects: attributes.side_effects || dbProduct.side_effects || null,
precautions: attributes.precautions || dbProduct.precautions || null,
expiry_date: attributes.expiry_date || dbProduct.expiry_date || null,
storage_conditions: attributes.storage_conditions || dbProduct.storage_conditions || null,
approval_number: attributes.approval_number || dbProduct.approval_number || null,
tags: dbProduct.tags ? (typeof dbProduct.tags === 'string' ? JSON.parse(dbProduct.tags) : dbProduct.tags) : []
} as ProductType
console.log('页面 product 对象已更新:', this.product)
console.log('商品图片数组:', this.product.images)
console.log('商品价格:', this.product.price, '库存:', this.product.stock, '销量:', this.product.sales)
}
} else {
console.log('数据库无数据或加载失败,使用模拟数据')
// 数据库无数据时,使用原有模拟逻辑
const generatePriceFromId = (id: string): number => {
let hash = 0
for (let i = 0; i < id.length; i++) {
hash = (hash << 5) - hash + id.charCodeAt(i)
hash |= 0
}
const price = 50 + Math.abs(hash % 450)
return parseFloat(price.toFixed(2))
}
const basePrice = options.price ? parseFloat(options.price) : generatePriceFromId(productId)
const originalPrice = options.originalPrice ? parseFloat(options.originalPrice) : parseFloat((basePrice * 1.2).toFixed(2))
const productName = options.name ? decodeURIComponent(options.name) : (() => {
const productNames = ['高品质运动休闲鞋', '时尚简约双肩背包', '多功能智能手环', '便携式蓝牙音箱', '全自动雨伞', '抗菌防螨床上四件套', '不锈钢保温杯', '无线充电器', '高清行车记录仪', '智能体脂秤']
const nameIndex = Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % productNames.length
return productNames[nameIndex]
})()
const productImage = options.image ? decodeURIComponent(options.image) : '/static/product1.jpg'
const sales = 1000 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5000
const stock = 50 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 200
this.product = {
id: productId,
merchant_id: 'merchant_' + (Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5 + 1).toString().padStart(3, '0'),
category_id: 'cat_' + (Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 10 + 1).toString().padStart(3, '0'),
name: productName,
description: '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。',
images: [productImage, '/static/product2.jpg', '/static/product3.jpg'],
price: basePrice,
original_price: originalPrice,
stock: stock,
sales: sales,
status: 1,
created_at: '2024-01-15'
}
console.error('Failed to load product detail:', e)
// Fallback to options if available
this.product.id = productId
this.product.name = options.name ? decodeURIComponent(options.name) : '未知商品'
this.product.price = options.price ? parseFloat(options.price) : 0
this.product.images = options.image ? [decodeURIComponent(options.image)] : ['/static/default-product.png']
}
// 尝试加载真实商户信息
// Load Merchant and SKUs
if (this.product.merchant_id) {
await this.loadMerchantInfo(this.product.merchant_id)
}
if (this.product.id) {
this.loadProductSkus(this.product.id)
}
uni.hideLoading()
},
async loadMerchantInfo(merchantId: string) {
let realMerchantLoaded = false
// 只有当 ID 是 UUID 格式(包含-)或者是真实数据时才尝试查询
if (this.product.merchant_id && (this.product.merchant_id.includes('-') || !this.product.merchant_id.startsWith('merchant_'))) {
console.log('尝试加载商户信息:', this.product.merchant_id)
if (merchantId.includes('-') || !merchantId.startsWith('merchant_')) {
try {
const shop = await supabaseService.getShopByMerchantId(this.product.merchant_id)
const shop = await supabaseService.getShopByMerchantId(merchantId)
if (shop) {
console.log('加载到商户信息:', shop.shop_name)
// 确保字段存在,避免 undefined 导致构造失败
this.merchant = {
id: shop.id || '',
user_id: shop.merchant_id || '',
shop_name: shop.shop_name || '未命名店铺',
id: shop.id,
user_id: shop.merchant_id,
shop_name: shop.shop_name,
shop_logo: shop.shop_logo || '/static/default-shop.png',
shop_banner: shop.shop_banner || '/static/default-banner.png',
shop_description: shop.description || '',
contact_name: shop.contact_name || '店主',
contact_phone: shop.contact_phone || '',
shop_status: 1,
// 优先使用 avg_rating没有则使用默认值
rating: shop.rating_avg !== undefined && shop.rating_avg !== null ? shop.rating_avg : 4.8,
// 使用 order_count 或 product_count 作为销量/活跃度指标,如果没有则默认 0
total_sales: shop.total_sales !== undefined ? shop.total_sales : (shop.order_count !== undefined ? shop.order_count : 0),
rating: shop.rating_avg || 5.0,
total_sales: shop.total_sales || 0,
created_at: shop.created_at || new Date().toISOString()
} as MerchantType
realMerchantLoaded = true
}
} catch (e) {
console.error('加载商户信息失败', e)
console.error('Load shop failed', e)
}
}
if (!realMerchantLoaded) {
// 根据商家ID生成不同的商家信息
const merchantIndex = Math.abs(this.product.merchant_id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5
const merchantIndex = Math.abs(merchantId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5
const shopNames = ['优质好店', '品牌直营店', '官方旗舰店', '专卖店', '精品小店']
const shopDescriptions = [
'专注品质生活',
'品牌官方直营,正品保障',
'厂家直销,价格优惠',
'专注本领域十年老店',
'用心服务每一位顾客'
]
const contactNames = ['店主小王', '店长小李', '经理小张', '客服小赵', '老板小钱']
this.merchant = {
id: this.product.merchant_id,
user_id: 'user_' + (merchantIndex + 1).toString().padStart(3, '0'),
id: merchantId,
user_id: 'user_mock_' + merchantIndex,
shop_name: shopNames[merchantIndex],
shop_logo: '/static/shop-logo.png',
shop_banner: '/static/shop-banner.png',
shop_description: shopDescriptions[merchantIndex],
contact_name: contactNames[merchantIndex],
contact_phone: '138' + (10000000 + merchantIndex * 1111111).toString().substring(0, 8),
shop_description: '优质服务,正品保障',
contact_name: '店主',
contact_phone: '',
shop_status: 1,
rating: 4.5 + (merchantIndex * 0.1),
total_sales: 10000 + merchantIndex * 5000,
created_at: '2023-06-01'
}
rating: 4.8,
total_sales: 999,
created_at: '2023-01-01'
} as MerchantType
}
this.loadProductSkus(productId)
},
async loadProductSkus(productId: string) {
@@ -674,32 +509,9 @@ export default {
console.error('Fetch SKUs error', e)
}
// 模拟加载商品SKU数据
const basePrice = this.product.price
// 使用 productId 作为前缀生成唯一的 SKU ID防止不同商品的 SKU ID 冲突
this.productSkus = [
{
id: `${productId}_sku_001`,
product_id: productId,
sku_code: 'SKU001',
specifications: { color: '红色', size: 'M' },
price: basePrice,
stock: 50,
image_url: '/static/sku1.jpg',
status: 1
},
{
id: `${productId}_sku_002`,
product_id: productId,
sku_code: 'SKU002',
specifications: { color: '蓝色', size: 'L' },
price: parseFloat((basePrice * 1.1).toFixed(2)),
stock: 30,
image_url: '/static/sku2.jpg',
status: 1
}
]
// 如果没有从数据库加载到SKU则不显示规格选择直接作为无规格商品添加
// 移除之前的Mock逻辑因为Mock的ID不符合UUID格式会导致数据库错误
},
onSwiperChange(e: any) {
@@ -729,7 +541,7 @@ export default {
},
async addToCart() {
if (!this.selectedSkuId) {
if (this.productSkus.length > 0 && !this.selectedSkuId) {
uni.showToast({
title: '请选择规格',
icon: 'none'
@@ -776,7 +588,7 @@ export default {
},
buyNow() {
if (!this.selectedSkuId) {
if (this.productSkus.length > 0 && !this.selectedSkuId) {
uni.showToast({
title: '请选择规格',
icon: 'none'
@@ -784,7 +596,7 @@ export default {
return
}
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
const sku = this.selectedSkuId ? this.productSkus.find(s => s.id === this.selectedSkuId) : null
// 调试:打印价格信息
console.log('立即购买 - 商品价格信息:')
@@ -829,56 +641,53 @@ export default {
},
checkFavoriteStatus(id: string) {
const storedFavorites = uni.getStorageSync('favorites')
if (storedFavorites) {
try {
const favorites = JSON.parse(storedFavorites as string) as any[]
this.isFavorite = favorites.some(item => item.id === id)
} catch (e) {
console.error('Failed to parse favorites', e)
}
}
// console.log('product-detail checkFavoriteStatus id:', id)
this.checkFavorite(id)
},
async checkFavorite(id: string) {
const isFav = await supabaseService.checkFavorite(id)
this.isFavorite = isFav
},
toggleFavorite() {
const storedFavorites = uni.getStorageSync('favorites')
let favorites: any[] = []
async toggleFavorite() {
if (!this.product.id) return
if (storedFavorites) {
try {
favorites = JSON.parse(storedFavorites as string) as any[]
} catch (e) {
console.error('Failed to parse favorites', e)
}
// 显示loading
uni.showLoading({ title: '处理中' })
try {
// 记录操作前的状态
const wasFavorite = this.isFavorite
// 执行切换返回的是最新的状态true=已收藏false=未收藏)
const isNowFavorite = await supabaseService.toggleFavorite(this.product.id)
uni.hideLoading()
if (isNowFavorite !== wasFavorite) {
// 状态发生了改变,说明操作成功
this.isFavorite = isNowFavorite
uni.showToast({
title: isNowFavorite ? '收藏成功' : '已取消收藏',
icon: 'success'
})
} else {
// 状态未改变,说明操作失败
uni.showToast({
title: '操作失败',
icon: 'none'
})
// 确保状态同步
this.checkFavoriteStatus(this.product.id)
}
} catch (e) {
uni.hideLoading()
console.error('Toggle favorite failed', e)
uni.showToast({
title: '操作异常',
icon: 'none'
})
}
if (this.isFavorite) {
// 取消收藏
favorites = favorites.filter(item => item.id !== this.product.id)
uni.showToast({
title: '已取消收藏',
icon: 'none'
})
} else {
// 添加收藏
favorites.push({
id: this.product.id,
name: this.product.name,
price: this.product.price,
original_price: this.product.original_price, // 保存原价
image: this.product.images[0],
sales: this.product.sales,
shopId: this.merchant.id,
shopName: this.merchant.shop_name
})
uni.showToast({
title: '收藏成功',
icon: 'success'
})
}
uni.setStorageSync('favorites', JSON.stringify(favorites))
this.isFavorite = !this.isFavorite
},
goToHome() {

View File

@@ -1,909 +0,0 @@
<!-- 消费者端 - 个人中心 -->
<template>
<view class="consumer-profile">
<!-- 用户信息头部 -->
<view class="profile-header">
<image :src="userInfo.avatar_url || '/static/default-avatar.png'" class="user-avatar" @click="editProfile" />
<view class="user-info">
<text class="user-name">{{ userInfo.nickname || userInfo.phone }}</text>
<text class="user-level">{{ getUserLevel() }}</text>
<view class="user-stats">
<text class="stat-item">积分: {{ userStats.points }}</text>
<text class="stat-item">余额: ¥{{ userStats.balance }}</text>
</view>
</view>
<view class="settings-icon" @click="goToSettings">⚙️</view>
</view>
<!-- 订单状态快捷入口 -->
<view class="order-shortcuts">
<view class="section-title">我的订单</view>
<view class="order-tabs">
<view class="order-tab" @click="goToOrders('all')">
<text class="tab-icon">📋</text>
<text class="tab-text">全部订单</text>
<text v-if="orderCounts.total > 0" class="tab-badge">{{ orderCounts.total }}</text>
</view>
<view class="order-tab" @click="goToOrders('pending')">
<text class="tab-icon">💰</text>
<text class="tab-text">待支付</text>
<text v-if="orderCounts.pending > 0" class="tab-badge">{{ orderCounts.pending }}</text>
</view>
<view class="order-tab" @click="goToOrders('shipped')">
<text class="tab-icon">🚚</text>
<text class="tab-text">待收货</text>
<text v-if="orderCounts.shipped > 0" class="tab-badge">{{ orderCounts.shipped }}</text>
</view>
<view class="order-tab" @click="goToOrders('completed')">
<text class="tab-icon">⭐</text>
<text class="tab-text">待评价</text>
<text v-if="orderCounts.review > 0" class="tab-badge">{{ orderCounts.review }}</text>
</view>
</view>
</view>
<!-- 最近订单 -->
<view class="recent-orders">
<view class="section-header">
<text class="section-title">最近订单</text>
<text class="view-all" @click="goToOrders('all')">查看全部 ></text>
</view>
<view v-if="recentOrders.length === 0" class="empty-orders">
<text class="empty-text">暂无订单记录</text>
<button class="start-shopping" @click="goShopping">去逛逛</button>
</view>
<view v-for="order in recentOrders" :key="order.id" class="order-item" @click="viewOrderDetail(order)">
<view class="order-header">
<text class="order-no">订单号: {{ order.order_no }}</text>
<text class="order-status" :class="getOrderStatusClass(order.status)">{{ getOrderStatusText(order.status) }}</text>
</view>
<view class="order-content">
<image :src="getOrderMainImage(order)" class="order-image" />
<view class="order-info">
<text class="order-title">{{ getOrderTitle(order) }}</text>
<text class="order-amount">¥{{ order.actual_amount }}</text>
<text class="order-time">{{ formatTime(order.created_at) }}</text>
</view>
</view>
<view class="order-actions">
<button v-if="order.status === 1" class="action-btn pay" @click.stop="payOrder(order)">立即支付</button>
<button v-if="order.status === 3" class="action-btn confirm" @click.stop="confirmReceive(order)">确认收货</button>
<button v-if="order.status === 4" class="action-btn review" @click.stop="reviewOrder(order)">评价</button>
</view>
</view>
</view>
<!-- 我的服务 -->
<view class="my-services">
<view class="section-title">我的服务</view>
<view class="service-grid">
<view class="service-item" @click="goToCoupons">
<text class="service-icon">🎫</text>
<text class="service-text">优惠券</text>
<text v-if="serviceCounts.coupons > 0" class="service-badge">{{ serviceCounts.coupons }}</text>
</view>
<view class="service-item" @click="goToAddress">
<text class="service-icon">📍</text>
<text class="service-text">收货地址</text>
</view>
<view class="service-item" @click="goToFavorites">
<text class="service-icon">❤️</text>
<text class="service-text">我的收藏</text>
<text v-if="serviceCounts.favorites > 0" class="service-badge">{{ serviceCounts.favorites }}</text>
</view>
<view class="service-item" @click="goToFootprint">
<text class="service-icon">👣</text>
<text class="service-text">浏览足迹</text>
</view>
<view class="service-item" @click="goToRefund">
<text class="service-icon">🔄</text>
<text class="service-text">退款/售后</text>
</view>
<view class="service-item" @click="contactService">
<text class="service-icon">💬</text>
<text class="service-text">在线客服</text>
</view>
<view class="service-item" @click="goToMySubscriptions">
<text class="service-icon">🧩</text>
<text class="service-text">我的订阅</text>
</view>
<view class="service-item" @click="goToSubscriptions">
<text class="service-icon">🧩</text>
<text class="service-text">软件订阅</text>
</view>
</view>
</view>
<!-- 消费统计 -->
<view class="consumption-stats">
<view class="section-title">消费统计</view>
<view class="stats-period">
<text v-for="period in statsPeriods" :key="period.key"
class="period-tab"
:class="{ active: activeStatsPeriod === period.key }"
@click="switchStatsPeriod(period.key)">{{ period.label }}</text>
</view>
<view class="stats-content">
<view class="stat-card">
<text class="stat-value">¥{{ currentStats.total_amount }}</text>
<text class="stat-label">总消费</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ currentStats.order_count }}</text>
<text class="stat-label">订单数</text>
</view>
<view class="stat-card">
<text class="stat-value">¥{{ currentStats.avg_amount }}</text>
<text class="stat-label">平均消费</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ currentStats.save_amount }}</text>
<text class="stat-label">节省金额</text>
</view>
</view>
</view>
<!-- 账户安全 -->
<view class="account-security">
<view class="section-title">账户安全</view>
<view class="security-items">
<view class="security-item" @click="changePassword">
<text class="security-icon">🔒</text>
<text class="security-text">修改密码</text>
<text class="security-arrow">></text>
</view>
<view class="security-item" @click="bindPhone">
<text class="security-icon">📱</text>
<text class="security-text">手机绑定</text>
<view class="security-status">
<text class="status-text" :class="{ bound: userInfo.phone }">{{ userInfo.phone ? '已绑定' : '未绑定' }}</text>
<text class="security-arrow">></text>
</view>
</view>
<view class="security-item" @click="bindEmail">
<text class="security-icon">📧</text>
<text class="security-text">邮箱绑定</text>
<view class="security-status">
<text class="status-text" :class="{ bound: userInfo.email }">{{ userInfo.email ? '已绑定' : '未绑定' }}</text>
<text class="security-arrow">></text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { UserType, OrderType } from '@/types/mall-types.uts'
type UserStatsType = {
points: number
balance: number
level: number
}
type OrderCountsType = {
total: number
pending: number
shipped: number
review: number
}
type ServiceCountsType = {
coupons: number
favorites: number
}
type ConsumptionStatsType = {
total_amount: number
order_count: number
avg_amount: number
save_amount: number
}
type StatsPeriodType = {
key: string
label: string
}
export default {
data() {
return {
userInfo: {
id: '',
phone: '',
email: '',
nickname: '',
avatar_url: '',
gender: 0,
user_type: 0,
status: 0,
created_at: ''
} as UserType,
userStats: {
points: 0,
balance: 0,
level: 1
} as UserStatsType,
orderCounts: {
total: 0,
pending: 0,
shipped: 0,
review: 0
} as OrderCountsType,
serviceCounts: {
coupons: 0,
favorites: 0
} as ServiceCountsType,
recentOrders: [] as Array<OrderType>,
statsPeriods: [
{ key: 'month', label: '本月' },
{ key: 'quarter', label: '本季度' },
{ key: 'year', label: '本年' },
{ key: 'all', label: '全部' }
] as Array<StatsPeriodType>,
activeStatsPeriod: 'month',
currentStats: {
total_amount: 0,
order_count: 0,
avg_amount: 0,
save_amount: 0
} as ConsumptionStatsType
}
},
onLoad() {
this.loadUserProfile()
},
onShow() {
this.refreshData()
},
methods: {
loadUserProfile() {
// 模拟加载用户信息
this.userInfo = {
id: 'user_001',
phone: '13800138000',
email: 'user@example.com',
nickname: '张三',
avatar_url: '/static/avatar1.jpg',
gender: 1,
user_type: 1,
status: 1,
created_at: '2023-06-15T10:30:00'
}
this.userStats = {
points: 1580,
balance: 268.50,
level: 3
}
this.orderCounts = {
total: 23,
pending: 2,
shipped: 1,
review: 3
}
this.serviceCounts = {
coupons: 5,
favorites: 12
}
this.recentOrders = [
{
id: 'order_001',
order_no: 'ORD202401150001',
user_id: 'user_001',
merchant_id: 'merchant_001',
status: 3,
total_amount: 299.98,
discount_amount: 30.00,
delivery_fee: 8.00,
actual_amount: 277.98,
payment_method: 1,
payment_status: 1,
delivery_address: {},
created_at: '2024-01-15T14:30:00'
},
{
id: 'order_002',
order_no: 'ORD202401140002',
user_id: 'user_001',
merchant_id: 'merchant_002',
status: 4,
total_amount: 158.00,
discount_amount: 0,
delivery_fee: 6.00,
actual_amount: 164.00,
payment_method: 1,
payment_status: 1,
delivery_address: {},
created_at: '2024-01-14T09:20:00'
}
]
this.loadConsumptionStats()
},
loadConsumptionStats() {
// 模拟加载消费统计数据
const statsData: Record<string, ConsumptionStatsType> = {
month: {
total_amount: 1280.50,
order_count: 8,
avg_amount: 160.06,
save_amount: 85.20
},
quarter: {
total_amount: 3680.80,
order_count: 18,
avg_amount: 204.49,
save_amount: 256.30
},
year: {
total_amount: 15680.90,
order_count: 56,
avg_amount: 280.02,
save_amount: 986.50
},
all: {
total_amount: 25680.50,
order_count: 89,
avg_amount: 288.55,
save_amount: 1580.20
}
}
this.currentStats = statsData[this.activeStatsPeriod]
},
refreshData() {
// 刷新页面数据
this.loadUserProfile()
},
getUserLevel(): string {
const levels = ['新手', '铜牌会员', '银牌会员', '金牌会员', '钻石会员']
return levels[this.userStats.level] || '新手'
},
getOrderStatusText(status: number): string {
const statusTexts = ['异常', '待支付', '待发货', '待收货', '已完成', '已取消']
return statusTexts[status] || '未知'
},
getOrderStatusClass(status: number): string {
const statusClasses = ['error', 'pending', 'processing', 'shipping', 'completed', 'cancelled']
return statusClasses[status] || 'error'
},
getOrderMainImage(order: OrderType): string {
// 模拟获取订单主图
return '/static/product1.jpg'
},
getOrderTitle(order: OrderType): string {
// 模拟获取订单标题
return '精选商品等多件商品'
},
formatTime(timeStr: string): string {
const date = new Date(timeStr)
const now = new Date()
const diff = now.getTime() - date.getTime()
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
if (days === 0) {
return '今天'
} else if (days === 1) {
return '昨天'
} else {
return `${days}天前`
}
},
switchStatsPeriod(period: string) {
this.activeStatsPeriod = period
this.loadConsumptionStats()
},
editProfile() {
uni.navigateTo({
url: '/pages/mall/consumer/edit-profile'
})
},
goToSettings() {
uni.navigateTo({
url: '/pages/mall/consumer/settings'
})
},
goToOrders(type: string) {
uni.navigateTo({
url: `/pages/mall/consumer/orders?type=${type}`
})
},
goShopping() {
uni.switchTab({
url: '/pages/mall/consumer/index'
})
},
viewOrderDetail(order: OrderType) {
uni.navigateTo({
url: `/pages/mall/consumer/order-detail?orderId=${order.id}`
})
},
payOrder(order: OrderType) {
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${order.id}`
})
},
confirmReceive(order: OrderType) {
uni.showModal({
title: '确认收货',
content: '确认已收到商品吗?',
success: (res) => {
if (res.confirm) {
uni.showToast({
title: '确认收货成功',
icon: 'success'
})
this.refreshData()
}
}
})
},
reviewOrder(order: OrderType) {
uni.navigateTo({
url: `/pages/mall/consumer/review?orderId=${order.id}`
})
},
goToCoupons() {
uni.navigateTo({
url: '/pages/mall/consumer/coupons'
})
},
goToAddress() {
uni.navigateTo({
url: '/pages/mall/consumer/address'
})
},
goToFavorites() {
uni.navigateTo({
url: '/pages/mall/consumer/favorites'
})
},
goToFootprint() {
uni.navigateTo({
url: '/pages/mall/consumer/footprint'
})
},
goToRefund() {
uni.navigateTo({
url: '/pages/mall/consumer/refund'
})
},
contactService() {
uni.navigateTo({
url: '/pages/mall/service/chat'
})
},
goToMySubscriptions() {
uni.navigateTo({
url: '/pages/mall/consumer/subscription/my-subscriptions'
})
},
goToSubscriptions() {
uni.navigateTo({
url: '/pages/mall/consumer/subscription/plan-list'
})
},
changePassword() {
uni.navigateTo({
url: '/pages/mall/consumer/change-password'
})
},
bindPhone() {
uni.navigateTo({
url: '/pages/mall/consumer/bind-phone'
})
},
bindEmail() {
uni.navigateTo({
url: '/pages/mall/consumer/bind-email'
})
}
}
}
</script>
<style>
.consumer-profile {
background-color: #f5f5f5;
min-height: 100vh;
}
.profile-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 60rpx 30rpx 40rpx;
display: flex;
align-items: center;
color: #fff;
}
.user-avatar {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
margin-right: 30rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
}
.user-info {
flex: 1;
}
.user-name {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 8rpx;
}
.user-level {
font-size: 24rpx;
background-color: rgba(255, 255, 255, 0.2);
padding: 6rpx 12rpx;
border-radius: 12rpx;
margin-bottom: 15rpx;
display: inline-block;
}
.user-stats {
display: flex;
gap: 30rpx;
}
.stat-item {
font-size: 24rpx;
opacity: 0.9;
}
.settings-icon {
font-size: 32rpx;
padding: 10rpx;
}
.order-shortcuts, .recent-orders, .my-services, .consumption-stats, .account-security {
background-color: #fff;
margin-bottom: 20rpx;
padding: 30rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 25rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25rpx;
}
.view-all {
font-size: 24rpx;
color: #007aff;
}
.order-tabs {
display: flex;
justify-content: space-between;
}
.order-tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.tab-icon {
font-size: 40rpx;
margin-bottom: 10rpx;
}
.tab-text {
font-size: 24rpx;
color: #666;
}
.tab-badge {
position: absolute;
top: -8rpx;
right: 20rpx;
background-color: #ff4444;
color: #fff;
font-size: 20rpx;
padding: 4rpx 8rpx;
border-radius: 10rpx;
min-width: 32rpx;
text-align: center;
}
.empty-orders {
text-align: center;
padding: 80rpx 0;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-bottom: 30rpx;
}
.start-shopping {
background-color: #007aff;
color: #fff;
padding: 20rpx 40rpx;
border-radius: 25rpx;
font-size: 26rpx;
border: none;
}
.order-item {
padding: 25rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.order-item:last-child {
border-bottom: none;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
}
.order-no {
font-size: 26rpx;
color: #333;
}
.order-status {
font-size: 24rpx;
padding: 6rpx 12rpx;
border-radius: 10rpx;
color: #fff;
}
.order-status.pending {
background-color: #ffa726;
}
.order-status.processing {
background-color: #2196f3;
}
.order-status.shipping {
background-color: #9c27b0;
}
.order-status.completed {
background-color: #4caf50;
}
.order-content {
display: flex;
align-items: center;
margin-bottom: 15rpx;
}
.order-image {
width: 100rpx;
height: 100rpx;
border-radius: 8rpx;
margin-right: 20rpx;
}
.order-info {
flex: 1;
}
.order-title {
font-size: 26rpx;
color: #333;
margin-bottom: 8rpx;
}
.order-amount {
font-size: 28rpx;
color: #ff4444;
font-weight: bold;
margin-bottom: 5rpx;
}
.order-time {
font-size: 22rpx;
color: #999;
}
.order-actions {
display: flex;
justify-content: flex-end;
gap: 15rpx;
}
.action-btn {
padding: 12rpx 25rpx;
border-radius: 20rpx;
font-size: 24rpx;
border: none;
}
.action-btn.pay {
background-color: #ff4444;
color: #fff;
}
.action-btn.confirm {
background-color: #4caf50;
color: #fff;
}
.action-btn.review {
background-color: #ffa726;
color: #fff;
}
.service-grid {
display: flex;
flex-wrap: wrap;
gap: 30rpx;
}
.service-item {
width: 30%;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.service-icon {
font-size: 48rpx;
margin-bottom: 15rpx;
}
.service-text {
font-size: 24rpx;
color: #333;
}
.service-badge {
position: absolute;
top: -5rpx;
right: 10rpx;
background-color: #ff4444;
color: #fff;
font-size: 18rpx;
padding: 4rpx 6rpx;
border-radius: 8rpx;
min-width: 24rpx;
text-align: center;
}
.stats-period {
display: flex;
gap: 30rpx;
margin-bottom: 30rpx;
}
.period-tab {
font-size: 26rpx;
color: #666;
padding: 12rpx 24rpx;
border-radius: 20rpx;
background-color: #f0f0f0;
}
.period-tab.active {
background-color: #007aff;
color: #fff;
}
.stats-content {
display: flex;
gap: 20rpx;
}
.stat-card {
flex: 1;
text-align: center;
padding: 30rpx 0;
background-color: #f8f9fa;
border-radius: 10rpx;
}
.stat-value {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 22rpx;
color: #666;
}
.security-items {
margin-top: 25rpx;
}
.security-item {
display: flex;
align-items: center;
padding: 25rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.security-item:last-child {
border-bottom: none;
}
.security-icon {
font-size: 32rpx;
margin-right: 20rpx;
}
.security-text {
flex: 1;
font-size: 28rpx;
color: #333;
}
.security-status {
display: flex;
align-items: center;
}
.status-text {
font-size: 24rpx;
color: #999;
margin-right: 10rpx;
}
.status-text.bound {
color: #4caf50;
}
.security-arrow {
font-size: 24rpx;
color: #999;
}
</style>

View File

@@ -70,9 +70,9 @@
<text class="service-icon">🔄</text>
<text class="service-text">退款/售后</text>
</view>
<view class="service-item" @click="contactService">
<text class="service-icon">💬</text>
<text class="service-text">在线客服</text>
<view class="service-item" @click="goToOrderReviews">
<text class="service-icon">📝</text>
<text class="service-text">评价</text>
</view>
<view class="service-item" @click="goToMySubscriptions">
<text class="service-icon">🧩</text>
@@ -248,7 +248,7 @@
<script>
import { UserType, OrderType } from '@/types/mall-types.uts'
// import { supabase as supa } from '@/components/supadb/aksupainstance.uts'
import supabaseService from '@/utils/supabaseService.uts'
type UserStatsType = {
points: number
@@ -363,22 +363,27 @@ export default {
methods: {
// 加载订单数据
async loadOrders() {
const userStore = uni.getStorageSync('userInfo')
// const userId = userStore?.id
// if (!userId) return
try {
// 从本地存储加载订单数据
const storedOrders = uni.getStorageSync('orders')
let orders: any[] = []
if (storedOrders) {
orders = JSON.parse(storedOrders as string) as any[]
}
const orders = await supabaseService.getOrders()
this.allOrders = orders
// 按时间倒序
// 映射数据库字段到前端类型
this.allOrders = orders.map((o: any): OrderType => {
// 确保 status 字段存在
if (o['status'] == null && o['order_status'] != null) {
o['status'] = o['order_status']
}
// 确保 actual_amount 存在
if (o['actual_amount'] == null && o['total_amount'] != null) {
o['actual_amount'] = o['total_amount']
}
return o as OrderType
})
// 按时间倒序 (created_at)
this.allOrders.sort((a: any, b: any) => {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
const timeA = new Date(a.created_at || 0).getTime()
const timeB = new Date(b.created_at || 0).getTime()
return timeB - timeA
})
// 过滤最近的订单
@@ -388,8 +393,8 @@ export default {
this.orderCounts = {
total: this.allOrders.length,
pending: this.allOrders.filter((o: any) => o.status === 1).length,
toship: this.allOrders.filter((o: any) => o.status === 2).length, // 修复仅计算状态2为待发货
shipped: this.allOrders.filter((o: any) => o.status === 3).length, // 修复仅计算状态3为待收货
toship: this.allOrders.filter((o: any) => o.status === 2).length,
shipped: this.allOrders.filter((o: any) => o.status === 3).length,
review: this.allOrders.filter((o: any) => o.status === 4).length
}
} catch (e) {
@@ -437,11 +442,14 @@ export default {
level: 3
}
// orderCounts 将通过 loadOrders 从真实数据获取
// init with zeros
this.orderCounts = {
total: 23,
pending: 2,
shipped: 1,
review: 3
total: 0,
pending: 0,
toship: 0,
shipped: 0,
review: 0
}
this.serviceCounts = {
@@ -449,38 +457,8 @@ export default {
favorites: 12
}
this.recentOrders = [
{
id: 'order_001',
order_no: 'ORD202401150001',
user_id: 'user_001',
merchant_id: 'merchant_001',
status: 3,
total_amount: 299.98,
discount_amount: 30.00,
delivery_fee: 8.00,
actual_amount: 277.98,
payment_method: 1,
payment_status: 1,
delivery_address: {},
created_at: '2024-01-15T14:30:00'
},
{
id: 'order_002',
order_no: 'ORD202401140002',
user_id: 'user_001',
merchant_id: 'merchant_002',
status: 4,
total_amount: 158.00,
discount_amount: 0,
delivery_fee: 6.00,
actual_amount: 164.00,
payment_method: 1,
payment_status: 1,
delivery_address: {},
created_at: '2024-01-14T09:20:00'
}
]
// recentOrders 将通过 loadOrders 从真实数据获取
this.recentOrders = []
this.loadConsumptionStats()
},
@@ -546,14 +524,29 @@ export default {
return statusClasses[status] || 'error'
},
getOrderMainImage(order: OrderType): string {
// 模拟获取订单主
getOrderMainImage(order: any): string {
// 尝试从 ml_order_items 获取第一张
const items = order['ml_order_items'] as any[]
if (items && items.length > 0) {
const firstItem = items[0]
// 数据库字段通常是 image_url
const img = firstItem['image_url'] || firstItem['product_image']
if (img) return img as string
}
return '/static/product1.jpg'
},
getOrderTitle(order: OrderType): string {
// 模拟获取订单标题
return '精选商品等多件商品'
getOrderTitle(order: any): string {
const items = order['ml_order_items'] as any[]
if (items && items.length > 0) {
const firstItem = items[0]
const name = (firstItem['product_name'] || '商品') as string
if (items.length > 1) {
return `${name} 等${items.length}件商品`
}
return name
}
return '精选商品'
},
formatTime(timeStr: string): string {
@@ -669,7 +662,7 @@ export default {
goToRefund() {
uni.navigateTo({
url: '/pages/mall/consumer/refund'
url: '/pages/mall/consumer/orders?type=refund'
})
},
@@ -678,6 +671,11 @@ export default {
url: '/pages/mall/service/chat'
})
},
goToOrderReviews() {
uni.navigateTo({
url: '/pages/mall/consumer/orders?type=review'
})
},
goToMySubscriptions() {
uni.navigateTo({
url: '/pages/mall/consumer/subscription/my-subscriptions'

View File

@@ -101,7 +101,7 @@
<script setup lang="uts">
import { ref, onMounted, watch } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
import { supabaseService } from '@/utils/supabaseService.uts'
type RefundType = {
id: string
@@ -187,40 +187,48 @@ const loadRefunds = async (loadMore: boolean = false) => {
const page = loadMore ? currentPage.value + 1 : 1
let query = supa
.from('refunds')
.select(`
*,
order:order_id(
order_no,
created_at,
order_items(
*,
product:product_id(images)
)
)
`)
.eq('user_id', userId)
.order('created_at', { ascending: false })
// 根据标签页过滤
let statusList: number[] = []
if (activeTab.value === 'processing') {
query = query.in('status', [1, 2]) // 待处理和处理中
statusList = [1, 2] // 待处理和处理中
} else if (activeTab.value === 'completed') {
query = query.in('status', [3, 4, 5]) // 已完成、已取消、已拒绝
statusList = [3, 4, 5] // 已完成、已取消、已拒绝
}
// 分页
query = query.range((page - 1) * pageSize.value, page * pageSize.value - 1)
const { data, error } = await query
if (error !== null) {
console.error('加载售后记录失败:', error)
return
}
const newRefunds = data || []
const rawData = await supabaseService.getRefunds(statusList, page, pageSize.value)
// Map data to UI structure (RefundType)
const newRefunds = rawData.map((item: any): RefundType => {
const orderObj = item.order || {}
const dbItems = orderObj.ml_order_items || []
const uiItems = dbItems.map((di: any) : any => ({
id: di.id || '',
product_name: di.product_name,
sku_specifications: di.specifications,
price: 0,
quantity: di.quantity || 1,
product: { images: [di.image_url || '/static/default-product.png'] }
}))
return {
id: item.id,
user_id: item.user_id,
order_id: item.order_id,
refund_no: item.refund_no,
refund_type: item.refund_type,
refund_reason: item.refund_reason,
refund_amount: Number(item.refund_amount),
status: item.status,
// Handle missing timeline by defaulting or leaving empty
status_history: item.status_history || [],
created_at: item.created_at,
order: {
id: item.order_id,
order_no: orderObj.order_no,
created_at: orderObj.created_at,
order_items: uiItems
}
} as RefundType
})
if (loadMore) {
refunds.value.push(...newRefunds)

View File

@@ -200,7 +200,7 @@ onMounted(() => {
const loadOrderData = async () => {
try {
const { data: orderData, error: orderError } = await supa
.from('orders')
.from('ml_orders')
.select('*')
.eq('id', orderId.value)
.single()
@@ -214,7 +214,7 @@ const loadOrderData = async () => {
// 加载订单商品
const { data: itemsData, error: itemsError } = await supa
.from('order_items')
.from('ml_order_items')
.select(`
*,
product:product_id(images)
@@ -240,7 +240,7 @@ const loadOrderData = async () => {
// 加载商家信息
if (order.value.merchant_id) {
const { data: merchantData, error: merchantError } = await supa
.from('merchants')
.from('ml_shops')
.select('id, shop_name, rating')
.eq('id', order.value.merchant_id)
.single()
@@ -361,20 +361,18 @@ const submitReview = async () => {
}
// 提交商品评价
/* const productReviews = orderItems.value.map((item, index) => ({
const productReviews = orderItems.value.map((item, index) => ({
user_id: userId,
product_id: item.product_id,
order_id: orderId.value,
rating: ratings.value[index],
content: contents.value[index] || '',
images: images.value[index],
is_anonymous: anonymous.value,
is_valid: true,
created_at: new Date().toISOString()
is_anonymous: anonymous.value
}))
const { error: reviewsError } = await supa
.from('product_reviews')
.from('ml_product_reviews')
.insert(productReviews)
if (reviewsError !== null) {
@@ -385,22 +383,15 @@ const submitReview = async () => {
if (merchant.value) {
const merchantReview = {
user_id: userId,
merchant_id: merchant.value.id,
shop_id: merchant.value.id,
order_id: orderId.value,
description_rating: merchantRating.value.description,
logistics_rating: merchantRating.value.logistics,
service_rating: merchantRating.value.service,
average_rating: (
merchantRating.value.description +
merchantRating.value.logistics +
merchantRating.value.service
) / 3,
is_anonymous: anonymous.value,
created_at: new Date().toISOString()
service_rating: merchantRating.value.service
}
const { error: merchantError } = await supa
.from('merchant_reviews')
.from('ml_shop_reviews')
.insert(merchantReview)
if (merchantError !== null) {
@@ -408,18 +399,16 @@ const submitReview = async () => {
}
}
// 更新订单状态为已评价
// 更新订单状态为已评价 (如果需要标记为已评价,可以在这里处理,例如 status=5 implies Reviewed or keeping at 4)
// 这里保持为 4 (Completed)
const { error: orderError } = await supa
.from('orders')
.update({ status: 4 }) // 已完成
.from('ml_orders')
.update({ order_status: 4 })
.eq('id', orderId.value)
if (orderError !== null) {
console.error('更新订单状态失败:', orderError)
} */
// MOCK SUBMIT
await new Promise(resolve => setTimeout(resolve, 1000))
}
// 显示成功提示
uni.showToast({

View File

@@ -229,7 +229,7 @@
<script setup lang="uts">
import { ref, reactive, onMounted, computed } from 'vue'
import supabaseService from '@/utils/supabaseService.uts'
import { supabaseService } from '@/utils/supabaseService.uts'
import type { Product } from '@/utils/supabaseService.uts'
// 状态定义
@@ -248,28 +248,9 @@ const priceSortAsc = ref(false) // 价格排序是否为升序
const searchHistory = ref<string[]>([])
const hotSearchList = ref<any[]>([])
const guessList = ref<any[]>([])
const allGuessItems = ref<any[]>([]) // 缓存所有猜你喜欢商品
const searchResults = ref<any[]>([])
// 模拟数据库
const mockDatabase = {
hot: [
{ keyword: '感冒灵', hot: true },
{ keyword: '布洛芬', hot: true },
{ keyword: '口罩', hot: true },
{ keyword: '维生素C', hot: false },
{ keyword: '板蓝根', hot: false },
{ keyword: '创可贴', hot: false },
],
guess: [
{ id: 'g1', name: '医用外科口罩', price: 19.9, image: 'https://picsum.photos/200/200?random=1', sales: '1万+' },
{ id: 'g2', name: '酒精消毒液', price: 9.9, image: 'https://picsum.photos/200/200?random=2', sales: '5000+' },
{ id: 'g3', name: '电子体温计', price: 29.9, image: 'https://picsum.photos/200/200?random=3', sales: '2000+' },
{ id: 'g4', name: '碘伏消毒液', price: 5.5, image: 'https://picsum.photos/200/200?random=4', sales: '1000+' },
{ id: 'g5', name: '退热贴', price: 15.8, image: 'https://picsum.photos/200/200?random=5', sales: '3000+' },
{ id: 'g6', name: '棉签', price: 3.9, image: 'https://picsum.photos/200/200?random=6', sales: '8000+' },
]
}
// 搜索建议
const searchSuggestions = computed(() => {
if (!searchKeyword.value) return []
@@ -327,22 +308,35 @@ const initPage = () => {
}
// 加载基础数据
const loadData = () => {
// loading.value = true // 不使用全局loading避免影响搜索状态
const loadData = async () => {
isError.value = false
// 模拟网络请求
setTimeout(() => {
try {
loadSearchHistory()
hotSearchList.value = mockDatabase.hot
guessList.value = mockDatabase.guess
// loading.value = false // 不使用全局loading
} catch (e) {
isError.value = true
// loading.value = false
}
}, 500)
try {
loadSearchHistory()
// 获取热门商品作为热门搜索推荐和猜你喜欢
// 获取更多数据以便"换一批"
const hotProducts = await supabaseService.getHotProducts(30)
hotSearchList.value = hotProducts.slice(0, 10).map((p: any) => ({
keyword: p.name,
hot: true
}))
allGuessItems.value = hotProducts.map((p: any) => ({
id: p.id,
name: p.name,
price: p.base_price,
image: p.main_image_url || '/static/default.jpg',
sales: typeof p.sale_count === 'number' ? p.sale_count : 0
}))
// 初始显示随机6个
refreshGuessListItems()
} catch (e) {
console.error('Load data failed', e)
isError.value = true
}
}
// 点击重试
@@ -441,13 +435,12 @@ const selectSuggestion = (suggestion: string) => {
const currentPage = ref(1)
const performSearch = () => {
const performSearch = async () => {
// 再次强制设置状态,确保万无一失
showResults.value = true
loading.value = true
// 重置页码
currentPage.value = 1
// 保持旧数据直到新数据回来,或者依靠 loading 状态完全遮罩
// 使用 Supabase 搜索真实数据
const keyword = searchKeyword.value.trim()
@@ -462,25 +455,39 @@ const performSearch = () => {
if (activeSort.value === 'price') {
sortBy = 'price'
ascending = priceSortAsc.value
}
} else if (activeSort.value === 'default') {
sortBy = 'default'
}
supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending)
.then((response) => {
searchResults.value = response.data as any[]
hasMore.value = response.hasmore
loading.value = false
// 如果无结果,显示空状态
if (searchResults.value.length === 0) {
// empty-result 组件会自动显示
}
})
.catch((error) => {
console.error('搜索失败:', error)
loading.value = false
// 可以显示错误提示,但为了用户体验,先不显示
// 保持搜索结果为空让empty-result显示
})
try {
const response = await supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending)
searchResults.value = response.data.map((p: any) => {
let tag = ''
if (p.tags) {
try {
const tags = (typeof p.tags === 'string') ? JSON.parse(p.tags) : p.tags
if (Array.isArray(tags) && tags.length > 0) tag = String(tags[0])
} catch(e) {}
}
return {
id: p.id,
name: p.name,
image: p.main_image_url || '/static/default.jpg',
price: p.base_price,
specification: p.specification || '标准规格',
tag: tag,
sales: p.sale_count || 0
}
})
hasMore.value = response.hasmore
} catch(e) {
console.error('Search failed', e)
} finally {
loading.value = false
}
}
// 切换排序
@@ -499,7 +506,7 @@ const switchSort = (type: string) => {
performSearch()
}
const loadMore = () => {
const loadMore = async () => {
if (loading.value || !hasMore.value || !searchKeyword.value.trim()) return
loading.value = true
@@ -513,79 +520,70 @@ const loadMore = () => {
if (activeSort.value === 'price') {
sortBy = 'price'
ascending = priceSortAsc.value
}
} else if (activeSort.value === 'default') {
sortBy = 'default'
}
supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending)
.then((response) => {
searchResults.value.push(...(response.data as any[]))
hasMore.value = response.hasmore
loading.value = false
})
.catch((error) => {
console.error('加载更多失败:', error)
loading.value = false
// 加载失败时,假设没有更多数据
hasMore.value = false
})
try {
const response = await supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending)
const newItems = response.data.map((p: any) => {
let tag = ''
if (p.tags) {
try {
const tags = (typeof p.tags === 'string') ? JSON.parse(p.tags) : p.tags
if (Array.isArray(tags) && tags.length > 0) tag = String(tags[0])
} catch(e) {}
}
return {
id: p.id,
name: p.name,
image: p.main_image_url || '/static/default.jpg',
price: p.base_price,
specification: p.specification || '标准规格',
tag: tag,
sales: p.sale_count || 0
}
})
searchResults.value.push(...newItems)
hasMore.value = response.hasmore
} catch(e) {
console.error('Load more failed', e)
hasMore.value = false
} finally {
loading.value = false
}
}
const refreshGuessList = () => {
uni.showLoading({ title: '刷新中' })
setTimeout(() => {
guessList.value = guessList.value.sort(() => Math.random() - 0.5)
uni.hideLoading()
}, 500)
setTimeout(() => {
refreshGuessListItems()
uni.hideLoading()
}, 500)
}
const refreshGuessListItems = () => {
if (allGuessItems.value.length > 0) {
// 简单的随机乱序并取前6个
const shuffled = [...allGuessItems.value].sort(() => Math.random() - 0.5)
guessList.value = shuffled.slice(0, 6)
}
}
const viewProductDetail = (item: any) => {
// 跳转详情页逻辑
console.log('查看商品', item)
// 跳转详情页逻辑 - 传递必要的参数作为预加载/fallback
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${item.id}&price=${item.price}&originalPrice=${item.original_price || ''}&name=${encodeURIComponent(item.name)}&image=${encodeURIComponent(item.image)}`
url: `/pages/mall/consumer/product-detail?productId=${item.id}&price=${item.price}&name=${encodeURIComponent(item.name)}`
})
}
// 添加到购物车
// 添加到购物车 - 搜索列表无法选择规格,跳转详情页
const addToCart = (product: any) => {
// 获取现有购物车数据
const cartData = uni.getStorageSync('cart')
let cartItems: any[] = []
if (cartData) {
try {
cartItems = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
}
}
// 检查商品是否已存在
const existingItem = cartItems.find((item: any) => item.id === product.id)
if (existingItem) {
existingItem.quantity++
} else {
// 添加新商品
cartItems.push({
id: product.id,
shopId: product.shopId || 'shop_search_default',
shopName: product.shopName || (product.tag === '自营' ? '平台自营大药房' : '优质大药房'),
name: product.name,
price: product.price,
image: product.image,
spec: product.specification || '默认规格',
quantity: 1,
selected: true
})
}
// 保存回存储
uni.setStorageSync('cart', JSON.stringify(cartItems))
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
uni.showToast({ title: '请选择规格', icon: 'none' })
setTimeout(() => {
viewProductDetail(product)
}, 800)
}
const openCamera = () => {

View File

@@ -1,702 +0,0 @@
<!-- 设置页面 -->
<template>
<view class="settings-page">
<!-- 顶部栏 -->
<view class="settings-header">
<text class="back-btn" @click="goBack"></text>
<text class="header-title">设置</text>
</view>
<scroll-view class="settings-content" scroll-y>
<!-- 账户设置 -->
<view class="settings-section">
<text class="section-title">账户设置</text>
<view class="section-list">
<view class="list-item" @click="goToProfile">
<text class="item-icon">👤</text>
<text class="item-text">个人资料</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="goToAddress">
<text class="item-icon">📍</text>
<text class="item-text">收货地址</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="changePassword">
<text class="item-icon">🔒</text>
<text class="item-text">修改密码</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="bindPhone">
<text class="item-icon">📱</text>
<text class="item-text">手机绑定</text>
<view class="item-right">
<text class="item-status" :class="{ bound: userInfo.phone }">
{{ userInfo.phone ? '已绑定' : '未绑定' }}
</text>
<text class="item-arrow"></text>
</view>
</view>
<view class="list-item" @click="bindEmail">
<text class="item-icon">📧</text>
<text class="item-text">邮箱绑定</text>
<view class="item-right">
<text class="item-status" :class="{ bound: userInfo.email }">
{{ userInfo.email ? '已绑定' : '未绑定' }}
</text>
<text class="item-arrow"></text>
</view>
</view>
</view>
</view>
<!-- 消息通知 -->
<view class="settings-section">
<text class="section-title">消息通知</text>
<view class="section-list">
<view class="list-item">
<text class="item-icon">🔔</text>
<text class="item-text">订单消息</text>
<switch :checked="notifications.order" @change="toggleNotification('order')" />
</view>
<view class="list-item">
<text class="item-icon">🎁</text>
<text class="item-text">促销活动</text>
<switch :checked="notifications.promotion" @change="toggleNotification('promotion')" />
</view>
<view class="list-item">
<text class="item-icon">⭐</text>
<text class="item-text">评价提醒</text>
<switch :checked="notifications.review" @change="toggleNotification('review')" />
</view>
</view>
</view>
<!-- 隐私设置 -->
<view class="settings-section">
<text class="section-title">隐私设置</text>
<view class="section-list">
<view class="list-item">
<text class="item-icon">👁️</text>
<text class="item-text">隐藏购物记录</text>
<switch :checked="privacy.hidePurchase" @change="togglePrivacy('hidePurchase')" />
</view>
<view class="list-item">
<text class="item-icon">🔍</text>
<text class="item-text">允许通过手机号找到我</text>
<switch :checked="privacy.allowSearchByPhone" @change="togglePrivacy('allowSearchByPhone')" />
</view>
<view class="list-item">
<text class="item-icon">💬</text>
<text class="item-text">接收商家消息</text>
<switch :checked="privacy.receiveMerchantMsg" @change="togglePrivacy('receiveMerchantMsg')" />
</view>
</view>
</view>
<!-- 通用设置 -->
<view class="settings-section">
<text class="section-title">通用设置</text>
<view class="section-list">
<view class="list-item" @click="clearCache">
<text class="item-icon">🗑️</text>
<text class="item-text">清除缓存</text>
<view class="item-right">
<text class="item-cache">{{ cacheSize }}</text>
<text class="item-arrow"></text>
</view>
</view>
<view class="list-item" @click="changeLanguage">
<text class="item-icon">🌐</text>
<text class="item-text">语言设置</text>
<view class="item-right">
<text class="item-status">{{ currentLanguage }}</text>
<text class="item-arrow"></text>
</view>
</view>
<view class="list-item" @click="changeTheme">
<text class="item-icon">🎨</text>
<text class="item-text">主题设置</text>
<view class="item-right">
<text class="item-status">{{ currentTheme }}</text>
<text class="item-arrow"></text>
</view>
</view>
</view>
</view>
<!-- 关于我们 -->
<view class="settings-section">
<text class="section-title">关于我们</text>
<view class="section-list">
<view class="list-item" @click="aboutUs">
<text class="item-icon"></text>
<text class="item-text">关于商城</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="userAgreement">
<text class="item-icon">📜</text>
<text class="item-text">用户协议</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="privacyPolicy">
<text class="item-icon">🛡️</text>
<text class="item-text">隐私政策</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="checkUpdate">
<text class="item-icon">🔄</text>
<text class="item-text">检查更新</text>
<view class="item-right">
<text class="item-status">{{ appVersion }}</text>
<text class="item-arrow"></text>
</view>
</view>
</view>
</view>
<!-- 客服与反馈 -->
<view class="settings-section">
<text class="section-title">客服与反馈</text>
<view class="section-list">
<view class="list-item" @click="contactService">
<text class="item-icon">💬</text>
<text class="item-text">联系客服</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="feedback">
<text class="item-icon">📝</text>
<text class="item-text">意见反馈</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="rateApp">
<text class="item-icon">⭐</text>
<text class="item-text">给个好评</text>
<text class="item-arrow"></text>
</view>
</view>
</view>
<!-- 退出登录 -->
<view class="logout-section">
<button class="logout-btn" @click="logout">退出登录</button>
</view>
<!-- 账号注销 -->
<view class="delete-account-section">
<text class="delete-account" @click="deleteAccount">注销账号</text>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
// import supa from '@/components/supadb/aksupainstance.uts'
type UserType = {
id: string
phone: string | null
email: string | null
nickname: string | null
avatar_url: string | null
}
type NotificationType = {
order: boolean
promotion: boolean
review: boolean
}
type PrivacyType = {
hidePurchase: boolean
allowSearchByPhone: boolean
receiveMerchantMsg: boolean
}
const userInfo = ref<UserType>({
id: '',
phone: null,
email: null,
nickname: null,
avatar_url: null
})
const notifications = ref<NotificationType>({
order: true,
promotion: true,
review: true
})
const privacy = ref<PrivacyType>({
hidePurchase: false,
allowSearchByPhone: true,
receiveMerchantMsg: true
})
const cacheSize = ref<string>('0.0 MB')
const currentLanguage = ref<string>('简体中文')
const currentTheme = ref<string>('自动')
const appVersion = ref<string>('1.0.0')
// 生命周期
onMounted(() => {
loadUserInfo()
loadSettings()
})
// 加载用户信息
const loadUserInfo = () => {
const userStore = uni.getStorageSync('userInfo')
if (userStore) {
userInfo.value = userStore
}
}
// 加载设置
const loadSettings = () => {
// 从本地存储加载设置
const savedNotifications = uni.getStorageSync('userNotifications')
if (savedNotifications) {
notifications.value = savedNotifications
}
const savedPrivacy = uni.getStorageSync('userPrivacy')
if (savedPrivacy) {
privacy.value = savedPrivacy
}
// 计算缓存大小
calculateCacheSize()
// 获取应用版本
// @ts-ignore
const appInfo = uni.getAppBaseInfo()
if (appInfo?.appVersion) {
appVersion.value = appInfo.appVersion
}
}
// 计算缓存大小
const calculateCacheSize = () => {
// 这里应该计算实际缓存大小,这里使用模拟数据
cacheSize.value = '12.5 MB'
}
// 跳转到个人资料
const goToProfile = () => {
uni.navigateTo({
url: '/pages/mall/consumer/profile'
})
}
// 跳转到地址管理
const goToAddress = () => {
uni.navigateTo({
url: '/pages/mall/consumer/address'
})
}
// 修改密码
const changePassword = () => {
uni.navigateTo({
url: '/pages/user/change-password'
})
}
// 绑定手机
const bindPhone = () => {
uni.navigateTo({
url: '/pages/user/bind-phone'
})
}
// 绑定邮箱
const bindEmail = () => {
uni.navigateTo({
url: '/pages/user/bind-email'
})
}
// 切换通知设置
const toggleNotification = (type: keyof NotificationType) => {
notifications.value[type] = !notifications.value[type]
uni.setStorageSync('userNotifications', notifications.value)
}
// 切换隐私设置
const togglePrivacy = (type: keyof PrivacyType) => {
privacy.value[type] = !privacy.value[type]
uni.setStorageSync('userPrivacy', privacy.value)
}
// 清除缓存
const clearCache = () => {
uni.showModal({
title: '清除缓存',
content: `确定要清除 ${cacheSize.value} 缓存吗?`,
success: (res) => {
if (res.confirm) {
// 这里应该清除实际缓存
uni.showLoading({
title: '清除中...'
})
setTimeout(() => {
cacheSize.value = '0.0 MB'
uni.hideLoading()
uni.showToast({
title: '缓存已清除',
icon: 'success'
})
}, 1000)
}
}
})
}
// 切换语言
const changeLanguage = () => {
uni.showActionSheet({
itemList: ['简体中文', 'English', '日本語'],
success: (res) => {
const languages = ['简体中文', 'English', '日本語']
currentLanguage.value = languages[res.tapIndex]
uni.setStorageSync('appLanguage', currentLanguage.value)
uni.showToast({
title: '语言已切换',
icon: 'success'
})
}
})
}
// 切换主题
const changeTheme = () => {
uni.showActionSheet({
itemList: ['自动', '浅色模式', '深色模式'],
success: (res) => {
const themes = ['自动', '浅色模式', '深色模式']
currentTheme.value = themes[res.tapIndex]
uni.setStorageSync('appTheme', currentTheme.value)
uni.showToast({
title: '主题已切换',
icon: 'success'
})
}
})
}
// 关于我们
const aboutUs = () => {
uni.navigateTo({
url: '/pages/info/about'
})
}
// 用户协议
const userAgreement = () => {
uni.navigateTo({
url: '/pages/user/terms'
})
}
// 隐私政策
const privacyPolicy = () => {
uni.navigateTo({
url: '/pages/info/privacy'
})
}
// 检查更新
const checkUpdate = () => {
uni.showLoading({
title: '检查更新中...'
})
setTimeout(() => {
uni.hideLoading()
uni.showModal({
title: '检查更新',
content: '当前已是最新版本',
showCancel: false
})
}, 1000)
}
// 联系客服
const contactService = () => {
uni.navigateTo({
url: '/pages/mall/service/chat'
})
}
// 意见反馈
const feedback = () => {
uni.navigateTo({
url: '/pages/info/feedback'
})
}
// 给个好评
const rateApp = () => {
// 这里应该跳转到应用商店评分
uni.showModal({
title: '给个好评',
content: '如果喜欢我们的应用,请给个好评吧!',
confirmText: '去评分',
success: (res) => {
if (res.confirm) {
// 跳转到应用商店
// @ts-ignore
uni.navigateToMiniProgram({
appId: 'wx1234567890', // 示例AppID
fail: () => {
uni.showToast({
title: '跳转失败',
icon: 'none'
})
}
})
}
}
})
}
// 退出登录
const logout = () => {
uni.showModal({
title: '退出登录',
content: '确定要退出登录吗?',
success: async (res) => {
if (res.confirm) {
try {
// 调用登出接口
/*
const { error } = await supa.auth.signOut()
if (error !== null) {
console.error('登出失败:', error)
uni.showToast({
title: '登出失败',
icon: 'none'
})
return
}
*/
// 清除本地存储
uni.removeStorageSync('userInfo')
uni.removeStorageSync('token')
uni.removeStorageSync('userSettings')
// 跳转到登录页
uni.reLaunch({
url: '/pages/user/login'
})
} catch (err) {
console.error('登出异常:', err)
uni.showToast({
title: '登出失败',
icon: 'none'
})
}
}
}
})
}
// 注销账号
const deleteAccount = () => {
uni.showModal({
title: '注销账号',
content: '确定要注销账号吗?此操作不可恢复,所有数据将被删除!',
confirmText: '注销',
confirmColor: '#ff4757',
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: '处理中...'
})
try {
const userId = userInfo.value.id
// 这里应该调用注销账号的API
/*
const { error } = await supa
.from('users')
.update({ status: 0 }) // 标记为注销状态
.eq('id', userId)
if (error !== null) {
throw error
}
*/
// 清除本地存储
uni.removeStorageSync('userInfo')
uni.removeStorageSync('token')
// 提示并跳转
uni.hideLoading()
uni.showToast({
title: '账号已注销',
icon: 'success',
duration: 2000
})
setTimeout(() => {
uni.reLaunch({
url: '/pages/user/login'
})
}, 1500)
} catch (err) {
uni.hideLoading()
console.error('注销账号失败:', err)
uni.showToast({
title: '注销失败',
icon: 'none'
})
}
}
}
})
}
// 返回
const goBack = () => {
uni.navigateBack()
}
</script>
<style scoped>
.settings-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.settings-header {
background-color: #ffffff;
padding: 15px;
display: flex;
align-items: center;
border-bottom: 1px solid #e5e5e5;
}
.back-btn {
font-size: 24px;
color: #333333;
padding: 5px;
margin-right: 15px;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.settings-content {
flex: 1;
}
.settings-section {
background-color: #ffffff;
margin-bottom: 10px;
padding: 15px;
}
.section-title {
display: block;
font-size: 16px;
font-weight: bold;
color: #333333;
margin-bottom: 15px;
}
.section-list {
display: flex;
flex-direction: column;
}
.list-item {
display: flex;
align-items: center;
padding: 15px 0;
border-bottom: 1px solid #f5f5f5;
}
.list-item:last-child {
border-bottom: none;
}
.item-icon {
font-size: 20px;
margin-right: 15px;
}
.item-text {
flex: 1;
font-size: 14px;
color: #333333;
}
.item-arrow {
color: #999999;
font-size: 16px;
margin-left: 10px;
}
.item-right {
display: flex;
align-items: center;
}
.item-status {
font-size: 12px;
color: #999999;
margin-right: 10px;
}
.item-status.bound {
color: #4caf50;
}
.item-cache {
font-size: 12px;
color: #999999;
margin-right: 10px;
}
.logout-section {
background-color: #ffffff;
margin-top: 10px;
padding: 15px;
}
.logout-btn {
background-color: #ffffff;
color: #ff4757;
height: 45px;
border: 1px solid #ff4757;
border-radius: 22.5px;
font-size: 16px;
font-weight: bold;
}
.delete-account-section {
background-color: #ffffff;
padding: 20px 15px;
text-align: center;
}
.delete-account {
color: #999999;
font-size: 14px;
text-decoration: underline;
}
</style>

View File

@@ -17,11 +17,11 @@
<text class="item-text">个人资料</text>
<text class="item-arrow"></text>
</view>
<!--<view class="list-item" @click="goToAddressList">
<view class="list-item" @click="goToAddressList">
<text class="item-icon">📍</text>
<text class="item-text">收货地址</text>
<text class="item-arrow"></text>
</view>-->
</view>
<view class="list-item" @click="changePassword">
<text class="item-icon">🔒</text>
<text class="item-text">修改密码</text>
@@ -125,6 +125,18 @@
</view>
</view>
<!-- 我的服务 -->
<view class="settings-section">
<text class="section-title">我的服务</text>
<view class="section-list">
<view class="list-item" @click="goToMyReviews">
<text class="item-icon">📝</text>
<text class="item-text">我的评价</text>
<text class="item-arrow"></text>
</view>
</view>
</view>
<!-- 关于我们 -->
<view class="settings-section">
<text class="section-title">关于我们</text>
@@ -193,7 +205,7 @@
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { onBackPress } from '@dcloudio/uni-app'
// import supa from '@/components/supadb/aksupainstance.uts'
import supa from '@/components/supadb/aksupainstance.uts'
// 拦截返回事件,强制跳转到个人中心页
onBackPress((options) => {
@@ -402,24 +414,32 @@ const changeTheme = () => {
})
}
// 我的评价
const goToMyReviews = () => {
// 跳转到订单列表的已完成或者是评价相关的页面
uni.navigateTo({
url: '/pages/mall/consumer/orders?status=completed'
})
}
// 关于我们
const aboutUs = () => {
uni.navigateTo({
url: '/pages/info/about'
url: '/pages/user/terms?type=about'
})
}
// 用户协议
const userAgreement = () => {
uni.navigateTo({
url: '/pages/user/terms'
url: '/pages/user/terms?type=agreement'
})
}
// 隐私政策
const privacyPolicy = () => {
uni.navigateTo({
url: '/pages/info/privacy'
url: '/pages/user/terms?type=privacy'
})
}
@@ -442,7 +462,7 @@ const checkUpdate = () => {
// 联系客服
const contactService = () => {
uni.navigateTo({
url: '/pages/mall/service/chat'
url: '/pages/mall/consumer/chat'
})
}
@@ -486,42 +506,51 @@ const logout = () => {
success: async (res) => {
if (res.confirm) {
try {
uni.showLoading({
title: '正在退出...'
})
// 调用登出接口
/*
const { error } = await supa.auth.signOut()
if (error !== null) {
console.error('登出失败:', error)
uni.showToast({
title: '登出失败',
icon: 'none'
})
return
// 即使失败也继续清除本地状态
}
*/
// 清除本地存储
// 清除本地存储的用户信息
uni.removeStorageSync('userInfo')
uni.removeStorageSync('token')
uni.removeStorageSync('userSettings')
uni.removeStorageSync('user_id')
uni.removeStorageSync('access_token')
// 跳转到登录页
uni.reLaunch({
url: '/pages/user/login'
uni.hideLoading()
uni.showToast({
title: '已退出登录',
icon: 'success'
})
} catch (err) {
console.error('登出异常:', err)
setTimeout(() => {
uni.reLaunch({
url: '/pages/user/login'
})
}, 1000)
} catch (e) {
uni.hideLoading()
console.error('Logout Exception:', e)
uni.showToast({
title: '登出失败',
title: '退出异常',
icon: 'none'
})
// 强制退出
uni.removeStorageSync('userInfo')
uni.reLaunch({
url: '/pages/user/login'
})
}
}
}
})
}
// 注销账号
const deleteAccount = () => {
uni.showModal({
@@ -531,28 +560,32 @@ const deleteAccount = () => {
confirmColor: '#ff4757',
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: '处理中...'
})
try {
const userId = userInfo.value.id
// 这里应该调用注销账号的API
/*
const { error } = await supa
.from('users')
.update({ status: 0 }) // 标记为注销状态
.eq('id', userId)
if (error !== null) {
throw error
}
*/
uni.showLoading({
title: '注销中...'
})
const userId = userInfo.value.id || uni.getStorageSync('user_id')
if (userId) {
try {
// 标记用户状态为注销 (status=3)
await supa
.from('ml_user_profiles')
.update({ status: 3 })
.eq('user_id', userId)
} catch(e) {
console.error('Update status failed', e)
}
// 登出
await supa.auth.signOut()
}
// 清除本地存储
uni.removeStorageSync('userInfo')
uni.removeStorageSync('token')
uni.removeStorageSync('user_id')
uni.removeStorageSync('access_token')
// 提示并跳转
uni.hideLoading()

View File

@@ -0,0 +1,231 @@
# Supabase 消费者端集成指南 (Consumer Frontend Integration Guide)
本文档基于 `complete_mall_database.sql` 数据库设计,为消费者端 (Consumer App) 前端开发提供对接指引。
## 1. 核心表结构概览
所有商城相关表均以 `ml_` 开头。
| 功能模块 | 核心表 | 视图 (推荐使用) | 说明 |
| :--- | :--- | :--- | :--- |
| **用户** | `ml_user_profiles`, `ml_user_addresses` | `ml_users_view` | 用户扩展信息、收货地址 |
| **商品** | `ml_products`, `ml_categories`, `ml_product_skus` | `ml_products_detail_view` | 商品、分类、SKU库存 |
| **店铺** | `ml_shops` | - | 店铺基础信息 |
| **购物车** | `ml_shopping_cart` | - | 购物车数据 |
| **订单** | `ml_orders`, `ml_order_items` | `ml_orders_detail_view` | 订单主表及明细 |
| **营销** | `ml_user_coupons`, `ml_coupon_templates` | - | 优惠券 |
| **互动** | `ml_user_favorites`, `ml_product_reviews` | - | 收藏、评价 |
---
## 2. 关键业务场景与查询示例
### 2.1 首页展示
#### 获取一级分类
```typescript
const { data, error } = await supabase
.from('ml_categories')
.select('id, name, icon_url')
.eq('level', 1)
.eq('is_active', true)
.order('sort_order', { ascending: true });
```
#### 获取热销/推荐商品
```typescript
const { data, error } = await supabase
.from('ml_products')
.select('id, name, main_image_url, base_price, sale_count')
.eq('status', 1) // 上架状态
.eq('is_hot', true) // 热销标记
.limit(10);
```
### 2.2 商品详情页
建议优先使用 `ml_products` 表配合关联查询,或者使用视图。
#### 获取商品基础信息
```typescript
const { data, error } = await supabase
.from('ml_products')
.select(`
*,
category:ml_categories(id, name),
brand:ml_brands(id, name),
shop:ml_shops(id, shop_name, shop_logo),
skus:ml_product_skus(*),
specs:ml_product_specs(*)
`)
.eq('id', productId)
.single();
```
*注意:`skus` 和 `specs` 是通过外键关联获取的,确保前端处理好 `1:N` 的关系。*
### 2.3 购物车管理
前端需维护购物车逻辑,数据均存储在 `ml_shopping_cart`
#### 获取我的购物车 (含商品详情)
**重要**:务必使用内联查询 (`!inner` 或关联) 获取商品最新价格和图片。
```typescript
const { data, error } = await supabase
.from('ml_shopping_cart')
.select(`
id,
quantity,
selected,
sku_id,
product:ml_products!inner (
id,
name,
main_image_url,
base_price, -- 基础价格
status, -- 检查是否下架
merchant_id -- 用于店铺分组
),
sku:ml_product_skus (
id,
sku_code,
price, -- SKU价格如果有
specifications,
stock,
image_url
)
`)
.eq('user_id', currentUserId)
.order('created_at', { ascending: false });
```
#### 店铺分组逻辑 (前端处理)
前端获取数据后,应根据 `product.merchant_id` 进行分组,并聚合显示店铺名称 (需另外查询或关联 `ml_shops`)。
### 2.4 收货地址
#### 获取地址列表
```typescript
const { data, error } = await supabase
.from('ml_user_addresses')
.select('*')
.eq('user_id', currentUserId)
.eq('status', 1) // 1: 正常
.order('is_default', { ascending: false }) // 默认地址排最前
.order('updated_at', { ascending: false });
```
### 2.5 订单列表
#### 查询我的订单
```typescript
const { data, error } = await supabase
.from('ml_orders')
.select(`
id,
order_no,
total_amount,
order_status,
created_at,
items:ml_order_items (
id,
product_name,
image_url,
quantity,
price,
specifications
),
shop:ml_shops (
shop_name
)
`)
.eq('user_id', currentUserId)
.order('created_at', { ascending: false });
```
---
## 3. RLS (行级安全) 注意事项
数据库已配置 RLS 策略,前端直接调用 Supabase Client 即可,**无需在查询时手动增加 `user_id` 过滤** (除了显式需要对业务逻辑进行过滤的地方RLS 会自动兜底)。
- **`ml_shopping_cart`**: 用户只能查/改/删自己的购物车记录。
- **`ml_user_addresses`**: 用户只能查/改/删自己的地址。
- **`ml_orders`**: 用户只能查看自己的订单。
- **`ml_products`**: 设置为 `status = 1` 的商品所有人可读。
*确保在 App 启动时正确初始化 Supabase Auth 并处于登录状态。*
## 4. 推荐使用的数据库函数 (RPC)
可以直接通过 ` supabase.rpc('function_name', params)` 调用以下函数:
| 函数名 | 参数 | 描述 |
| :--- | :--- | :--- |
| `calculate_cart_total` | `p_user_id` | 计算当前用户购物车选中商品的总金额 (服务端计算更安全) |
| `get_product_available_stock` | `p_product_id`, `p_sku_id` | 获取特定商品或SKU的实时可用库存 |
| `get_user_default_address` | `p_user_id` | 快速获取用户的默认收货地址 |
### 调用示例
```typescript
// 计算购物车总价
const { data: total, error } = await supabase
.rpc('calculate_cart_total', {
p_user_id: currentUserId
});
```
## 5. 类型定义参考 (TypeScript)
为方便前端开发,以下是核心表对应的推荐接口定义:
```typescript
// 购物车项 (结合了关联查询的结果)
export interface CartItem {
id: string;
quantity: number;
selected: boolean;
product: {
id: string;
name: string;
main_image_url: string;
base_price: number;
merchant_id: string;
};
sku?: {
id: string;
price: number;
specifications: string; // JSON string
stock: number;
};
shop_name?: string; // 前端处理后注入
}
// 订单结构
export interface Order {
id: string;
order_no: string;
total_amount: number;
order_status: number; // 1:待付款 2:待发货 3:待收货 4:已完成 ...
items: Array<{
product_name: string;
image_url: string;
quantity: number;
price: number;
}>;
}
```
## 6. 特殊字段说明
- **Product Images**:
- `main_image_url`: 列表页和购物车主图。
- `image_urls`: JSONB 数组,用于商品详情轮播图。
- **Specifications**:
-`ml_product_skus` 表中 `specifications` 为 JSONB 格式 (例如 `{"color": "红色", "size": "L"}`),前端需解析展示。
- **Money**:
- 数据库使用 `DECIMAL`API 返回为 `number`,建议前端统一处理为两位小数展示。
---
*文档生成日期: 2026-02-02*