consumer模块完成90%,前端完成supabase对接
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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或者购物车ID,createOrder 中会使用 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
62
pages/mall/consumer/doc/FEATURES_&_PAGES.md
Normal file
62
pages/mall/consumer/doc/FEATURES_&_PAGES.md
Normal 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` | ✅ 正常 | 支持星级评分与文本评价。 |
|
||||
|
||||
44
pages/mall/consumer/doc/MOCK_DATA_GUIDE.md
Normal file
44
pages/mall/consumer/doc/MOCK_DATA_GUIDE.md
Normal 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. **验证**:
|
||||
* 各状态标签页下是否有对应的订单。
|
||||
* 商品图片和名称是否显示正常。
|
||||
* 在“待收货”或“已完成”订单上点击**申请售后**,验证是否跳转正确。
|
||||
62
pages/mall/consumer/doc/README.md
Normal file
62
pages/mall/consumer/doc/README.md
Normal 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)
|
||||
121
pages/mall/consumer/doc/SUPABASE_INTEGRATION.md
Normal file
121
pages/mall/consumer/doc/SUPABASE_INTEGRATION.md
Normal 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 实时推送。
|
||||
@@ -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'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
// 保存回本地存储
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新未读数量
|
||||
|
||||
1277
pages/mall/consumer/messages医药.uvue
Normal file
1277
pages/mall/consumer/messages医药.uvue
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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' })
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
|
||||
231
pages/mall/consumer/sql/SUPABASE_CONSUMER_INTEGRATION.md
Normal file
231
pages/mall/consumer/sql/SUPABASE_CONSUMER_INTEGRATION.md
Normal 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*
|
||||
Reference in New Issue
Block a user