consumerm模块完成度90%,完善消费者和商家端数据库表,商品、聊天、订单数据对接好了supabase,和商家端对接了聊天功能,安卓端编译通过了css样式,剩余几个页面在处理函数规范问题

This commit is contained in:
cyh666666
2026-02-24 17:17:49 +08:00
parent e2f1dfb097
commit e606c597ca
174 changed files with 37917 additions and 4444 deletions

View File

@@ -1,5 +1,6 @@
<template>
<view class="address-edit-page">
<view class="page-container">
<scroll-view class="address-edit-page" direction="vertical">
<view class="form-group">
<view class="form-item">
<text class="label">收货人</text>
@@ -44,16 +45,17 @@
</view>
<view class="footer-btn">
<button class="save-btn" @click="saveAddress">保存</button>
<button v-if="isEdit" class="delete-btn" @click="deleteAddress">删除收货地址</button>
</view>
<button class="save-btn" @click="saveAddress">保存</button>
<button v-if="isEdit" class="delete-btn" @click="deleteAddress">删除收货地址</button>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts'
import { supabaseService, AddAddressParams, UpdateAddressParams } from '@/utils/supabaseService.uts'
type Address = {
id: string
@@ -73,51 +75,45 @@ const regionString = ref('')
const tags = ['家', '公司', '学校']
const smartInput = ref('')
type AddressForm = {
name: string
phone: string
detail: string
isDefault: boolean
label: string
}
const formData = reactive({
name: '',
phone: '',
detail: '',
isDefault: false,
label: ''
} as {
name: string
phone: string
detail: string
isDefault: boolean
label: string
})
onLoad((options) => {
if (options['id']) {
isEdit.value = true
addressId.value = options['id'] as string
loadAddress(addressId.value)
}
})
} as AddressForm)
const loadAddress = async (id: string) => {
try {
// 从Supabase加载地址详情
const address = await supabaseService.getAddressById(id)
if (address) {
if (address != null) {
formData.name = address.recipient_name
formData.phone = address.phone
formData.detail = address.detail_address
formData.isDefault = address.is_default
formData.label = address.label || ''
formData.label = address.label ?? ''
regionString.value = `${address.province} ${address.city} ${address.district}`.trim()
} else {
// 如果Supabase没有找到尝试从本地存储加载
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
if (storedAddresses != null) {
const addresses = JSON.parse(storedAddresses as string) as Address[]
const localAddress = addresses.find(item => item.id === id)
if (localAddress) {
if (localAddress != null) {
formData.name = localAddress.name
formData.phone = localAddress.phone
formData.detail = localAddress.detail
formData.isDefault = localAddress.isDefault
formData.label = localAddress.label || ''
formData.label = localAddress.label ?? ''
regionString.value = `${localAddress.province} ${localAddress.city} ${localAddress.district}`.trim()
}
}
@@ -126,16 +122,16 @@ const loadAddress = async (id: string) => {
console.error('加载地址详情失败:', error)
// 失败时从本地存储加载
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
if (storedAddresses != null) {
try {
const addresses = JSON.parse(storedAddresses as string) as Address[]
const address = addresses.find(item => item.id === id)
if (address) {
if (address != null) {
formData.name = address.name
formData.phone = address.phone
formData.detail = address.detail
formData.isDefault = address.isDefault
formData.label = address.label || ''
formData.label = address.label ?? ''
regionString.value = `${address.province} ${address.city} ${address.district}`.trim()
}
} catch (e) {
@@ -145,6 +141,14 @@ const loadAddress = async (id: string) => {
}
}
onLoad((options) => {
if (options['id'] != null) {
isEdit.value = true
addressId.value = options['id'] as string
loadAddress(addressId.value)
}
})
const selectTag = (tag: string) => {
if (formData.label === tag) {
formData.label = ''
@@ -158,28 +162,28 @@ const onSwitchChange = (e: UniSwitchChangeEvent) => {
}
const saveAddress = async () => {
if (!formData.name) {
if (formData.name == '') {
uni.showToast({ title: '请填写收货人', icon: 'none' })
return
}
if (!formData.phone) {
if (formData.phone == '') {
uni.showToast({ title: '请填写手机号码', icon: 'none' })
return
}
if (!regionString.value) {
if (regionString.value == '') {
uni.showToast({ title: '请填写所在地区', icon: 'none' })
return
}
if (!formData.detail) {
if (formData.detail == '') {
uni.showToast({ title: '请填写详细地址', icon: 'none' })
return
}
// 简单解析地区(这里简化处理,实际应使用选择器)
const regions = regionString.value.split(' ')
const province = regions[0] || ''
const city = regions[1] || ''
const district = regions.slice(2).join(' ') || ''
const province = regions[0] ?? ''
const city = regions[1] ?? ''
const district = regions.slice(2).join(' ')
// 构建地址对象
const addressData = {
@@ -191,14 +195,25 @@ const saveAddress = async () => {
detail_address: formData.detail,
postal_code: '', // 如果需要可以添加邮政编码字段
is_default: formData.isDefault,
label: formData.label || ''
}
label: formData.label
} as AddAddressParams
let success = false
if (isEdit.value) {
// 更新地址
success = await supabaseService.updateAddress(addressId.value, addressData)
const updateData = {
recipient_name: formData.name,
phone: formData.phone,
province: province,
city: city,
district: district,
detail_address: formData.detail,
postal_code: '',
is_default: formData.isDefault,
label: formData.label
} as UpdateAddressParams
success = await supabaseService.updateAddress(addressId.value, updateData)
} else {
// 添加新地址
success = await supabaseService.addAddress(addressData)
@@ -208,7 +223,7 @@ const saveAddress = async () => {
// 同时更新本地存储作为缓存
const storedAddresses = uni.getStorageSync('addresses')
let addresses: Address[] = []
if (storedAddresses) {
if (storedAddresses != null) {
try {
addresses = JSON.parse(storedAddresses as string) as Address[]
} catch (e) {
@@ -274,72 +289,79 @@ const saveAddress = async () => {
const parseSmartInput = () => {
const input = smartInput.value.trim()
if (!input) return
if (input == '') return
// 提取手机号
const phoneRegex = /(1[3-9]\d{9})/
const phoneMatch = input.match(phoneRegex)
if (phoneMatch) {
formData.phone = phoneMatch[0]
if (phoneMatch != null) {
formData.phone = phoneMatch[0] ?? ''
}
// 提取姓名取第一个2-4位中文
const nameRegex = /([\u4e00-\u9fa5]{2,4})/
const nameMatch = input.match(nameRegex)
if (nameMatch) {
formData.name = nameMatch[0]
if (nameMatch != null) {
formData.name = nameMatch[0] ?? ''
}
// 去掉姓名和电话后剩余作为地址
let addrText = input
if (formData.name) addrText = addrText.replace(formData.name, '')
if (formData.phone) addrText = addrText.replace(formData.phone, '')
if (formData.name != '') addrText = addrText.replace(formData.name, '')
if (formData.phone != '') addrText = addrText.replace(formData.phone, '')
addrText = addrText.replace(/[,;\s]+/g, ' ').trim()
// 解析省市区
const pattern1 = /^(.*?省)?(.*?市)?(.*?[区县])?(.*)$/
const m = addrText.match(pattern1)
if (m) {
const [, province, city, district, detail] = m
regionString.value = `${(province||'').trim()} ${(city||'').trim()} ${(district||'').trim()}`.trim()
formData.detail = (detail||'').trim()
if (m != null) {
const province = m[1] ?? ''
const city = m[2] ?? ''
const district = m[3] ?? ''
const detail = m[4] ?? ''
regionString.value = `${province.trim()} ${city.trim()} ${district.trim()}`.trim()
formData.detail = detail.trim()
} else {
formData.detail = addrText
}
}
const deleteAddress = async () => {
const deleteAddress = () => {
uni.showModal({
title: '提示',
content: '确定要删除该地址吗?',
success: async (res) => {
success: (res: UniShowModalResult) => {
if (res.confirm) {
// 调用Supabase服务删除地址
const success = await supabaseService.deleteAddress(addressId.value)
if (success) {
// 同时从本地存储中移除
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
let addresses = JSON.parse(storedAddresses as string) as Address[]
addresses = addresses.filter(item => item.id !== addressId.value)
uni.setStorageSync('addresses', JSON.stringify(addresses))
supabaseService.deleteAddress(addressId.value).then((success) => {
if (success) {
// 同时从本地存储中移除
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses != null) {
try {
let addresses = JSON.parse(storedAddresses as string) as Address[]
addresses = addresses.filter(item => item.id !== addressId.value)
uni.setStorageSync('addresses', JSON.stringify(addresses))
} catch (e) {
console.error('解析本地地址数据失败', e)
}
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
console.error('删除地址失败')
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
console.error('删除地址失败')
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
})
}
}
})
@@ -347,10 +369,14 @@ const deleteAddress = async () => {
</script>
<style>
.page-container {
flex: 1;
display: flex;
flex-direction: column;
}
.address-edit-page {
min-height: 100vh;
background-color: #f5f5f5;
padding-top: 15px;
flex: 1; /* Replace min-height: 100vh */
}
.form-group {

View File

@@ -51,7 +51,7 @@ type Address = {
const addresses = ref<Address[]>([])
const selectionMode = ref<boolean>(false)
let openerEventChannel: any = null
let openerEventChannel: any | null = null
onShow(() => {
loadAddresses()

View File

@@ -34,8 +34,8 @@
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { ref, onMounted, getCurrentInstance } from 'vue'
import { onShow, onLoad } from '@dcloudio/uni-app'
import { supabaseService, type UserAddress as SupabaseUserAddress } from '@/utils/supabaseService.uts'
type Address = {
@@ -52,25 +52,6 @@ type Address = {
const addresses = ref<Address[]>([])
const selectionMode = ref<boolean>(false)
let openerEventChannel: any = null
onShow(() => {
loadAddresses()
})
onMounted(() => {
try {
const ec = uni.getOpenerEventChannel()
openerEventChannel = ec
ec?.on('setSelectMode', (data: any) => {
if (data && typeof data.selectMode === 'boolean') {
selectionMode.value = data.selectMode
}
})
} catch (e) {
// ignore
}
})
const loadAddresses = async () => {
try {
@@ -78,17 +59,19 @@ const loadAddresses = async () => {
const supabaseAddresses = await supabaseService.getAddresses()
// 转换数据格式以匹配前端界面
const transformedAddresses = supabaseAddresses.map((item: SupabaseUserAddress) => ({
id: item.id,
name: item.recipient_name,
phone: item.phone,
province: item.province,
city: item.city,
district: item.district,
detail: item.detail_address,
isDefault: item.is_default,
label: '' // Supabase表没有label字段可以后续考虑添加或使用其他字段
}))
const transformedAddresses = supabaseAddresses.map((item: SupabaseUserAddress): Address => {
return {
id: item.id,
name: item.recipient_name,
phone: item.phone,
province: item.province,
city: item.city,
district: item.district,
detail: item.detail_address,
isDefault: item.is_default,
label: '' // Supabase表没有label字段
} as Address
})
addresses.value = transformedAddresses
@@ -98,7 +81,7 @@ const loadAddresses = async () => {
console.error('加载地址数据失败:', error)
// 如果API调用失败尝试从本地存储加载
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
if (storedAddresses != null) {
try {
addresses.value = JSON.parse(storedAddresses as string) as Address[]
} catch (e) {
@@ -111,6 +94,19 @@ const loadAddresses = async () => {
}
}
onLoad((options) => {
if (options['selectMode'] == 'true') {
selectionMode.value = true
}
})
onShow(() => {
loadAddresses()
})
// onMounted logic for EventChannel removed as it is not fully supported in UTS Android
// Using uni.$emit for global event communication instead
const getFullAddress = (item: Address): string => {
return `${item.province}${item.city}${item.district} ${item.detail}`
}
@@ -122,34 +118,34 @@ const addAddress = () => {
}
// 删除地址
const deleteAddress = async (id: string) => {
const deleteAddress = (id: string) => {
uni.showModal({
title: '提示',
content: '确定要删除该地址吗?',
success: async (res) => {
success: (res) => {
if (res.confirm) {
// 调用Supabase服务删除地址
const success = await supabaseService.deleteAddress(id)
if (success) {
// 从本地列表移除
const index = addresses.value.findIndex(addr => addr.id === id)
if (index !== -1) {
addresses.value.splice(index, 1)
// 更新本地存储缓存
uni.setStorageSync('addresses', JSON.stringify(addresses.value))
supabaseService.deleteAddress(id).then((success) => {
if (success) {
// 从本地列表移除
const index = addresses.value.findIndex(addr => addr.id === id)
if (index !== -1) {
addresses.value.splice(index, 1)
// 更新本地存储缓存
uni.setStorageSync('addresses', JSON.stringify(addresses.value))
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
} else {
console.error('删除地址失败')
uni.showToast({
title: '删除成功',
icon: 'success'
title: '删除失败',
icon: 'none'
})
}
} else {
console.error('删除地址失败')
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
})
}
}
})
@@ -162,8 +158,8 @@ const editAddress = (id: string) => {
}
const selectAddress = (item: Address) => {
if (selectionMode.value && openerEventChannel) {
openerEventChannel.emit('addressSelected', {
if (selectionMode.value) {
uni.$emit('addressSelected', {
id: item.id,
recipient_name: item.name,
phone: item.phone,
@@ -188,7 +184,14 @@ const selectAddress = (item: Address) => {
flex-direction: column; /* 竖向排列图标 */
justify-content: center;
align-items: center;
gap: 15px;
}
.action-item {
margin-bottom: 15px;
}
.action-item:last-child {
margin-bottom: 0px;
}
.footer-btn {
@@ -198,11 +201,12 @@ const selectAddress = (item: Address) => {
right: 0;
background-color: white;
padding: 10px 15px;
padding-bottom: calc(10px + env(safe-area-inset-bottom));
padding-bottom: 30px;
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
display: flex;
justify-content: center; /* 居中显示 */
justify-content: center;
align-items: center;
z-index: 100;
}
.add-btn {
@@ -214,7 +218,6 @@ const selectAddress = (item: Address) => {
line-height: 44px;
border: none;
width: 100%; /* 默认占满 */
max-width: 100%;
}
/* 响应式布局优化 */
@@ -234,7 +237,7 @@ const selectAddress = (item: Address) => {
left: 50%;
transform: translateX(-50%);
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
border-radius: 12px 12px 0 0; /* 桌面端加点圆角更美观 */
border-radius: 12px 12px 0 0;
}
.add-btn {

View File

@@ -131,7 +131,8 @@
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import supa from '@/components/supadb/aksupainstance.uts'
type AddressType = {
@@ -150,7 +151,6 @@ type AddressType = {
const addressList = ref<Array<AddressType>>([])
const fromSelect = ref<boolean>(false)
const selectCallback = ref<any>(null)
const showNewAddressForm = ref<boolean>(false)
const newAddress = ref<any>({
recipient_name: '',
@@ -163,16 +163,14 @@ const newAddress = ref<any>({
})
const smartAddressInput = ref<string>('')
onLoad((options: any) => {
const optObj = (options instanceof UTSJSONObject) ? (options as UTSJSONObject) : (JSON.parse(JSON.stringify(options ?? {})) as UTSJSONObject)
const flag = optObj.getString('fromSelect') ?? ''
fromSelect.value = (flag == '1' || flag == 'true')
})
// 生命周期
onMounted(() => {
const eventChannel = uni.getEventChannel()
if (eventChannel) {
eventChannel.on('fromSelect', (data: any) => {
fromSelect.value = data.fromSelect || false
selectCallback.value = data.callback
})
}
loadAddresses()
// 监听地址更新事件从checkout页面或其他页面
@@ -222,7 +220,11 @@ const loadAddresses = async () => {
// 获取当前用户ID
const getCurrentUserId = (): string | null => {
const userStore = uni.getStorageSync('userInfo')
return userStore?.id || null
if (userStore instanceof UTSJSONObject) {
return userStore.getString('id')
}
const obj = JSON.parse(JSON.stringify(userStore ?? {})) as UTSJSONObject
return obj.getString('id')
}
// 获取完整地址
@@ -232,9 +234,11 @@ const getFullAddress = (address: AddressType): string => {
// 选择地址
const selectAddress = (address: AddressType) => {
if (fromSelect.value && selectCallback.value) {
// 返回选择的地址
selectCallback.value(address)
if (fromSelect.value) {
try {
uni.setStorageSync('selectedAddress', JSON.stringify(address))
} catch (e) {}
uni.$emit('addressSelected', address)
uni.navigateBack()
}
}

View File

@@ -4,11 +4,11 @@
<view class="section-title">退款类型</view>
<radio-group @change="handleTypeChange" class="type-group">
<label class="type-item">
<radio value="1" :checked="refundType === 1" color="#ff4444" />
<radio value="1" :checked="refundType === 1" color="#ff4444" class="type-radio" />
<text>仅退款</text>
</label>
<label class="type-item">
<radio value="2" :checked="refundType === 2" color="#ff4444" />
<radio value="2" :checked="refundType === 2" color="#ff4444" class="type-radio" />
<text>退货退款</text>
</label>
</radio-group>
@@ -56,7 +56,8 @@
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts'
const orderId = ref('')
@@ -79,24 +80,15 @@ const reasonList = [
'其他'
]
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options as any
if (options.orderId) {
orderId.value = options.orderId
loadOrderInfo()
}
})
const loadOrderInfo = async () => {
try {
const orderData = await supabaseService.getOrderDetail(orderId.value)
if (orderData != null) {
const total = Number(orderData['total_amount'] ?? 0)
const shipping = Number(orderData['shipping_fee'] ?? 0)
// Cast to UTSJSONObject to access properties safely
const order = orderData as UTSJSONObject
const total = order.getNumber('total_amount') ?? 0
const shipping = order.getNumber('shipping_fee') ?? 0
maxAmount.value = total
deliveryFee.value = shipping
@@ -111,17 +103,37 @@ const loadOrderInfo = async () => {
}
}
onLoad((options) => {
if (options['orderId'] != null) {
orderId.value = options['orderId'] as string
loadOrderInfo()
}
})
const handleTypeChange = (e: any) => {
refundType.value = parseInt(e.detail.value)
// Use bracket notation to access detail property safely on 'any' type in UTS
// The structure is e -> detail -> value
// We need to cast e to UTSJSONObject first if we want to use bracket notation,
// OR we can use JSON.parse/stringify trick if simple casting fails,
// BUT the most standard way for UTS 'any' which is actually a Map/JSONObject at runtime:
const target = e as UTSJSONObject
const detail = target['detail'] as UTSJSONObject
const value = detail['value'] as string
refundType.value = parseInt(value)
}
const handleReasonChange = (e: any) => {
const index = e.detail.value as number
// Use bracket notation to access detail property safely on 'any' type in UTS
const target = e as UTSJSONObject
const detail = target['detail'] as UTSJSONObject
const value = detail['value'] as number
const index = value
refundReason.value = reasonList[index]
}
const submitRefund = async () => {
if (!refundReason.value) {
if (refundReason.value == '') {
uni.showToast({ title: '请选择退款原因', icon: 'none' })
return
}
@@ -166,7 +178,7 @@ const submitRefund = async () => {
<style scoped>
.apply-refund-page {
min-height: 100vh;
flex: 1;
background-color: #f5f5f5;
padding: 15px;
padding-bottom: 80px;
@@ -189,16 +201,19 @@ const submitRefund = async () => {
.type-group {
display: flex;
flex-direction: column;
gap: 15px;
}
.type-item {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
font-size: 14px;
}
.type-radio {
margin-right: 10px;
}
.picker-content {
display: flex;
justify-content: space-between;

View File

@@ -25,7 +25,7 @@
</view>
<view class="action-section">
<button class="submit-btn" :disabled="loading" @click="submit">确认添加</button>
<button class="submit-btn" :class="{ disabled: loading }" :disabled="loading" @click="submit">确认添加</button>
</view>
</view>
</template>
@@ -34,6 +34,14 @@
import { ref, reactive } from 'vue'
import { supabaseService } from '@/utils/supabaseService.uts'
type BankCardForm = {
holder_name: string
card_no: string
bank_name: string
phone: string
is_default: boolean
}
const loading = ref(false)
const form = reactive({
holder_name: '',
@@ -41,7 +49,7 @@ const form = reactive({
bank_name: '',
phone: '',
is_default: false
})
} as BankCardForm)
const onSwitchChange = (e: UniSwitchChangeEvent) => {
form.is_default = e.detail.value
@@ -60,7 +68,7 @@ const detectBank = (e: any) => {
}
const submit = async () => {
if (!form.holder_name || !form.card_no || !form.bank_name) {
if (form.holder_name == '' || form.card_no == '' || form.bank_name == '') {
uni.showToast({ title: '请完善卡片信息', icon: 'none' })
return
}
@@ -71,6 +79,7 @@ const submit = async () => {
const cardData = new UTSJSONObject()
cardData.set('holder_name', form.holder_name)
cardData.set('bank_name', form.bank_name)
cardData.set('card_no', form.card_no) // Also save full card no if needed, or just last4
// 截取后4位
const last4 = form.card_no.length > 4 ? form.card_no.slice(-4) : form.card_no
cardData.set('card_no_last4', last4)
@@ -100,8 +109,7 @@ const submit = async () => {
<style>
.add-card-page {
background-color: #f5f5f5;
min-height: 100vh;
padding-top: 20px;
flex: 1;
}
.form-container {
@@ -146,7 +154,7 @@ const submit = async () => {
font-size: 16px;
}
.submit-btn[disabled] {
.submit-btn.disabled {
opacity: 0.6;
}
</style>

View File

@@ -28,7 +28,6 @@
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts'
type BankCard = {
@@ -44,15 +43,15 @@ type BankCard = {
const cards = ref<BankCard[]>([])
const loading = ref(true)
onShow(() => {
loadData()
})
const loadData = async () => {
loading.value = true
try {
const rawList = await supabaseService.getUserBankCards()
cards.value = rawList.map((item: any): BankCard => {
const cardList: BankCard[] = []
// Use for loop instead of map to avoid complex closure typing issues
for (let i = 0; i < rawList.length; i++) {
const item = rawList[i]
let id = ''
let bankName = ''
let last4 = ''
@@ -61,22 +60,23 @@ const loadData = async () => {
let isDef = false
if (item instanceof UTSJSONObject) {
id = item.getString('id') || ''
bankName = item.getString('bank_name') || ''
last4 = item.getString('card_no_last4') || ''
type = item.getString('card_type') || 'debit'
holder = item.getString('holder_name') || ''
isDef = item.getBoolean('is_default') || false
id = item.getString('id') ?? ''
bankName = item.getString('bank_name') ?? ''
last4 = item.getString('card_no_last4') ?? ''
type = item.getString('card_type') ?? 'debit'
holder = item.getString('holder_name') ?? ''
isDef = item.getBoolean('is_default') ?? false
} else {
id = (item['id'] as string) || ''
bankName = (item['bank_name'] as string) || ''
last4 = (item['card_no_last4'] as string) || ''
type = (item['card_type'] as string) || 'debit'
holder = (item['holder_name'] as string) || ''
isDef = (item['is_default'] as boolean) || false
const obj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
id = obj.getString('id') ?? ''
bankName = obj.getString('bank_name') ?? ''
last4 = obj.getString('card_no_last4') ?? ''
type = obj.getString('card_type') ?? 'debit'
holder = obj.getString('holder_name') ?? ''
isDef = obj.getBoolean('is_default') ?? false
}
return {
cardList.push({
id: id,
user_id: '',
bank_name: bankName,
@@ -84,8 +84,10 @@ const loadData = async () => {
card_type: type,
holder_name: holder,
is_default: isDef
} as BankCard
})
} as BankCard)
}
cards.value = cardList
} catch (e) {
console.error(e)
} finally {
@@ -93,6 +95,10 @@ const loadData = async () => {
}
}
onShow(() => {
loadData()
})
const addCard = () => {
uni.navigateTo({
url: '/pages/mall/consumer/bank-cards/add'
@@ -103,15 +109,16 @@ const deleteCard = (card: BankCard) => {
uni.showModal({
title: '删除银行卡',
content: `确认删除尾号${card.card_no_last4}的${card.bank_name}卡片吗?`,
success: async (res) => {
success: (res) => {
if (res.confirm) {
const success = await supabaseService.deleteBankCard(card.id)
if (success) {
uni.showToast({ title: '已删除' })
loadData()
} else {
uni.showToast({ title: '删除失败', icon: 'none' })
}
supabaseService.deleteBankCard(card.id).then((success) => {
if (success) {
uni.showToast({ title: '已删除' })
loadData()
} else {
uni.showToast({ title: '删除失败', icon: 'none' })
}
})
}
}
})
@@ -131,10 +138,7 @@ const getCardClass = (bankName: string): string => {
.bank-cards-page {
padding: 15px;
background-color: #f5f5f5;
min-height: 100vh;
}
.card-item {
flex: 1;
height: 140px;
border-radius: 12px;
margin-bottom: 15px;
@@ -212,7 +216,7 @@ const getCardClass = (bankName: string): string => {
.plus-icon {
font-size: 24px;
margin-right: 5px;
font-weight: 300;
/* font-weight: 300; removed */
}
.delete-btn {
@@ -222,7 +226,7 @@ const getCardClass = (bankName: string): string => {
width: 24px;
height: 24px;
background-color: rgba(0,0,0,0.2);
border-radius: 50%;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;

View File

@@ -41,7 +41,7 @@
<text v-else class="unselected-icon"></text>
</view>
<text class="shop-icon" @click="navigateToShop(group.shopId, group.merchantId)">🏪</text>
<text class="shop-name" @click="navigateToShop(group.shopId, group.merchantId)">{{ group.shopName }}</text>
<text class="shop-name" :lines="1" @click="navigateToShop(group.shopId, group.merchantId)">{{ group.shopName }}</text>
<text class="shop-arrow" @click="navigateToShop(group.shopId, group.merchantId)">></text>
</view>
@@ -65,7 +65,7 @@
<view class="item-info">
<view class="info-top">
<text class="item-name">{{ item.name }}</text>
<text class="item-name" :lines="1">{{ item.name }}</text>
<text class="item-spec">{{ item.spec }}</text>
</view>
@@ -125,7 +125,7 @@
:src="product.image"
mode="aspectFill"
/>
<text class="recommend-name">{{ product.name }}</text>
<text class="recommend-name" :lines="2">{{ product.name }}</text>
<view class="recommend-bottom">
<text class="recommend-price">¥{{ product.price }}</text>
<view class="recommend-add-btn" @click.stop="addToCart(product)">
@@ -170,55 +170,93 @@ import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { supabaseService, type CartItem as SupabaseCartItem, type Product } from '@/utils/supabaseService.uts'
type LocalCartItem = {
id: string
shopId: string
shopName: string
name: string
price: number
image: string
spec: string
quantity: number
selected: boolean
productId: string
skuId: string
merchantId: string
}
type CartGroup = {
shopId: string
shopName: string
merchantId: string
items: LocalCartItem[]
}
type RecommendProduct = {
id: string
shopId: string
shopName: string
name: string
price: number
image: string
skuId: string
}
// 响应式数据
const cartItems = ref<any[]>([])
const recommendProducts = ref<any[]>([])
const cartItems = ref<LocalCartItem[]>([])
const recommendProducts = ref<RecommendProduct[]>([])
const loading = ref<boolean>(false)
const statusBarHeight = ref(0)
const isManageMode = ref(false)
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 => {
// Build a unique key for the shop
const shopKey = item.shopId || 'unknown'
const cartGroups = computed<CartGroup[]>(() => {
const groups = new Map<string, CartGroup>()
cartItems.value.forEach((item: LocalCartItem) => {
const shopKey = item.shopId
if (!groups.has(shopKey)) {
groups.set(shopKey, {
shopId: item.shopId,
shopName: item.shopName || '商城优选', // Better default name
merchantId: item.merchantId, // Add merchantId for navigation
items: [] as any[]
shopName: item.shopName,
merchantId: item.merchantId,
items: []
})
}
const group = groups.get(shopKey)
if (group) {
if (group != null) {
group.items.push(item)
}
})
return Array.from(groups.values())
const groupArray: CartGroup[] = []
groups.forEach((value: CartGroup) => {
groupArray.push(value)
})
return groupArray
})
const allSelected = computed(() => {
return cartItems.value.length > 0 && cartItems.value.every(item => item.selected)
return cartItems.value.length > 0 && cartItems.value.every((item: LocalCartItem) => item.selected)
})
const selectedCount = computed(() => {
return cartItems.value.filter(item => item.selected).reduce((sum, item) => sum + item.quantity, 0)
return cartItems.value.filter((item: LocalCartItem) => item.selected).reduce((sum: number, item: LocalCartItem) => sum + item.quantity, 0)
})
const totalPrice = computed(() => {
return cartItems.value
.filter(item => item.selected)
.reduce((sum, item) => sum + item.price * item.quantity, 0)
.filter((item: LocalCartItem) => item.selected)
.reduce((sum: number, item: LocalCartItem) => sum + item.price * item.quantity, 0)
.toFixed(2)
})
// 检查店铺是否全选
const isShopSelected = (shopId: string) => {
const group = cartGroups.value.find(g => g.shopId === shopId)
return group ? group.items.every(item => item.selected) : false
const isShopSelected = (shopId: string): boolean => {
const shopItems = cartItems.value.filter((item: LocalCartItem): boolean => item.shopId === shopId)
return shopItems.length > 0 && shopItems.every((item: LocalCartItem): boolean => item.selected)
}
const toggleManageMode = () => {
@@ -228,7 +266,7 @@ const toggleManageMode = () => {
// 初始化页面数据
const initPage = () => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
statusBarHeight.value = systemInfo.statusBarHeight ?? 0
}
// 生命周期
@@ -236,10 +274,6 @@ onMounted(() => {
initPage()
})
onShow(() => {
loadCartData()
})
// 加载数据
const loadCartData = async () => {
loading.value = true
@@ -249,26 +283,28 @@ const loadCartData = async () => {
const supabaseCartItems = await supabaseService.getCartItems()
// 转换数据格式以匹配前端界面
const transformedItems = supabaseCartItems.map((item: SupabaseCartItem) => {
const transformedItems = supabaseCartItems.map((item: SupabaseCartItem): LocalCartItem => {
// 调试日志:打印每条商品数据的关键字段
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}`);
// 关键修复确保shopId有值如果后端返回null/undefined使用'default_shop'作为分组键
const shopId = (item.shop_id != null && item.shop_id !== '') ? item.shop_id : 'default_shop'
const shopName = (item.shop_name != null && item.shop_name !== '') ? item.shop_name : '商城优选'
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 || '未知商品',
shopId: shopId,
shopName: shopName,
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
}
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 ?? ''
} as LocalCartItem
})
console.log('Transformed items count:', transformedItems.length);
@@ -284,18 +320,17 @@ const loadCartData = async () => {
}
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: {}
}
})
recommendProducts.value = recommends.map((p: Product): RecommendProduct => {
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',
skuId: ''
}
})
} else {
recommendProducts.value = []
}
@@ -307,6 +342,10 @@ const loadCartData = async () => {
}
}
onShow(() => {
loadCartData()
})
// 商品操作 - 更新选中状态到Supabase
const toggleSelect = async (itemId: string) => {
// 乐观更新
@@ -329,18 +368,14 @@ const toggleSelect = async (itemId: string) => {
}
const toggleShopSelect = async (shopId: string) => {
// 查找该组是否已存在,并判断目标状态
const group = cartGroups.value.find((g: any) => g.shopId === shopId)
if (!group) return
// 获取该店铺下所有商品的ID
const shopItems = cartItems.value.filter((item: LocalCartItem): boolean => item.shopId === shopId)
if (shopItems.length === 0) return
// 检查当前是否全选: 如果所有都选中,则目标是全不选(false);否则全选(true)
const isAllShopSelected = (group.items as any[]).every((item: any) => item.selected)
const isAllShopSelected = shopItems.every((item: LocalCartItem): boolean => item.selected)
const newState = !isAllShopSelected
// 获取该店铺下所有商品的ID
const shopItemIds = cartItems.value
.filter(item => item.shopId === shopId)
.map(item => item.id)
const shopItemIds = shopItems.map((item: LocalCartItem): string => item.id)
// 乐观更新本地状态
const oldStates = new Map<string, boolean>()
@@ -376,11 +411,11 @@ const toggleSelectAll = async () => {
const newSelectedState = !allSelected.value
// 乐观更新
const oldItems = JSON.parse(JSON.stringify(cartItems.value))
const selectedItems = cartItems.value.map(item => ({
...item,
selected: newSelectedState
}))
const oldItems = JSON.parse(JSON.stringify(cartItems.value)) as LocalCartItem[]
const selectedItems = cartItems.value.map((item): LocalCartItem => {
item.selected = newSelectedState
return item
})
cartItems.value = selectedItems
// 更新到Supabase
@@ -391,7 +426,7 @@ const toggleSelectAll = async () => {
if (!success) {
console.error('批量更新选中状态失败')
cartItems.value = oldItems
cartItems.value = oldItems
uni.showToast({
title: '操作失败',
icon: 'none'
@@ -450,24 +485,25 @@ const decreaseQuantity = async (itemId: string) => {
uni.showModal({
title: '提示',
content: '确定要从购物车移除该商品吗?',
success: async (res) => {
success: (res) => {
if (res.confirm) {
// 从Supabase删除
const success = await supabaseService.deleteCartItem(itemId)
if (success) {
cartItems.value.splice(index, 1)
cartItems.value = [...cartItems.value]
uni.showToast({
title: '已移除',
icon: 'none'
})
} else {
console.error('删除商品失败')
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
supabaseService.deleteCartItem(itemId).then((success) => {
if (success) {
cartItems.value.splice(index, 1)
cartItems.value = [...cartItems.value]
uni.showToast({
title: '已移除',
icon: 'none'
})
} else {
console.error('删除商品失败')
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
})
}
}
})
@@ -488,7 +524,7 @@ const deleteSelectedItems = async () => {
uni.showModal({
title: '提示',
content: `确定要删除选中的 ${selectedCount.value} 件商品吗?`,
success: async (res) => {
success: (res) => {
if (res.confirm) {
// 获取选中的商品ID
const selectedItemIds = cartItems.value
@@ -496,27 +532,27 @@ const deleteSelectedItems = async () => {
.map(item => item.id)
// 批量删除到Supabase
const success = await supabaseService.batchDeleteCartItems(selectedItemIds)
if (success) {
// 从本地列表移除
cartItems.value = cartItems.value.filter(item => !item.selected)
// 如果购物车删空了,退出管理模式
if (cartItems.value.length === 0) {
isManageMode.value = false
supabaseService.batchDeleteCartItems(selectedItemIds).then((success) => {
if (success) {
// 从本地列表移除
cartItems.value = cartItems.value.filter(item => !item.selected)
// 如果购物车删空了,退出管理模式
if (cartItems.value.length === 0) {
isManageMode.value = false
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
} else {
console.error('批量删除商品失败')
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
} else {
console.error('批量删除商品失败')
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
})
}
}
})
@@ -525,7 +561,11 @@ const deleteSelectedItems = async () => {
const addToCart = async (product: any) => {
try {
// 调用SupabaseService添加商品到购物车
const success = await supabaseService.addToCart(product.id, 1, product.skuId)
// 显式访问属性避免any类型导致的编译错误
const target = product as UTSJSONObject
const productId = target.getString('id') ?? ''
const skuId = target.getString('skuId') ?? ''
const success = await supabaseService.addToCart(productId, 1, skuId)
if (success) {
uni.showToast({
title: '已添加到购物车',
@@ -553,13 +593,16 @@ const addToCart = async (product: any) => {
// 导航函数
const navigateToShop = (shopId: string, merchantId: any) => {
// Prevent navigation for invalid shops
if (!shopId || shopId === 'default_shop' || shopId === 'unknown') return
if (shopId == '' || shopId === 'default_shop' || shopId === 'unknown') return
let url = `/pages/mall/consumer/shop-detail?id=${shopId}`
if (merchantId) {
url += `&merchantId=${merchantId}`
}
uni.navigateTo({ url })
let url = `/pages/mall/consumer/shop-detail?id=${shopId}`
if (merchantId != null) {
const mId = `${merchantId}`
if (mId !== '' && mId !== 'null' && mId !== 'undefined' && mId !== 'false') {
url += `&merchantId=${mId}`
}
}
uni.navigateTo({ url })
}
const goShopping = () => {
@@ -567,20 +610,50 @@ const goShopping = () => {
}
const navigateToProduct = (product: any) => {
console.log('navigateToProduct', product)
// 使用 JSON 转换确保可以作为 JSONObject 处理,兼容 LocalCartItem 类型和普通对象
const productJson = JSON.parse(JSON.stringify(product)) as UTSJSONObject
// 使用productId如果存在作为跳转的商品ID否则使用id
const productId = product.productId || product.id
let productId = productJson.getString('productId')
if (productId == null || productId == '') {
productId = productJson.getString('id')
}
if (productId == null || productId == '') {
console.error('无法获取商品ID', product)
return
}
// 传递完整的参数,确保商品详情页能正确加载
const params = new URLSearchParams()
params.append('id', productId)
params.append('productId', productId)
params.append('price', product.price?.toString() || '0')
// 商品详情页期望的参数名是originalPrice
params.append('originalPrice', (product.original_price || product.originalPrice || (product.price * 1.2).toFixed(2))?.toString())
params.append('name', encodeURIComponent(product.name || ''))
params.append('image', encodeURIComponent(product.image || '/static/product1.jpg'))
let paramsArr: string[] = []
paramsArr.push('id=' + encodeURIComponent(productId))
paramsArr.push('productId=' + encodeURIComponent(productId))
const price = productJson.getNumber('price') ?? 0
paramsArr.push('price=' + price)
let originalPrice = productJson.getNumber('original_price')
if (originalPrice == null) {
originalPrice = productJson.getNumber('originalPrice')
}
if (originalPrice == null) {
originalPrice = parseFloat((price * 1.2).toFixed(2))
}
paramsArr.push('originalPrice=' + originalPrice)
const name = productJson.getString('name') ?? ''
paramsArr.push('name=' + encodeURIComponent(name))
const image = productJson.getString('image') ?? '/static/product1.jpg'
paramsArr.push('image=' + encodeURIComponent(image))
const url = `/pages/mall/consumer/product-detail?${paramsArr.join('&')}`
console.log('Navigate to:', url)
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?${params.toString()}`
url: url
})
}
@@ -598,16 +671,16 @@ const goToCheckout = () => {
.filter(item => item.selected)
.map(item => ({
id: item.id,
product_id: item.productId || item.id,
sku_id: item.skuId || item.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) // 确保是数字
price: item.price, // 确保是数字
quantity: item.quantity // 确保是数字
}))
// 关键修复:将结算数据写入 Storage确保 checkout 页面能稳定获取
@@ -623,13 +696,7 @@ const goToCheckout = () => {
// 跳转到结算页面并传递数据
uni.navigateTo({
url: '/pages/mall/consumer/checkout',
success: (res) => {
// 通过eventChannel传递数据 (作为备份)
res.eventChannel.emit('acceptData', {
selectedItems: selectedItems
})
}
url: '/pages/mall/consumer/checkout'
})
}
</script>
@@ -637,7 +704,7 @@ const goToCheckout = () => {
<style>
.cart-page {
width: 100%;
height: 100vh;
height: 100%;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
@@ -690,7 +757,7 @@ const goToCheckout = () => {
background: rgba(255, 255, 255, 0.2);
padding: 4px 12px;
border-radius: 20px;
cursor: pointer;
/* cursor: pointer; REMOVED */
transition: all 0.2s ease;
}
@@ -707,7 +774,7 @@ const goToCheckout = () => {
.action-text {
font-size: 12px;
color: white;
font-weight: 500;
font-weight: bold;
}
/* 导航栏占位符 */
@@ -802,7 +869,7 @@ const goToCheckout = () => {
.shop-name {
font-size: 14px;
font-weight: 600;
font-weight: 700;
color: #333;
margin-right: 4px;
/* 自适应宽度,但不超过剩余空间 */
@@ -844,7 +911,7 @@ const goToCheckout = () => {
height: 18px;
background-color: #ff5000;
color: white;
border-radius: 50%;
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
@@ -855,7 +922,7 @@ const goToCheckout = () => {
width: 18px;
height: 18px;
border: 1px solid #ddd;
border-radius: 50%;
border-radius: 9px;
}
.item-image {
@@ -884,11 +951,12 @@ const goToCheckout = () => {
font-size: 14px; /* 稍微减小字体 */
color: #333;
margin-bottom: 2px;
display: -webkit-box;
-webkit-line-clamp: 1; /* 单行显示 */
-webkit-box-orient: vertical;
/* display: -webkit-box; REMOVED */
/* -webkit-line-clamp: 1; REMOVED */
/* -webkit-box-orient: vertical; REMOVED */
overflow: hidden;
font-weight: 500;
font-weight: bold;
text-overflow: ellipsis;
}
.item-spec {
@@ -957,9 +1025,12 @@ const goToCheckout = () => {
}
.recommend-list {
display: grid;
grid-template-columns: repeat(2, 1fr); /* 手机端默认双列 */
gap: 12px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
/* grid-template-columns: repeat(2, 1fr); REMOVED */
/* gap: 12px; REMOVED */
}
.recommend-item {
@@ -968,13 +1039,15 @@ const goToCheckout = () => {
background: #fff;
border-radius: 8px;
overflow: hidden;
width: 48%; /* 替换 grid 1fr auto fit */
margin-bottom: 12px;
}
.recommend-image {
width: 100%;
aspect-ratio: 1; /* 保持正方形比例 */
height: auto; /* 自动高度 */
object-fit: cover;
height: 170px; /* 显式高度 */
/* aspect-ratio: 1; REMOVED */
/* object-fit: cover; REMOVED */
border-radius: 8px;
margin-bottom: 8px;
background: #f5f5f5;
@@ -987,9 +1060,10 @@ const goToCheckout = () => {
line-height: 1.4;
height: 36px;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
/* display: -webkit-box; REMOVED */
/* -webkit-line-clamp: 2; REMOVED */
/* -webkit-box-orient: vertical; REMOVED */
text-overflow: ellipsis;
}
.recommend-bottom {
@@ -1010,7 +1084,7 @@ const goToCheckout = () => {
width: 24px;
height: 24px;
background-color: #ff5000;
border-radius: 50%;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
@@ -1028,12 +1102,17 @@ const goToCheckout = () => {
.cart-list,
.recommend-section {
margin: 20px auto;
max-width: 95%;
width: 95%; /* max-width -> width */
}
.recommend-list {
grid-template-columns: repeat(4, 1fr); /* 平板显示4列 */
gap: 16px;
/* grid-template-columns: repeat(4, 1fr); REMOVED */
/* gap: 16px; REMOVED */
/* Flex 布局参数调整在下方 update */
}
.recommend-item {
width: 23%;
margin-bottom: 16px;
}
}
@@ -1047,12 +1126,14 @@ const goToCheckout = () => {
.cart-list,
.recommend-section {
margin: 20px auto;
max-width: 1200px; /* 限制最大宽度 */
width: 96%; /* max-width -> width: percentage is safer */
max-width: 1200px;
}
/* 店铺分组在桌面端显示为网格布局 */
.shop-group {
display: block;
display: flex;
flex-direction: column;
background: transparent;
box-shadow: none;
border-radius: 0;
@@ -1063,14 +1144,14 @@ const goToCheckout = () => {
background: white;
border-radius: 12px;
margin-bottom: 12px;
padding: 16px 24px;
padding: 16px 80px 16px 24px; /* 同步增加右侧内边距 */
}
/* 购物车商品列表转为列表布局 */
.cart-item {
background: white;
border-radius: 0;
padding: 15px 30px;
padding: 15px 80px 15px 30px; /* 进一步增加右侧内边距 */
height: 80px; /* 固定高度 */
border-bottom: 1px solid #eee;
box-shadow: none;
@@ -1137,14 +1218,15 @@ const goToCheckout = () => {
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: 40px;
/* gap: 40px; REMOVED */
height: 100%;
}
.item-price {
width: 100px;
text-align: right;
margin-bottom: 0;
margin-right: 40px; /* Replace gap */
}
.quantity-control {
@@ -1155,8 +1237,12 @@ const goToCheckout = () => {
/* 推荐商品优化 */
.recommend-list {
grid-template-columns: repeat(5, 1fr); /* 桌面端显示5列 */
gap: 20px;
/* grid-template-columns: repeat(5, 1fr); REMOVED */
/* gap: 20px; REMOVED */
}
.recommend-item {
width: 18%; /* 5列 */
margin-bottom: 20px;
}
.recommend-image {
@@ -1166,7 +1252,7 @@ const goToCheckout = () => {
/* 底部结算栏优化 */
.cart-footer {
padding: 0 40px;
max-width: 100%;
width: 100%; /* max-width -> width */
margin: 0 auto;
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
}
@@ -1184,7 +1270,7 @@ const goToCheckout = () => {
@media screen and (min-width: 1400px) {
.cart-list,
.recommend-section {
max-width: 1400px;
width: 1400px;
}
/* 大屏下购物车商品显示3列 - 移除,保持单列列表 */
@@ -1193,11 +1279,14 @@ const goToCheckout = () => {
} */
.recommend-list {
grid-template-columns: repeat(6, 1fr); /* 大屏幕显示6列 */
/* grid-template-columns: repeat(6, 1fr); REMOVED */
}
.recommend-item {
width: 15%; /* 6 columns approx */
}
.footer-content {
max-width: 1400px;
width: 1400px;
}
}
@@ -1216,6 +1305,7 @@ const goToCheckout = () => {
align-items: center;
justify-content: space-between;
width: 100%;
/* gap: 20px; REMOVED from .action-bar-content usually in desktop */
}
.action-left {
@@ -1324,12 +1414,12 @@ const goToCheckout = () => {
@media screen and (min-width: 768px) {
.cart-action-bar {
margin: 20px auto;
max-width: 95%;
width: 95%; /* max-width -> width */
padding: 20px;
}
.action-bar-content {
gap: 20px;
/* gap: 20px; REMOVED */
}
.total-price {
@@ -1339,13 +1429,14 @@ const goToCheckout = () => {
.checkout-btn, .delete-btn {
padding: 10px 30px;
font-size: 16px;
margin-left: 20px; /* Replace gap */
}
}
/* 桌面端优化 */
@media screen and (min-width: 1024px) {
.cart-action-bar {
max-width: 1200px;
width: 1200px; /* max-width -> width */
padding: 20px 30px;
}
@@ -1378,7 +1469,7 @@ const goToCheckout = () => {
/* 大屏幕优化 */
@media screen and (min-width: 1400px) {
.cart-action-bar {
max-width: 1400px;
width: 1400px; /* max-width -> width */
}
}
</style>

View File

@@ -802,7 +802,7 @@ const goToCheckout = () => {
.shop-name {
font-size: 14px;
font-weight: 600;
font-weight: 700;
color: #333;
margin-right: 4px;
/* 自适应宽度,但不超过剩余空间 */

View File

@@ -25,14 +25,11 @@
</view>
</view>
<!-- 导航栏占位 -->
<view class="navbar-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 分类内容区 -->
<view
class="category-content"
:style="{
marginTop: (statusBarHeight + headerHeight + 10) + 'px',
height: `calc(100vh - ${statusBarHeight + headerHeight + 10}px)`
}"
>
<view class="category-content">
<!-- 左侧一级分类 -->
<scroll-view scroll-y class="primary-category">
<view
@@ -88,7 +85,7 @@
</view>
<view class="product-meta">
<text class="manufacturer">{{ product.brand_name || product.shop_name || '自营' }}</text>
<text class="manufacturer">{{ product.brand_name ?? product.shop_name ?? '自营' }}</text>
<view class="sales-info">
<text class="sales-count">已售{{ product.sale_count }}</text>
</view>
@@ -116,12 +113,20 @@
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import supabaseService from '@/utils/supabaseService.uts'
import type { Category, Product } from '@/utils/supabaseService.uts'
import type { Product } from '@/utils/supabaseService.uts'
type LocalCategory = {
id: string
name: string
icon: string
description: string
color: string
}
// 响应式数据
const statusBarHeight = ref(0)
const headerHeight = ref(44) // 默认头部高度
const primaryCategories = ref<Category[]>([])
const primaryCategories = ref<LocalCategory[]>([])
const productList = ref<Product[]>([])
const activePrimary = ref<string>('')
const cartCount = ref(3)
@@ -137,67 +142,10 @@ const currentCategoryDesc = ref('')
// 页面参数
const pageParams = ref<any>({})
// 生命周期
onMounted(async() => {
await loadCategories()
// 等待分类加载完成后,再检查是否需要加载默认分类的商品
// 延迟一点时间,确保页面参数处理完成
setTimeout(async () => {
if (!hasLoadedFromParams.value && activePrimary.value) {
await loadProducts()
}
}, 300)
})
// 添加加载分类的方法
const loadCategories = async () => {
try {
const categoriesData = await supabaseService.getCategories()
console.log('加载分类数据成功,数量:', categoriesData.length)
// 映射数据并添加默认颜色,防止选中时背景透明导致文字看不清
const categories = categoriesData.map((cat: any) => ({
id: cat.id,
name: cat.name,
icon: cat.icon_url || '📦',
desc: cat.description || '',
description: cat.description || '', // 兼容不同字段名
color: cat.color || '#4CAF50' // 默认绿色如果有color字段则使用
})) as Category[]
if (categories.length > 0) {
primaryCategories.value = categories
// 如果没有通过参数设置分类,则设置默认选中第一个分类
if (!activePrimary.value && categories[0]) {
activePrimary.value = categories[0].id
console.log('设置默认分类为:', categories[0].name, 'ID:', categories[0].id)
currentCategoryName.value = categories[0].name
currentCategoryDesc.value = categories[0].description || ''
} else if (activePrimary.value) {
// 如果已经选中了分类可能来自Storage更新显示信息
const current = categories.find(c => c.id == activePrimary.value)
if (current) {
currentCategoryName.value = current.name
currentCategoryDesc.value = current.description || ''
// 如果此时没有商品列表(且没有正在加载),可能需要加载
if (productList.value.length === 0 && !loading.value) {
loadProducts()
}
}
}
} else {
console.warn('从Supabase获取的分类数据为空')
}
} catch (error) {
console.error('加载分类数据失败:', error)
}
}
// 加载商品数据
const loadProducts = async () => {
async function loadProducts(): Promise<void> {
if (loading.value) return
if (!activePrimary.value) {
if (activePrimary.value == '') {
console.warn('activePrimary为空无法加载商品')
return
}
@@ -222,10 +170,10 @@ const loadProducts = async () => {
hasMore.value = response.hasmore
// 更新当前分类信息
const category = primaryCategories.value.find(cat => cat.id === activePrimary.value)
if (category) {
const category = primaryCategories.value.find((cat: LocalCategory): boolean => cat.id === activePrimary.value)
if (category != null) {
currentCategoryName.value = category.name
currentCategoryDesc.value = category.description || ''
currentCategoryDesc.value = category.description
}
console.log('商品列表加载完成,当前总数量:', productList.value.length)
@@ -239,175 +187,82 @@ const loadProducts = async () => {
}
}
async function loadCategories(): Promise<void> {
try {
const categoriesData = await supabaseService.getCategories()
console.log('加载分类数据成功,数量:', categoriesData.length)
// 映射数据并添加默认颜色,防止选中时背景透明导致文字看不清
// 过滤掉医药健康相关分类
const categories: LocalCategory[] = []
const rawList = categoriesData as any[]
for (let i = 0; i < rawList.length; i++) {
const raw = rawList[i]
const catObj = (raw instanceof UTSJSONObject) ? (raw as UTSJSONObject) : (JSON.parse(JSON.stringify(raw)) as UTSJSONObject)
const name = catObj.getString('name') ?? ''
if (name.includes('医药') || name.includes('健康')) {
continue
}
const id = catObj.getString('id') ?? ''
const description = catObj.getString('description') ?? ''
const icon = catObj.getString('icon') ?? catObj.getString('icon_url') ?? '📦'
const color = catObj.getString('color') ?? '#4CAF50'
categories.push({
id,
name,
icon,
description,
color
})
}
if (categories.length > 0) {
primaryCategories.value = categories
// 如果没有通过参数设置分类,则设置默认选中一个分类
if (activePrimary.value == '') {
// 优先查找"厨具"相关的分类作为默认
const defaultCategory = categories.find((c: LocalCategory): boolean => c.name.includes('厨具')) ?? categories[0]
activePrimary.value = defaultCategory.id
console.log('设置默认分类为:', defaultCategory.name, 'ID:', defaultCategory.id)
currentCategoryName.value = defaultCategory.name
currentCategoryDesc.value = defaultCategory.description
} else {
// 如果已经选中了分类可能来自Storage更新显示信息
const current = categories.find((c: LocalCategory): boolean => c.id == activePrimary.value)
if (current != null) {
currentCategoryName.value = current.name
currentCategoryDesc.value = current.description
// 如果此时没有商品列表(且没有正在加载),可能需要加载
if (productList.value.length === 0 && !loading.value) {
loadProducts()
}
}
}
} else {
console.warn('从Supabase获取的分类数据为空')
}
} catch (error) {
console.error('加载分类数据失败:', error)
}
}
// 加载更多
const loadMore = () => {
function loadMore(): void {
if (hasMore.value && !loading.value) {
currentPage.value++
loadProducts()
}
}
// 页面加载时处理参数 - 这是处理分类切换的主要入口
onLoad((options: any) => {
console.log('=== category页面onLoad被调用 ===')
console.log('页面加载时间:', Date.now())
console.log('传入的options参数:', options)
console.log('当前活动分类:', activePrimary.value)
let categoryId = ''
let categoryName = ''
// 首先检查传入的options参数
if (options && options.categoryId) {
categoryId = options.categoryId
categoryName = options.name || ''
console.log('✅ onLoad中找到分类参数:', categoryId, categoryName)
}
// 如果options中没有尝试从getCurrentPages()获取
if (!categoryId) {
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
const pageOptions = currentPage.options || {}
console.log('从getCurrentPages()获取参数:', pageOptions)
if (pageOptions.categoryId) {
categoryId = pageOptions.categoryId
categoryName = pageOptions.name || ''
console.log('✅ 从getCurrentPages()找到分类参数:', categoryId, categoryName)
}
}
}
// 如果有找到分类ID则选中对应的分类
if (categoryId) {
hasLoadedFromParams.value = true
console.log('✅ 准备选中分类:', categoryId)
console.log('分类名称:', categoryName || '未指定')
// 检查是否需要更新分类
if (activePrimary.value !== categoryId) {
console.log('当前分类:', activePrimary.value, '与目标分类:', categoryId, '不同,需要更新')
console.log('准备调用selectPrimaryCategory函数...')
selectPrimaryCategory(categoryId)
} else {
console.log('当前分类已经是目标分类,但可能用户想要刷新页面')
console.log('当前分类:', activePrimary.value, '目标分类:', categoryId)
// 即使分类相同,也重新加载数据,确保数据是最新的
// 添加一个小的延迟,确保页面完全显示后再更新数据
setTimeout(() => {
selectPrimaryCategory(categoryId)
}, 100)
}
} else {
console.log('⚠️ onLoad中未找到分类参数将使用从数据库加载的第一个分类')
// 不再使用硬编码的默认分类loadCategories 会设置第一个分类
}
console.log('=== category页面onLoad执行完成 ===')
})
// 页面显示时也检查参数,确保从其他页面返回时能正确显示
onShow(() => {
console.log('=== category页面onShow被调用 ===')
console.log('页面显示时间:', Date.now())
console.log('当前活动分类:', activePrimary.value)
// 1. 优先检查 Storage 中的参数 (由首页传入)
const storageCategoryId = uni.getStorageSync('selectedCategory')
if (storageCategoryId) {
console.log('✅ onShow中找到Storage分类参数:', storageCategoryId)
hasLoadedFromParams.value = true
// 清除Storage防止下次误读
uni.removeStorageSync('selectedCategory')
if (activePrimary.value !== storageCategoryId) {
selectPrimaryCategory(storageCategoryId)
}
// 如果分类还没加载完这里设置了ID等loadCategories完成后会自动匹配信息
return
}
// 在onShow中我们也需要检查是否有新的参数
// 因为当从主页再次点击分类跳转过来时可能不会触发onLoad
// 而是触发onShow
// 获取当前页面实例和参数
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
const pageOptions = currentPage.options || {}
console.log('onShow中获取参数:', pageOptions)
// 检查是否有分类参数
if (pageOptions.categoryId) {
hasLoadedFromParams.value = true
const categoryId = pageOptions.categoryId
const categoryName = pageOptions.name || ''
console.log('✅ onShow中找到分类参数:', categoryId, categoryName)
console.log('URL中的时间戳参数:', pageOptions.timestamp)
console.log('URL中的随机参数:', pageOptions.random)
// 检查是否需要更新分类
if (activePrimary.value !== categoryId) {
console.log('当前分类:', activePrimary.value, '与目标分类:', categoryId, '不同,需要更新')
console.log('准备调用selectPrimaryCategory函数...')
selectPrimaryCategory(categoryId)
} else {
console.log('当前分类已经是目标分类,但可能用户想要刷新页面')
console.log('当前分类:', activePrimary.value, '目标分类:', categoryId)
// 即使分类相同,也重新加载数据,确保数据是最新的
// 添加一个小的延迟,确保页面完全显示后再更新数据
setTimeout(() => {
selectPrimaryCategory(categoryId)
}, 100)
}
} else {
console.log('⚠️ onShow中未找到分类参数')
console.log('尝试从URL中解析参数...')
// 尝试从当前页面的URL中解析参数
const currentUrl = currentPage.route || ''
console.log('当前页面路由:', currentUrl)
// 如果URL中有查询参数尝试解析
if (currentPage.$page && currentPage.$page.fullPath) {
const fullPath = currentPage.$page.fullPath
console.log('完整路径:', fullPath)
// 尝试解析查询参数
const queryIndex = fullPath.indexOf('?')
if (queryIndex > -1) {
const queryString = fullPath.substring(queryIndex + 1)
console.log('查询字符串:', queryString)
// 简单解析查询参数
const params = new URLSearchParams(queryString)
const urlCategoryId = params.get('categoryId')
if (urlCategoryId) {
hasLoadedFromParams.value = true
console.log('✅ 从URL解析到分类参数:', urlCategoryId)
selectPrimaryCategory(urlCategoryId)
}
}
}
}
}
console.log('=== category页面onShow执行完成 ===')
})
// 选择一级分类
const selectPrimaryCategory = async (categoryId: string) => {
async function selectPrimaryCategory(categoryId: string): Promise<void> {
console.log('=== selectPrimaryCategory函数开始执行 ===')
console.log('传入的categoryId:', categoryId)
console.log('当前时间:', Date.now())
// 验证categoryId是否有效
if (!categoryId) {
if (categoryId == '') {
console.error('categoryId为空尝试使用第一个分类')
if (primaryCategories.value.length > 0) {
categoryId = primaryCategories.value[0].id
@@ -425,8 +280,8 @@ const selectPrimaryCategory = async (categoryId: string) => {
console.log('更新后的activePrimary:', activePrimary.value)
// 更新当前分类信息
const category = primaryCategories.value.find(cat => cat.id === categoryId)
if (category) {
const category = primaryCategories.value.find((cat: LocalCategory): boolean => cat.id === categoryId)
if (category != null) {
currentCategoryName.value = category.name
currentCategoryDesc.value = category.description
console.log('✅ 找到分类:', category.name, '描述:', category.description)
@@ -465,11 +320,159 @@ const selectPrimaryCategory = async (categoryId: string) => {
console.log('=== selectPrimaryCategory函数执行完成 ===')
}
// 生命周期
onMounted(() => {
loadCategories().then(() => {
setTimeout(() => {
if (!hasLoadedFromParams.value && activePrimary.value != '') {
loadProducts()
}
}, 300)
})
})
// 页面加载时处理参数 - 这是处理分类切换的主要入口
onLoad((options: any) => {
console.log('=== category页面onLoad被调用 ===')
console.log('页面加载时间:', Date.now())
console.log('传入的options参数:', options)
console.log('当前活动分类:', activePrimary.value)
let categoryId = ''
let categoryName = ''
// 首先检查传入的options参数
const optObj = (options instanceof UTSJSONObject) ? (options as UTSJSONObject) : (JSON.parse(JSON.stringify(options ?? {})) as UTSJSONObject)
const optCategoryId = optObj.getString('categoryId') ?? ''
if (optCategoryId !== '') {
categoryId = optCategoryId
categoryName = optObj.getString('name') ?? ''
console.log('✅ onLoad中找到分类参数:', categoryId, categoryName)
}
// 如果options中没有尝试从getCurrentPages()获取
if (categoryId == '') {
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
const rawPageOptions = currentPage.options ?? {}
console.log('从getCurrentPages()获取参数:', rawPageOptions)
const pageOptObj = (rawPageOptions instanceof UTSJSONObject) ? (rawPageOptions as UTSJSONObject) : (JSON.parse(JSON.stringify(rawPageOptions)) as UTSJSONObject)
const pageCategoryId = pageOptObj.getString('categoryId') ?? ''
if (pageCategoryId !== '') {
categoryId = pageCategoryId
categoryName = pageOptObj.getString('name') ?? ''
console.log('✅ 从getCurrentPages()找到分类参数:', categoryId, categoryName)
}
}
}
// 如果有找到分类ID则选中对应的分类
if (categoryId != '') {
hasLoadedFromParams.value = true
console.log('✅ 准备选中分类:', categoryId)
console.log('分类名称:', categoryName ?? '未指定')
// 检查是否需要更新分类
if (activePrimary.value !== categoryId) {
console.log('当前分类:', activePrimary.value, '与目标分类:', categoryId, '不同,需要更新')
console.log('准备调用selectPrimaryCategory函数...')
selectPrimaryCategory(categoryId)
} else {
console.log('当前分类已经是目标分类,但可能用户想要刷新页面')
console.log('当前分类:', activePrimary.value, '目标分类:', categoryId)
// 即使分类相同,也重新加载数据,确保数据是最新的
// 添加一个小的延迟,确保页面完全显示后再更新数据
setTimeout(() => {
selectPrimaryCategory(categoryId)
}, 100)
}
} else {
console.log('⚠️ onLoad中未找到分类参数将使用从数据库加载的第一个分类')
// 不再使用硬编码的默认分类loadCategories 会设置第一个分类
}
console.log('=== category页面onLoad执行完成 ===')
})
// 页面显示时也检查参数,确保从其他页面返回时能正确显示
onShow(() => {
console.log('=== category页面onShow被调用 ===')
console.log('页面显示时间:', Date.now())
console.log('当前活动分类:', activePrimary.value)
// 1. 优先检查 Storage 中的参数 (由首页传入)
const storageCategoryId = (uni.getStorageSync('selectedCategory') as string) ?? ''
if (storageCategoryId !== '') {
console.log('✅ onShow中找到Storage分类参数:', storageCategoryId)
hasLoadedFromParams.value = true
// 清除Storage防止下次误读
uni.removeStorageSync('selectedCategory')
if (activePrimary.value !== storageCategoryId) {
selectPrimaryCategory(storageCategoryId)
}
// 如果分类还没加载完这里设置了ID等loadCategories完成后会自动匹配信息
return
}
// 在onShow中我们也需要检查是否有新的参数
// 因为当从主页再次点击分类跳转过来时可能不会触发onLoad
// 而是触发onShow
// 获取当前页面实例和参数
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
const rawPageOptions = currentPage.options ?? {}
console.log('onShow中获取参数:', rawPageOptions)
const pageOptObj = (rawPageOptions instanceof UTSJSONObject) ? (rawPageOptions as UTSJSONObject) : (JSON.parse(JSON.stringify(rawPageOptions)) as UTSJSONObject)
// 检查是否有分类参数
const pageCategoryId = pageOptObj.getString('categoryId') ?? ''
if (pageCategoryId !== '') {
hasLoadedFromParams.value = true
const categoryId = pageCategoryId
const categoryName = pageOptObj.getString('name') ?? ''
console.log('✅ onShow中找到分类参数:', categoryId, categoryName)
console.log('URL中的时间戳参数:', pageOptObj.getString('timestamp') ?? '')
console.log('URL中的随机参数:', pageOptObj.getString('random') ?? '')
// 检查是否需要更新分类
if (activePrimary.value !== categoryId) {
console.log('当前分类:', activePrimary.value, '与目标分类:', categoryId, '不同,需要更新')
console.log('准备调用selectPrimaryCategory函数...')
selectPrimaryCategory(categoryId)
} else {
console.log('当前分类已经是目标分类,但可能用户想要刷新页面')
console.log('当前分类:', activePrimary.value, '目标分类:', categoryId)
// 即使分类相同,也重新加载数据,确保数据是最新的
// 添加一个小的延迟,确保页面完全显示后再更新数据
setTimeout(() => {
selectPrimaryCategory(categoryId)
}, 100)
}
} else {
console.log('⚠️ onShow中未找到分类参数')
}
}
console.log('=== category页面onShow执行完成 ===')
})
// 添加到购物车
const addToCart = async (product: any) => {
async function addToCart(product: Product): Promise<void> {
uni.showLoading({ title: '添加中...' })
try {
const success = await supabaseService.addToCart(product.id, 1)
const pid = (product.id ?? '').toString()
if (pid === '') {
uni.hideLoading()
uni.showToast({ title: '商品无效', icon: 'none' })
return
}
const success = await supabaseService.addToCart(pid, 1, '')
if (success) {
uni.showToast({
title: '已添加到购物车',
@@ -491,14 +494,15 @@ const addToCart = async (product: any) => {
}
// 导航函数
const navigateToSearch = () => uni.navigateTo({ url: '/pages/mall/consumer/search' })
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 || '')
function navigateToSearch(): void { uni.navigateTo({ url: '/pages/mall/consumer/search' }) }
function navigateToCart(): void { uni.navigateTo({ url: '/pages/mall/consumer/cart' }) }
function navigateToProduct(product: Product): void {
const id = (product.id ?? '').toString()
if (id === '') return
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?id=${id}&productId=${id}&price=${price}&originalPrice=${originalPrice}&name=${name}&image=${image}`
@@ -506,7 +510,7 @@ const navigateToProduct = (product: any) => {
}
// 相机功能
const onCamera = () => {
function onCamera(): void {
uni.chooseImage({
count: 1,
sourceType: ['camera'],
@@ -531,7 +535,7 @@ const onCamera = () => {
}
// 扫码功能
const onScan = () => {
function onScan(): void {
uni.scanCode({
success: (res) => {
console.log('扫码成功:', res)
@@ -550,7 +554,8 @@ const onScan = () => {
<style>
.category-page {
width: 100%;
min-height: 100vh;
height: 100%;
overflow: hidden;
background-color: #f8fafc;
display: flex;
flex-direction: column;
@@ -567,14 +572,20 @@ const onScan = () => {
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
}
/* 导航栏占位 */
.navbar-placeholder {
flex-shrink: 0;
}
/* 搜索栏 */
/* 导航栏搜索框容器内边距调整 */
.search-container {
height: 44px; /* 调整为与消息页一致的高度 */
height: 44px;
padding: 0 16px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
justify-content: center;
max-width: 1400px;
margin: 0 auto;
width: 100%;
@@ -593,12 +604,11 @@ const onScan = () => {
border-radius: 20px;
padding: 0 4px 0 12px;
display: flex;
flex-direction: row; /* UVUE 显式设置 row */
flex-direction: row;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
height: 32px; /* 减小高度与顶部高度44px适配略小于顶部高度 */
height: 32px;
}
.search-placeholder {
@@ -613,7 +623,7 @@ const onScan = () => {
.nav-inner-search-text {
font-size: 12px; /* 字体稍微变小 */
color: #ffffff;
font-weight: 500;
font-weight: normal;
}
.icon {
@@ -683,29 +693,28 @@ const onScan = () => {
/* 分类内容区 */
.category-content {
flex: 1;
height: 0px;
display: flex;
flex-direction: row; /* 强制水平排列 */
/* margin-top: 44px; 已通过 style 动态绑定 */
flex-direction: row;
padding: 0 16px;
max-width: 1400px;
margin-left: auto;
margin-right: auto;
width: 100%;
gap: 20px;
/* height: calc(100vh - 44px); 已通过 style 动态绑定 */
overflow: hidden; /* 防止整体滚动 */
overflow: hidden;
}
/* 左侧一级分类 */
.primary-category {
width: 120px;
height: 100%; /* 占满父容器高度 */
margin-right: 20px; /* gap replacement */
background: white;
border-radius: 12px;
padding: 12px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
flex-shrink: 0;
overflow-y: auto; /* 允许内部滚动 */
}
.primary-item {
@@ -716,7 +725,7 @@ const onScan = () => {
padding: 12px 8px;
margin: 4px 8px;
border-radius: 8px;
cursor: pointer;
/* cursor: pointer; removed for uniapp-x support */
transition: all 0.2s ease;
color: #666;
text-align: center;
@@ -736,13 +745,13 @@ const onScan = () => {
margin-bottom: 6px;
margin-right: 0; /* 移除右边距 */
text-align: center;
display: block;
/* display: block; removed for uniapp-x support */
}
.primary-name {
font-size: 13px;
line-height: 1.4;
display: block;
/* display: block; removed for uniapp-x support */
}
/* 右侧内容区 */
@@ -750,14 +759,13 @@ const onScan = () => {
flex: 1;
height: 100%; /* 占满父容器高度 */
padding: 0; /* 移除内边距,交给内部元素 */
overflow-y: auto; /* 允许内部滚动 */
}
.category-header {
margin-bottom: 16px;
padding: 16px 8px 0 8px;
position: sticky;
top: 0;
/* position: sticky; REMOVED for uniapp-x support */
/* top: 0; */
background-color: #f8fafc;
z-index: 10;
}
@@ -776,19 +784,27 @@ const onScan = () => {
/* 商品网格 */
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
/* grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); REMOVED for uniapp-x support */
/* gap: 20px; removed for compatibility */
padding: 10px; /* add padding to compensate */
width: 100%;
}
.product-card {
background: white;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
/* cursor: pointer; removed for uniapp-x support */
transition: all 0.3s ease;
border: 1px solid #e0e0e0;
position: relative;
/* margin: 10px; gap replacement - moved to logic */
width: 44%; /* Decreased to 44% to ensure it fits (44 + 3 + 3 = 50%) */
margin: 3%; /* Increased margin */
box-sizing: border-box; /* Ensure border IS included in width */
}
.product-card:hover {
@@ -805,14 +821,14 @@ const onScan = () => {
font-size: 11px;
padding: 4px 12px;
border-radius: 12px;
font-weight: 600;
font-weight: 700;
z-index: 2;
}
.product-image {
width: 100%;
height: 160px;
object-fit: cover;
/* object-fit: cover; REMOVED for uniapp-x support - default behavior is often acceptable or handle via image mode */
background: white;
}
@@ -822,10 +838,10 @@ const onScan = () => {
.product-name {
font-size: 15px;
font-weight: 600;
font-weight: 700;
color: #333;
margin-bottom: 4px;
display: block;
/* display: block; REMOVED for uniapp-x support */
line-height: 1.4;
}
@@ -833,19 +849,22 @@ const onScan = () => {
font-size: 13px;
color: #666;
margin-bottom: 12px;
display: block;
/* display: block; REMOVED for uniapp-x support */
}
.price-section {
display: flex;
align-items: baseline;
gap: 8px;
flex-direction: row;
align-items: flex-end; /* changed from baseline */
/* gap: 8px; */
margin-bottom: 12px;
}
.current-price {
display: flex;
align-items: baseline;
flex-direction: row;
align-items: flex-end; /* changed from baseline */
margin-right: 8px; /* gap replacement */
}
.price-symbol {
@@ -863,7 +882,7 @@ const onScan = () => {
.original-price {
font-size: 13px;
color: #999;
text-decoration: line-through;
/* text-decoration: line-through; REMOVED for uniapp-x support */
}
.product-meta {
@@ -890,14 +909,14 @@ const onScan = () => {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
/* gap: 6px; */
background: #4CAF50;
color: white;
padding: 8px 12px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
font-weight: bold;
/* cursor: pointer; removed for uniapp-x support */
transition: all 0.2s ease;
}
@@ -907,6 +926,7 @@ const onScan = () => {
.cart-icon {
font-size: 14px;
margin-right: 6px; /* gap replacement */
}
/* 空状态 */
@@ -946,7 +966,7 @@ const onScan = () => {
}
.load-text {
display: inline-block;
/* display: inline-block; REMOVED for uniapp-x support */
padding: 8px 16px;
background: #f5f5f5;
border-radius: 20px;
@@ -956,10 +976,19 @@ const onScan = () => {
/* 小屏手机 (小于414px) */
@media screen and (max-width: 414px) {
.search-container {
padding: 0 12px;
height: 44px;
}
.search-box {
padding: 0 4px 0 12px;
margin: 0;
height: 30px;
}
.category-content {
/* flex-direction: column; 移除这一行,保持 row 布局 */
padding: 0 8px;
gap: 10px;
}
.primary-category {
@@ -967,6 +996,7 @@ const onScan = () => {
/* display: flex; 移除flex布局保持默认 */
/* flex-wrap: wrap; 移除换行 */
padding: 8px 0;
margin-right: 10px; /* Gap replacement */
}
.primary-item {
@@ -988,10 +1018,15 @@ const onScan = () => {
}
.product-grid {
grid-template-columns: repeat(2, 1fr); /* 改为双列显示 */
gap: 8px;
/* grid-template-columns: repeat(2, 1fr); REMOVED */
/* gap: 8px; REMOVED */
padding: 0 4px 20px 4px; /* 增加底部内边距 */
}
.product-card {
width: 48%; /* 2 columns for mobile */
margin: 1%;
}
/* 手机端商品卡片极简模式 - 仿照主页样式 */
.product-spec,
@@ -1017,9 +1052,10 @@ const onScan = () => {
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
/* display: -webkit-box; REMOVED for support */
/* -webkit-line-clamp: 2; REMOVED for support */
/* -webkit-box-orient: vertical; REMOVED for support */
lines: 2; /* UTS text truncation */
}
.price-section {
@@ -1043,7 +1079,7 @@ const onScan = () => {
.search-container {
padding: 0 12px;
height: 55px;
height: 44px;
}
.search-box {
@@ -1057,30 +1093,45 @@ const onScan = () => {
/* 中屏手机/小平板 (415px-768px) */
@media screen and (min-width: 415px) and (max-width: 768px) {
.product-grid {
grid-template-columns: repeat(2, 1fr);
gap: 16px;
.search-container {
padding: 0 16px;
height: 44px;
}
.product-card {
width: 46%;
margin: 2%;
}
}
/* 平板设备 (769px-1024px) */
@media screen and (min-width: 769px) and (max-width: 1024px) {
.product-grid {
grid-template-columns: repeat(3, 1fr);
gap: 20px;
.search-container {
padding: 0 16px;
height: 44px;
}
.product-card {
width: 30%;
margin: 1.5%;
}
}
/* 桌面端 (1025px以上) */
@media screen and (min-width: 1025px) {
.search-container {
padding: 0 16px;
height: 44px;
}
.category-content {
gap: 30px;
padding: 0 24px;
}
.primary-category {
width: 160px;
padding: 16px 0;
margin-right: 30px; /* Gap replacement */
}
.primary-item {
@@ -1116,12 +1167,14 @@ const onScan = () => {
}
.product-grid {
grid-template-columns: repeat(4, 1fr);
gap: 24px;
/* grid-template-columns: repeat(4, 1fr); REMOVED */
/* gap: 24px; REMOVED */
}
.product-card {
border-radius: 14px;
width: 22%; /* 4 columns */
margin: 1.5%;
}
.product-info {
@@ -1145,10 +1198,14 @@ const onScan = () => {
@media screen and (min-width: 1400px) {
.category-content {
max-width: 1600px;
gap: 40px;
/* gap: 40px; REMOVED */
padding: 0 32px;
}
.primary-category {
margin-right: 40px; /* Gap replacement */
}
.primary-category {
width: 200px;
padding: 20px 0;
@@ -1187,12 +1244,14 @@ const onScan = () => {
}
.product-grid {
grid-template-columns: repeat(5, 1fr);
gap: 28px;
/* grid-template-columns: repeat(5, 1fr); REMOVED */
/* gap: 28px; REMOVED */
}
.product-card {
border-radius: 16px;
width: 17%; /* 5 columns */
margin: 1.5%;
}
.product-image {

View File

@@ -718,7 +718,7 @@ const onScan = () => {
font-size: 11px;
padding: 4px 12px;
border-radius: 12px;
font-weight: 600;
font-weight: 700;
z-index: 2;
}
@@ -735,7 +735,7 @@ const onScan = () => {
.product-name {
font-size: 15px;
font-weight: 600;
font-weight: 700;
color: #333;
margin-bottom: 4px;
display: block;

View File

@@ -31,7 +31,7 @@
<!-- 时间分割线 -->
<view class="time-divider">
<text>今天 14:30</text>
<text class="time-text">今天 14:30</text>
</view>
<!-- 消息项 -->
@@ -45,11 +45,11 @@
<view v-if="message.type === 'received'" class="message-wrapper">
<image
class="avatar"
src="https://picsum.photos/40/40?random=1"
src="/static/icons/shop-default.png"
mode="aspectFill"
/>
<view class="message-content-wrapper">
<text class="sender-name">客服小美</text>
<text class="sender-name">{{ headerTitle }}</text>
<view class="message-bubble">
<text class="message-text">{{ message.content }}</text>
<text class="message-time">{{ message.time }}</text>
@@ -67,7 +67,7 @@
</view>
<image
class="avatar me"
src="https://picsum.photos/40/40?random=2"
src="/static/avatar-default.png"
mode="aspectFill"
/>
</view>
@@ -103,7 +103,7 @@
</view>
<!-- 表情选择器 -->
<view v-if="showEmoji" class="emoji-picker">
<scroll-view v-if="showEmoji" class="emoji-picker" direction="vertical">
<view class="emoji-category">
<text
v-for="emoji in emojiList"
@@ -114,7 +114,7 @@
{{ emoji }}
</text>
</view>
</view>
</scroll-view>
</view>
</template>
@@ -122,10 +122,18 @@
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
import { supabaseService, type ChatMessage } from '@/utils/supabaseService.uts'
import supa from '@/components/supadb/aksupainstance.uts'
import type { AkSupaRealtimeChannel } from '@/components/supadb/aksupa.uts'
import { getCurrentUser } from '@/utils/store.uts'
type UiChatMessage = {
id: string
type: string
content: string
time: string
}
// 响应式数据
const messages = ref<any[]>([])
const messages = ref<UiChatMessage[]>([])
const inputMessage = ref<string>('')
const inputFocus = ref<boolean>(false)
const showEmoji = ref<boolean>(false)
@@ -134,11 +142,106 @@ const currentUserId = ref<string>('')
const merchantId = ref<string>('') // 商家ID
const headerTitle = ref<string>('在线客服')
const navPaddingTop = ref<string>('30px') // 默认值,包含状态栏高度+原有内边距
let realtimeChannel: any = null
let realtimeChannel: AkSupaRealtimeChannel | null = null
// 模拟表情列表
const emojiList = ['😊', '😂', '🤣', '😍', '😘', '🥰', '😭', '😡', '👍', '👏', '🙏', '🎉', '❤️', '🔥', '⭐']
function scrollToBottom(): void {
nextTick(() => {
if (messages.value.length > 0) {
const lastMsgId = messages.value[messages.value.length - 1].id
scrollToView.value = 'msg-' + lastMsgId
}
})
}
function getCurrentTime(): string {
const now = new Date()
const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
return `${hours}:${minutes}`
}
function setupRealtimeSubscription(): void {
console.log('开始建立聊天实时订阅...')
const filter = ({
event: 'INSERT',
schema: 'public',
table: 'ml_chat_messages'
} as UTSJSONObject)
realtimeChannel = supa.channel('public:ml_chat_messages')
.on('postgres_changes', filter, (payload: any) => {
const payloadObj = (payload instanceof UTSJSONObject) ? (payload as UTSJSONObject) : (JSON.parse(JSON.stringify(payload ?? {})) as UTSJSONObject)
const newMsgAny = payloadObj.get('new')
if (newMsgAny == null) return
const newMsg = (newMsgAny instanceof UTSJSONObject) ? (newMsgAny as UTSJSONObject) : (JSON.parse(JSON.stringify(newMsgAny)) as UTSJSONObject)
console.log('收到新消息:', newMsg)
const senderId = newMsg.getString('sender_id') ?? ''
const receiverId = newMsg.getString('receiver_id') ?? ''
if (senderId === currentUserId.value) {
return
}
if (receiverId === currentUserId.value) {
if (merchantId.value != '' && senderId !== merchantId.value) {
return
}
const createdAt = newMsg.getString('created_at') ?? new Date().toISOString()
const date = new Date(createdAt)
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
const incomingMsg: UiChatMessage = {
id: newMsg.getString('id') ?? Date.now().toString(),
type: 'received',
content: newMsg.getString('content') ?? '',
time: timeStr
}
messages.value.push(incomingMsg)
scrollToBottom()
}
})
.subscribe((status: string, err: any | null) => {
console.log('订阅状态:', status)
})
}
async function loadChatHistory(): Promise<void> {
let rawMsgs: ChatMessage[] = []
if (merchantId.value != '') {
rawMsgs = await supabaseService.getChatMessages(merchantId.value)
} else {
console.warn("No merchant ID provided for chat")
return
}
messages.value = rawMsgs.reverse().map((m: ChatMessage): UiChatMessage => {
const date = new Date(m.created_at ?? new Date().toISOString())
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
const sender = m.sender_id ?? ''
const msgType = (currentUserId.value != '' && sender == currentUserId.value) ? 'sent' : 'received'
const rawId = (m.id ?? '').toString()
const msgId = rawId !== '' ? rawId : Date.now().toString()
return {
id: msgId,
type: msgType,
content: m.content ?? '',
time: timeStr
}
})
setTimeout(() => {
scrollToBottom()
}, 100)
}
// 生命周期
onLoad((options: any) => {
// 动态获取状态栏高度
@@ -147,113 +250,47 @@ onLoad((options: any) => {
// 状态栏高度 + 10px 原有顶部内边距
navPaddingTop.value = (statusBarH + 10) + 'px'
if (options.merchantId) {
merchantId.value = options.merchantId
}
if (options.merchantName) {
headerTitle.value = options.merchantName
}
const optObj = (options instanceof UTSJSONObject) ? (options as UTSJSONObject) : (JSON.parse(JSON.stringify(options ?? {})) as UTSJSONObject)
const mid = optObj.getString('merchantId') ?? ''
if (mid !== '') {
merchantId.value = mid
}
const mname = optObj.getString('merchantName') ?? ''
if (mname !== '') {
headerTitle.value = mname
}
})
onMounted(async () => {
const user = await getCurrentUser()
if (user) {
currentUserId.value = user.id
}
loadChatHistory()
setupRealtimeSubscription()
onMounted(() => {
supabaseService.ensureSession().then((uid) => {
if (uid != null) {
currentUserId.value = uid
} else {
getCurrentUser().then((user) => {
if (user != null) {
currentUserId.value = user.id ?? ''
}
})
}
loadChatHistory()
setupRealtimeSubscription()
})
})
onUnmounted(() => {
if (realtimeChannel) {
supa.removeChannel(realtimeChannel)
if (realtimeChannel != null) {
supa.removeChannel(realtimeChannel!!)
}
})
// 建立实时订阅
const setupRealtimeSubscription = () => {
console.log('开始建立聊天实时订阅...')
realtimeChannel = supa.channel('public:ml_chat_messages')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'ml_chat_messages' }, (payload: any) => {
const newMsg = payload.new
console.log('收到新消息:', newMsg)
// 如果是我发的消息因为已经乐观更新了所以忽略或者根据ID更新状态
if (newMsg.sender_id === currentUserId.value) {
return
}
// 如果是发给我的消息
if (newMsg.receiver_id === currentUserId.value) {
// 如果指定了商家,只接收该商家的消息
if (merchantId.value && newMsg.sender_id !== merchantId.value) {
return
}
// 转换为UI消息格式
const date = new Date(newMsg.created_at || new Date().toISOString())
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
const incomingMsg = {
id: newMsg.id || Date.now(), // 优先使用DB ID
type: 'received',
content: newMsg.content,
time: timeStr
}
messages.value.push(incomingMsg)
scrollToBottom()
}
})
.subscribe((status: string) => {
console.log('订阅状态:', status)
})
}
// 加载聊天记录
const loadChatHistory = async () => {
let rawMsgs: ChatMessage[] = []
if (merchantId.value) {
rawMsgs = await supabaseService.getChatMessages(merchantId.value)
} else {
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')}`
const msg : any = {
id: m.id,
type: m.is_from_user ? 'sent' : 'received', // 假设is_from_user标志是准确的或者比较 sender_id
content: m.content,
time: timeStr
}
// 双重确认类型
if (currentUserId.value && m.sender_id === currentUserId.value) {
msg.type = 'sent'
} else if (currentUserId.value && m.sender_id !== currentUserId.value) {
msg.type = 'received'
}
return msg
})
// 滚动到底部
setTimeout(() => {
scrollToBottom()
}, 100)
}
// 发送消息
const sendMessage = async () => {
const content = inputMessage.value.trim()
if (!content) return
if (content == '') return
// 添加发送的消息 (乐观更新)
const newMessage = {
id: Date.now(),
const newMessage: UiChatMessage = {
id: Date.now().toString(),
type: 'sent',
content: content,
time: getCurrentTime()
@@ -266,23 +303,15 @@ const sendMessage = async () => {
scrollToBottom()
// 发送到 Supabase
// 如果有 merchantId,发送给指定商家,否则可能是发给系统或默认客服
const success = await supabaseService.sendChatMessage(content, merchantId.value || null)
if (!success) {
uni.showToast({
title: '发送失败',
icon: 'none'
})
// 实际项目中可能需要标记消息为发送失败状态
if (merchantId.value != '') {
const success = await supabaseService.sendMessage(merchantId.value, content)
if (!success) {
uni.showToast({
title: '发送失败',
icon: 'none'
})
}
}
// 移除模拟回复,依赖 Realtime 接收真实回复
/*
setTimeout(() => {
simulateCustomerReply()
}, 2000)
*/
}
// 模拟客服回复 (已禁用,改用 Realtime)
@@ -294,37 +323,19 @@ const simulateCustomerReply = async () => {
/* 移除不再使用的 simulateCustomerReply 和 addReceivedMessage */
// 滚动到底部
const scrollToBottom = () => {
nextTick(() => {
if (messages.value.length > 0) {
const lastMsgId = messages.value[messages.value.length - 1].id
scrollToView.value = 'msg-' + lastMsgId
}
})
}
// 获取当前时间
const getCurrentTime = (): string => {
const now = new Date()
const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
return `${hours}:${minutes}`
}
// 插入表情
const insertEmoji = (emoji: string) => {
function insertEmoji(emoji: string): void {
inputMessage.value += emoji
inputFocus.value = true
}
// 显示表情选择器
const showEmojiPicker = () => {
function showEmojiPicker(): void {
showEmoji.value = !showEmoji.value
}
// 显示图片选择器
const showImagePicker = () => {
function showImagePicker(): void {
uni.chooseImage({
count: 1,
success: (res) => {
@@ -335,7 +346,7 @@ const showImagePicker = () => {
}
// 显示更多工具
const showMoreTools = () => {
function showMoreTools(): void {
uni.showActionSheet({
itemList: ['发送位置', '发送文件', '发送语音'],
success: (res) => {
@@ -345,7 +356,7 @@ const showMoreTools = () => {
}
// 显示更多操作
const showMoreActions = () => {
function showMoreActions(): void {
uni.showActionSheet({
itemList: ['投诉客服', '结束对话', '清除记录'],
success: (res) => {
@@ -389,7 +400,7 @@ const goBack = () => {
<style>
.chat-page {
width: 100%;
height: 100vh;
flex: 1;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
@@ -425,7 +436,6 @@ const goBack = () => {
font-size: 16px;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 2px;
}
@@ -460,7 +470,7 @@ const goBack = () => {
background-color: #f0f0f0;
padding: 5px 15px;
border-radius: 15px;
display: inline-block;
}
/* 时间分割线 */
@@ -469,7 +479,7 @@ const goBack = () => {
margin: 20px 0;
}
.time-divider text {
.time-text {
font-size: 12px;
color: #999;
background-color: #f0f0f0;
@@ -499,18 +509,17 @@ const goBack = () => {
.avatar.me {
margin-right: 0;
margin-left: 10px;
order: 2;
/* order: 2; removed for uni-app-x */
}
.message-content-wrapper {
max-width: 70%;
/* max-width removed */
}
.sender-name {
font-size: 12px;
color: #999;
margin-bottom: 5px;
display: block;
}
.message-bubble {
@@ -519,8 +528,7 @@ const goBack = () => {
border-radius: 18px;
position: relative;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
max-width: 100%;
word-wrap: break-word;
/* max-width wrap removed */
}
.message-bubble.me {
@@ -528,7 +536,7 @@ const goBack = () => {
border-bottom-right-radius: 4px;
}
.message-bubble:not(.me) {
.message-bubble-not-me .message-content {
border-bottom-left-radius: 4px;
}
@@ -536,14 +544,12 @@ const goBack = () => {
font-size: 15px;
color: #333;
line-height: 1.4;
display: block;
margin-bottom: 5px;
}
.message-time {
font-size: 11px;
color: #999;
display: block;
text-align: right;
}
@@ -608,8 +614,8 @@ const goBack = () => {
background-color: white;
border-top: 1px solid #eee;
padding: 10px;
max-height: 200px;
overflow-y: auto;
height: 200px;
/* overflow-y removed */
position: fixed;
bottom: 60px;
left: 0;
@@ -633,24 +639,5 @@ const goBack = () => {
justify-content: center;
}
/* 响应式适配 */
@media screen and (max-width: 320px) {
.message-content-wrapper {
max-width: 65%;
}
.message-text {
font-size: 14px;
}
}
@media screen and (min-width: 415px) {
.message-content-wrapper {
max-width: 75%;
}
.chat-input {
padding: 15px 20px;
}
}
</style>
/* 响应式适配 removed for strict uv-app-x compliance */
</style>

View File

@@ -146,24 +146,24 @@ const currentUserId = ref('')
const messages = ref<any[]>([])
const emojiList = ['😊', '😂', '👍', '👌', '❤️', '🌹', '🙏', '🎉', '😡', '😭', '🤔', '👋', '🤝', '💊', '🏥']
let realtimeChannel: any = null
let realtimeChannel: any | null = null
onLoad((options: any) => {
// 获取状态栏高度
const sys = uni.getSystemInfoSync()
statusBarHeight.value = sys.statusBarHeight || 0
statusBarHeight.value = sys.statusBarHeight ?? 0
// 获取参数
if (options.merchantId) {
merchantId.value = options.merchantId
merchantName.value = options.merchantName || '商家'
merchantLogo.value = options.merchantLogo || ''
console.log('开始聊天商家ID:', merchantId.value)
} else {
// 测试模式或默认客服
// uni.showToast({ title: '参数缺失', icon: 'none' })
merchantName.value = '平台客服'
}
const optObj = (options instanceof UTSJSONObject) ? (options as UTSJSONObject) : (JSON.parse(JSON.stringify(options ?? {})) as UTSJSONObject)
const mid = optObj.getString('merchantId') ?? ''
if (mid !== '') {
merchantId.value = mid
merchantName.value = optObj.getString('merchantName') ?? '商家'
merchantLogo.value = optObj.getString('merchantLogo') ?? ''
console.log('开始聊天商家ID:', merchantId.value)
} else {
merchantName.value = '平台客服'
}
// 获取当前用户
const uid = supabaseService.getCurrentUserId()
@@ -210,31 +210,35 @@ const loadMoreHistory = () => {
// 开启实时订阅
const startRealtimeSubscription = () => {
if (!currentUserId.value) return
if (currentUserId.value == '') return
console.log('开启消息监听...')
const filterObj = ({
event: 'INSERT',
schema: 'public',
table: 'ml_chat_messages'
} as UTSJSONObject)
realtimeChannel = supa.channel(`chat_${currentUserId.value}`)
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'ml_chat_messages',
filter: `receiver_id=eq.${currentUserId.value}`
},
(payload) => {
filterObj,
(payload: any) => {
console.log('收到新消息:', payload)
const newMsg = payload.new as any
const payloadObj = (payload instanceof UTSJSONObject) ? (payload as UTSJSONObject) : (JSON.parse(JSON.stringify(payload ?? {})) as UTSJSONObject)
const newAny = payloadObj.get('new')
if (newAny == null) return
const newMsg = (newAny instanceof UTSJSONObject) ? (newAny as UTSJSONObject) : (JSON.parse(JSON.stringify(newAny)) as UTSJSONObject)
// 只有来自当前聊天的商家的消息才显示,或者如果是全局客服模式
if (newMsg.sender_id === merchantId.value || !merchantId.value) {
const senderId = newMsg.getString('sender_id') ?? ''
if (senderId === merchantId.value || merchantId.value == '') {
const formatted = formatMessage({
id: newMsg.id,
content: newMsg.content,
msg_type: newMsg.msg_type,
sender_id: newMsg.sender_id,
receiver_id: newMsg.receiver_id,
id: newMsg.getString('id') ?? '',
content: newMsg.getString('content') ?? '',
msg_type: newMsg.getString('msg_type') ?? '',
sender_id: senderId,
receiver_id: newMsg.getString('receiver_id') ?? '',
is_from_user: false, // 收到的一定不是自己发的
created_at: newMsg.created_at
created_at: newMsg.getString('created_at') ?? ''
} as ChatMessage)
messages.value.push(formatted)
@@ -271,7 +275,7 @@ const formatMessage = (m: ChatMessage): any => {
const sendMessage = async () => {
const text = inputMessage.value.trim()
if (!text) return
if (text == '') return
// 乐观更新 UI
const tempId = 'temp_' + Date.now()

File diff suppressed because it is too large Load Diff

View File

@@ -35,20 +35,20 @@ type Coupon = {
const coupons = ref<Coupon[]>([])
onMounted(() => {
loadCoupons()
})
const loadCoupons = async () => {
uni.showLoading({ title: '加载中...' })
try {
const userCoupons = await supabaseService.getUserCoupons(1) // 1: unused
coupons.value = userCoupons.map((item: UserCoupon) => {
const userCoupons = await supabaseService.getUserCoupons(1)
coupons.value = userCoupons.map((item: UserCoupon): Coupon => {
const amountVal = item.amount ?? 0
const expiryVal = (item.expire_at != null && item.expire_at !== '')
? item.expire_at.substring(0, 10)
: '长期有效'
return {
id: item.id,
title: item.template_name || '优惠券',
amount: `¥${item.amount || 0}`,
expiry: item.expire_at ? item.expire_at.substring(0, 10) : '长期有效'
title: (item.template_name != null && item.template_name !== '') ? item.template_name : '优惠券',
amount: `¥${amountVal}`,
expiry: expiryVal
} as Coupon
})
} catch (e) {
@@ -59,6 +59,10 @@ const loadCoupons = async () => {
}
}
onMounted(() => {
loadCoupons()
})
const useCoupon = (coupon: Coupon) => {
uni.switchTab({
url: '/pages/mall/consumer/index'
@@ -70,7 +74,7 @@ const useCoupon = (coupon: Coupon) => {
.coupons-page {
padding: 15px;
background-color: #f5f5f5;
min-height: 100vh;
flex: 1;
}
.empty-state {

View File

@@ -0,0 +1,76 @@
1、在 uni-app x 的 UTS 语法中:
- 不能直接访问 `any` 类型对象的属性
- 需要将对象转换为 `UTSJSONObject` 类型后使用 `getString()`、`getNumber()` 等方法访问属性
- 数组元素需要明确的类型定义才能在模板中正确访问属性
- 空数组需要明确指定类型,如 `[] as string[]`
- 函数必须在调用前定义(不支持函数提升)
- 访问动态对象属性需要使用 `UTSJSONObject` 的 `getString()`、`getNumber()` 等方法
- 类型导入需要使用 `type` 关键字
-UTS 不允许直接访问 `any` 类型的属性。需要将参数类型改为 `MessageItem`
- UTS 不允许直接访问 `any` 类型参数的属性
- 必须使用明确的类型定义才能访问属性
- 函数必须先定义后使用(包括生命周期钩子调用的函数)
- 不允许直接访问 `any` 类型参数的属性
- `if` 条件必须是 `boolean` 类型,不能是 `Any?`
- `forEach` 箭头函数中不能使用赋值表达式作为函数体
- 函数必须先定义后使用
- `any` 类型不能直接使用点语法访问属性,必须使用索引访问 `obj['property']`
- 访问后需要类型转换 `as number` / `as string`
- `if` 条件必须是 `boolean` 类型
- 函数必须先定义后调用
- `any` 类型不能直接使用点语法访问属性
- 需要转换为 `Record<string, any>` 后使用索引访问 `obj['property']`
- 模板中的 `||` 运算符左边必须是 boolean 类型
- 可空类型使用可选链 `?.` 和空值合并 `??`
- `uni.showModal` 的 `success` 回调不能是 `async` 函数
- 解决方案:创建独立的 async 函数(如 `doCancelOrder`、`doConfirmReceive`、`doApplyRefund`),在回调中调用
- 模板中访问可空类型属性前必须先判空 `v-if="order != null"`
- 使用可选链 `?.` 和空值合并 `??` 处理可空类型
- `uni.showModal` 的 `success` 回调不能是 `async` 函数
- 函数必须在使用前定义
- 不支持 `Record<K, V>` 对象字面量语法
- 模板中可空类型必须使用 `?.` 安全访问
- `uni.showModal` 的 `success` 回调不能是 `async` 函数
- `any` 类型属性访问需转换为 `Record<string, any>` 后用索引访问
- 不支持 `Record<K, V>` 对象字面量语法
- `any` 类型属性访问需转换为 `Record<string, any>` 后用索引访问
- `uni.showModal` 的 `success` 回调不能是 `async` 函数
- 模板中可空类型必须使用 `?.` 安全访问
- 不支持 `Object.values()` 和 `Object.entries()`
- 嵌套的数组方法调用可能导致类型推断失败,应拆分为独立变量
- `any` 类型属性访问需转换为 `Record<string, any>` 后用索引访问
- 不支持 `Record<K, V>` 对象字面量语法
- 不支持 `Object.keys()`、`Object.values()`、`Object.entries()`
- 嵌套的数组方法调用可能导致类型推断失败,应改用 for 循环
- `any` 类型属性访问需转换为 `Record<string, any>` 后用索引访问
- 函数必须在使用前定义
- 对象字面量 `{...}` 只能用于构造类型class不能用于接口interface
- 需要创建动态对象时,应使用 `new UTSJSONObject()` 然后调用 `.set()` 方法
- 对于 `type` 定义的对象类型,同样需要使用 `UTSJSONObject`
- 对于 `any[]` 或 `reactive` 数组,访问元素属性时需要先转换为 `Record<string, any>` 或 `any[]`
- 使用索引访问属性时,推荐使用方括号语法 `obj['property']` 而非点语法 `obj.property`
- 这是因为 UTS 的类型系统比 TypeScript 更严格,需要在运行时明确类型转换
- `reactive` 对象在 UTS 中不支持索引器赋值操作
- 对于需要整体替换的数组,推荐使用 `ref` 而非 `reactive`
- 使用 `ref` 时,通过 `.value` 进行整体替换可以正确触发响应式更新
- 函数必须在使用前声明(不支持函数提升)
- 访问 `any` 类型对象的属性需要先转换为 `Record<string, any>`
- `ref` 数组元素不能直接整体替换,需要修改元素属性
- 对于可能为 `null` 的参数,需要显式检查后再传递给函数
- 函数必须在使用前声明(不支持函数提升)
- 依赖关系需要明确:被调用的函数必须先定义
- 这与 JavaScript 的函数提升行为不同UTS 更接近 C/Java 的编译方式
- UTS中箭头函数 `() => {}` 有时会导致"Parenthesized expression cannot be empty"错误
- 解决方案:使用普通函数 `function name(): Type {}` 代替箭头函数
- 变量声明必须有显式类型或初始化值
- 对象属性访问使用 `obj['key']` 而非 `obj.key`
- 数组赋值时类型必须匹配
PS D:\companyproject\mall-trae2> npm run build:app 2>&1
npm error Missing script: "build:app"
npm error
npm error To see a list of scripts, run:
npm error npm run
npm error A complete log of this run can be found in: C:\Users\Huawei\AppData\Local\npm-cache\_logs\2026-02-24T00_58_23_018Z-debug-0.log

View File

@@ -1,5 +1,5 @@
<template>
<view class="favorites-page">
<scroll-view class="favorites-page" direction="vertical">
<view class="product-grid">
<view v-if="favorites.length === 0" class="empty-state">
<text class="empty-icon">❤️</text>
@@ -26,7 +26,7 @@
</view>
</view>
</view>
</view>
</scroll-view>
</template>
<script setup lang="uts">
@@ -45,13 +45,9 @@ type Product = {
const favorites = ref<Product[]>([])
onMounted(() => {
loadFavorites()
})
const addToCart = async (product: Product) => {
uni.showLoading({ title: '添加中' })
const success = await supabaseService.addToCart(product.id, 1)
const success = await supabaseService.addToCart(product.id, 1, '')
uni.hideLoading()
if (success) {
uni.showToast({ title: '已添加到购物车', icon: 'success' })
@@ -63,13 +59,16 @@ const addToCart = async (product: Product) => {
const loadFavorites = async () => {
const res = await supabaseService.getFavorites()
// Map response
favorites.value = res.map((item: any): Product => {
let prod: any = null
let prod: any | null = null
let itemObj: UTSJSONObject | null = null
if (item instanceof UTSJSONObject) {
prod = item.get('ml_products')
itemObj = item as UTSJSONObject
prod = itemObj.get('ml_products')
} else {
prod = item['ml_products']
itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
prod = itemObj.get('ml_products')
}
let image = '/static/default-product.png'
@@ -79,46 +78,33 @@ const loadFavorites = async () => {
let sales = 0
if (prod != null) {
let prodObj: UTSJSONObject
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) {}
}
}
prodObj = prod as UTSJSONObject
} 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) {}
}
}
prodObj = JSON.parse(JSON.stringify(prod)) as UTSJSONObject
}
id = prodObj.getString('id') ?? ''
name = prodObj.getString('name') ?? '未知商品'
price = prodObj.getNumber('base_price') ?? 0
image = prodObj.getString('main_image_url') ?? image
sales = prodObj.getNumber('sale_count') ?? 0
if (image === '/static/default-product.png') {
const imgUrls = prodObj.getString('image_urls')
if (imgUrls != null) {
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) || ''
if (itemObj != null) {
id = itemObj.getString('target_id') ?? ''
}
}
@@ -130,7 +116,7 @@ const loadFavorites = async () => {
sales: sales,
shopId: '',
shopName: ''
}
} as Product
})
}
@@ -146,50 +132,53 @@ const goToDetail = (id: string) => {
})
}
const removeFavorite = async (id: string) => {
const removeFavorite = (id: string) => {
uni.showModal({
title: '取消收藏',
content: '确定要取消收藏该商品吗?',
success: async (res) => {
success: (res) => {
if (res.confirm) {
// toggleFavorite 返回最新的状态true=已收藏false=未收藏
const isStillFavorite = await supabaseService.toggleFavorite(id)
if (!isStillFavorite) {
// 现在的状态是"未收藏",说明取消成功
// Remove from local list
const index = favorites.value.findIndex(item => item.id === id)
supabaseService.toggleFavorite(id).then((isStillFavorite) => {
if (!isStillFavorite) {
const index = favorites.value.findIndex((item): Boolean => {
return item.id === id
})
if (index !== -1) {
favorites.value.splice(index, 1)
favorites.value.splice(index, 1)
}
uni.showToast({
title: '已取消收藏',
icon: 'none'
title: '已取消收藏',
icon: 'none'
})
} else {
} else {
uni.showToast({
title: '取消失败',
icon: 'none'
title: '取消失败',
icon: 'none'
})
}
}
})
}
}
})
}
onMounted(() => {
loadFavorites()
})
</script>
<style>
.favorites-page {
padding: 15px;
background-color: #f5f5f5;
min-height: 100vh;
flex: 1;
}
.product-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 15px;
justify-content: space-between;
width: 100%;
}
@@ -224,7 +213,7 @@ const removeFavorite = async (id: string) => {
}
.product-item {
width: calc(50% - 8px); /* Default Mobile: 2 items per row */
width: 48%; /* Default Mobile: 2 items per row */
background-color: white;
border-radius: 8px;
overflow: hidden;
@@ -236,13 +225,13 @@ const removeFavorite = async (id: string) => {
/* PC/Tablet Responsive */
@media (min-width: 768px) {
.product-item {
width: calc(33.33% - 10px) !important; /* Tablet: 3 items (gap 15px roughly distributed) */
width: 31% !important; /* Tablet: 3 items (gap 15px roughly distributed) */
}
}
@media (min-width: 1024px) {
.product-item {
width: calc(16.66% - 13px) !important; /* PC: 6 items */
width: 15% !important; /* PC: 6 items */
}
/* Center content on large screens */
@@ -269,9 +258,8 @@ const removeFavorite = async (id: string) => {
font-size: 14px;
color: #333;
margin-bottom: 6px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
lines: 2;
overflow: hidden;
height: 40px;
line-height: 20px;
@@ -300,16 +288,16 @@ const removeFavorite = async (id: string) => {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.cart-btn, .remove-btn {
width: 28px;
height: 28px;
border-radius: 50%;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
margin-left: 8px; /* Replacement for gap */
}
.cart-btn {
@@ -328,4 +316,4 @@ const removeFavorite = async (id: string) => {
.remove-icon {
font-size: 14px;
}
</style>
</style>

View File

@@ -1,21 +1,14 @@
<!-- 足迹页面 -->
<template>
<view class="footprint-page">
<!-- 顶部栏 -->
<view class="footprint-header">
<!-- Title hidden as requested -->
<!-- <view class="header-title">
<text class="title-text">我的足迹</text>
</view> -->
<view v-if="footprints.length > 0" class="header-actions">
<text class="action-btn" @click="toggleEditMode">{{ isEditMode ? '完成' : '编辑' }}</text>
<text class="action-btn" @click="clearAll">清空</text>
</view>
</view>
<!-- 日期分组 -->
<scroll-view class="footprint-content" scroll-y @scrolltolower="loadMore">
<!-- 空状态 -->
<view v-if="footprints.length === 0 && !isLoading" class="empty-footprints">
<text class="empty-icon">👣</text>
<text class="empty-text">暂无浏览记录</text>
@@ -23,20 +16,19 @@
<button class="go-shopping-btn" @click="goShopping">去逛逛</button>
</view>
<!-- 按日期分组 -->
<view v-for="(group, date) in groupedFootprints" :key="date" class="date-group">
<view v-for="(group, index) in groupedFootprints" :key="index" class="date-group">
<view class="group-header">
<text class="group-date">{{ formatGroupDate(date) }}</text>
<text v-if="isEditMode" class="group-select" @click="toggleGroupSelect(date)">
{{ isGroupSelected(date) ? '取消全选' : '全选' }}
<text class="group-date">{{ group.dateLabel }}</text>
<text v-if="isEditMode" class="group-select" @click="toggleGroupSelect(index)">
{{ isGroupSelected(index) ? '取消全选' : '全选' }}
</text>
</view>
<view class="group-items">
<view v-for="item in group" :key="item.id" class="footprint-item">
<view v-for="item in group.items" :key="item.id" class="footprint-item">
<view v-if="isEditMode" class="item-selector" @click="toggleSelect(item)">
<view :class="['select-icon', { selected: item.selected }]">
<text v-if="item.selected" class="icon-text">✓</text>
<view :class="['select-icon', { selected: item.selected === true }]">
<text v-if="item.selected === true" class="icon-text">✓</text>
</view>
</view>
<view class="item-content" @click="viewProduct(item)">
@@ -46,13 +38,7 @@
<view class="product-bottom">
<view class="product-price-row">
<text class="current-price">¥{{ item.price }}</text>
<!-- <text v-if="item.original_price && item.original_price > item.price"
class="original-price">¥{{ item.original_price }}</text> -->
</view>
<!-- <view class="product-meta">
<text class="sales-text">已售{{ item.sales }}</text>
<text class="time-text">{{ formatTime(item.viewTime) }}</text>
</view> -->
</view>
</view>
</view>
@@ -60,7 +46,6 @@
</view>
</view>
<!-- 加载更多 -->
<view v-if="isLoading" class="loading-more">
<text class="loading-text">加载中...</text>
</view>
@@ -69,7 +54,6 @@
</view>
</scroll-view>
<!-- 编辑操作栏 -->
<view v-if="isEditMode && footprints.length > 0" class="edit-bar">
<view class="select-all" @click="toggleSelectAll">
<view :class="['all-select-icon', { selected: isAllSelected }]">
@@ -92,151 +76,46 @@ type FootprintType = {
id: string
name: string
price: number
original_price?: number
original_price: number
image: string
sales: number
shopId: string
shopName: string
viewTime: number
selected?: boolean
selected: boolean
}
const footprints = ref<Array<FootprintType>>([])
type FootprintGroup = {
dateLabel: string
dateKey: string
items: FootprintType[]
}
type FootprintSaveType = {
id: string
name: string
price: number
original_price: number
image: string
sales: number
shopId: string
shopName: string
viewTime: number
}
const footprints = ref<FootprintType[]>([])
const isEditMode = ref<boolean>(false)
const isLoading = ref<boolean>(false)
const hasMore = ref<boolean>(false)
// 计算属性
const selectedCount = computed(() => {
return footprints.value.filter(item => item.selected).length
const selectedCount = computed((): number => {
return footprints.value.filter((item): Boolean => item.selected === true).length
})
const isAllSelected = computed(() => {
return footprints.value.length > 0 && footprints.value.every(item => item.selected)
const isAllSelected = computed((): boolean => {
return footprints.value.length > 0 && footprints.value.every((item): Boolean => item.selected === true)
})
const groupedFootprints = computed(() => {
const groups: Record<string, FootprintType[]> = {}
footprints.value.forEach(item => {
const date = new Date(item.viewTime).toDateString()
if (!groups[date]) {
groups[date] = []
}
groups[date].push(item)
})
return groups
})
// 生命周期
onMounted(() => {
loadFootprints()
})
// 加载足迹数据
const loadFootprints = async (loadMore: boolean = false) => {
isLoading.value = true
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 = []
}
}
}
isLoading.value = false
hasMore.value = false // 本地存储一次性加载完
}
// 格式化日期分组
const formatGroupDate = (dateStr: string): string => {
const date = new Date(dateStr)
const today = new Date()
@@ -254,24 +133,43 @@ const formatGroupDate = (dateStr: string): string => {
}
}
// 格式化时间
const formatTime = (timestamp: number): string => {
const date = new Date(timestamp)
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
return `${hours}:${minutes}`
}
const groupedFootprints = computed((): FootprintGroup[] => {
const result: FootprintGroup[] = []
for (let i = 0; i < footprints.value.length; i++) {
const item = footprints.value[i]
const dateKey = new Date(item.viewTime).toDateString()
let foundGroup: FootprintGroup | null = null
for (let j = 0; j < result.length; j++) {
if (result[j].dateKey === dateKey) {
foundGroup = result[j]
break
}
}
if (foundGroup != null) {
foundGroup.items.push(item)
} else {
const newGroup: FootprintGroup = {
dateLabel: formatGroupDate(dateKey),
dateKey: dateKey,
items: [item]
} as FootprintGroup
result.push(newGroup)
}
}
return result
})
// 切换编辑模式
const toggleEditMode = () => {
isEditMode.value = !isEditMode.value
// 重置选择状态
footprints.value.forEach(item => {
item.selected = false
})
for (let i = 0; i < footprints.value.length; i++) {
footprints.value[i].selected = false
}
}
// 清空所有足迹
const clearAll = () => {
if (footprints.value.length === 0) return
@@ -292,46 +190,41 @@ const clearAll = () => {
})
}
// 切换选择状态
const toggleSelect = (item: FootprintType) => {
item.selected = !item.selected
item.selected = !(item.selected === true)
footprints.value = [...footprints.value]
}
// 切换分组全选
const toggleGroupSelect = (dateStr: string) => {
const group = groupedFootprints.value[dateStr]
if (!group) return
const toggleGroupSelect = (groupIndex: number) => {
const group = groupedFootprints.value[groupIndex]
if (group == null) return
const isAllSelected = group.every(item => item.selected)
const newSelectedState = !isAllSelected
const allSelected = group.items.every((item): Boolean => item.selected === true)
const newSelectedState = !allSelected
group.forEach(item => {
item.selected = newSelectedState
})
for (let i = 0; i < group.items.length; i++) {
group.items[i].selected = newSelectedState
}
footprints.value = [...footprints.value]
}
// 检查组是否全选
const isGroupSelected = (dateStr: string): boolean => {
const group = groupedFootprints.value[dateStr]
if (!group || group.length === 0) return false
return group.every(item => item.selected)
const isGroupSelected = (groupIndex: number): boolean => {
const group = groupedFootprints.value[groupIndex]
if (group == null || group.items.length === 0) return false
return group.items.every((item): Boolean => item.selected === true)
}
// 全选/取消全选
const toggleSelectAll = () => {
const newSelectedState = !isAllSelected.value
footprints.value.forEach(item => {
item.selected = newSelectedState
})
for (let i = 0; i < footprints.value.length; i++) {
footprints.value[i].selected = newSelectedState
}
footprints.value = [...footprints.value]
}
// 删除选中项
const deleteSelected = () => {
const selectedItems = footprints.value.filter(item => item.selected)
const selectedItems = footprints.value.filter((item): Boolean => item.selected === true)
if (selectedItems.length === 0) {
uni.showToast({
title: '请选择要删除的记录',
@@ -343,25 +236,29 @@ const deleteSelected = () => {
uni.showModal({
title: '确认删除',
content: `确定要删除选中的${selectedItems.length}条记录吗?`,
success: async (res) => {
success: (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)
footprints.value = footprints.value.filter((item): Boolean => item.selected !== true)
// 保存回本地存储
const dataToSave = footprints.value.map(item => {
const { selected, ...rest } = item
return rest
})
const dataToSave: FootprintSaveType[] = []
for (let i = 0; i < footprints.value.length; i++) {
const item = footprints.value[i]
dataToSave.push({
id: item.id,
name: item.name,
price: item.price,
original_price: item.original_price,
image: item.image,
sales: item.sales,
shopId: item.shopId,
shopName: item.shopName,
viewTime: item.viewTime
} as FootprintSaveType)
}
uni.setStorageSync('footprints', JSON.stringify(dataToSave))
uni.showToast({
@@ -369,7 +266,6 @@ const deleteSelected = () => {
icon: 'success'
})
// 如果删完了,退出编辑模式
if (footprints.value.length === 0) {
isEditMode.value = false
}
@@ -378,34 +274,124 @@ const deleteSelected = () => {
})
}
// 查看商品
const viewProduct = (item: FootprintType) => {
if (isEditMode.value) return
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${item.id}&price=${item.price}&originalPrice=${item.original_price || ''}`
url: `/pages/mall/consumer/product-detail?productId=${item.id}&price=${item.price}&originalPrice=${item.original_price}`
})
}
// 加载更多
const loadMore = () => {
// 本地存储模式下暂不需要加载更多逻辑
}
// 去逛逛
const goShopping = () => {
uni.switchTab({
url: '/pages/mall/consumer/index'
})
}
const parseFootprintItem = (item: any): FootprintType => {
let itemObj: UTSJSONObject
if (item instanceof UTSJSONObject) {
itemObj = item as UTSJSONObject
} else {
itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
}
return {
id: itemObj.getString('id') ?? '',
name: itemObj.getString('name') ?? '',
price: itemObj.getNumber('price') ?? 0,
original_price: itemObj.getNumber('original_price') ?? 0,
image: itemObj.getString('image') ?? '',
sales: itemObj.getNumber('sales') ?? 0,
shopId: itemObj.getString('shopId') ?? '',
shopName: itemObj.getString('shopName') ?? '',
viewTime: itemObj.getNumber('viewTime') ?? 0,
selected: false
} as FootprintType
}
const loadFootprints = async () => {
isLoading.value = true
try {
const remoteData = await supabaseService.getFootprints()
if (remoteData.length > 0) {
console.log('获取到远程足迹数据:', remoteData.length)
const newFootprints: FootprintType[] = []
for (let i = 0; i < remoteData.length; i++) {
newFootprints.push(parseFootprintItem(remoteData[i]))
}
footprints.value = newFootprints
const dataToSave: FootprintSaveType[] = []
for (let i = 0; i < footprints.value.length; i++) {
const item = footprints.value[i]
dataToSave.push({
id: item.id,
name: item.name,
price: item.price,
original_price: item.original_price,
image: item.image,
sales: item.sales,
shopId: item.shopId,
shopName: item.shopName,
viewTime: item.viewTime
} as FootprintSaveType)
}
uni.setStorageSync('footprints', JSON.stringify(dataToSave))
} else {
const storedFootprints = uni.getStorageSync('footprints')
if (storedFootprints != null) {
try {
const data = JSON.parse(storedFootprints as string) as any[]
const newFootprints: FootprintType[] = []
for (let i = 0; i < data.length; i++) {
newFootprints.push(parseFootprintItem(data[i]))
}
footprints.value = newFootprints
} 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 != null) {
try {
const data = JSON.parse(storedFootprints as string) as any[]
const newFootprints: FootprintType[] = []
for (let i = 0; i < data.length; i++) {
newFootprints.push(parseFootprintItem(data[i]))
}
footprints.value = newFootprints
} catch (err) {
footprints.value = []
}
}
}
isLoading.value = false
hasMore.value = false
}
onMounted(() => {
loadFootprints()
})
</script>
<style scoped>
.footprint-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
flex: 1;
}
.footprint-header {
@@ -417,32 +403,20 @@ const goShopping = () => {
justify-content: space-between;
}
.header-title {
/* margin-left: 15px; */ /* Removed */
display: none;
}
.title-text {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.header-actions {
display: flex;
flex-direction: row;
gap: 20px;
flex: 1;
justify-content: flex-end; /* Align Edit/Clear to the right */
align-items: center;
padding-right: 0;
flex-direction: row;
flex: 1;
justify-content: flex-end;
align-items: center;
padding-right: 0;
}
.action-btn {
color: #007aff;
font-size: 14px;
padding: 5px;
margin-left: 20px;
margin-left: 20px;
}
.footprint-content {
@@ -492,7 +466,6 @@ const goShopping = () => {
.group-header {
padding: 15px 5px;
/* border-bottom: 1px solid #f5f5f5; */
display: flex;
align-items: center;
justify-content: space-between;
@@ -514,28 +487,19 @@ const goShopping = () => {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
justify-content: space-between;
}
.footprint-item {
display: flex;
flex-direction: column; /* 垂直排列内容 */
/* padding: 15px 0; */ /* Grid layout uses gap */
flex-direction: column;
margin-bottom: 10px;
border-bottom: none;
width: calc(50% - 5px); /* Mobile: 2 items per row */
width: 48%;
background-color: #fff;
/* border-radius: 8px; */ /* Optional card style */
/* overflow: hidden; */
position: relative; /* For absolute positioning of selector */
position: relative;
}
/*
.footprint-item:last-child {
border-bottom: none;
}
*/
.item-selector {
position: absolute;
top: 5px;
@@ -543,7 +507,6 @@ const goShopping = () => {
z-index: 10;
width: 30px;
height: 30px;
/* background-color: rgba(255,255,255,0.8); */
border-radius: 15px;
display: flex;
align-items: center;
@@ -574,12 +537,12 @@ const goShopping = () => {
.item-content {
flex: 1;
display: flex;
flex-direction: column; /* 垂直堆叠 */
flex-direction: column;
}
.product-image {
width: 100%;
height: 170px; /* Adjust height based on aspect ratio preference */
height: 170px;
border-radius: 5px;
margin-right: 0;
margin-bottom: 8px;
@@ -599,11 +562,10 @@ const goShopping = () => {
color: #333333;
line-height: 1.4;
margin-bottom: 6px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
height: 40px;
text-overflow: ellipsis;
lines: 2;
height: 40px;
}
.product-bottom {
@@ -615,8 +577,7 @@ const goShopping = () => {
.product-price-row {
display: flex;
align-items: baseline;
margin-bottom: 0;
align-items: flex-end;
}
.current-price {
@@ -626,44 +587,21 @@ const goShopping = () => {
margin-right: 0;
}
/* PC/Tablet Responsive */
@media (min-width: 768px) {
.footprint-item {
width: calc(33.33% - 7px) !important; /* Tablet: 3 items */
}
.footprint-item {
width: 32% !important;
}
}
@media (min-width: 1024px) {
.footprint-item {
width: calc(16.66% - 9px) !important; /* PC: 6 items */
}
.footprint-content, .footprint-header {
max-width: 1200px;
margin: 0 auto;
}
}
.original-price {
font-size: 12px;
color: #999999;
text-decoration: line-through;
}
.product-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.sales-text {
font-size: 12px;
color: #999999;
}
.time-text {
font-size: 12px;
color: #666666;
.footprint-item {
width: 16% !important;
}
.footprint-content, .footprint-header {
max-width: 1200px;
margin: 0 auto;
}
}
.loading-more,
@@ -725,4 +663,4 @@ const goShopping = () => {
font-size: 14px;
font-weight: bold;
}
</style>
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -674,7 +674,7 @@ const addToCart = async (product: any) => {
uni.showLoading({ title: '添加中...' })
try {
// 尝试调用 Supabase 服务添加
const success = await supabaseService.addToCart(product.id, 1)
const success = await supabaseService.addToCart(product.id, 1, '')
if (success) {
uni.showToast({
title: '已添加到购物车',
@@ -990,7 +990,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.card-name {
font-size: 15px;
font-weight: 600;
font-weight: 700;
color: #333;
margin-bottom: 4px;
text-align: center;
@@ -1046,7 +1046,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.news-image {
width: 100%;
height: 100%;
display: block;
/* display: block; REMOVED for uniapp-x support */
}
.news-overlay {
@@ -1071,8 +1071,8 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.news-caption {
font-size: 16px;
font-weight: 600;
display: block;
font-weight: 700;
/* display: block; REMOVED for uniapp-x support */
line-height: 1.4;
}
@@ -1086,9 +1086,11 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
/* grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); REMOVED */
/* gap: 20px; REMOVED */
}
.service-card {
@@ -1098,7 +1100,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
padding: 20px;
background: #f8f9fa;
border-radius: 12px;
cursor: pointer;
/* cursor: pointer; REMOVED for uniapp-x support */
transition: all 0.3s ease;
}
@@ -1124,7 +1126,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.service-name {
font-size: 15px;
font-weight: 600;
font-weight: 700;
color: #333;
margin-bottom: 4px;
}
@@ -1154,13 +1156,14 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.title-section {
display: flex;
align-items: center;
gap: 8px;
/* gap: 8px; REMOVED */
width: 100%;
}
.section-icon {
font-size: 20px;
color: #4CAF50;
margin-right: 8px;
}
.sort-tabs {
@@ -1180,9 +1183,9 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
padding: 8px 12px; /* 增加左右内边距 */
border-radius: 20px;
border: 1px solid #e0e0e0;
cursor: pointer;
/* cursor: pointer; REMOVED */
transition: all 0.2s ease;
white-space: nowrap;
/* white-space: nowrap; REMOVED for uniapp-x support on view */
flex: 1; /* 均分宽度 */
min-width: 70px; /* 设置最小宽度防止过窄 */
text-align: center;
@@ -1207,13 +1210,18 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
/* 产品网格 */
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
/* grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); REMOVED */
/* gap: 20px; REMOVED */
margin-top: 20px;
margin: 20px -1.5% 0 -1.5%;
}
.product-card {
width: 47%;
margin: 0 1.5% 20px 1.5%;
background: #f8f9fa;
border-radius: 12px;
overflow: hidden;
@@ -1237,7 +1245,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
font-size: 11px;
padding: 4px 12px;
border-radius: 12px;
font-weight: 600;
font-weight: 700;
z-index: 2;
}
@@ -1254,10 +1262,10 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.product-name {
font-size: 15px;
font-weight: 600;
font-weight: 700;
color: #333;
margin-bottom: 4px;
display: block;
/* display: block; REMOVED */
line-height: 1.4;
}
@@ -1265,7 +1273,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
font-size: 13px;
color: #666;
margin-bottom: 12px;
display: block;
/* display: block; REMOVED */
}
.price-section {
@@ -1361,9 +1369,11 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.family-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 16px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
/* grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); REMOVED */
/* gap: 16px; REMOVED */
margin-top: 20px;
}
@@ -1400,7 +1410,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.family-name {
font-size: 14px;
font-weight: 600;
font-weight: 700;
color: #333;
margin-bottom: 4px;
}
@@ -1436,9 +1446,9 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
padding: 8px 12px; /* 增加左右内边距 */
border-radius: 20px;
border: 1px solid #e0e0e0;
cursor: pointer;
/* cursor: pointer; REMOVED */
transition: all 0.2s ease;
white-space: nowrap;
/* white-space: nowrap; REMOVED */
flex: 1; /* 均分宽度 */
min-width: 80px; /* 设置最小宽度防止过窄 */
text-align: center;
@@ -1462,13 +1472,18 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.recommend-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
/* grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); REMOVED */
/* gap: 20px; REMOVED */
margin-top: 20px;
margin: 20px -1.5% 0 -1.5%;
}
.recommend-product {
width: 97%; /* Single col by default or adjust */
margin: 0 1.5% 20px 1.5%;
background: #f8f9fa;
border-radius: 12px;
overflow: hidden;
@@ -1506,7 +1521,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
padding: 4px 10px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
font-weight: 700;
color: white;
}
@@ -1524,7 +1539,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.product-title {
font-size: 16px;
font-weight: 600;
font-weight: 700;
color: #333;
margin-bottom: 4px;
display: block;
@@ -1558,7 +1573,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.rating-value {
font-weight: 600;
font-weight: 700;
color: #333;
}
@@ -1622,7 +1637,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.reminder-title {
font-size: 15px;
font-weight: 600;
font-weight: 700;
}
.reminder-desc {
@@ -1661,15 +1676,17 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
height: 32px;
border: 3px solid #f0f0f0;
border-top-color: #4CAF50;
border-radius: 50%;
animation: spin 1s linear infinite;
border-radius: 16px;
/* animation: spin 1s linear infinite; REMOVED for uniapp-x support */
margin-bottom: 12px;
}
/*
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
}
*/
.loading-text {
font-size: 14px;
@@ -1705,7 +1722,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.search-box {
padding: 8px 16px; /* 与分类页保持一致 */
max-width: 100%;
/* max-width: 100%; REMOVED for uniapp-x support */
margin: 0;
}
@@ -1783,9 +1800,16 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.products-grid,
.recommend-grid {
grid-template-columns: repeat(2, 1fr); /* 手机端调整为双列显示 */
gap: 8px; /* 减小间距 */
/* grid-template-columns: repeat(2, 1fr); REMOVED */
/* gap: 8px; REMOVED */
margin: 20px -1% 0 -1%;
}
.products-grid .product-card,
.recommend-grid .recommend-product {
width: 48%;
margin: 0 1% 8px 1%;
}
.product-info,
.product-details {
@@ -1858,7 +1882,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.hot-products .price-value,
.smart-recommend .price-value {
font-size: 14px; /* 字体变小 */
font-weight: 600;
font-weight: 700;
}
.family-grid {

View File

@@ -70,17 +70,22 @@ const trackList = ref<TrackItem[]>([
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options as any
if (options.orderId) {
orderId.value = options.orderId
// 这里可以根据orderId去请求真实的物流信息
const options = currentPage.options
if (options != null) {
const optionsObj = options as UTSJSONObject
const orderIdValue = optionsObj.getString('orderId')
if (orderIdValue != null) {
orderId.value = orderIdValue
// 这里可以根据orderId去请求真实的物流信息
}
}
})
</script>
<style scoped>
.logistics-page {
min-height: 100vh;
/* min-height: 100vh; */
flex: 1;
background-color: #f5f5f5;
padding-bottom: 20px;
}
@@ -158,10 +163,7 @@ onMounted(() => {
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #ccc;
margin-top: 6px;
z-index: 1;
border-radius: 4px;
}
.first .dot {

View File

@@ -86,7 +86,7 @@
mode="aspectFill"
/>
<view v-else class="message-icon-default" :style="{ backgroundColor: message.color }">
<text>{{ message.icon }}</text>
<text class="message-icon-text">{{ message.icon }}</text>
</view>
<view v-if="message.online" class="online-dot"></view>
</view>
@@ -202,20 +202,53 @@
<view class="safe-area"></view>
</scroll-view>
<!-- 底部固定按钮 -->
<view class="floating-action">
<!-- 底部固定按钮 (Hidden) -->
<!-- <view class="floating-action">
<button class="action-button" @click="startNewChat">
<text class="button-icon">✏️</text>
<text class="button-text">新建聊天</text>
</button>
</view>
</view> -->
</view>
</template>
<script setup lang="uts">
import { ref, reactive, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { supabaseService, type Notification, type ChatMessage } from '@/utils/supabaseService.uts'
import { supabaseService, type Notification, type ChatMessage, type ChatRoom } from '@/utils/supabaseService.uts'
// 定义消息项类型
type MessageItem = {
id: string,
title: string,
content: string,
time: string,
read: boolean,
type: string,
avatar: string | null,
important: boolean,
coupon: string,
expiry: string,
claimed: boolean,
order_no: string,
status: string,
statusText: string,
role: string,
lastMessage: string,
online: boolean,
unreadCount: number,
tags: string[],
icon: string,
color: string,
active: boolean
}
// 定义标签类型
type MessageTab = {
id: string,
name: string,
unread: number
}
// 响应式数据
const activeTab = ref<string>('service')
@@ -226,33 +259,22 @@ const statusBarHeight = ref(0)
const scrollTop = ref(0)
const scrollHeight = ref(0)
// 初始化页面布局数据
const initPage = () => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
// 计算滚动区域高度:屏幕高度 - 状态栏 - 导航栏(44) - 标签栏(42)
const windowHeight = systemInfo.windowHeight
scrollHeight.value = windowHeight - statusBarHeight.value - 44 - 42
}
// 消息分类标签
const messageTabs = reactive([
const messageTabs = reactive<MessageTab[]>([
{ id: 'service', name: '客服消息', unread: 5 },
{ id: 'system', name: '系统通知', unread: 3 },
{ id: 'order', name: '订单消息', unread: 2 },
{ id: 'promo', name: '优惠活动', unread: 2 }
])
// Mock 客服消息数据
const serviceMessages = reactive<any[]>([])
const systemMessages = reactive<any[]>([])
const orderMessages = reactive<any[]>([])
// Mock 优惠活动数据
const promoMessages = reactive<any[]>([])
// 消息数据
const serviceMessages = reactive<MessageItem[]>([])
const systemMessages = reactive<MessageItem[]>([])
const orderMessages = reactive<MessageItem[]>([])
const promoMessages = reactive<MessageItem[]>([])
// 计算当前显示的消息
const currentMessages = computed(() => {
const currentMessages = computed<MessageItem[]>(() => {
switch (activeTab.value) {
case 'system': return systemMessages
case 'order': return orderMessages
@@ -262,21 +284,9 @@ const currentMessages = computed(() => {
}
})
// 生命周期
onMounted(() => {
console.log('Messages Page Mounted')
initPage()
// loadMessages() // 移至 onShow 调用
})
onShow(() => {
console.log('Messages Page Show')
loadMessages()
})
// 简单的日期格式化
const formatTime = (isoString: string): string => {
if (!isoString) return ''
if (isoString == '') return ''
try {
return isoString.split('T')[0]
} catch(e) {
@@ -284,12 +294,56 @@ const formatTime = (isoString: string): string => {
}
}
// 加载消息
// 更新未读数量 - 必须在 loadMessages 之前定义
const updateUnreadCount = () => {
let totalUnread = 0
let serviceUnread = 0
serviceMessages.forEach((msg: MessageItem) => {
if (!msg.read) serviceUnread++
})
messageTabs[0].unread = serviceUnread
totalUnread += serviceUnread
let systemUnread = 0
systemMessages.forEach((msg: MessageItem) => {
if (!msg.read) systemUnread++
})
messageTabs[1].unread = systemUnread
totalUnread += systemUnread
let orderUnread = 0
orderMessages.forEach((msg: MessageItem) => {
if (!msg.read) orderUnread++
})
messageTabs[2].unread = orderUnread
totalUnread += orderUnread
let promoUnread = 0
promoMessages.forEach((msg: MessageItem) => {
if (!msg.read) promoUnread++
})
messageTabs[3].unread = promoUnread
totalUnread += promoUnread
unreadCount.value = totalUnread
}
// 初始化页面布局数据
const initPage = () => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight
const windowHeight = systemInfo.windowHeight
scrollHeight.value = windowHeight - statusBarHeight.value - 44 - 42
}
// 加载消息函数 - 必须在 updateUnreadCount 之后定义
const loadMessages = async () => {
loading.value = true
try {
// 清空现有Mock数据
// 清空现有数据
serviceMessages.length = 0
systemMessages.length = 0
orderMessages.length = 0
@@ -299,17 +353,15 @@ const loadMessages = async () => {
const notes = await supabaseService.getUserNotifications()
notes.forEach((note: Notification) => {
// 这里使用 any 类型构建对象,以匹配 reactive 数组的结构
const item = {
const item: MessageItem = {
id: note.id,
title: note.title,
content: note.content,
time: formatTime(note.created_at || ''),
time: formatTime(note.created_at ?? ''),
read: note.is_read,
type: note.type, // 'system', 'order', 'promotion' => 'promo'
// 默认填充字段以避免渲染报错
type: note.type,
avatar: note.icon_url,
important: note.type === 'system', // 简单逻辑
important: note.type === 'system',
coupon: '点击查看',
expiry: '',
claimed: false,
@@ -320,9 +372,10 @@ const loadMessages = async () => {
lastMessage: '',
online: false,
unreadCount: 0,
tags: [],
tags: [] as string[],
icon: '',
color: ''
color: '',
active: false
}
if (note.type === 'system') {
@@ -330,85 +383,44 @@ const loadMessages = async () => {
} 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()
// console.log('Raw chats:', chats)
if (chats.length > 0) {
const currentUserId = supabaseService.getCurrentUserId()
const conversations = new Map<string, any>()
// 1. Group by conversation partner
for (const msg of chats) {
const partnerId = (msg.sender_id == currentUserId) ? msg.receiver_id : msg.sender_id
// Skip if partner is null/invalid
if (!partnerId) continue;
if (!conversations.has(partnerId)) {
conversations.set(partnerId, {
partnerId: partnerId,
lastMessage: msg,
unreadCount: 0
})
}
const conv = conversations.get(partnerId)
// Since chats are likely sorted desc, the first one seen is the latest.
// Just count unread: if I am the receiver and it's not read
if (msg.receiver_id == currentUserId && !msg.is_read) {
conv.unreadCount++
}
}
console.log('Conversations found:', conversations.size)
// 2. Fetch shop details for each conversation
const convList = Array.from(conversations.values())
const promises = convList.map(async (conv) => {
const shop = await supabaseService.getShopByMerchantId(conv.partnerId)
const shopName = shop ? shop.shop_name : '未知商家'
const shopAvatar = (shop && shop.logo_url) ? shop.logo_url : '/static/icons/shop-default.png'
return {
id: conv.partnerId, // Use partnerId as the ID for navigation
title: shopName,
role: '商家客服',
content: conv.lastMessage.content,
lastMessage: conv.lastMessage.content,
time: formatTime(conv.lastMessage.created_at || ''),
read: conv.unreadCount === 0,
type: 'service',
avatar: shopAvatar,
online: true,
unreadCount: conv.unreadCount,
tags: shop ? ['官方认证'] : [],
icon: '🏪',
color: '#FF9800',
important: false,
coupon: '',
expiry: '',
claimed: false,
order_no: '',
status: '',
statusText: ''
}
})
const renderedMessages = await Promise.all(promises)
serviceMessages.push(...renderedMessages)
}
const rooms = await supabaseService.getChatRooms()
rooms.forEach((room: ChatRoom) => {
const msgItem: MessageItem = {
id: room.merchant_id,
title: room.shop_name,
role: '商家客服',
content: room.last_message ?? '暂无消息',
lastMessage: room.last_message ?? '暂无消息',
time: formatTime(room.last_message_at ?? ''),
read: room.unread_count === 0,
type: 'service',
avatar: room.shop_logo ?? '/static/icons/shop-default.png',
online: true,
unreadCount: room.unread_count,
tags: [] as string[],
icon: '🏪',
color: '#FF9800',
important: false,
coupon: '',
expiry: '',
claimed: false,
order_no: '',
status: '',
statusText: '',
active: false
}
serviceMessages.push(msgItem)
})
// 如果没有消息,为了演示效果(或者真的需要),可以保留一个默认的系统客服
// 如果没有消息,添加默认客服
if (serviceMessages.length === 0) {
serviceMessages.push({
const defaultService: MessageItem = {
id: 'default_service',
title: '平台客服',
role: '智能助手',
@@ -429,8 +441,10 @@ const loadMessages = async () => {
claimed: false,
order_no: '',
status: '',
statusText: ''
})
statusText: '',
active: false
}
serviceMessages.push(defaultService)
}
} catch (e) {
@@ -441,28 +455,16 @@ const loadMessages = async () => {
}
}
// 更新未读数量
const updateUnreadCount = () => {
let totalUnread = 0
const serviceUnread = serviceMessages.filter(msg => !msg.read).length
messageTabs[0].unread = serviceUnread
totalUnread += serviceUnread
const systemUnread = systemMessages.filter(msg => !msg.read).length
messageTabs[1].unread = systemUnread
totalUnread += systemUnread
const orderUnread = orderMessages.filter(msg => !msg.read).length
messageTabs[2].unread = orderUnread
totalUnread += orderUnread
const promoUnread = promoMessages.filter(msg => !msg.read).length
messageTabs[3].unread = promoUnread
totalUnread += promoUnread
unreadCount.value = totalUnread
}
// 生命周期钩子
onMounted(() => {
console.log('Messages Page Mounted')
initPage()
})
onShow(() => {
console.log('Messages Page Show')
loadMessages()
})
// 切换标签
const switchTab = (tabId: string) => {
@@ -472,7 +474,7 @@ const switchTab = (tabId: string) => {
}
// 开始与客服聊天
const startChatWithService = (message: any) => {
const startChatWithService = (message: MessageItem) => {
message.read = true
message.unreadCount = 0
updateUnreadCount()
@@ -506,7 +508,7 @@ const startNewChat = () => {
}
// 查看系统消息
const viewSystemMessage = (message: any) => {
const viewSystemMessage = (message: MessageItem) => {
message.read = true
updateUnreadCount()
uni.navigateTo({
@@ -515,7 +517,7 @@ const viewSystemMessage = (message: any) => {
}
// 查看订单消息
const viewOrderMessage = (message: any) => {
const viewOrderMessage = (message: MessageItem) => {
message.read = true
updateUnreadCount()
uni.navigateTo({
@@ -524,7 +526,7 @@ const viewOrderMessage = (message: any) => {
}
// 查看优惠活动
const viewPromoMessage = (message: any) => {
const viewPromoMessage = (message: MessageItem) => {
message.read = true
updateUnreadCount()
uni.navigateTo({
@@ -533,7 +535,7 @@ const viewPromoMessage = (message: any) => {
}
// 领取优惠券
const claimCoupon = (message: any) => {
const claimCoupon = (message: MessageItem) => {
if (message.claimed) {
uni.showToast({
title: '您已领取该优惠券',
@@ -544,13 +546,14 @@ const claimCoupon = (message: any) => {
message.claimed = true
// 保存领取状态到本地存储,供个人页读取
const claimedCouponsCount = uni.getStorageSync('claimedCoupons') || 0
uni.setStorageSync('claimedCoupons', (claimedCouponsCount as number) + 1)
const claimedCouponsCount = uni.getStorageSync('claimedCoupons')
const count = (claimedCouponsCount != null) ? (claimedCouponsCount as number) : 0
uni.setStorageSync('claimedCoupons', count + 1)
// 保存详细的优惠券信息到 myCoupons 列表
const myCoupons = uni.getStorageSync('myCoupons')
let couponsList: any[] = []
if (myCoupons) {
if (myCoupons != null) {
try {
couponsList = JSON.parse(myCoupons as string) as any[]
} catch (e) {
@@ -579,15 +582,23 @@ const clearAllUnread = () => {
content: '确定要标记所有消息为已读吗?',
success: (res) => {
if (res.confirm) {
serviceMessages.forEach(msg => {
serviceMessages.forEach((msg: MessageItem) => {
msg.read = true
msg.unreadCount = 0
})
systemMessages.forEach(msg => msg.read = true)
orderMessages.forEach(msg => msg.read = true)
promoMessages.forEach(msg => msg.read = true)
systemMessages.forEach((msg: MessageItem) => {
msg.read = true
})
orderMessages.forEach((msg: MessageItem) => {
msg.read = true
})
promoMessages.forEach((msg: MessageItem) => {
msg.read = true
})
messageTabs.forEach(tab => tab.unread = 0)
messageTabs.forEach((tab: MessageTab) => {
tab.unread = 0
})
unreadCount.value = 0
uni.showToast({
@@ -617,7 +628,7 @@ const onRefresh = () => {
/* 页面结构优化 - 避免双滚动条 */
.messages-page {
width: 100%;
height: 100vh;
height: 100%;
background-color: #f8fafc;
display: flex;
flex-direction: column;
@@ -673,7 +684,7 @@ const onRefresh = () => {
background: rgba(255, 255, 255, 0.2);
padding: 4px 12px;
border-radius: 20px;
cursor: pointer;
/* cursor: pointer; removed for uniapp-x support */
transition: all 0.2s ease;
}
@@ -689,7 +700,7 @@ const onRefresh = () => {
.action-text {
font-size: 12px;
color: white;
font-weight: 500;
font-weight: bold;
}
/* 导航栏占位符 */
@@ -716,7 +727,7 @@ const onRefresh = () => {
/* overflow-x: auto; 移除自动滚动,改为自适应宽度 */
max-width: 1400px;
margin: 0 auto;
gap: 4px; /* 减小间距 */
/* gap: 4px; removed for uniapp-x support */
justify-content: space-between; /* 均匀分布 */
}
@@ -726,6 +737,7 @@ const onRefresh = () => {
.tab-item {
padding: 0 4px;
margin: 0 2px; /* replaced gap */
display: flex; /* 改为 flex 布局 */
flex-direction: row; /* 关键:横向排列 文字和数字 */
align-items: center; /* 垂直居中 */
@@ -804,7 +816,7 @@ const onRefresh = () => {
font-size: 12px;
padding: 4px 10px;
border-radius: 12px;
font-weight: 500;
font-weight: bold;
}
.service-status.online {
@@ -820,9 +832,12 @@ const onRefresh = () => {
}
.service-categories {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
display: flex;
flex-direction: row;
flex-wrap: wrap; /* allow wrapping to simulate grid */
/* grid-template-columns: repeat(2, 1fr); REMOVED */
/* gap: 12px; removed for uniapp-x support */
padding: 6px; /* compensated padding */
}
.category-item {
@@ -832,8 +847,10 @@ const onRefresh = () => {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
/* cursor: pointer; removed for uniapp-x support */
transition: all 0.3s ease;
margin: 1%; /* replaced gap */
width: 48%; /* 2 columns */
}
.category-item:hover {
@@ -850,7 +867,7 @@ const onRefresh = () => {
.category-name {
font-size: 13px;
color: #333;
font-weight: 500;
font-weight: bold;
}
/* 消息项 */
@@ -867,7 +884,7 @@ const onRefresh = () => {
align-items: flex-start;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
cursor: pointer;
/* cursor: pointer; removed for uniapp-x support */
}
.message-item:hover {
@@ -906,7 +923,7 @@ const onRefresh = () => {
justify-content: center;
}
.message-icon-default text {
.message-icon-text {
font-size: 24px;
color: white;
}
@@ -950,7 +967,7 @@ const onRefresh = () => {
font-size: 16px;
color: #333;
font-weight: bold;
display: block;
/* display: block; REMOVED for uniapp-x support */
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
@@ -963,20 +980,21 @@ const onRefresh = () => {
background: #E8F5E9;
padding: 2px 8px;
border-radius: 10px;
display: inline-block;
/* display: inline-block; REMOVED for uniapp-x support */
}
.message-header-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
/* gap: 4px; removed for uniapp-x support */
}
.message-time {
font-size: 12px;
color: #999;
white-space: nowrap;
margin-bottom: 4px; /* replaced gap */
}
.message-unread-count {
@@ -999,21 +1017,23 @@ const onRefresh = () => {
color: #666;
line-height: 1.4;
margin-bottom: 4px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
/* display: -webkit-box; REMOVED for uniapp-x support */
/* -webkit-line-clamp: 2; REMOVED for uniapp-x support */
/* -webkit-box-orient: vertical; REMOVED for uniapp-x support */
lines: 2; /* UTS text truncation */
overflow: hidden;
text-overflow: ellipsis; /* Ensure standard CSS property is present */
}
.last-message {
font-size: 13px;
color: #999;
display: block;
/* display: block; REMOVED for uniapp-x support */
}
.message-tags {
display: flex;
gap: 6px;
/* gap: 6px; removed for uniapp-x support */
flex-wrap: wrap;
}
@@ -1023,6 +1043,8 @@ const onRefresh = () => {
background: #f0f0f0;
padding: 3px 8px;
border-radius: 10px;
margin-right: 6px; /* replaced gap */
margin-bottom: 4px; /* for wrapping */
}
.order-info {
@@ -1031,17 +1053,19 @@ const onRefresh = () => {
background-color: #E8F5E9;
padding: 4px 10px;
border-radius: 4px;
display: inline-block;
/* display: inline-block; REMOVED for uniapp-x support */
margin-top: 8px;
align-self: flex-start; /* Ensure it doesn't stretch */
}
.order-status {
display: inline-block;
/* display: inline-block; REMOVED for uniapp-x support */
font-size: 12px;
padding: 4px 10px;
border-radius: 12px;
margin-top: 8px;
margin-left: 8px;
align-self: flex-start; /* Ensure it doesn't stretch */
}
.order-status.shipping {
@@ -1060,13 +1084,14 @@ const onRefresh = () => {
}
.important-tag {
display: inline-block;
/* display: inline-block; REMOVED for uniapp-x support */
background-color: #FF5722;
color: white;
font-size: 11px;
padding: 3px 8px;
border-radius: 10px;
margin-top: 8px;
align-self: flex-start; /* Ensure it doesn't stretch */
}
.coupon-info {
@@ -1080,7 +1105,7 @@ const onRefresh = () => {
.coupon-text {
font-size: 14px;
font-weight: bold;
display: block;
/* display: block; REMOVED for uniapp-x support */
margin-bottom: 4px;
}
@@ -1107,7 +1132,7 @@ const onRefresh = () => {
margin: 15px;
display: flex;
align-items: flex-start;
gap: 10px;
/* gap: 10px; removed for uniapp-x support */
}
.tip-icon {
@@ -1115,6 +1140,7 @@ const onRefresh = () => {
color: #FF9800;
flex-shrink: 0;
margin-top: 2px;
margin-right: 10px; /* replaced gap */
}
.tip-text {
@@ -1167,7 +1193,7 @@ const onRefresh = () => {
display: flex;
align-items: center;
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
font-weight: 500;
font-weight: bold;
}
.button-icon {
@@ -1191,8 +1217,12 @@ const onRefresh = () => {
}
.service-categories {
grid-template-columns: 1fr;
/* grid-template-columns: 1fr; REMOVED */
}
.category-item {
width: 100%; /* 1 column */
}
}
@media screen and (min-width: 415px) {
@@ -1206,7 +1236,7 @@ const onRefresh = () => {
border-radius: 30px;
}
.message-icon-default text {
.message-icon-text {
font-size: 28px;
}
@@ -1215,9 +1245,16 @@ const onRefresh = () => {
}
.service-categories {
grid-template-columns: repeat(4, 1fr);
/* grid-template-columns: repeat(4, 1fr); REMOVED */
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.service-categories .category-item {
width: 25%;
}
/* 平板和桌面端优化标签栏显示 */
.message-tabs {
justify-content: flex-start; /* 左对齐 */

View File

@@ -664,7 +664,7 @@ const onRefresh = () => {
/* overflow-x: auto; 移除自动滚动,改为自适应宽度 */
max-width: 1400px;
margin: 0 auto;
gap: 4px; /* 减小间距 */
/* gap: 4px; removed for uniapp-x support */
justify-content: space-between; /* 均匀分布 */
}
@@ -674,6 +674,7 @@ const onRefresh = () => {
.tab-item {
padding: 0 4px;
margin: 0 2px;
display: flex; /* 改为 flex 布局 */
flex-direction: row; /* 关键:横向排列 文字和数字 */
align-items: center; /* 垂直居中 */

View File

@@ -14,7 +14,7 @@
</view>
<!-- 配送信息 -->
<view v-if="order.order_status >= 2" class="delivery-info card">
<view v-if="order != null && (order?.order_status ?? 0) >= 2" class="delivery-info card">
<view class="delivery-header">
<text class="section-title">配送信息</text>
</view>
@@ -22,17 +22,17 @@
<view class="address-icon">📍</view>
<view class="address-content">
<view class="address-user">
<text class="recipient">{{ deliveryAddress.name }}</text>
<text class="phone">{{ deliveryAddress.phone }}</text>
<text class="recipient">{{ deliveryAddress?.name ?? '' }}</text>
<text class="phone">{{ deliveryAddress?.phone ?? '' }}</text>
</view>
<text class="address-detail">{{ getFullAddress(deliveryAddress) }}</text>
<text class="address-detail">{{ getFullAddress(deliveryAddress as any) }}</text>
</view>
</view>
<!-- 如果有物流信息显示 -->
<view v-if="deliveryInfo.tracking_no" class="courier-info">
<view v-if="deliveryInfo != null && deliveryInfo?.tracking_no != ''" class="courier-info">
<text class="courier-label">物流单号:</text>
<text class="courier-value">{{ deliveryInfo.tracking_no }}</text>
<text class="copy-btn" @click="copyText(deliveryInfo.tracking_no)">复制</text>
<text class="courier-value">{{ deliveryInfo?.tracking_no ?? '' }}</text>
<text class="copy-btn" @click="copyText(deliveryInfo?.tracking_no ?? '')">复制</text>
</view>
</view>
@@ -44,7 +44,7 @@
<text class="arrow-right">></text>
</view>
<view v-for="item in orderItems" :key="item.id" class="product-item" @click="goToProduct(item.product_id)">
<image :src="item.image_url || '/static/default-product.png'" class="product-image" mode="aspectFill"/>
<image :src="item.image_url != null && item.image_url != '' ? item.image_url : '/static/default-product.png'" class="product-image" mode="aspectFill"/>
<view class="product-info">
<text class="product-name">{{ item.product_name }}</text>
<text v-if="item.specifications" class="product-spec">{{ getSpecText(item.specifications) }}</text>
@@ -57,56 +57,56 @@
</view>
<!-- 订单信息 -->
<view class="order-info card">
<view class="order-info card" v-if="order != null">
<view class="info-row">
<text class="info-label">订单编号</text>
<text class="info-value copyable" @click="copyText(order.order_no)">{{ order.order_no }} <text class="copy-icon">📄</text></text>
<text class="info-value copyable" @click="copyText(order?.order_no ?? '')">{{ order?.order_no ?? '' }} <text class="copy-icon">📄</text></text>
</view>
<view class="info-row">
<text class="info-label">下单时间</text>
<text class="info-value">{{ formatTime(order.created_at) }}</text>
<text class="info-value">{{ formatTime(order?.created_at ?? '') }}</text>
</view>
<view class="info-row" v-if="order.payment_method">
<view class="info-row" v-if="order?.payment_method != null && order?.payment_method != ''">
<text class="info-label">支付方式</text>
<text class="info-value">{{ getPaymentMethodText(order.payment_method) }}</text>
<text class="info-value">{{ getPaymentMethodText(order?.payment_method as any) }}</text>
</view>
<view class="info-row" v-if="order.paid_at">
<view class="info-row" v-if="order?.paid_at != null && order?.paid_at != ''">
<text class="info-label">支付时间</text>
<text class="info-value">{{ formatTime(order.paid_at) }}</text>
<text class="info-value">{{ formatTime(order?.paid_at ?? '') }}</text>
</view>
<view class="info-row" v-if="order.shipped_at">
<view class="info-row" v-if="order?.shipped_at != null && order?.shipped_at != ''">
<text class="info-label">发货时间</text>
<text class="info-value">{{ formatTime(order.shipped_at) }}</text>
<text class="info-value">{{ formatTime(order?.shipped_at ?? '') }}</text>
</view>
<view class="info-row" v-if="order.completed_at">
<view class="info-row" v-if="order?.completed_at != null && order?.completed_at != ''">
<text class="info-label">完成时间</text>
<text class="info-value">{{ formatTime(order.completed_at) }}</text>
<text class="info-value">{{ formatTime(order?.completed_at ?? '') }}</text>
</view>
</view>
<!-- 费用明细 -->
<view class="cost-detail card">
<view class="cost-detail card" v-if="order != null">
<view class="cost-row">
<text class="cost-label">商品总额</text>
<text class="cost-value">¥{{ order.product_amount }}</text>
<text class="cost-value">¥{{ order?.product_amount ?? 0 }}</text>
</view>
<view class="cost-row">
<text class="cost-label">运费</text>
<text class="cost-value">+¥{{ order.shipping_fee || 0 }}</text>
<text class="cost-value">+¥{{ order?.shipping_fee != null ? order?.shipping_fee : 0 }}</text>
</view>
<view class="cost-row" v-if="order.discount_amount > 0">
<view class="cost-row" v-if="(order?.discount_amount ?? 0) > 0">
<text class="cost-label">优惠金额</text>
<text class="cost-value">-¥{{ order.discount_amount }}</text>
<text class="cost-value">-¥{{ order?.discount_amount ?? 0 }}</text>
</view>
<view class="cost-row total">
<text class="cost-label">实付金额</text>
<text class="cost-value price">¥{{ order.total_amount }}</text>
<text class="cost-value price">¥{{ order?.total_amount ?? 0 }}</text>
</view>
</view>
</scroll-view>
<!-- 底部操作 -->
<view class="bottom-actions">
<view class="bottom-actions" v-if="order != null">
<view class="action-left">
<view class="service-btn" @click="contactService">
<text class="service-icon">🎧</text>
@@ -114,20 +114,20 @@
</view>
</view>
<view class="action-right">
<button v-if="order.order_status === 1" class="btn primary" @click="payOrder">立即支付</button>
<button v-if="order.order_status === 1" class="btn" @click="cancelOrder">取消订单</button>
<button v-if="order?.order_status === 1" class="btn primary" @click="payOrder">立即支付</button>
<button v-if="order?.order_status === 1" class="btn" @click="cancelOrder">取消订单</button>
<button v-if="order.order_status === 2" class="btn" @click="remindDelivery">提醒发货</button>
<button v-if="order.order_status === 2" class="btn" @click="applyRefund">申请退款</button>
<button v-if="order?.order_status === 2" class="btn" @click="remindDelivery">提醒发货</button>
<button v-if="order?.order_status === 2" class="btn" @click="applyRefund">申请退款</button>
<button v-if="order.order_status === 3" class="btn primary" @click="confirmReceive">确认收货</button>
<button v-if="order.order_status === 3" class="btn" @click="viewLogistics">查看物流</button>
<button v-if="order?.order_status === 3" class="btn primary" @click="confirmReceive">确认收货</button>
<button v-if="order?.order_status === 3" class="btn" @click="viewLogistics">查看物流</button>
<button v-if="order.order_status === 4" class="btn primary" @click="goToReview">评价</button>
<button v-if="order.order_status === 4" class="btn" @click="rePurchase">再次购买</button>
<button v-if="order.order_status === 4" class="btn" @click="applyAfterSales">申请售后</button>
<button v-if="order?.order_status === 4" class="btn primary" @click="goToReview">评价</button>
<button v-if="order?.order_status === 4" class="btn" @click="rePurchase">再次购买</button>
<button v-if="order?.order_status === 4" class="btn" @click="applyAfterSales">申请售后</button>
<button v-if="order.order_status === 5" class="btn" @click="rePurchase">重新购买</button>
<button v-if="order?.order_status === 5" class="btn" @click="rePurchase">重新购买</button>
</view>
</view>
</view>
@@ -139,60 +139,57 @@ import { onLoad } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts'
import supa from '@/components/supadb/aksupainstance.uts'
// 定义订单类型
type OrderType = {
order_no: string,
order_status: number,
total_amount: number,
product_amount: number,
shipping_fee: number,
discount_amount: number,
payment_method: string,
created_at: string,
paid_at: string,
shipped_at: string,
completed_at: string,
merchant_id: string,
shipping_address: any
}
type OrderItemType = {
id: string,
product_id: string,
product_name: string,
image_url: string,
price: number,
quantity: number,
specifications: any
}
type AddressType = {
name: string,
phone: string,
province: string,
city: string,
district: string,
detail: string,
address: string
}
type DeliveryInfoType = {
tracking_no: string
}
const orderId = ref('')
const order = ref<any>({})
const orderItems = ref<any[]>([])
const order = ref<OrderType | null>(null)
const orderItems = ref<OrderItemType[]>([])
const shopName = ref('店铺名称')
const deliveryAddress = ref<any>({})
const deliveryInfo = ref<any>({})
const deliveryAddress = ref<AddressType | null>(null)
const deliveryInfo = ref<DeliveryInfoType | null>(null)
onLoad((options) => {
if (options['id']) {
orderId.value = options['id'] as string
loadOrderDetail()
} else if (options['orderId']) {
orderId.value = options['orderId'] as string
loadOrderDetail()
}
})
const loadOrderDetail = async () => {
uni.showLoading({ title: '加载中' })
try {
const data = await supabaseService.getOrderDetail(orderId.value)
if (data) {
order.value = data
orderItems.value = data.ml_order_items || []
deliveryAddress.value = data.shipping_address || {}
// 获取店铺信息
if (data.merchant_id) {
loadShopInfo(data.merchant_id)
}
} else {
uni.showToast({ title: '订单不存在', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
const loadShopInfo = async (merchantId: string) => {
const { data } = await supa
.from('ml_shops')
.select('shop_name')
.eq('merchant_id', merchantId)
.single()
if (data) {
shopName.value = data['shop_name'] as string
}
}
// 辅助函数
const getStatusText = () => {
const status = order.value.order_status
// 辅助函数 - 必须在调用前定义
const getStatusText = (): string => {
const status = order.value?.order_status ?? 0
if (status == 1) return '待付款'
if (status == 2) return '待发货'
if (status == 3) return '待收货'
@@ -203,8 +200,8 @@ const getStatusText = () => {
return '未知状态'
}
const getStatusDesc = () => {
const status = order.value.order_status
const getStatusDesc = (): string => {
const status = order.value?.order_status ?? 0
if (status == 1) return '请尽快完成支付'
if (status == 2) return '商家正在打包商品'
if (status == 3) return '商品正在赶往您的地址'
@@ -215,8 +212,8 @@ const getStatusDesc = () => {
return ''
}
const getStatusIcon = () => {
const status = order.value.order_status
const getStatusIcon = (): string => {
const status = order.value?.order_status ?? 0
if (status === 1) return '💳'
if (status === 2) return '📦'
if (status === 3) return '🚚'
@@ -224,43 +221,85 @@ const getStatusIcon = () => {
return '📝'
}
const getStatusClass = () => {
const status = order.value.order_status
const getStatusClass = (): string => {
const status = order.value?.order_status ?? 0
return `status-${status}`
}
const getFullAddress = (addr: any) => {
if (!addr) return ''
const getFullAddress = (addr: any): string => {
if (addr == null) return ''
// 兼容简单的字符串地址和对象地址
if (typeof addr === 'string') return addr
if (addr.address) return addr.address
return (addr.province || '') + (addr.city || '') + (addr.district || '') + (addr.detail || addr.address_detail || '')
const addrObj = addr as Record<string, any>
if (addrObj['address'] != null) return addrObj['address'] as string
return ((addrObj['province'] as string) ?? '') + ((addrObj['city'] as string) ?? '') + ((addrObj['district'] as string) ?? '') + ((addrObj['detail'] as string) ?? (addrObj['address_detail'] as string) ?? '')
}
const getSpecText = (specs: any) => {
if (!specs) return ''
const getSpecText = (specs: any): string => {
if (specs == null) return ''
if (typeof specs === 'string') return specs
return Object.keys(specs).map(k => `${k}:${specs[k]}`).join(' ')
// 简化处理:直接返回字符串形式
return JSON.stringify(specs)
}
const formatTime = (iso: string) => {
if (!iso) return ''
const formatTime = (iso: string): string => {
if (iso == '') return ''
const d = new Date(iso)
return `${d.getFullYear()}-${d.getMonth()+1}-${d.getDate()} ${d.getHours()}:${d.getMinutes()}`
}
const getPaymentMethodText = (method: any) => {
const getPaymentMethodText = (method: any): string => {
return '在线支付'
}
const copyText = (text: string) => {
if(!text) return
if(text == '') return
uni.setClipboardData({
data: text,
success: () => uni.showToast({ title: '已复制' })
})
}
const loadShopInfo = async (merchantId: string) => {
const result = await supa
.from('ml_shops')
.select('shop_name')
.eq('merchant_id', merchantId)
.single()
const resultObj = result as Record<string, any>
const resultData = resultObj['data']
if (resultData != null) {
const dataObj = resultData as Record<string, any>
shopName.value = dataObj['shop_name'] as string
}
}
const loadOrderDetail = async () => {
uni.showLoading({ title: '加载中' })
try {
const data = await supabaseService.getOrderDetail(orderId.value)
if (data != null) {
const dataObj = data as Record<string, any>
order.value = data as OrderType
const items = dataObj['ml_order_items']
orderItems.value = items != null ? (items as OrderItemType[]) : []
deliveryAddress.value = dataObj['shipping_address'] as AddressType
// 获取店铺信息
const merchantId = dataObj['merchant_id'] as string
if (merchantId != null && merchantId != '') {
loadShopInfo(merchantId)
}
} else {
uni.showToast({ title: '订单不存在', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
// 动作函数
const contactService = () => {
uni.showActionSheet({
@@ -278,22 +317,31 @@ const contactService = () => {
}
const payOrder = () => {
const totalAmount = order.value?.total_amount ?? 0
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${orderId.value}&amount=${order.value.total_amount}`
url: `/pages/mall/consumer/payment?orderId=${orderId.value}&amount=${totalAmount}`
})
}
const doCancelOrder = async () => {
const result = await supa.from('ml_orders').update({ order_status: 5 }).eq('id', orderId.value)
const resultObj = result as Record<string, any>
const resultError = resultObj['error']
if(resultError == null) {
if (order.value != null) {
order.value.order_status = 5
}
uni.showToast({ title: '订单已取消' })
}
}
const cancelOrder = () => {
uni.showModal({
title: '提示',
content: '确定要取消订单吗?',
success: async (res) => {
success: (res) => {
if (res.confirm) {
const { error } = await supa.from('ml_orders').update({ order_status: 5 }).eq('id', orderId.value)
if(!error) {
order.value.order_status = 5
uni.showToast({ title: '订单已取消' })
}
doCancelOrder()
}
}
})
@@ -307,32 +355,39 @@ const viewLogistics = () => {
uni.navigateTo({ url: `/pages/mall/consumer/logistics?orderId=${orderId.value}` })
}
const confirmReceive = async () => {
const goToReview = () => {
uni.navigateTo({ url: `/pages/mall/consumer/review?orderId=${orderId.value}` })
}
const doConfirmReceive = async () => {
const result = await supabaseService.confirmReceipt(orderId.value)
if (result.success) {
if (order.value != null) {
order.value.order_status = 4
}
uni.showToast({ title: '收货成功' })
setTimeout(() => goToReview(), 1500)
} else {
uni.showToast({ title: result.error ?? '失败', icon: 'none' })
}
}
const confirmReceive = () => {
uni.showModal({
title: '确认收货',
content: '确保您已收到货物',
success: async (res) => {
success: (res) => {
if (res.confirm) {
const result = await supabaseService.confirmReceipt(orderId.value)
if (result.success) {
order.value.order_status = 4
uni.showToast({ title: '收货成功' })
setTimeout(() => goToReview(), 1500)
} else {
uni.showToast({ title: result.error || '失败', icon: 'none' })
}
doConfirmReceive()
}
}
})
}
const goToReview = () => {
uni.navigateTo({ url: `/pages/mall/consumer/review?orderId=${orderId.value}` })
}
const rePurchase = async () => {
uni.showLoading({ title: '处理中' })
const success = await supabaseService.rePurchase(order.value)
const orderData = order.value as any
const success = await supabaseService.rePurchase(orderData)
uni.hideLoading()
if (success) {
uni.showToast({ title: '已加入购物车' })
@@ -344,21 +399,27 @@ const rePurchase = async () => {
}
}
const doApplyRefund = async (reason: string) => {
const success = await supabaseService.applyRefund(orderId.value, reason)
if (success) {
if (order.value != null) {
order.value.order_status = 6
}
uni.showToast({ title: '申请已提交' })
} else {
uni.showToast({ title: '提交失败', icon: 'none' })
}
}
const applyRefund = () => {
uni.showModal({
title: '申请退款',
editable: true,
placeholderText: '请输入退款原因',
success: async (res) => {
success: (res) => {
if (res.confirm) {
const reason = res.content || '用户主动申请'
const success = await supabaseService.applyRefund(orderId.value, reason)
if (success) {
order.value.order_status = 6
uni.showToast({ title: '申请已提交' })
} else {
uni.showToast({ title: '提交失败', icon: 'none' })
}
const reason = res.content ?? '用户主动申请'
doApplyRefund(reason)
}
}
})
@@ -379,19 +440,31 @@ const goToProduct = (pid: string) => {
uni.navigateTo({ url: `/pages/mall/consumer/product-detail?id=${pid}` })
}
// 生命周期 - 在所有函数定义之后
onLoad((options) => {
const id = options['id']
const orderIdParam = options['orderId']
if (id != null && id != '') {
orderId.value = id as string
loadOrderDetail()
} else if (orderIdParam != null && orderIdParam != '') {
orderId.value = orderIdParam as string
loadOrderDetail()
}
})
</script>
<style scoped>
.order-detail-page {
display: flex;
flex-direction: column;
height: 100vh;
flex: 1;
background-color: #f5f5f5;
}
.scroll-content {
flex: 1;
overflow-y: auto;
padding-bottom: 20px;
}
@@ -419,14 +492,12 @@ const goToProduct = (pid: string) => {
.status-text {
font-size: 18px;
font-weight: bold;
display: block;
}
.status-desc {
font-size: 12px;
opacity: 0.9;
margin-top: 5px;
display: block;
}
/* 配送信息 */
@@ -434,7 +505,6 @@ const goToProduct = (pid: string) => {
font-weight: bold;
font-size: 16px;
margin-bottom: 10px;
display: block;
}
.delivery-address {
@@ -535,10 +605,9 @@ const goToProduct = (pid: string) => {
font-size: 14px;
line-height: 1.4;
color: #333;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
lines: 2; /* uvue specific */
}
.product-spec {
@@ -618,7 +687,7 @@ const goToProduct = (pid: string) => {
justify-content: space-between;
align-items: center;
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
padding-bottom: calc(10px + env(safe-area-inset-bottom));
padding-bottom: 30px;
}
.action-left {
@@ -631,7 +700,7 @@ const goToProduct = (pid: string) => {
align-items: center;
font-size: 10px;
color: #666;
background: none;
background-color: transparent;
line-height: 1.2;
}
@@ -642,7 +711,11 @@ const goToProduct = (pid: string) => {
.action-right {
display: flex;
gap: 10px;
}
/* Add margin to buttons inside action-right for spacing */
.action-right .btn {
margin-left: 10px;
}
.btn {
@@ -665,4 +738,4 @@ const goToProduct = (pid: string) => {
/* 状态样式 */
.status-4 .status-text { /* Completed */ }
</style>
</style>

View File

@@ -29,6 +29,7 @@
>
<text class="tab-name">{{ tab.name }}</text>
<text v-if="tab.count > 0" class="tab-count">{{ tab.count }}</text>
<view v-if="activeTab === tab.id" class="active-indicator"></view>
</view>
</view>
</scroll-view>
@@ -179,9 +180,38 @@ onBackPress((options) => {
return false
})
// 定义标签页类型
type OrderTabItem = {
id: string,
name: string,
count: number
}
// 定义订单产品类型
type OrderProduct = {
id: string,
name: string,
price: number,
image: string,
spec: string,
quantity: number
}
// 定义订单类型
type OrderItem = {
id: string,
order_no: string,
status: number,
create_time: string,
product_amount: number,
shipping_fee: number,
total_amount: number,
products: OrderProduct[]
}
// 响应式数据
const orders = ref<any[]>([])
const allOrdersList = ref<any[]>([]) // Store all fetched orders for client-side filtering
const orders = ref<OrderItem[]>([])
const allOrdersList = ref<OrderItem[]>([]) // Store all fetched orders for client-side filtering
const loading = ref<boolean>(false)
const loadingMore = ref<boolean>(false)
const hasMore = ref<boolean>(true)
@@ -190,8 +220,8 @@ const page = ref<number>(1)
const activeTab = ref<string>('all')
const searchKeyword = ref<string>('')
// 订单标签页
const orderTabs = reactive([
// 订单标签页 - 使用 ref 以便整体替换
const orderTabs = ref<OrderTabItem[]>([
{ id: 'all', name: '全部', count: 0 },
{ id: 'pending', name: '待付款', count: 0 },
{ id: 'shipping', name: '待发货', count: 0 },
@@ -203,44 +233,82 @@ const orderTabs = reactive([
// Removed Mock Data
// 计算属性:根据当前标签筛选订单
const filteredOrders = computed(() => {
// 辅助函数:获取状态码
const getStatusByTab = (tabId: string): number => {
if (tabId == 'pending') return 1
if (tabId == 'shipping') return 2
if (tabId == 'delivering') return 3
if (tabId == 'completed') return 4
if (tabId == 'cancelled') return 5
return 0
}
// 辅助函数:解析规格文本
const parseSpecText = (specs: any): string => {
if (specs == null) return ''
if (typeof specs === 'string') return specs
// 对于对象类型尝试转为JSON字符串或简单处理
try {
return JSON.stringify(specs)
} catch (e) {
return ''
}
}
// 辅助函数:更新标签计数
const updateTabsCounts = (allOrders: any[]) => {
// 直接重新赋值整个数组
const tabsData = orderTabs.value
// 计算各状态数量
const countAll = allOrders.length
const countPending = allOrders.filter((o: any) => {
const obj = o as Record<string, any>
return obj['status'] === 1
}).length
const countShipping = allOrders.filter((o: any) => {
const obj = o as Record<string, any>
return obj['status'] === 2
}).length
const countDelivering = allOrders.filter((o: any) => {
const obj = o as Record<string, any>
return obj['status'] === 3
}).length
const countCompleted = allOrders.filter((o: any) => {
const obj = o as Record<string, any>
return obj['status'] === 4
}).length
const countCancelled = allOrders.filter((o: any) => {
const obj = o as Record<string, any>
return obj['status'] === 5
}).length
// 更新数组元素
const tabsArr = tabsData as any[]
const tab0 = tabsArr[0] as Record<string, any>
tab0['count'] = countAll
const tab1 = tabsArr[1] as Record<string, any>
tab1['count'] = countPending
const tab2 = tabsArr[2] as Record<string, any>
tab2['count'] = countShipping
const tab3 = tabsArr[3] as Record<string, any>
tab3['count'] = countDelivering
const tab4 = tabsArr[4] as Record<string, any>
tab4['count'] = countCompleted
const tab5 = tabsArr[5] as Record<string, any>
tab5['count'] = countCancelled
}
// 辅助函数:按标签筛选订单
const filterOrdersByTab = () => {
if (activeTab.value === 'all') {
return orders.value
orders.value = allOrdersList.value
} else {
const targetStatus = getStatusByTab(activeTab.value)
orders.value = allOrdersList.value.filter((o: OrderItem) => {
return o.status === targetStatus
})
}
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' // 映射到已完成
else if (type === 'refund') activeTab.value = 'all' // 申请售后默认显示全部
}
})
onShow(() => {
loadOrders()
})
}
// 加载订单数据
const loadOrders = async () => {
@@ -251,32 +319,67 @@ const loadOrders = async () => {
const fetchedOrders = await supabaseService.getOrders(0)
// Map to View Model
const mappedOrders = fetchedOrders.map((order: any) => ({
id: order.id,
order_no: order.order_no,
status: order.order_status,
create_time: order.created_at,
product_amount: order.product_amount || order.actual_amount,
shipping_fee: order.delivery_fee,
total_amount: order.actual_amount,
products: (order.ml_order_items || []).map((item: any) => ({
id: item.product_id,
name: item.product_name,
price: item.price,
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
}))
}))
const mappedOrders: any[] = []
for (let i = 0; i < fetchedOrders.length; i++) {
const order = fetchedOrders[i]
const orderObj = order as Record<string, any>
const items = orderObj['ml_order_items'] as any[]
const productsList: any[] = []
if (items != null) {
for (let j = 0; j < items.length; j++) {
const item = items[j]
const itemObj = item as Record<string, any>
const specRaw = itemObj['specifications']
const specText = specRaw != null ? parseSpecText(specRaw) : ''
productsList.push({
id: itemObj['product_id'],
name: itemObj['product_name'],
price: itemObj['price'],
image: itemObj['image_url'] ?? '/static/default-product.png',
spec: specText,
quantity: itemObj['quantity']
})
}
}
mappedOrders.push({
id: orderObj['id'],
order_no: orderObj['order_no'],
status: orderObj['order_status'],
create_time: orderObj['created_at'],
product_amount: orderObj['product_amount'] ?? 0,
shipping_fee: orderObj['shipping_fee'] ?? 0,
total_amount: orderObj['total_amount'] ?? orderObj['paid_amount'] ?? 0,
products: productsList
})
}
// Sort by created_at desc
mappedOrders.sort((a: any, b: any) => {
const timeA = new Date(a.create_time).getTime()
const timeB = new Date(b.create_time).getTime()
const aObj = a as Record<string, any>
const bObj = b as Record<string, any>
const timeA = new Date(aObj['create_time'] as string).getTime()
const timeB = new Date(bObj['create_time'] as string).getTime()
return timeB - timeA
})
allOrdersList.value = mappedOrders
// 将 mappedOrders 转换为 OrderItem[] 类型
const typedOrders: OrderItem[] = []
for (let i = 0; i < mappedOrders.length; i++) {
const mo = mappedOrders[i] as Record<string, any>
typedOrders.push({
id: mo['id'] as string,
order_no: mo['order_no'] as string,
status: mo['status'] as number,
create_time: mo['create_time'] as string,
product_amount: mo['product_amount'] as number,
shipping_fee: mo['shipping_fee'] as number,
total_amount: mo['total_amount'] as number,
products: mo['products'] as OrderProduct[]
})
}
allOrdersList.value = typedOrders
// Update tab counts
updateTabsCounts(mappedOrders)
@@ -292,41 +395,77 @@ const loadOrders = async () => {
}
}
const updateTabsCounts = (allOrders: any[]) => {
orderTabs[0].count = allOrders.length
orderTabs[1].count = allOrders.filter((o: any) => o.status === 1).length
orderTabs[2].count = allOrders.filter((o: any) => o.status === 2).length
orderTabs[3].count = allOrders.filter((o: any) => o.status === 3).length
orderTabs[4].count = allOrders.filter((o: any) => o.status === 4).length
orderTabs[5].count = allOrders.filter((o: any) => o.status === 5).length
}
// 生命周期
onLoad((options) => {
if (options['status'] != null) {
const status = options['status'] as string
if (['all', 'pending', 'shipping', 'delivering', 'completed', 'cancelled'].includes(status)) {
activeTab.value = status
}
}
if (options['type'] != null) {
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' // 映射到已完成
else if (type === 'refund') activeTab.value = 'all' // 申请售后默认显示全部
}
})
const filterOrdersByTab = () => {
const statusMap: Record<string, number> = {
'pending': 1,
'shipping': 2,
'delivering': 3,
'completed': 4,
'cancelled': 5
}
if (activeTab.value === 'all') {
orders.value = allOrdersList.value
} else {
const targetStatus = statusMap[activeTab.value]
orders.value = allOrdersList.value.filter((o: any) => o.status === targetStatus)
}
}
onShow(() => {
loadOrders()
})
const formatDate = (isoString: string): string => {
if (!isoString) return ''
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')}`
}
// 辅助函数:获取当前订单数据(必须在 performSearch 之前定义)
function getCurrentOrderData(): OrderItem[] {
return allOrdersList.value
}
// 搜索执行函数(必须在 onSearchInput 等之前定义)
const performSearch = () => {
const keyword = searchKeyword.value.trim().toLowerCase()
if (keyword == '') {
loadOrders()
return
}
// 在当前订单数据中搜索
const allOrders = getCurrentOrderData()
const filtered = allOrders.filter((order: any) => {
const orderObj = order as Record<string, any>
// 搜索订单号
const orderNo = orderObj['order_no'] as string
if (orderNo != null && orderNo.toLowerCase().includes(keyword)) {
return true
}
// 搜索商品名称
const products = orderObj['products']
if (products != null && Array.isArray(products)) {
return products.some((product: any) => {
const productObj = product as Record<string, any>
const name = productObj['name'] as string
return name != null && name.toLowerCase().includes(keyword)
})
}
return false
})
orders.value = filtered
}
// 搜索相关函数
const onSearchInput = (e: any) => {
searchKeyword.value = e.detail.value
const eObj = e as Record<string, any>
const detail = eObj['detail'] as Record<string, any>
searchKeyword.value = detail['value'] as string
performSearch()
}
@@ -339,44 +478,13 @@ const clearSearch = () => {
performSearch()
}
const performSearch = () => {
const keyword = searchKeyword.value.trim().toLowerCase()
if (!keyword) {
loadOrders()
return
}
// 在当前订单数据中搜索
const allOrders = getCurrentOrderData() // 这里需要获取完整的订单数据
const filtered = allOrders.filter((order: any) => {
// 搜索订单号
if (order.order_no && order.order_no.toLowerCase().includes(keyword)) {
return true
}
// 搜索商品名称
if (order.products && Array.isArray(order.products)) {
return order.products.some((product: any) => {
return product.name && product.name.toLowerCase().includes(keyword)
})
}
return false
})
orders.value = filtered
}
const getCurrentOrderData = () => {
return allOrdersList.value
}
const formatSpec = (specs: any): string => {
if (!specs) return ''
if (specs == null) return ''
if (typeof specs === 'string') return specs
if (typeof specs === 'object') {
return Object.keys(specs).map(key => `${key}:${specs[key]}`).join(' ')
return JSON.stringify(specs)
}
return String(specs)
return ''
}
// 切换标签
@@ -387,26 +495,22 @@ const switchTab = (tabId: string) => {
// 获取状态文本
const getStatusText = (status: number): string => {
const statusMap: Record<number, string> = {
1: '待付款',
2: '待货',
3: '待收货',
4: '已完成',
5: '已取消'
}
return statusMap[status] || '未知状态'
if (status == 1) return '待付款'
if (status == 2) return '待发货'
if (status == 3) return '待货'
if (status == 4) return '已完成'
if (status == 5) return '已取消'
return '未知状态'
}
// 获取状态类名
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'
if (status == 1) return 'status-pending'
if (status == 2) return 'status-shipping'
if (status == 3) return 'status-delivering'
if (status == 4) return 'status-completed'
if (status == 5) return 'status-cancelled'
return 'status-unknown'
}
// 下拉刷新
@@ -444,9 +548,13 @@ const cancelOrder = (orderId: string) => {
})
// 更新订单状态
const index = orders.value.findIndex(order => order.id === orderId)
const index = orders.value.findIndex((o: any) => {
const obj = o as Record<string, any>
return obj['id'] === orderId
})
if (index !== -1) {
orders.value[index].status = 5
const orderObj = orders.value[index] as Record<string, any>
orderObj['status'] = 5
orders.value = [...orders.value]
}
}
@@ -473,59 +581,78 @@ const viewLogistics = (orderId: string) => {
})
}
const confirmReceipt = async (orderId: string) => {
uni.showModal({
title: '确认收货',
content: '请确认您已收到商品,且商品无误',
success: async (res) => {
if (res.confirm) {
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'
})
}
}
}
// goReview 必须在 doConfirmReceipt 之前定义,因为 doConfirmReceipt 会调用它
const goReview = (order: any) => {
const orderObj = order as Record<string, any>
const products = orderObj['products'] as any[]
const productIds = products.map((p: any) => {
const pObj = p as Record<string, any>
const pid = pObj['id']
return pid != null ? pid as string : ''
}).join(',')
const orderId = orderObj['id'] as string
uni.navigateTo({
url: `/pages/mall/consumer/review?orderId=${orderId}&productIds=${productIds}`
})
}
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 doConfirmReceipt = async (orderId: string) => {
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((o: any) => {
const obj = o as Record<string, any>
return obj['id'] === orderId
})
if (index !== -1) {
const orderObj = orders.value[index] as Record<string, any>
orderObj['status'] = 4
orders.value = [...orders.value]
}
// 跳转到评价页面
setTimeout(() => {
const order = orders.value.find((o: any) => {
const obj = o as Record<string, any>
return obj['id'] === orderId
})
if (order != null) {
goReview(order)
}
}, 1000)
} else {
uni.showToast({
title: result.error ?? '确认收货失败',
icon: 'none'
})
}
} catch (e) {
uni.hideLoading()
uni.showToast({
title: '系统异常',
icon: 'none'
})
}
}
const confirmReceipt = (orderId: string) => {
uni.showModal({
title: '确认收货',
content: '请确认您已收到商品,且商品无误',
success: (res) => {
if (res.confirm) {
doConfirmReceipt(orderId)
}
}
})
}
@@ -552,8 +679,10 @@ const viewOrderDetail = (orderId: string) => {
}
const onApplyRefund = (order: any) => {
const orderObj = order as Record<string, any>
const orderId = orderObj['id']
uni.navigateTo({
url: `/pages/mall/consumer/apply-refund?orderId=${order.id}`
url: `/pages/mall/consumer/apply-refund?orderId=${orderId}`
})
}
@@ -563,7 +692,9 @@ const navigateToSearch = () => {
}
const navigateToProduct = (product: any) => {
uni.navigateTo({ url: `/pages/mall/consumer/product-detail?id=${product.id}` })
const productObj = product as Record<string, any>
const productId = productObj['id']
uni.navigateTo({ url: `/pages/mall/consumer/product-detail?id=${productId}` })
}
const goShopping = () => {
@@ -574,7 +705,9 @@ const goShopping = () => {
<style>
.orders-page {
width: 100%;
min-height: 100vh;
flex: 1;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
}
@@ -586,8 +719,8 @@ const goShopping = () => {
align-items: center;
justify-content: center;
border-bottom: 1px solid #eee;
position: sticky;
top: 0;
/* position: sticky; removed */
/* top: 0; removed */
z-index: 10;
}
@@ -614,9 +747,7 @@ const goShopping = () => {
color: #999;
font-size: 12px;
}
.search-input:focus {
outline: none;
border-color: #ff5000;
background-color: white;
}
@@ -637,17 +768,17 @@ const goShopping = () => {
height: 20px;
line-height: 18px;
text-align: center;
border-radius: 50%;
border-radius: 10px; /* fixed 50% */
background-color: #ddd;
cursor: pointer;
/* cursor: pointer; removed */
}
/* 标签页 */
.order-tabs {
background-color: #ffffff;
border-bottom: 1px solid #e5e5e5;
position: sticky;
top: 50px;
/* position: sticky; removed */
/* top: 50px; removed */
z-index: 10;
}
@@ -660,9 +791,8 @@ const goShopping = () => {
display: flex;
flex-direction: row;
padding: 0 10px;
/* 关键:确保宽度包含所有子元素,允许滚动 */
width: max-content;
min-width: 100%;
/* width: max-content; removed */
/* min-width: 100%; removed */
}
.tab-item {
@@ -683,7 +813,17 @@ const goShopping = () => {
}
.tab-item.active::after {
content: '';
/* content: ''; removed */
/* content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #ff5000; */
}
.active-indicator {
position: absolute;
bottom: 0;
left: 0;
@@ -709,7 +849,7 @@ const goShopping = () => {
/* 内容区 */
.orders-content {
height: calc(100vh - 100px);
flex: 1;
}
/* 空状态 */
@@ -830,7 +970,6 @@ const goShopping = () => {
font-size: 15px;
color: #333;
margin-bottom: 5px;
display: block;
line-height: 1.4;
}
@@ -838,7 +977,6 @@ const goShopping = () => {
font-size: 13px;
color: #999;
margin-bottom: 10px;
display: block;
}
.product-footer {
@@ -906,7 +1044,7 @@ const goShopping = () => {
.action-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
/* gap: 10px; removed */
}
.action-btn {
@@ -914,7 +1052,8 @@ const goShopping = () => {
border-radius: 15px;
font-size: 13px;
border: 1px solid;
background: none;
background-color: transparent; /* fixed background: none */
margin-left: 10px; /* alternative to gap */
}
.action-btn.cancel {
@@ -966,16 +1105,10 @@ const goShopping = () => {
height: 24px;
border: 2px solid #f0f5ff;
border-top-color: #ff5000;
border-radius: 50%;
animation: spin 1s linear infinite;
border-radius: 12px;
margin-bottom: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.no-more {
text-align: center;
color: #999;

View File

@@ -22,61 +22,71 @@
<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 orderNo = ref('')
const amount = ref(0)
// 定义 loadOrderInfo 函数(必须在 onMounted 之前)
const loadOrderInfo = async () => {
try {
const response = await supabaseService.getOrderById(orderId.value)
console.log('[payment-success] getOrderById response:', JSON.stringify(response))
if (response != null) {
const orderData = response as UTSJSONObject
const totalAmount = orderData.getNumber('total_amount')
const paidAmount = orderData.getNumber('paid_amount')
console.log('[payment-success] total_amount:', totalAmount, 'paid_amount:', paidAmount)
if (paidAmount != null && paidAmount > 0) {
amount.value = paidAmount
} else if (totalAmount != null && totalAmount > 0) {
amount.value = totalAmount
}
const orderNoVal = orderData.getString('order_no')
if (orderNoVal != null && orderNoVal != '') {
orderNo.value = orderNoVal
}
}
} catch (err) {
console.error('[payment-success] 加载订单信息失败:', err)
}
}
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options as any
const options = currentPage.options as Record<string, any>
if (options.orderId) {
orderId.value = options.orderId
orderNo.value = options.orderId // 使用订单ID作为订单号
console.log('[payment-success] options:', JSON.stringify(options))
const orderIdValue = options['orderId']
if (orderIdValue != null) {
orderId.value = orderIdValue as string
orderNo.value = orderIdValue as string
// 优先使用传递的 amount
if (options.amount) {
amount.value = parseFloat(options.amount)
} else {
// 如果没有传 amount尝试从本地存储查找订单
try {
const ordersStr = uni.getStorageSync('orders')
if (ordersStr) {
const orders = JSON.parse(ordersStr as string) as any[]
const order = orders.find((o: any) => o.id === orderId.value)
if (order) {
amount.value = order.actual_amount || order.total_amount || 0
}
}
} catch (e) {
console.error('读取本地订单失败', e)
const amountValue = options['amount']
if (amountValue != null) {
const amountStr = amountValue.toString()
console.log('[payment-success] amountStr:', amountStr)
const parsed = parseFloat(amountStr)
console.log('[payment-success] parsed:', parsed)
if (isNaN(parsed) == false) {
amount.value = parsed
}
}
// loadOrderInfo() // 暂时注释掉数据库查询
if (amount.value == 0) {
console.log('[payment-success] amount为0尝试从数据库查询')
}
loadOrderInfo()
}
})
// const loadOrderInfo = async () => {
// try {
// const { data, error } = await supa
// .from('orders')
// .select('order_no, actual_amount')
// .eq('id', orderId.value)
// .single()
//
// if (error == null && data != null) {
// orderNo.value = data['order_no'] as string
// amount.value = data['actual_amount'] as number
// }
// } catch (err) {
// console.error('加载订单信息失败', err)
// }
// }
const viewOrder = () => {
uni.navigateTo({
url: '/pages/mall/consumer/orders'
@@ -96,7 +106,8 @@ const goHome = () => {
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
/* height: 100vh; */
flex: 1;
background-color: #ffffff;
padding: 0 30px;
}
@@ -162,7 +173,7 @@ const goHome = () => {
width: 100%;
display: flex;
flex-direction: column;
gap: 15px;
/* gap: 15px; */
}
.btn {
@@ -174,6 +185,7 @@ const goHome = () => {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 15px;
}
.primary-btn {

View File

@@ -66,7 +66,7 @@
<view v-for="(_, index) in 6"
:key="index"
class="password-dot">
<text v-if="password.length > index">●</text>
<text v-if="password.length > index" class="password-dot-text">●</text>
</view>
</view>
<text class="forgot-password" @click="forgotPassword">忘记密码?</text>
@@ -136,109 +136,9 @@ const productAmount = ref<number>(0) // 商品总价
const deliveryFee = ref<number>(0) // 运费
const discountAmount = ref<number>(0) // 优惠减免
// 生命周期
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options as any
if (options.orderId) {
orderId.value = options.orderId
loadOrderInfo()
}
if (options.amount) {
amount.value = parseFloat(options.amount)
}
// 获取传递的价格详情
if (options.productAmount) {
productAmount.value = parseFloat(options.productAmount)
}
if (options.deliveryFee) {
deliveryFee.value = parseFloat(options.deliveryFee)
}
if (options.discountAmount) {
discountAmount.value = parseFloat(options.discountAmount)
}
// 如果没有传详情,尝试根据总价估算(兼容旧逻辑,但优先使用传参)
if (!options.productAmount && amount.value > 0) {
calculatePriceDetails(amount.value)
}
loadPaymentMethods()
loadUserBalance()
})
// 监听返回操作(包含系统返回键和导航栏返回按钮)
onBackPress((options) => {
// 如果是通过代码主动调用 navigateBack 返回,则允许
if (options.from === 'navigateBack') {
return false
}
// 否则拦截返回,显示确认弹窗
goBack()
return true
})
// 更新本地存储中的订单状态
const updateOrderInStorage = (status: number) => {
try {
// 尝试从 'orders' 读取 (checkout页面写入的key)
const ordersStr = uni.getStorageSync('orders')
let orders: any[] = []
if (ordersStr) {
orders = JSON.parse(ordersStr as string) as any[]
}
const index = orders.findIndex((o: any) => o.id === orderId.value)
if (index !== -1) {
orders[index].status = status
orders[index].payment_status = status === 2 ? 1 : 0 // 2=待发货(已支付), 1=待支付(未支付)
orders[index].updated_at = new Date().toISOString()
// 确保更新的是 'orders' key
uni.setStorageSync('orders', JSON.stringify(orders))
console.log('订单状态已更新到Storage (orders):', orderId.value, status)
} else {
console.warn('在Storage (orders)中未找到订单:', orderId.value)
}
} catch (e) {
console.error('更新订单状态失败', e)
}
}
// 加载订单信息
const loadOrderInfo = async () => {
try {
if (!orderId.value) return
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)
}
}
// 加载支付方式
// 加载支付方式(必须在 onMounted 之前定义)
const loadPaymentMethods = () => {
paymentMethods.value = [
const methods: PaymentMethodType[] = [
{
id: 'wechat',
name: '微信支付',
@@ -268,9 +168,10 @@ const loadPaymentMethods = () => {
enabled: true
}
]
paymentMethods.value = methods
}
// 加载用户余额
// 加载用户余额(必须在 onMounted 之前定义)
const loadUserBalance = async () => {
try {
const balance = await supabaseService.getUserBalance()
@@ -281,21 +182,215 @@ const loadUserBalance = async () => {
}
}
// 计算价格明细(必须在 onMounted 之前定义)
const calculatePriceDetails = (totalAmount: number) => {
// 模拟计算各项费用
// 假设商品总价占总金额的80%运费占10%优惠减免占10%
productAmount.value = totalAmount * 0.8
deliveryFee.value = totalAmount * 0.1
discountAmount.value = totalAmount * 0.1
// 确保总和等于应付金额
const calculatedTotal = productAmount.value + deliveryFee.value - discountAmount.value
if (Math.abs(calculatedTotal - totalAmount) > 0.01) {
// 调整商品总价以匹配应付金额
productAmount.value = totalAmount + discountAmount.value - deliveryFee.value
}
}
// 更新本地存储中的订单状态(必须在 onMounted 之前定义)
const updateOrderInStorage = (targetOrderId: string, status: number) => {
try {
// 尝试从 'orders' 读取 (checkout页面写入的key)
const ordersStr = uni.getStorageSync('orders')
let orders: Record<string, any>[] = []
if (ordersStr != null) {
const parsed = JSON.parse(ordersStr as string)
if (Array.isArray(parsed)) {
for (let i = 0; i < parsed.length; i++) {
orders.push(parsed[i] as Record<string, any>)
}
}
}
let foundIndex = -1
for (let i = 0; i < orders.length; i++) {
const o = orders[i]
if (o['id'] === targetOrderId) {
foundIndex = i
break
}
}
if (foundIndex !== -1) {
orders[foundIndex]['status'] = status
orders[foundIndex]['payment_status'] = status === 2 ? 1 : 0 // 2=待发货(已支付), 1=待支付(未支付)
orders[foundIndex]['updated_at'] = new Date().toISOString()
// 确保更新的是 'orders' key
uni.setStorageSync('orders', JSON.stringify(orders))
console.log('订单状态已更新到Storage (orders):', targetOrderId, status)
} else {
console.warn('在Storage (orders)中未找到订单:', targetOrderId)
}
} catch (e) {
console.error('更新订单状态失败', e)
}
}
// 取消支付,更新订单状态(必须在 goBack 之前定义)
const cancelPayment = async () => {
try {
// 这里应该调用API更新订单状态为待支付status: 1
// 模拟更新订单状态
// 更新本地存储
updateOrderInStorage(orderId.value, 1) // 1: 待支付
// 发布订单更新事件让profile页面可以刷新数据
uni.$emit('orderUpdated', { orderId: orderId.value, status: 1 })
uni.showToast({
title: '已保存到待支付订单',
icon: 'success'
})
// 延迟返回,让用户看到提示
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (err) {
console.error('取消支付异常:', err)
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
}
// 返回(必须在 onBackPress 之前定义)
const goBack = () => {
uni.showModal({
title: '取消支付',
content: '确定要取消支付吗?取消后订单将保存到待支付订单中',
confirmText: '取消支付',
cancelText: '继续支付',
success: (res) => {
if (res.confirm) {
// 用户确认取消支付,更新订单状态为待支付
cancelPayment()
}
}
})
}
// 加载订单信息(必须在 onMounted 之前定义)
const loadOrderInfo = async () => {
try {
if (orderId.value == '') return
const order = await supabaseService.getOrderDetail(orderId.value)
if (order != null) {
const orderObj = order as Record<string, any>
orderNo.value = orderObj['order_no'] as string
// 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 totalAmount = orderObj['total_amount']
const dbAmount = totalAmount != null ? parseFloat(totalAmount.toString()) : 0
if (dbAmount > 0) {
amount.value = dbAmount
}
const items = orderObj['items']
if (items != null && Array.isArray(items) && 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)
}
}
// 生命周期
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options as Record<string, any>
const orderIdValue = options['orderId']
if (orderIdValue != null) {
orderId.value = orderIdValue as string
loadOrderInfo()
}
const amountValue = options['amount']
if (amountValue != null) {
amount.value = parseFloat(amountValue.toString())
}
// 获取传递的价格详情
const productAmountValue = options['productAmount']
if (productAmountValue != null) {
productAmount.value = parseFloat(productAmountValue.toString())
}
const deliveryFeeValue = options['deliveryFee']
if (deliveryFeeValue != null) {
deliveryFee.value = parseFloat(deliveryFeeValue.toString())
}
const discountAmountValue = options['discountAmount']
if (discountAmountValue != null) {
discountAmount.value = parseFloat(discountAmountValue.toString())
}
// 如果没有传详情,尝试根据总价估算(兼容旧逻辑,但优先使用传参)
if (productAmountValue == null && amount.value > 0) {
calculatePriceDetails(amount.value)
}
loadPaymentMethods()
loadUserBalance()
})
// 监听返回操作(包含系统返回键和导航栏返回按钮)
onBackPress((options) => {
// 如果是通过代码主动调用 navigateBack 返回,则允许
if (options.from === 'navigateBack') {
return false
}
// 否则拦截返回,显示确认弹窗
goBack()
return true
})
// 获取当前用户ID
const getCurrentUserId = (): string => {
const userStore = uni.getStorageSync('userInfo')
return userStore?.id || ''
if (userStore != null) {
const userObj = userStore as Record<string, any>
const id = userObj['id']
if (id != null) {
return id as string
}
}
return ''
}
// 获取支付方式图标
const getMethodIcon = (methodId: string): string => {
const icons: Record<string, string> = {
wechat: '💳',
alipay: '💳',
balance: '💰',
bankcard: '💳'
if (methodId === 'wechat') {
return '💳'
} else if (methodId === 'alipay') {
return '💳'
} else if (methodId === 'balance') {
return '💰'
} else if (methodId === 'bankcard') {
return '💳'
}
return icons[methodId] || '💳'
return '💳'
}
// 选择支付方式
@@ -319,14 +414,16 @@ const getPayButtonText = (): string => {
return '余额不足'
}
const texts: Record<string, string> = {
wechat: '微信支付',
alipay: '支付宝支付',
balance: '余额支付',
bankcard: '银行卡支付'
if (selectedMethod.value === 'wechat') {
return '微信支付'
} else if (selectedMethod.value === 'alipay') {
return '支付宝支付'
} else if (selectedMethod.value === 'balance') {
return '余额支付'
} else if (selectedMethod.value === 'bankcard') {
return '银行卡支付'
}
return texts[selectedMethod.value] || '确认支付'
return '确认支付'
}
// 减少商品库存
@@ -363,29 +460,37 @@ const confirmPayment = async () => {
}
isPaying.value = true
uni.showLoading({ title: '支付中...' })
try {
// Call Supabase Service to handle payment
console.log('[confirmPayment] 开始支付, orderId:', orderId.value, 'method:', selectedMethod.value)
const success = await supabaseService.payOrder(orderId.value, selectedMethod.value, amount.value)
console.log('[confirmPayment] 支付结果:', success)
if (!success) {
throw new Error('Payment processing failed')
console.error('[confirmPayment] payOrder 返回 false')
uni.hideLoading()
uni.showToast({
title: '支付处理失败',
icon: 'none'
})
isPaying.value = false
return
}
// 更新订单状态
updateOrderInStorage(2) // 2: 待发货(已支付)
uni.hideLoading()
updateOrderInStorage(orderId.value, 2)
// 支付成功
uni.showToast({
title: '支付成功',
icon: 'success',
duration: 2000
})
// 发布订单更新事件让profile页面可以刷新数据
uni.$emit('orderUpdated', { orderId: orderId.value, status: 2 }) // 2: 待发货
uni.$emit('orderUpdated', { orderId: orderId.value, status: 2 })
// 跳转到支付成功页面
setTimeout(() => {
uni.redirectTo({
url: `/pages/mall/consumer/payment-success?orderId=${orderId.value}`
@@ -393,56 +498,42 @@ const confirmPayment = async () => {
}, 1500)
} catch (err) {
console.error('支付失败:', err)
console.error('[confirmPayment] 支付异常:', err)
uni.hideLoading()
uni.showToast({
title: '支付失败',
icon: 'none'
})
} finally {
isPaying.value = false
}
}
// 获取支付方式代码
const getPaymentMethodCode = (methodId: string): number => {
const codes: Record<string, number> = {
wechat: 1,
alipay: 2,
balance: 3,
bankcard: 4
if (methodId === 'wechat') {
return 1
} else if (methodId === 'alipay') {
return 2
} else if (methodId === 'balance') {
return 3
} else if (methodId === 'bankcard') {
return 4
}
return codes[methodId] || 0
return 0
}
// 输入密码
const inputPassword = (num: string) => {
if (password.value.length >= 6) return
password.value += num
}
// 删除密码
const deletePassword = () => {
if (password.value.length > 0) {
password.value = password.value.slice(0, -1)
}
}
// 监听密码输入
watch(password, (newPassword) => {
if (newPassword.length === 6) {
// 自动验证密码
verifyPassword()
}
})
// 验证密码
// 验证密码(必须在 watch 之前定义)
const verifyPassword = async () => {
// 这里应该验证支付密码,这里简单模拟
const userId = getCurrentUserId()
try {
// 模拟验证
await new Promise(resolve => setTimeout(resolve, 500))
await new Promise<void>((resolve: (value: void) => void) => {
setTimeout(() => {
resolve()
}, 500)
})
// 假设密码正确
const isCorrect = true
@@ -463,6 +554,27 @@ const verifyPassword = async () => {
}
}
// 输入密码
const inputPassword = (num: string) => {
if (password.value.length >= 6) return
password.value += num
}
// 删除密码
const deletePassword = () => {
if (password.value.length > 0) {
password.value = password.value.slice(0, -1)
}
}
// 监听密码输入
watch(password, (newPassword: string) => {
if (newPassword.length === 6) {
// 自动验证密码
verifyPassword()
}
})
// 忘记密码
const forgotPassword = () => {
uni.navigateTo({
@@ -470,99 +582,17 @@ const forgotPassword = () => {
})
}
// 计算价格明细
const calculatePriceDetails = (totalAmount: number) => {
// 模拟计算各项费用
// 假设商品总价占总金额的80%运费占10%优惠减免占10%
productAmount.value = totalAmount * 0.8
deliveryFee.value = totalAmount * 0.1
discountAmount.value = totalAmount * 0.1
// 确保总和等于应付金额
const calculatedTotal = productAmount.value + deliveryFee.value - discountAmount.value
if (Math.abs(calculatedTotal - totalAmount) > 0.01) {
// 调整商品总价以匹配应付金额
productAmount.value = totalAmount + discountAmount.value - deliveryFee.value
}
}
// 在组件卸载时移除返回键监听
onUnmounted(() => {
// uni.offBackPress() 在uni-app中不需要手动移除
})
// 返回
const goBack = () => {
uni.showModal({
title: '取消支付',
content: '确定要取消支付吗?取消后订单将保存到待支付订单中',
confirmText: '取消支付',
cancelText: '继续支付',
success: async (res) => {
if (res.confirm) {
// 用户确认取消支付,更新订单状态为待支付
await cancelPayment()
} else {
// 用户选择继续支付,留在当前页面
return
}
}
})
}
// 取消支付,更新订单状态
const cancelPayment = async () => {
try {
// 这里应该调用API更新订单状态为待支付status: 1
// 模拟更新订单状态
/* const { error } = await supa
.from('orders')
.update({
status: 1, // 待支付
updated_at: new Date().toISOString()
})
.eq('id', orderId.value)
if (error !== null) {
console.error('更新订单状态失败:', error)
uni.showToast({
title: '操作失败',
icon: 'none'
})
return
} */
// 更新本地存储
updateOrderInStorage(orderId.value, 1) // 1: 待支付
// 发布订单更新事件让profile页面可以刷新数据
uni.$emit('orderUpdated', { orderId: orderId.value, status: 1 })
uni.showToast({
title: '已保存到待支付订单',
icon: 'success'
})
// 延迟返回,让用户看到提示
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (err) {
console.error('取消支付异常:', err)
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
}
</script>
<style scoped>
.payment-page {
display: flex;
flex-direction: column;
height: 100vh;
flex: 1;
background-color: #f5f5f5;
}
@@ -589,7 +619,7 @@ const cancelPayment = async () => {
.payment-content {
flex: 1;
overflow-y: auto;
/* overflow-y: auto; */
}
/* 价格明细部分 */
@@ -640,7 +670,7 @@ const cancelPayment = async () => {
}
.order-no {
display: block;
/* display: block; */
font-size: 12px;
color: #999999;
text-align: center;
@@ -653,7 +683,7 @@ const cancelPayment = async () => {
}
.section-title {
display: block;
/* display: block; */
font-size: 16px;
font-weight: bold;
color: #333333;
@@ -663,7 +693,7 @@ const cancelPayment = async () => {
.method-list {
display: flex;
flex-direction: column;
gap: 10px;
/* gap: 10px; */
}
.method-item {
@@ -673,6 +703,7 @@ const cancelPayment = async () => {
padding: 15px;
border: 1px solid #e5e5e5;
border-radius: 8px;
margin-bottom: 10px;
}
.method-item.selected {
@@ -765,7 +796,7 @@ const cancelPayment = async () => {
}
.password-title {
display: block;
/* display: block; */
font-size: 16px;
color: #333333;
margin-bottom: 30px;
@@ -774,7 +805,7 @@ const cancelPayment = async () => {
.password-input {
display: flex;
justify-content: center;
gap: 15px;
/* gap: 15px; */
margin-bottom: 20px;
}
@@ -786,9 +817,10 @@ const cancelPayment = async () => {
display: flex;
align-items: center;
justify-content: center;
margin: 0 7.5px;
}
.password-dot text {
.password-dot-text {
color: #ffffff;
font-size: 8px;
}
@@ -809,7 +841,7 @@ const cancelPayment = async () => {
.price-summary {
display: flex;
align-items: baseline;
align-items: flex-end;
}
.summary-label {
@@ -847,18 +879,22 @@ const cancelPayment = async () => {
}
.keyboard-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 1px;
display: flex;
flex-wrap: wrap;
/* grid-template-columns: repeat(3, 1fr); uvue unsupported */
/* grid-gap: 1px; uvue unsupported */
background-color: #e5e5e5;
}
.keyboard-key {
width: 33.33%;
background-color: #ffffff;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #f5f5f5; /* mimic grid gap */
box-sizing: border-box;
}
.key-text {

View File

@@ -24,7 +24,7 @@
<view v-else class="record-list">
<view v-for="item in records" :key="item.id" class="record-item">
<view class="record-left">
<text class="record-title">{{ item.description || getTypeText(item.type) }}</text>
<text class="record-title">{{ item.description ?? getTypeText(item.type) }}</text>
<text class="record-time">{{ formatTime(item.created_at) }}</text>
</view>
<view class="record-right">
@@ -105,11 +105,11 @@ const getTypeText = (type: string): string => {
'admin': '系统调整',
'register': '注册赠送'
}
return map[type] || '积分变动'
return map[type] ?? '积分变动'
}
const formatTime = (timeStr: string): string => {
if (!timeStr) return ''
if (timeStr == '') return ''
const date = new Date(timeStr)
const y = date.getFullYear()
const m = (date.getMonth() + 1).toString().padStart(2, '0')
@@ -122,8 +122,7 @@ const formatTime = (timeStr: string): string => {
<style>
.points-page {
min-height: 100vh;
background-color: #f5f7fa;
flex: 1;
}
.points-header {
@@ -174,7 +173,7 @@ const formatTime = (timeStr: string): string => {
font-weight: bold;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
display: block;
display: flex;
}
.record-item {
@@ -188,10 +187,7 @@ const formatTime = (timeStr: string): string => {
.record-left {
display: flex;
flex-direction: column;
gap: 4px;
}
.record-title {
margin-bottom: 4px;
font-size: 15px;
color: #333;
}

View File

@@ -1,6 +1,7 @@
<!-- 消费者端 - 商品详情页 -->
<template>
<view class="product-detail-page">
<scroll-view class="page-scroll" scroll-y="true">
<!-- 商品图片轮播 -->
<view class="product-images">
<swiper class="image-swiper" :indicator-dots="true" :autoplay="false" @change="onSwiperChange">
@@ -23,7 +24,7 @@
<!-- 店铺信息 -->
<view class="shop-info" @click="goToShop">
<image :src="merchant.shop_logo || '/static/default-shop.png'" class="shop-logo" />
<image :src="merchant.shop_logo ?? '/static/default-shop.png'" class="shop-logo" />
<view class="shop-details">
<text class="shop-name" @click.stop="goToShop">{{ merchant.shop_name }}</text>
<view class="shop-stats-row">
@@ -67,7 +68,7 @@
<!-- 规格选择 -->
<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-selected">{{ selectedSpec ?? '请选择规格' }}</text>
<text class="spec-arrow">></text>
</view>
@@ -94,7 +95,7 @@
<!-- 商品详情 -->
<view class="product-description">
<view class="section-title">商品详情</view>
<text class="description-text">{{ product.description || '暂无详细描述' }}</text>
<text class="description-text">{{ product.description ?? '暂无详细描述' }}</text>
<!-- 商品详情图片 -->
<view class="detail-images" v-if="product.images && product.images.length > 0">
<image v-for="(img, index) in product.images"
@@ -105,6 +106,7 @@
@click="previewImage(index)" />
</view>
</view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="bottom-actions">
@@ -136,7 +138,7 @@
<text class="spec-title">选择规格</text>
<text class="close-btn" @click="hideSpecModal">×</text>
</view>
<view class="spec-list">
<scroll-view class="spec-list" direction="vertical">
<view v-for="sku in productSkus" :key="sku.id"
class="spec-item"
:class="{ active: selectedSkuId === sku.id }"
@@ -145,7 +147,7 @@
<text class="spec-price">¥{{ sku.price }}</text>
<text class="spec-stock">库存{{ sku.stock }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
@@ -156,7 +158,7 @@
<text class="params-title">商品参数</text>
<text class="close-btn" @click="hideParamsModal">×</text>
</view>
<view class="params-list">
<scroll-view class="params-list" direction="vertical">
<view class="params-item" v-if="product.specification">
<text class="params-label">规格</text>
<text class="params-value">{{ product.specification }}</text>
@@ -189,7 +191,7 @@
<text class="params-label">标签</text>
<text class="params-value">{{ product.tags.join(', ') }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
@@ -272,7 +274,7 @@ export default {
}
},
onLoad(options: any) {
const productId = options.productId as string || options.id as string
const productId = (options['productId'] ?? options['id']) as string
const productPrice = options.price ? parseFloat(options.price) : null
const productOriginalPrice = options.originalPrice ? parseFloat(options.originalPrice) : null
@@ -370,7 +372,7 @@ export default {
uni.showLoading({ title: '加载中...' })
try {
const dbProductResponse = await supabaseService.getProductById(productId)
let dbProduct: any = null
let dbProduct: any | null = null
if (Array.isArray(dbProductResponse) && dbProductResponse.length > 0) {
dbProduct = dbProductResponse[0]
} else if (dbProductResponse && !Array.isArray(dbProductResponse)) {
@@ -381,25 +383,25 @@ export default {
// Map DB product to local product
this.product = {
id: dbProduct.id,
merchant_id: dbProduct.merchant_id || dbProduct.shop_id || '',
category_id: dbProduct.category_id || '',
merchant_id: dbProduct.merchant_id ?? dbProduct.shop_id ?? '',
category_id: dbProduct.category_id ?? '',
name: dbProduct.name,
description: dbProduct.description || '',
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,
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(),
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,
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
@@ -482,15 +484,15 @@ export default {
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_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,
rating: shop.rating_avg || 5.0,
total_sales: shop.total_sales || 0,
created_at: shop.created_at || new Date().toISOString()
rating: shop.rating_avg ?? 5.0,
total_sales: shop.total_sales ?? 0,
created_at: shop.created_at ?? new Date().toISOString()
} as MerchantType
realMerchantLoaded = true
}
@@ -547,7 +549,7 @@ export default {
specifications: specs,
price: sku.price,
stock: sku.stock !== undefined ? sku.stock : 0,
image_url: sku.image_url || '',
image_url: sku.image_url != null ? sku.image_url : '',
status: sku.status !== undefined ? sku.status : 1
} as ProductSkuType
})
@@ -560,7 +562,7 @@ export default {
// 新增:加载优惠券
async loadCoupons() {
if (!this.product.merchant_id) return
if (this.product.merchant_id == '') return
// Safety check for cached service definition
// @ts-ignore
if (typeof supabaseService.fetchShopCoupons === 'function') {
@@ -574,12 +576,12 @@ export default {
// 新增:联系客服(商家)
contactMerchant() {
if (!supabaseService.getCurrentUserId()) {
if (supabaseService.getCurrentUserId() == '') {
uni.navigateTo({ url: '/pages/auth/login' })
return
}
// Navigate to chat
const merchId = this.merchant.user_id || this.merchant.id || this.product.merchant_id;
const merchId = this.merchant.user_id ?? this.merchant.id ?? this.product.merchant_id;
uni.navigateTo({
url: `/pages/mall/consumer/chat?merchantId=${merchId}&merchantName=${this.merchant.shop_name}`
})
@@ -596,7 +598,7 @@ export default {
// 新增:领取优惠券
async claimCoupon(coupon: any) {
const userId = supabaseService.getCurrentUserId()
if (!userId) {
if (userId == '') {
uni.navigateTo({ url: '/pages/auth/login' })
return
}
@@ -621,7 +623,7 @@ export default {
},
formatDate(dateStr: string): string {
if (!dateStr) return ''
if (dateStr == '') return ''
const date = new Date(dateStr)
return `${date.getFullYear()}.${date.getMonth()+1}.${date.getDate()}`
},
@@ -729,7 +731,7 @@ export default {
},
async toggleFavorite() {
if (!this.product.id) return
if (this.product.id == '') return
uni.showLoading({ title: '处理中' })
try {
@@ -830,8 +832,15 @@ export default {
<style>
.product-detail-page {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 120rpx;
flex: 1;
display: flex;
flex-direction: column;
}
.page-scroll {
flex: 1;
height: 0;
width: 100%;
}
.product-images {
@@ -881,7 +890,7 @@ export default {
.original-price {
font-size: 28rpx;
color: #999;
text-decoration: line-through;
text-decoration-line: line-through;
}
.product-name {
@@ -991,15 +1000,18 @@ export default {
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-end;
justify-content: flex-end;
flex-direction: column;
z-index: 1000;
}
.popup-content {
background-color: #fff;
width: 100%;
max-height: 80vh;
border-radius: 20rpx 20rpx 0 0;
padding: 30rpx;
display: flex;
flex-direction: column;
max-height: 1000rpx;
}
.popup-header {
display: flex;
@@ -1019,7 +1031,7 @@ export default {
color: #999;
}
.coupon-list-scroll {
max-height: 60vh;
flex: 1;
}
.coupon-item {
display: flex;
@@ -1168,6 +1180,7 @@ export default {
.product-description {
background-color: #fff;
padding: 30rpx;
padding-bottom: 140rpx;
margin-bottom: 20rpx;
}
@@ -1259,16 +1272,19 @@ export default {
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-end;
justify-content: flex-end; /* UVUE 推荐用 flex 布局对齐 */
flex-direction: column;
z-index: 999;
}
.spec-content {
background-color: #fff;
width: 100%;
max-height: 80vh;
border-radius: 20rpx 20rpx 0 0;
padding: 30rpx;
display: flex;
flex-direction: column;
max-height: 1000rpx;
}
.spec-header {
@@ -1287,8 +1303,7 @@ export default {
}
.spec-list {
max-height: 60vh;
overflow-y: auto;
flex: 1;
}
.spec-item {
@@ -1333,7 +1348,6 @@ export default {
color: #333;
font-weight: bold;
margin-bottom: 15rpx;
display: block;
}
.function-content {
@@ -1391,16 +1405,19 @@ export default {
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-end;
justify-content: flex-end; /* UVUE 推荐用 flex 布局对齐 */
flex-direction: column;
z-index: 1000;
}
.params-content {
background-color: #fff;
width: 100%;
max-height: 80vh;
border-radius: 20rpx 20rpx 0 0;
padding: 30rpx;
display: flex;
flex-direction: column;
max-height: 1000rpx;
}
.params-header {
@@ -1420,8 +1437,7 @@ export default {
}
.params-list {
max-height: 60vh;
overflow-y: auto;
flex: 1;
}
.params-item {
@@ -1473,8 +1489,6 @@ export default {
flex: 1;
margin-right: 0;
text-align: center;
white-space: normal;
word-break: break-word;
padding: 0 10rpx;
}
@@ -1482,4 +1496,4 @@ export default {
margin-left: 20rpx;
}
}
</style>
</style>

View File

@@ -6,14 +6,14 @@
<view class="nav-container">
<!-- 头像 -->
<image
:src="userInfo.avatar_url || '/static/default-avatar.png'"
:src="userInfo.avatar_url != '' ? userInfo.avatar_url : '/static/default-avatar.png'"
class="nav-avatar"
@click="editProfile"
/>
<!-- 用户信息横向排列 (名字、积分、余额、优惠券) -->
<view class="nav-user-stats">
<text class="nav-user-name">{{ userInfo.nickname || userInfo.phone }}</text>
<text class="nav-user-name">{{ userInfo.nickname != '' ? userInfo.nickname : userInfo.phone }}</text>
<view class="nav-stat-item" @click="goToPoints">
<text class="nav-stat-label">积分</text>
@@ -40,6 +40,7 @@
</view>
</view>
<scroll-view class="profile-scroll-content" direction="vertical" style="flex:1; height: 0; width: 100%;">
<!-- 导航栏占位符 - 恢复 -->
<view :style="{ height: (statusBarHeight + 10) + 'px' }"></view>
@@ -243,6 +244,7 @@
</view>
</view>
</view> -->
</scroll-view>
</view>
</template>
@@ -381,8 +383,10 @@ export default {
// 按时间倒序 (created_at)
this.allOrders.sort((a: any, b: any) => {
const timeA = new Date(a.created_at || 0).getTime()
const timeB = new Date(b.created_at || 0).getTime()
const dateA = a['created_at']
const dateB = b['created_at']
const timeA = new Date(dateA != null ? dateA : 0).getTime()
const timeB = new Date(dateB != null ? dateB : 0).getTime()
return timeB - timeA
})
@@ -415,12 +419,13 @@ export default {
'shipped': '待收货订单',
'review': '待评价订单'
}
return titles[this.currentOrderTab] || '我的订单'
const title = titles[this.currentOrderTab]
return title != null ? title : '我的订单'
},
initPage() {
const systemInfo = uni.getSystemInfoSync()
this.statusBarHeight = systemInfo.statusBarHeight || 0
this.statusBarHeight = systemInfo.statusBarHeight ?? 0
},
async loadUserProfile() {
try {
@@ -436,19 +441,19 @@ export default {
let uGender = 0
if (profile instanceof UTSJSONObject) {
uId = profile.getString('user_id') || ''
uPhone = profile.getString('phone') || ''
uEmail = profile.getString('email') || ''
uNickname = profile.getString('nickname') || ''
uAvatar = profile.getString('avatar_url') || ''
uGender = profile.getNumber('gender') || 0
uId = profile.getString('user_id') ?? ''
uPhone = profile.getString('phone') ?? ''
uEmail = profile.getString('email') ?? ''
uNickname = profile.getString('nickname') ?? ''
uAvatar = profile.getString('avatar_url') ?? ''
uGender = profile.getNumber('gender') ?? 0
} else {
uId = (profile['user_id'] as string) || ''
uPhone = (profile['phone'] as string) || ''
uEmail = (profile['email'] as string) || ''
uNickname = (profile['nickname'] as string) || ''
uAvatar = (profile['avatar_url'] as string) || ''
uGender = (profile['gender'] as number) || 0
uId = (profile['user_id'] as string) ?? ''
uPhone = (profile['phone'] as string) ?? ''
uEmail = (profile['email'] as string) ?? ''
uNickname = (profile['nickname'] as string) ?? ''
uAvatar = (profile['avatar_url'] as string) ?? ''
uGender = (profile['gender'] as number) ?? 0
}
// 如果昵称为空,使用手机号脱敏显示
@@ -460,8 +465,8 @@ export default {
id: uId,
phone: uPhone,
email: uEmail,
nickname: uNickname || '微信用户',
avatar_url: uAvatar || '/static/default-avatar.png',
nickname: uNickname != '' ? uNickname : '微信用户',
avatar_url: uAvatar != '' ? uAvatar : '/static/default-avatar.png',
gender: uGender,
user_type: 1,
status: 1,
@@ -556,36 +561,50 @@ export default {
getUserLevel(): string {
const levels = ['新手', '铜牌会员', '银牌会员', '金牌会员', '钻石会员']
return levels[this.userStats.level] || '新手'
if (this.userStats.level >= 0 && this.userStats.level < levels.length) {
return levels[this.userStats.level]
}
return '新手'
},
getOrderStatusText(status: number): string {
const statusTexts = ['异常', '待支付', '待发货', '待收货', '已完成', '已取消']
return statusTexts[status] || '未知'
if (status >= 0 && status < statusTexts.length) {
return statusTexts[status]
}
return '未知'
},
getOrderStatusClass(status: number): string {
const statusClasses = ['error', 'pending', 'processing', 'shipping', 'completed', 'cancelled']
return statusClasses[status] || 'error'
if (status >= 0 && status < statusClasses.length) {
return statusClasses[status]
}
return 'error'
},
getOrderMainImage(order: any): string {
// 尝试从 ml_order_items 获取第一张图
const items = order['ml_order_items'] as any[]
if (items && items.length > 0) {
if (items != null && items.length > 0) {
const firstItem = items[0]
// 数据库字段通常是 image_url
const img = firstItem['image_url'] || firstItem['product_image']
if (img) return img as string
const imgUrl = firstItem['image_url'] as string
const prodImg = firstItem['product_image'] as string
const img = (imgUrl != null && imgUrl != '') ? imgUrl : prodImg
if (img != null && img != '') return img
}
return '/static/product1.jpg'
},
getOrderTitle(order: any): string {
const items = order['ml_order_items'] as any[]
if (items && items.length > 0) {
if (items != null && items.length > 0) {
const firstItem = items[0]
const name = (firstItem['product_name'] || '商品') as string
const pName = firstItem['product_name'] as string
const name = (pName != null && pName != '') ? pName : '商品'
if (items.length > 1) {
return `${name} 等${items.length}件商品`
}
@@ -787,9 +806,10 @@ export default {
<style>
.consumer-profile {
background-color: #f5f5f5;
min-height: 100vh;
flex: 1;
display: flex;
flex-direction: column;
}
/* 智能顶部导航栏 */
.smart-navbar {
position: fixed;
@@ -833,7 +853,8 @@ export default {
font-weight: bold;
color: white;
margin-right: 12px;
max-width: 30%; /* 限制名字宽度 */
/* max-width: 30%; REMOVED */
width: 100px; /* Use fixed width approx */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -886,7 +907,7 @@ export default {
height: 32px;
background: rgba(255, 255, 255, 0.2);
border-radius: 16px;
cursor: pointer;
/* cursor: pointer; REMOVED */
}
.action-icon {
@@ -1071,7 +1092,11 @@ export default {
.order-actions {
display: flex;
justify-content: flex-end;
gap: 15rpx;
/* gap: 15rpx; REMOVED */
}
.order-actions .action-btn {
margin-left: 15px; /* Replace gap */
}
.action-btn {
@@ -1100,7 +1125,7 @@ export default {
display: flex;
flex-direction: row;
flex-wrap: wrap; /* 允许换行 */
gap: 16px 0; /* 行间距16px列间距由 flex 控制 */
/* gap: 16px 0; REMOVED */
justify-content: flex-start; /* 从左开始排列 */
}
@@ -1111,6 +1136,7 @@ export default {
align-items: center;
position: relative;
box-sizing: border-box; /* 确保 padding 不影响宽度 */
margin-bottom: 16px; /* Replace gap row */
}
.service-icon {
@@ -1138,7 +1164,7 @@ export default {
.stats-period {
display: flex;
gap: 30rpx;
/* gap: 30rpx; REMOVED */
margin-bottom: 30rpx;
}
@@ -1147,6 +1173,7 @@ export default {
color: #666;
padding: 12rpx 24rpx;
border-radius: 20rpx;
margin-right: 30rpx; /* Replace gap */
background-color: #f0f0f0;
}
@@ -1157,7 +1184,7 @@ export default {
.stats-content {
display: flex;
gap: 20rpx;
/* gap: 20rpx; REMOVED */
}
.stat-card {
@@ -1166,6 +1193,11 @@ export default {
padding: 30rpx 0;
background-color: #f8f9fa;
border-radius: 10rpx;
margin-right: 20rpx; /* Replace gap */
}
.stat-card:last-child {
margin-right: 0;
}
.stat-value {

View File

@@ -1,6 +1,6 @@
<template>
<view class="red-packets-page">
<view class="tab-header">
<view class="tab-header" style="position: fixed; top: 0; left: 0; right: 0; z-index: 10;">
<text
class="tab-item"
:class="{ active: currentTab === 0 }"
@@ -82,19 +82,19 @@ const loadData = async () => {
let createdAt = ''
if (item instanceof UTSJSONObject) {
id = item.getString('id') || ''
amount = item.getNumber('amount') || 0
name = item.getString('name') || ''
status = item.getNumber('status') || 0
expireAt = item.getString('expire_at') || ''
createdAt = item.getString('created_at') || ''
id = item.getString('id') ?? ''
amount = item.getNumber('amount') ?? 0
name = item.getString('name') ?? ''
status = item.getNumber('status') ?? 0
expireAt = item.getString('expire_at') ?? ''
createdAt = item.getString('created_at') ?? ''
} else {
id = (item['id'] as string) || ''
amount = (item['amount'] as number) || 0
name = (item['name'] as string) || ''
status = (item['status'] as number) || 0
expireAt = (item['expire_at'] as string) || ''
createdAt = (item['created_at'] as string) || ''
id = (item['id'] as string) ?? ''
amount = (item['amount'] as number) ?? 0
name = (item['name'] as string) ?? ''
status = (item['status'] as number) ?? 0
expireAt = (item['expire_at'] as string) ?? ''
createdAt = (item['created_at'] as string) ?? ''
}
return {
@@ -127,7 +127,7 @@ const getStatusText = (status: number): string => {
}
const formatTime = (timeStr: string): string => {
if (!timeStr) return '永久有效'
if (timeStr == '') return '永久有效'
const date = new Date(timeStr)
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
}
@@ -136,18 +136,14 @@ const formatTime = (timeStr: string): string => {
<style>
.red-packets-page {
background-color: #f5f5f5;
min-height: 100vh;
display: flex;
flex-direction: column;
flex: 1;
}
.tab-header {
display: flex;
background-color: #fff;
padding: 10px 0;
position: sticky;
top: 0;
z-index: 10;
/* position: sticky is removed in flavor of inline fixed style */
}
.tab-item {

View File

@@ -71,7 +71,7 @@ const submitReview = () => {
<style scoped>
.review-page {
min-height: 100vh;
flex: 1;
background-color: #ffffff;
padding: 20px;
}
@@ -85,7 +85,6 @@ const submitReview = () => {
font-size: 20px;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 10px;
}
@@ -109,11 +108,11 @@ const submitReview = () => {
.stars {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.star {
margin-right: 10px;
font-size: 32px;
color: #ddd;
transition: color 0.2s;

View File

@@ -103,6 +103,32 @@
import { ref, onMounted, watch } from 'vue'
import { supabaseService } from '@/utils/supabaseService.uts'
type RefundStatusHistoryItem = {
status: number
remark: string
created_at: string
}
type RefundProductInfo = {
images: string[]
}
type RefundOrderItem = {
id: string
product_name: string
sku_specifications: any
price: number
quantity: number
product?: RefundProductInfo
}
type RefundOrderInfo = {
id: string
order_no: string
created_at: string
order_items: RefundOrderItem[]
}
type RefundType = {
id: string
user_id: string
@@ -112,27 +138,9 @@ type RefundType = {
refund_reason: string
refund_amount: number
status: number // 1:待处理 2:处理中 3:已完成 4:已取消 5:已拒绝
status_history: Array<{
status: number
remark: string
created_at: string
}> | null
status_history: RefundStatusHistoryItem[] | null
created_at: string
order?: {
id: string
order_no: string
created_at: string
order_items: Array<{
id: string
product_name: string
sku_specifications: any
price: number
quantity: number
product?: {
images: string[]
}
}>
}
order?: RefundOrderInfo
}
type TabCountsType = {
@@ -178,7 +186,7 @@ const loadRefunds = async (loadMore: boolean = false) => {
try {
const userId = getCurrentUserId()
if (!userId) {
if (userId == '') {
uni.navigateTo({
url: '/pages/user/login'
})
@@ -198,28 +206,28 @@ const loadRefunds = async (loadMore: boolean = false) => {
// 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,
const orderObj: any = item['order'] ?? {}
const dbItems: any[] = (orderObj['ml_order_items'] as any[]) ?? []
const uiItems = dbItems.map((di: any) : RefundOrderItem => ({
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'] }
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,
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 || [],
status_history: (item['status_history'] as RefundStatusHistoryItem[]) ?? [],
created_at: item.created_at,
order: {
id: item.order_id,
@@ -249,7 +257,7 @@ const loadRefunds = async (loadMore: boolean = false) => {
// 加载标签页计数
const loadTabCounts = async () => {
const userId = getCurrentUserId()
if (!userId) return
if (userId == '') return
try {
const { count, error } = await supa
@@ -263,7 +271,7 @@ const loadTabCounts = async () => {
return
}
tabCounts.value.processing = count || 0
tabCounts.value.processing = count ?? 0
} catch (err) {
console.error('加载计数异常:', err)
}
@@ -272,7 +280,7 @@ const loadTabCounts = async () => {
// 获取当前用户ID
const getCurrentUserId = (): string => {
const userStore = uni.getStorageSync('userInfo')
return userStore?.id || ''
return userStore['id'] ?? ''
}
// 获取状态文本
@@ -284,7 +292,7 @@ const getStatusText = (status: number): string => {
4: '已取消',
5: '已拒绝'
}
return statusMap[status] || '未知状态'
return statusMap[status] ?? '未知状态'
}
// 获取状态样式类
@@ -296,21 +304,21 @@ const getStatusClass = (status: number): string => {
4: 'status-cancelled',
5: 'status-rejected'
}
return classMap[status] || 'status-unknown'
return classMap[status] ?? 'status-unknown'
}
// 获取商品图片
const getProductImage = (refund: RefundType): string => {
const firstItem = refund.order?.order_items?.[0]
if (!firstItem?.product?.images?.[0]) {
if (firstItem?.product?.images == null || firstItem?.product?.images.length == 0) {
return '/static/default-product.png'
}
return firstItem.product.images[0]
return firstItem.product!.images[0]
}
// 获取商品名称
const getProductName = (refund: RefundType): string => {
const items = refund.order?.order_items || []
const items = refund.order?.order_items ?? []
if (items.length === 0) return '未知商品'
if (items.length === 1) {
@@ -322,7 +330,7 @@ const getProductName = (refund: RefundType): string => {
// 格式化时间
const formatTime = (timeStr?: string): string => {
if (!timeStr) return ''
if (timeStr == null || timeStr == '') return ''
const date = new Date(timeStr)
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
@@ -401,7 +409,7 @@ const cancelRefund = (refund: RefundType) => {
.from('refunds')
.update({
status: 4, // 已取消
status_history: [...(refund.status_history || []), {
status_history: [...(refund.status_history ?? []), {
status: 4,
remark: '用户取消申请',
created_at: new Date().toISOString()
@@ -511,7 +519,7 @@ const goBack = () => {
.refund-page {
display: flex;
flex-direction: column;
height: 100vh;
flex: 1;
background-color: #f5f5f5;
}
@@ -551,16 +559,7 @@ const goBack = () => {
.refund-tab.active {
color: #007aff;
}
.refund-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #007aff;
border-bottom: 2px solid #007aff;
}
.tab-text {
@@ -725,7 +724,7 @@ const goBack = () => {
.refund-amount {
display: flex;
align-items: baseline;
align-items: flex-end;
}
.amount-label {
@@ -783,32 +782,30 @@ const goBack = () => {
color: #333333;
font-weight: bold;
margin-bottom: 3px;
display: block;
}
.step-time {
font-size: 12px;
color: #999999;
margin-bottom: 3px;
display: block;
}
.step-desc {
font-size: 12px;
color: #666666;
display: block;
}
.refund-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
/* gap: 10px; removed for uni-app-x */
padding-top: 15px;
border-top: 1px solid #f5f5f5;
}
.action-btn {
padding: 8px 15px;
margin-left: 10px;
padding: 6px 15px;
border-radius: 15px;
font-size: 12px;
border: 1px solid;

View File

@@ -18,7 +18,7 @@
<view class="products-section">
<view v-for="(item, index) in orderItems" :key="item.id" class="product-review">
<view class="product-header">
<image class="product-image" :src="item.product_image || '/static/default-product.png'" />
<image class="product-image" :src="item.product_image ?? '/static/default-product.png'" />
<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>
@@ -46,7 +46,7 @@
v-model="contents[index]"
placeholder="请写下您的使用感受,分享给其他小伙伴吧"
maxlength="500" />
<text class="word-count">{{ contents[index]?.length || 0 }}/500</text>
<text class="word-count">{{ contents[index]?.length ?? 0 }}/500</text>
</view>
<!-- 图片上传 -->
@@ -143,7 +143,8 @@
</template>
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import supa from '@/components/supadb/aksupainstance.uts'
type OrderItemType = {
@@ -163,7 +164,7 @@ type MerchantType = {
}
const orderId = ref<string>('')
const order = ref<any>(null)
const order = ref<any>({})
const orderItems = ref<Array<OrderItemType>>([])
const merchant = ref<MerchantType | null>(null)
const ratings = ref<Array<number>>([])
@@ -185,15 +186,10 @@ const canSubmit = computed(() => {
})
// 生命周期
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options as any
if (options.orderId) {
orderId.value = options.orderId
loadOrderData()
}
onLoad((options: any) => {
const optObj = (options instanceof UTSJSONObject) ? (options as UTSJSONObject) : (JSON.parse(JSON.stringify(options ?? {})) as UTSJSONObject)
orderId.value = optObj.getString('orderId') ?? ''
if (orderId.value != '') loadOrderData()
})
// 加载订单数据
@@ -226,9 +222,9 @@ const loadOrderData = async () => {
return
}
orderItems.value = (itemsData || []).map((item: any) => ({
orderItems.value = (itemsData ?? []).map((item: any) => ({
...item,
product_image: item.product?.images?.[0] || '/static/default-product.png'
product_image: item.product?.images?.[0] ?? '/static/default-product.png'
}))
// 初始化评分和内容数组
@@ -245,7 +241,7 @@ const loadOrderData = async () => {
.eq('id', order.value.merchant_id)
.single()
if (!merchantError) {
if (merchantError == null) {
merchant.value = merchantData
}
}
@@ -257,7 +253,7 @@ const loadOrderData = async () => {
// 格式化时间
const formatTime = (timeStr?: string): string => {
if (!timeStr) return ''
if (timeStr == null) return ''
const date = new Date(timeStr)
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
@@ -267,7 +263,7 @@ const formatTime = (timeStr?: string): string => {
// 获取规格文本
const getSpecText = (specs: any): string => {
if (!specs) return ''
if (specs == null) return ''
if (typeof specs === 'object') {
return Object.keys(specs)
.map(key => `${key}: ${specs[key]}`)
@@ -279,7 +275,7 @@ const getSpecText = (specs: any): string => {
// 获取评分文本
const getRatingText = (rating: number): string => {
const texts = ['非常差', '差', '一般', '好', '非常好']
return texts[rating - 1] || '未评价'
return texts[rating - 1] ?? '未评价'
}
// 设置商品评分
@@ -352,7 +348,7 @@ const submitReview = async () => {
try {
const userId = getCurrentUserId()
if (!userId) {
if (userId == '') {
uni.showToast({
title: '用户信息错误',
icon: 'none'
@@ -366,7 +362,7 @@ const submitReview = async () => {
product_id: item.product_id,
order_id: orderId.value,
rating: ratings.value[index],
content: contents.value[index] || '',
content: contents.value[index] != '' ? contents.value[index] : '',
images: images.value[index],
is_anonymous: anonymous.value
}))
@@ -436,7 +432,7 @@ const submitReview = async () => {
// 获取当前用户ID
const getCurrentUserId = (): string => {
const userStore = uni.getStorageSync('userInfo')
return userStore?.id || ''
return userStore?.getString('id') ?? ''
}
// 返回
@@ -449,7 +445,7 @@ const goBack = () => {
.review-page {
display: flex;
flex-direction: column;
height: 100vh;
flex: 1;
background-color: #f5f5f5;
}
@@ -532,13 +528,13 @@ const goBack = () => {
color: #333333;
line-height: 1.4;
margin-bottom: 5px;
display: block;
/* display: block; removed */
}
.product-spec {
font-size: 12px;
color: #999999;
display: block;
/* display: block; removed */
}
.rating-section {
@@ -555,16 +551,17 @@ const goBack = () => {
.rating-stars {
display: flex;
gap: 10px;
/* gap: 10px; removed */
}
.rating-stars.small {
gap: 5px;
/* gap: 5px; removed */
}
.star-icon {
font-size: 24px;
color: #cccccc;
margin-right: 10px;
}
.star-icon.active {
@@ -593,7 +590,7 @@ const goBack = () => {
}
.word-count {
display: block;
/* display: block; removed */
text-align: right;
font-size: 12px;
color: #999999;
@@ -605,7 +602,7 @@ const goBack = () => {
}
.images-label {
display: block;
/* display: block; removed */
font-size: 14px;
color: #333333;
margin-bottom: 10px;
@@ -614,10 +611,12 @@ const goBack = () => {
.images-grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
/* gap: 10px; removed */
}
.image-item {
margin-right: 10px;
margin-bottom: 10px;
width: 70px;
height: 70px;
border-radius: 5px;
@@ -644,7 +643,9 @@ const goBack = () => {
align-items: center;
justify-content: center;
}
margin-right: 10px;
margin-bottom: 10px;
.upload-btn {
width: 70px;
height: 70px;
@@ -680,11 +681,11 @@ const goBack = () => {
.switch-label {
font-size: 14px;
color: #333333;
/* display: block; removed */
}
.anonymous-tip {
display: block;
/* display: block; removed */
font-size: 12px;
color: #999999;
}
@@ -696,7 +697,7 @@ const goBack = () => {
}
.section-title {
display: block;
/* display: block; removed */
font-size: 16px;
font-weight: bold;
color: #333333;
@@ -722,7 +723,7 @@ const goBack = () => {
}
.tip-title {
display: block;
/* display: block; removed */
font-size: 16px;
font-weight: bold;
color: #333333;
@@ -730,7 +731,7 @@ const goBack = () => {
}
.tip-item {
display: block;
/* display: block; removed */
font-size: 12px;
color: #666666;
line-height: 1.6;
@@ -761,4 +762,4 @@ const goBack = () => {
background-color: #cccccc;
opacity: 0.6;
}
</style>
</style>

View File

@@ -285,7 +285,7 @@ onMounted(() => {
const initPage = () => {
try {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
statusBarHeight.value = systemInfo.statusBarHeight ?? 0
const windowHeight = systemInfo.windowHeight
// 减去头部高度 (约60px + statusBarHeight)
scrollHeight.value = windowHeight - (60 + statusBarHeight.value)
@@ -343,7 +343,7 @@ const loadData = async () => {
id: p.id,
name: p.name,
price: p.base_price,
image: p.main_image_url || '/static/default.jpg',
image: p.main_image_url ?? '/static/default.jpg',
sales: typeof p.sale_count === 'number' ? p.sale_count : 0
}))
@@ -386,7 +386,7 @@ const saveSearchHistory = () => {
}
const addToHistory = (keyword: string) => {
if (!keyword) return
if (keyword == '') return
const index = searchHistory.value.indexOf(keyword)
if (index > -1) {
searchHistory.value.splice(index, 1)
@@ -418,30 +418,11 @@ const deleteHistoryItem = (index: number) => {
const searchSuggestions = ref<string[]>([])
let suggestTimer = 0
const fetchSuggestions = async (kw: string) => {
if (!kw || showResults.value) return
// 简单搜索前5个相关商品作为建议
try {
const res = await supabaseService.searchProducts(kw.trim(), 1, 5)
if (res.data.length > 0) {
// 去重
const names = res.data.map((p:any) => p.name as string)
// @ts-ignore
searchSuggestions.value = [...new Set(names)]
} else {
searchSuggestions.value = []
}
} catch(e) {
searchSuggestions.value = []
}
}
// 搜索逻辑
const onInput = (e: any) => {
const val = e.detail.value
searchKeyword.value = val
if (!val) {
if (val == '') {
showResults.value = false
searchSuggestions.value = []
return
@@ -454,13 +435,37 @@ const onInput = (e: any) => {
}, 300)
}
const fetchSuggestions = async (kw: string) => {
if (kw == '' || showResults.value) return
// 简单搜索前5个相关商品作为建议
try {
const res = await supabaseService.searchProducts(kw.trim(), 1, 5)
if (Array.isArray(res.data) && res.data.length > 0) {
// 去重
const names = res.data.map((p:any) :string => {
if(p instanceof UTSJSONObject){
return p.getString('name') ?? ''
}
return p['name'] as string
})
// @ts-ignore
searchSuggestions.value = Array.from(new Set(names))
} else {
searchSuggestions.value = []
}
} catch(e) {
searchSuggestions.value = []
}
}
const clearSearch = () => {
searchKeyword.value = ''
showResults.value = false
}
const onSearch = () => {
if (!searchKeyword.value.trim()) return
if (searchKeyword.value.trim() == '') return
addToHistory(searchKeyword.value.trim())
performSearch()
}
@@ -493,7 +498,7 @@ const performSearch = async () => {
// 使用 Supabase 搜索真实数据
const keyword = searchKeyword.value.trim()
if (!keyword) {
if (keyword == '') {
loading.value = false
return
}
@@ -523,8 +528,8 @@ const performSearch = async () => {
searchShopResults.value = shopResp.data.map((s: any) => ({
id: s.id,
name: s.shop_name,
logo: s.shop_logo || '/static/shop_logo_default.png',
productCount: s.product_count || 0
logo: s.shop_logo ?? '/static/shop_logo_default.png',
productCount: s.product_count ?? 0
}))
} else {
searchShopResults.value = []
@@ -543,11 +548,11 @@ const performSearch = async () => {
return {
id: p.id,
name: p.name,
image: p.main_image_url || '/static/default.jpg',
image: p.main_image_url ?? '/static/default.jpg',
price: p.base_price,
specification: p.specification || '标准规格',
specification: p.specification ?? '标准规格',
tag: tag,
sales: p.sale_count || 0
sales: p.sale_count ?? 0
}
})
@@ -576,7 +581,7 @@ const switchSort = (type: string) => {
}
const loadMore = async () => {
if (loading.value || !hasMore.value || !searchKeyword.value.trim()) return
if (loading.value || !hasMore.value || searchKeyword.value.trim() == '') return
loading.value = true
// 增加页码
@@ -607,11 +612,11 @@ const loadMore = async () => {
return {
id: p.id,
name: p.name,
image: p.main_image_url || '/static/default.jpg',
image: p.main_image_url ?? '/static/default.jpg',
price: p.base_price,
specification: p.specification || '标准规格',
specification: p.specification ?? '标准规格',
tag: tag,
sales: p.sale_count || 0
sales: p.sale_count ?? 0
}
})
searchResults.value.push(...newItems)
@@ -698,7 +703,7 @@ const goBack = () => {
<style>
.search-page {
width: 100%;
height: 100vh;
flex: 1; /* Fixed 100vh */
background-color: #f5f5f5;
display: flex;
flex-direction: column;
@@ -764,7 +769,7 @@ const goBack = () => {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: block;
/* display: block; REMOVED */
margin-bottom: 2px;
}
@@ -787,7 +792,6 @@ const goBack = () => {
flex-direction: row; /* UVUE 必须显式设置 row */
align-items: center;
padding: 10px 16px;
gap: 12px;
width: 100%; /* 确保占满宽度 */
}
@@ -799,6 +803,7 @@ const goBack = () => {
justify-content: center;
width: 32px; /* 固定宽度防止压缩 */
height: 32px;
margin-right: 12px;
}
.back-icon {
@@ -875,7 +880,7 @@ const goBack = () => {
.inner-search-text {
font-size: 13px;
color: #ffffff;
font-weight: 500;
font-weight: bold;
}
/* 内容区域 */
@@ -907,11 +912,12 @@ const goBack = () => {
display: flex;
flex-direction: row; /* UVUE 显式设置 row */
align-items: center;
gap: 4px;
/* gap: 4px; REMOVED */
flex-shrink: 0; /* 防止被压缩 */
}
.clear-text {
margin-right: 4px; /* REPLACED gap */
font-size: 12px;
color: #999;
}
@@ -929,7 +935,7 @@ const goBack = () => {
.history-tags {
display: flex;
flex-direction: row; /* UVUE 显式设置 row */
gap: 10px;
/* gap: 10px; REMOVED */
flex-wrap: wrap; /* 允许换行 */
padding: 0 4px;
align-items: center;
@@ -941,8 +947,10 @@ const goBack = () => {
border-radius: 16px;
display: flex;
align-items: center;
gap: 6px;
/* gap: 6px; REMOVED */
flex-shrink: 0; /* 防止被压缩 */
margin-right: 10px; /* REPLACED gap */
margin-bottom: 10px; /* REPLACED gap */
}
.history-text {
@@ -951,6 +959,7 @@ const goBack = () => {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 6px; /* REPLACED gap */
}
.delete-tag-btn {
@@ -959,7 +968,7 @@ const goBack = () => {
justify-content: center;
width: 16px;
height: 16px;
border-radius: 50%;
border-radius: 8px;
background-color: #f0f0f0;
}
@@ -978,10 +987,16 @@ const goBack = () => {
display: flex;
flex-direction: row; /* UVUE 显式设置 row */
flex-wrap: wrap; /* 允许换行 */
gap: 10px;
/* gap: 10px; REMOVED */
padding: 0 4px;
}
.hot-tag {
/* ... existing styles ... */
margin-right: 10px; /* REPLACED gap */
margin-bottom: 10px; /* REPLACED gap */
}
.hot-tag {
background-color: #fff;
padding: 6px 12px;
@@ -989,8 +1004,9 @@ const goBack = () => {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
flex-shrink: 0; /* 防止被压缩 */
margin-right: 10px; /* REPLACED gap */
margin-bottom: 10px; /* REPLACED gap */
}
.hot-tag.hot {
@@ -1001,7 +1017,7 @@ const goBack = () => {
font-size: 12px;
color: #999;
font-weight: bold;
margin-right: 2px;
margin-right: 6px;
}
.hot-rank.top-three {
@@ -1015,6 +1031,7 @@ const goBack = () => {
.hot-icon {
font-size: 12px;
margin-left: 4px;
}
/* 猜你需要 */
@@ -1025,11 +1042,11 @@ const goBack = () => {
.title-with-icon {
display: flex;
align-items: center;
gap: 6px;
}
.section-icon {
font-size: 16px;
margin-right: 6px;
}
.refresh-btn {
@@ -1038,9 +1055,11 @@ const goBack = () => {
}
.guess-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
padding: 0 4px;
}
.guess-item {
@@ -1048,6 +1067,8 @@ const goBack = () => {
border-radius: 8px;
overflow: hidden;
padding-bottom: 8px;
width: 48%;
margin-bottom: 10px;
}
.guess-img-box {
@@ -1074,15 +1095,15 @@ const goBack = () => {
font-size: 13px;
color: #333;
margin-bottom: 6px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.3;
height: 34px; /* 限制2行高度 */
}
.guess-price-row {
display: flex;
align-items: baseline;
align-items: flex-end; /* REPLACED baseline */
}
.price-symbol {
@@ -1139,19 +1160,18 @@ const goBack = () => {
align-items: center;
margin-bottom: 12px;
flex-wrap: wrap; /* 允许换行以适应小屏 */
gap: 8px;
}
.results-title {
font-size: 15px;
font-weight: bold;
color: #333;
margin-right: 8px;
}
.filter-tabs {
display: flex;
flex-direction: row; /* UVUE 显式设置 row */
gap: 16px;
flex: 1; /* 自适应填充剩余空间 */
justify-content: flex-end; /* 靠右对齐 */
}
@@ -1160,17 +1180,19 @@ const goBack = () => {
font-size: 13px;
color: #666;
padding: 4px 8px; /* 增加点击区域 */
margin-left: 16px;
}
.filter-tab.active {
color: #4CAF50;
font-weight: 500;
font-weight: bold; /* REPLACED 500 */
}
.results-list {
display: grid;
grid-template-columns: repeat(2, 1fr); /* 默认移动端双列 */
gap: 10px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
padding: 0 4px;
}
@@ -1178,26 +1200,30 @@ const goBack = () => {
/* 平板设备 (768px以上) */
@media screen and (min-width: 768px) {
.results-list {
grid-template-columns: repeat(3, 1fr); /* 平板显示3列 */
gap: 16px;
padding: 0 16px;
}
.guess-grid {
grid-template-columns: repeat(4, 1fr); /* 猜你喜欢在平板上显示4列 */
.result-item {
width: 32%;
}
.guess-item {
width: 24%; /* 猜你喜欢在平板上显示4列 */
}
}
/* 桌面设备 (1024px以上) */
@media screen and (min-width: 1024px) {
.results-list {
grid-template-columns: repeat(4, 1fr); /* 桌面显示4列 */
gap: 20px;
padding: 0 24px;
}
.result-item {
width: 24%;
}
.guess-grid {
grid-template-columns: repeat(6, 1fr); /* 猜你喜欢在桌面上显示6列 */
.guess-item {
width: 16%; /* 猜你喜欢在桌面上显示6列 */
}
/* 桌面端调整图片高度 */
@@ -1212,7 +1238,8 @@ const goBack = () => {
padding: 8px;
display: flex;
flex-direction: column; /* 垂直排列 */
gap: 8px;
width: 48%;
margin-bottom: 10px;
}
.product-image {
@@ -1227,19 +1254,17 @@ const goBack = () => {
display: flex;
flex-direction: column;
justify-content: space-between;
margin-top: 8px;
}
.product-name {
font-size: 13px; /* 减小字号 */
color: #333;
font-weight: 500;
font-weight: bold;
line-height: 1.3;
height: 34px; /* 限制高度 */
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.product-tags-row {
@@ -1261,7 +1286,7 @@ const goBack = () => {
.price-box {
color: #ff5000;
display: flex;
align-items: baseline;
align-items: flex-end;
}
.price-symbol {
@@ -1270,7 +1295,7 @@ const goBack = () => {
.price-value {
font-size: 16px; /* 减小价格字号 */
font-weight: 600;
font-weight: 700;
}
.add-cart-btn {
@@ -1302,17 +1327,18 @@ const goBack = () => {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.error-icon {
font-size: 48px;
margin-bottom: 12px;
}
.error-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 12px;
}
.error-desc {
@@ -1333,16 +1359,10 @@ const goBack = () => {
height: 24px;
border: 2px solid #f0f0f0;
border-top-color: #4CAF50;
border-radius: 50%;
animation: spin 1s linear infinite;
border-radius: 12px;
margin-bottom: 8px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 12px;
color: #999;

View File

@@ -57,17 +57,17 @@
<view class="list-item">
<text class="item-icon">🔔</text>
<text class="item-text">订单消息</text>
<switch :checked="notifications.order" @change="toggleNotification('order')" />
<switch class="settings-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')" />
<switch class="settings-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')" />
<switch class="settings-switch" :checked="notifications.review" @change="toggleNotification('review')" />
</view>
</view>
</view>
@@ -79,17 +79,17 @@
<view class="list-item">
<text class="item-icon">👁️</text>
<text class="item-text">隐藏购物记录</text>
<switch :checked="privacy.hidePurchase" @change="togglePrivacy('hidePurchase')" />
<switch class="settings-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')" />
<switch class="settings-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')" />
<switch class="settings-switch" :checked="privacy.receiveMerchantMsg" @change="togglePrivacy('receiveMerchantMsg')" />
</view>
</view>
</view>
@@ -265,7 +265,7 @@ const statusBarHeight = ref(0)
// 生命周期
onMounted(() => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
statusBarHeight.value = systemInfo.statusBarHeight
loadUserInfo()
loadSettings()
})
@@ -565,9 +565,13 @@ const deleteAccount = () => {
title: '注销中...'
})
const userId = userInfo.value.id || uni.getStorageSync('user_id')
let userId = userInfo.value.getString('id')
if (userId == null) {
const storageId = uni.getStorageSync('user_id')
userId = (storageId != null) ? storageId as string : null
}
if (userId) {
if (userId != null) {
try {
// 标记用户状态为注销 (status=3)
await supa
@@ -661,7 +665,7 @@ const deleteAccount = () => {
}
/* 针对 switch 组件的特殊处理,如果需要显示开关,可能需要调整布局 */
.list-item switch {
.settings-switch {
transform: scale(0.7);
margin-top: 5px;
}
@@ -669,7 +673,7 @@ const deleteAccount = () => {
/* 屏幕宽度大于 480px (大屏手机/平板/PC) 时,启用更宽的网格布局或列表布局 */
@media screen and (min-width: 480px) {
.list-item {
width: calc(50% - 10px); /* 每行两个,留出间隙 */
width: 47%; /* width: calc(50% - 10px); REPLACED */
margin: 5px;
border: 1px solid #f0f0f0;
border-radius: 8px;
@@ -697,7 +701,7 @@ const deleteAccount = () => {
/* 增加针对手机横屏的媒体查询 */
@media screen and (orientation: landscape) and (max-height: 500px) {
.list-item {
width: calc(25% - 10px); /* 横屏也保持4个一行或者根据需要调整 */
width: 22%; /* width: calc(25% - 10px); REPLACED */
margin: 5px;
border: 1px solid #f0f0f0;
border-radius: 8px;
@@ -723,7 +727,7 @@ const deleteAccount = () => {
}
.list-item {
width: calc(33.33% - 10px); /* 每行三个 */
width: 31%; /* width: calc(33.33% - 10px); REPLACED */
flex-direction: row; /* PC端保持水平排列 */
justify-content: flex-start;
text-align: left;
@@ -743,7 +747,7 @@ const deleteAccount = () => {
.settings-page {
display: flex;
flex-direction: column;
height: 100vh;
height: 100%;
background-color: #f5f5f5;
}
@@ -779,7 +783,6 @@ const deleteAccount = () => {
}
.section-title {
display: block;
font-size: 16px;
font-weight: bold;
color: #333333;
@@ -853,6 +856,5 @@ const deleteAccount = () => {
.delete-account {
color: #999999;
font-size: 14px;
text-decoration: underline;
}
}/* text-decoration: underline; REMOVED */
</style>

View File

@@ -1,10 +1,11 @@
<template>
<view class="shop-detail-page">
<scroll-view class="page-scroll" scroll-y="true" @scrolltolower="onScrollToLower" refresher-enabled="true" @refresherrefresh="onRefresherRefresh" :refresher-triggered="isRefresherTriggered">
<!-- 店铺头部信息 -->
<view class="shop-header">
<image :src="merchant.shop_banner || '/static/default-banner.png'" class="shop-banner" mode="aspectFill" />
<image :src="merchant.shop_banner != '' ? merchant.shop_banner : '/static/default-banner.png'" class="shop-banner" mode="aspectFill" />
<view class="shop-info-card">
<image :src="merchant.shop_logo || '/static/default-shop.png'" class="shop-logo" />
<image :src="merchant.shop_logo != '' ? merchant.shop_logo : '/static/default-shop.png'" class="shop-logo" />
<view class="shop-basic-info">
<text class="shop-name">{{ merchant.shop_name }}</text>
<view class="shop-stats">
@@ -21,7 +22,7 @@
</view>
</view>
</view>
<text class="shop-desc">{{ merchant.shop_description || '这家店很懒,什么都没写~' }}</text>
<text class="shop-desc">{{ merchant.shop_description != '' ? merchant.shop_description : '这家店很懒,什么都没写~' }}</text>
<!-- 优惠券列表 (新增) -->
<view class="shop-coupons" v-if="coupons.length > 0">
@@ -63,12 +64,12 @@
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app'
import { MerchantType, ProductType } from '@/types/mall-types.uts'
import { supabaseService } from '@/utils/supabaseService.uts'
@@ -97,12 +98,15 @@ const merchant = ref<MerchantType>({
const products = ref<ProductType[]>([])
const isFollowed = ref(false)
const coupons = ref<any[]>([]) // 新增优惠券
const isRefresherTriggered = ref(false)
onMounted(() => {
const pages = getCurrentPages()
const options = pages[pages.length - 1].options as any
// Search传递的是 id (shop_id), 其他地方可能传递 merchantId
const paramId = (options['merchantId'] || options['id']) as string
const mId = options['merchantId']
const pId = options['id']
const paramId = (mId != null ? mId : pId) as string
if (paramId) {
console.log('Page mounted with params:', paramId)
@@ -129,31 +133,41 @@ onMounted(() => {
}
})
onPullDownRefresh(() => {
// 下拉刷新
const onRefresherRefresh = () => {
isRefresherTriggered.value = true
currentPage.value = 1
hasMore.value = true
isLoading.value = false
if (currentMerchantId.value != '') {
const id = currentMerchantId.value
// 重新加载所有数据
loadShopData(id)
loadCoupons(id)
loadShopProducts(id)
Promise.all([
loadShopData(id),
loadCoupons(id),
loadShopProducts(id)
]).then(() => {
isRefresherTriggered.value = false
})
} else {
setTimeout(() => {
uni.stopPullDownRefresh()
isRefresherTriggered.value = false
}, 500)
}
}
const onScrollToLower = () => {
if (hasMore.value && !isLoading.value && currentMerchantId.value != '') {
console.log('Scroll to lower, loading more...')
loadShopProducts(currentMerchantId.value)
}
}
onPullDownRefresh(() => {
onRefresherRefresh()
})
onReachBottom(() => {
// 触底加载更多
if (hasMore.value && !isLoading.value && currentMerchantId.value != '') {
console.log('Reach bottom, loading more...')
loadShopProducts(currentMerchantId.value)
}
onScrollToLower()
})
const loadShopData = async (id: string) => {
@@ -166,15 +180,15 @@ const loadShopData = async (id: string) => {
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_logo: shop.shop_logo != null ? shop.shop_logo : '/static/default-shop.png',
shop_banner: shop.shop_banner != null ? shop.shop_banner : '/static/default-banner.png',
shop_description: shop.description != null ? shop.description : '',
contact_name: shop.contact_name != null ? shop.contact_name : '',
contact_phone: shop.contact_phone != null ? shop.contact_phone : '',
shop_status: 1, // 默认正常
rating: shop.rating_avg || 5.0,
total_sales: shop.total_sales || 0,
created_at: shop.created_at || ''
rating: shop.rating_avg != null ? shop.rating_avg : 5.0,
total_sales: shop.total_sales != null ? shop.total_sales : 0,
created_at: shop.created_at != null ? shop.created_at : ''
}
// 检查关注状态
@@ -204,7 +218,7 @@ const loadCoupons = async (id: string) => {
const claimCoupon = async (coupon: any) => {
const userId = supabaseService.getCurrentUserId()
if (!userId) {
if (userId == null) {
uni.navigateTo({ url: '/pages/auth/login' })
return
}
@@ -297,24 +311,49 @@ const loadShopProducts = async (id: string) => {
// 安全获取属性的方式,处理字段名称不一样的问题
const safeItem = item as any
const safePrice = (safeItem['base_price'] || safeItem['price'] || 0) as number
const safeMarketPrice = (safeItem['market_price'] || safeItem['original_price'] || safePrice) as number
const safeStock = (safeItem['total_stock'] || safeItem['available_stock'] || safeItem['stock'] || 0) as number
const safeSales = (safeItem['sale_count'] || safeItem['sales'] || 0) as number
let safePrice = safeItem['base_price'] as number
if (safePrice == null) {
const p = safeItem['price'] as number
safePrice = p != null ? p : 0
}
let safeMarketPrice = safeItem['market_price'] as number
if (safeMarketPrice == null) {
const mp = safeItem['original_price'] as number
safeMarketPrice = mp != null ? mp : safePrice
}
let safeStock = safeItem['total_stock'] as number
if (safeStock == null) {
let as_ = safeItem['available_stock'] as number
if (as_ == null) {
const s = safeItem['stock'] as number
safeStock = s != null ? s : 0
} else {
safeStock = as_
}
}
let safeSales = safeItem['sale_count'] as number
if (safeSales == null) {
const s = safeItem['sales'] as number
safeSales = s != null ? s : 0
}
return {
id: item.id,
merchant_id: item.merchant_id,
category_id: item.category_id,
name: item.name,
description: item.description || '',
description: item.description != null ? item.description : '',
images: images,
price: safePrice,
original_price: safeMarketPrice,
stock: safeStock,
sales: safeSales,
status: 1,
created_at: item.created_at || ''
created_at: item.created_at != null ? item.created_at : '',
updated_at: item.updated_at != null ? item.updated_at : ''
}
})
@@ -355,14 +394,14 @@ const checkFollowStatus = async (shopId: string) => {
const toggleFollow = async () => {
const userId = supabaseService.getCurrentUserId()
if (!userId) {
if (userId == null) {
uni.navigateTo({ url: '/pages/auth/login' })
return
}
// 这里的 merchant.value.id 假如是 ML_SHOPS.id
const shopId = merchant.value.id
if (!shopId) return
if (shopId == null || shopId == '') return
uni.showLoading({ title: '处理中' })
@@ -393,7 +432,7 @@ const toggleFollow = async () => {
const contactService = () => {
const currentUser = supabaseService.getCurrentUserId()
if (!currentUser) {
if (currentUser == null) {
uni.navigateTo({ url: '/pages/user/login' })
return
}
@@ -410,7 +449,7 @@ const contactService = () => {
const addToCart = async (product: ProductType) => {
uni.showLoading({ title: '添加中...' })
const success = await supabaseService.addToCart(product.id, 1)
const success = await supabaseService.addToCart(product.id, 1, '')
uni.hideLoading()
@@ -437,7 +476,15 @@ const goToProduct = (id: string) => {
<style>
.shop-detail-page {
background-color: #f5f5f5;
min-height: 100vh;
flex: 1;
display: flex;
flex-direction: column;
}
.page-scroll {
flex: 1;
height: 0;
width: 100%;
}
.shop-header {
@@ -544,8 +591,7 @@ const goToProduct = (id: string) => {
}
.shop-desc {
display: block;
font-size: 14px;
color: #666;
padding: 10px 15px 0;
line-height: 1.4;
@@ -603,13 +649,15 @@ const goToProduct = (id: string) => {
justify-content: center;
align-items: center;
background-color: #ff4444;
writing-mode: vertical-rl; /* Note: writing-mode may not work in all environments, used flex direction in product detail instead, but let's try or use flex col */
}
.coupon-btn-label {
color: #fff;
font-size: 12px;
writing-mode: vertical-rl;
}
flex-direction: column;
}
.coupon-btn-label {
color: #fff;
font-size: 12px;
width: 14px; /* Force vertical flow by width constraint if needed, or just let it stack naturally if char by char */
text-align: center;
line-height: 1.2;
}
.product-section {
padding: 15px;
@@ -628,12 +676,12 @@ const goToProduct = (id: string) => {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
width: 100%;
justify-content: space-between;
}
.product-item {
width: calc(50% - 5px);
width: 48%; /* Fallback for calc(50% - 5px) */
background-color: white;
border-radius: 8px;
overflow: hidden;
@@ -659,9 +707,8 @@ const goToProduct = (id: string) => {
font-size: 14px;
color: #333;
margin-bottom: 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
lines: 2;
overflow: hidden;
height: 40px;
line-height: 20px;
@@ -677,17 +724,14 @@ const goToProduct = (id: string) => {
.price-left {
display: flex;
flex-direction: row;
align-items: baseline;
align-items: flex-end;
}
.cart-btn {
width: 24px;
height: 24px;
background-color: #ff4444;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
}
.cart-icon {
@@ -713,13 +757,13 @@ const goToProduct = (id: string) => {
*/
@media (min-width: 768px) {
.product-item {
width: calc(33.33% - 7px) !important; /* Tablet: 3 items */
width: 32% !important; /* Tablet: 3 items */
}
}
@media (min-width: 1024px) {
.product-item {
width: calc(16.66% - 9px) !important; /* PC: 6 items */
width: 16% !important; /* PC: 6 items */
}
.shop-info-card, .shop-header, .product-section {
@@ -729,4 +773,4 @@ const goToProduct = (id: string) => {
margin-right: auto;
}
}
</style>
</style>

View File

@@ -6,13 +6,13 @@
<view class="shop-list" v-if="shops.length > 0">
<view class="shop-item" v-for="shop in shops" :key="shop.id" @click="goToShop(shop)">
<image :src="shop.shop_logo || '/static/default-shop.png'" class="shop-logo" mode="aspectFill" />
<image :src="shop.shop_logo != null ? shop.shop_logo : '/static/default-shop.png'" class="shop-logo" mode="aspectFill" />
<view class="shop-info">
<text class="shop-name">{{ shop.shop_name }}</text>
<text class="shop-desc">{{ shop.description || '暂无介绍' }}</text>
<text class="shop-desc">{{ shop.description != null ? shop.description : '暂无介绍' }}</text>
<view class="shop-meta">
<text class="rating">⭐ {{ shop.rating_avg || 5.0 }}</text>
<text class="sales">销量: {{ shop.total_sales || 0 }}</text>
<text class="rating shop-meta-text">⭐ {{ shop.rating_avg }}</text>
<text class="sales shop-meta-text">销量: {{ shop.total_sales }}</text>
</view>
</view>
<button class="unfollow-btn" @click.stop="unfollow(shop)">已关注</button>
@@ -72,8 +72,8 @@ const loadFollowedShops = async () => {
shop_name: shopData['shop_name'] as string,
shop_logo: shopData['shop_logo'] as string | null,
description: shopData['description'] as string | null,
rating_avg: (shopData['rating_avg'] || 5.0) as number,
total_sales: (shopData['total_sales'] || 0) as number
rating_avg: (shopData['rating_avg'] != null) ? (shopData['rating_avg'] as number) : 5.0,
total_sales: (shopData['total_sales'] != null) ? (shopData['total_sales'] as number) : 0
})
}
})
@@ -111,8 +111,9 @@ const goToShop = (shop: FollowedShop) => {
// Since shop-detail handles both, passing shop.id (which is ml_shops.id) is fine?
// Wait, shop-detail logic: 1. getShopByMerchantId(id) [tries merchant_id then id].
// So passing shop.id is safer if merchant_id is not unique or confusing.
const targetId = shop.merchant_id != '' ? shop.merchant_id : shop.id
uni.navigateTo({
url: `/pages/mall/consumer/shop-detail?merchantId=${shop.merchant_id || shop.id}`
url: `/pages/mall/consumer/shop-detail?merchantId=${targetId}`
})
}
@@ -125,7 +126,7 @@ const goHome = () => {
.followed-shops-page {
padding: 15px;
background-color: #f5f5f5;
min-height: 100vh;
flex: 1;
}
.header {
margin-bottom: 15px;
@@ -137,7 +138,6 @@ const goHome = () => {
.shop-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.shop-item {
background-color: #fff;
@@ -146,6 +146,10 @@ const goHome = () => {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 10px;
}
.shop-item:last-child {
margin-bottom: 0;
}
.shop-logo {
width: 50px;
@@ -178,7 +182,9 @@ const goHome = () => {
color: #999;
margin-top: 4px;
display: flex;
gap: 8px;
}
.shop-meta-text {
margin-right: 8px;
}
.unfollow-btn {
font-size: 12px;

View File

@@ -11,8 +11,8 @@
<view v-else class="list">
<view class="card" v-for="s in items" :key="s['id']">
<view class="row between">
<text class="name">{{ s['plan']?.['name'] || '订阅' }}</text>
<text class="status" :class="'st-' + (s['status'] || 'active')">{{ statusText(s['status'] as string) }}</text>
<text class="name">{{ s['plan']?.['name'] != null ? s['plan']?.['name'] : '订阅' }}</text>
<text class="status" :class="'st-' + (s['status'] != null ? s['status'] : 'active')">{{ statusText(s['status'] as string) }}</text>
</view>
<view class="row">
<text class="label">周期</text>
@@ -33,7 +33,7 @@
<view class="actions">
<label class="toggle">
<switch :checked="!!s['auto_renew']" @change="e => toggleAutoRenew(s, e.detail.value as boolean)" />
<text>自动续费</text>
<text class="toggle-text">自动续费</text>
</label>
<button class="danger" @click="cancelAtPeriodEnd(s)" :disabled="(s['status'] as string) !== 'active'">到期取消</button>
</view>
@@ -59,7 +59,8 @@ const fmt = (s: string | null): string => {
const statusText = (st: string): string => {
const map: UTSJSONObject = { trial: '试用', active: '生效', past_due: '逾期', canceled: '已取消', expired: '已过期' } as UTSJSONObject
return (map[st] as string) || st
const val = map[st] as string | null
return val != null ? val : st
}
const loadSubs = async () => {
@@ -133,14 +134,16 @@ onMounted(loadSubs)
<style scoped>
.my-subs { padding: 12px; }
.header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.title { font-size: 18px; font-weight: 600; }
.title { font-size: 18px; font-weight: 700; }
.ghost { background: #fff; border: 1px solid #ddd; color: #333; border-radius: 6px; padding: 6px 10px; }
.loading, .empty { padding: 24px; text-align: center; color: #888; }
.list { display: flex; flex-direction: column; gap: 12px; }
.card { background: #fff; border-radius: 10px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); }
.row { display: flex; gap: 8px; padding: 4px 0; }
.list { display: flex; flex-direction: column; }
.card { background: #fff; border-radius: 10px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); margin-bottom: 12px; }
.card:last-child { margin-bottom: 0; }
.row { display: flex; padding: 4px 0; }
.label { margin-right: 8px; }
.between { justify-content: space-between; align-items: center; }
.name { font-size: 16px; font-weight: 600; }
.name { font-size: 16px; font-weight: 700; }
.status { font-size: 12px; padding: 2px 8px; border-radius: 999px; background: #eee; color: #333; }
.st-trial { background: #e6f7ff; color: #1677ff; }
.st-active { background: #f6ffed; color: #52c41a; }
@@ -149,6 +152,7 @@ onMounted(loadSubs)
.label { color: #666; width: 80px; }
.value { color: #111; flex: 1; }
.actions { display: flex; align-items: center; justify-content: space-between; margin-top: 8px; }
.toggle { display: flex; align-items: center; gap: 6px; }
.toggle { display: flex; align-items: center; }
.toggle-text { margin-right: 6px; }
.danger { background: #f5222d; color: #fff; border-radius: 6px; padding: 6px 10px; }
</style>

View File

@@ -96,16 +96,17 @@ onMounted(loadPlan)
<style scoped>
.plan-detail { padding: 12px; }
.header { margin-bottom: 8px; }
.title { font-size: 18px; font-weight: 600; }
.title { font-size: 18px; font-weight: 700; }
.card { background: #fff; border-radius: 10px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); }
.name { font-size: 16px; font-weight: 600; }
.name { font-size: 16px; font-weight: 700; }
.desc { color: #666; margin: 6px 0; }
.price-row { display: flex; align-items: baseline; gap: 4px; margin: 8px 0; }
.price { font-size: 22px; color: #ff4d4f; font-weight: 700; }
.period { color: #999; }
.features { margin-top: 8px; }
.f-title { font-weight: 600; margin-bottom: 4px; }
.f-list { display: flex; flex-direction: column; gap: 2px; color: #444; }
.price-row { display: flex; align-items: flex-end; margin: 8px 0; }
.price { font-size: 22px; color: #ff4d4f; font-weight: 700; margin-right: 4px; }
.period { color: #999; }
.features { margin-top: 8px; }
.f-title { font-weight: 700; margin-bottom: 4px; }
.f-list { display: flex; flex-direction: column; color: #444; }
.f-item { margin-bottom: 2px; }
.actions { display: flex; justify-content: flex-end; margin-top: 12px; }
.primary { background: #3cc51f; color: #fff; border-radius: 6px; padding: 8px 12px; }
.loading, .empty { padding: 24px; text-align: center; color: #888; }

View File

@@ -92,18 +92,19 @@ onMounted(loadPlans)
<style scoped>
.sub-plan-list { padding: 12px; }
.header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.title { font-size: 18px; font-weight: 600; }
.plan-container { display: flex; flex-direction: column; gap: 12px; }
.plan-card { background: #fff; border-radius: 10px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); }
.plan-header { display: flex; align-items: center; justify-content: space-between; }
.plan-name { font-size: 16px; font-weight: 600; }
.badge { font-size: 12px; color: #fff; background: #3cc51f; border-radius: 999px; padding: 2px 8px; }
.plan-desc { color: #666; margin: 6px 0; line-height: 1.5; }
.price-row { display: flex; align-items: baseline; gap: 4px; margin: 6px 0; }
.price { font-size: 22px; color: #ff4d4f; font-weight: 700; }
.period { color: #999; }
.feature-list { color: #444; display: flex; flex-direction: column; gap: 2px; margin: 6px 0; }
.feature-item { font-size: 12px; color: #555; }
.title { font-size: 18px; font-weight: 700; }
.plan-container { display: flex; flex-direction: column; }
.plan-card { background: #fff; border-radius: 10px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); margin-bottom: 12px; }
.plan-card:last-child { margin-bottom: 0; }
.plan-header { display: flex; align-items: center; justify-content: space-between; }
.plan-name { font-size: 16px; font-weight: 700; color: #333; }
.badge { font-size: 12px; color: #fff; background: #3cc51f; border-radius: 999px; padding: 2px 8px; }
.plan-desc { color: #666; margin: 6px 0; line-height: 1.5; }
.price-row { display: flex; align-items: flex-end; margin: 6px 0; }
.price { font-size: 22px; color: #ff4d4f; font-weight: 700; margin-right: 4px; }
.period { color: #999; }
.feature-list { color: #444; display: flex; flex-direction: column; margin: 6px 0; }
.feature-item { font-size: 12px; color: #555; margin-bottom: 2px; }
.actions { display: flex; justify-content: flex-end; margin-top: 8px; }
.primary { background: #3cc51f; color: #fff; border-radius: 6px; padding: 8px 12px; }
.loading, .empty { padding: 24px; text-align: center; color: #888; }

View File

@@ -91,7 +91,10 @@ const selPay = (v: number) => { payMethod.value = v }
// 获取当前用户ID按现有store实现替换
const getCurrentUserId = (): string => {
try { return (uni.getStorageSync('current_user_id') as string) || '' } catch { return '' }
try {
const u = uni.getStorageSync('current_user_id')
return (u != null) ? (u as string) : ''
} catch { return '' }
}
const confirmSubscribe = async () => {
@@ -135,7 +138,7 @@ const confirmSubscribe = async () => {
uni.redirectTo({ url: '/pages/mall/consumer/profile' })
}, 600)
} else {
uni.showToast({ title: ins?.error?.message || '订阅失败', icon: 'none' })
uni.showToast({ title: ins?.error?.message ?? '订阅失败', icon: 'none' })
}
} catch (e) {
console.error('订阅失败:', e)
@@ -149,15 +152,17 @@ const confirmSubscribe = async () => {
<style scoped>
.subscribe-checkout { padding: 12px; }
.header { margin-bottom: 8px; }
.title { font-size: 18px; font-weight: 600; }
.title { font-size: 18px; font-weight: 700; }
.card { background: #fff; border-radius: 10px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); }
.row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
.row:last-child { border-bottom: none; }
.label { color: #666; }
.value { color: #111; font-weight: 600; }
.section-title { margin-top: 12px; font-weight: 600; }
.pay-methods { display: flex; flex-direction: column; gap: 8px; padding: 8px 0; }
.pay-item { display: flex; align-items: center; gap: 8px; }
.value { color: #111; font-weight: 700; }
.section-title { margin-top: 12px; font-weight: 700; }
.pay-methods { display: flex; flex-direction: column; padding: 8px 0; }
.pay-item { display: flex; align-items: center; margin-bottom: 8px; }
.pay-item:last-child { margin-bottom: 0; }
.pay-icon { margin-right: 8px; }
.actions { display: flex; justify-content: flex-end; margin-top: 12px; }
.primary { background: #3cc51f; color: #fff; border-radius: 6px; padding: 8px 12px; }
.loading, .empty { padding: 24px; text-align: center; color: #888; }

View File

@@ -241,7 +241,7 @@ const resetTransactions = () => {
// 加载钱包数据
const loadWalletData = async () => {
const userId = getCurrentUserId()
if (userId == null) {
if (userId == '') {
// uni.navigateTo({
// url: '/pages/user/login'
// })
@@ -282,7 +282,7 @@ const loadTransactions = async (loadMore: boolean = false) => {
try {
const userId = getCurrentUserId()
if (!userId) {
if (userId == '') {
isLoading.value = false
return
}
@@ -305,19 +305,19 @@ const loadTransactions = async (loadMore: boolean = false) => {
let createdAt = ''
if (item instanceof UTSJSONObject) {
id = item.getString('id') || ''
amount = item.getNumber('amount') || 0
balance = item.getNumber('balance_after') || 0
type = item.getString('type') || 'consume'
remark = item.getString('description') || ''
createdAt = item.getString('created_at') || ''
id = item.getString('id') ?? ''
amount = item.getNumber('amount') ?? 0
balance = item.getNumber('balance_after') ?? 0
type = item.getString('type') ?? 'consume'
remark = item.getString('description') ?? ''
createdAt = item.getString('created_at') ?? ''
} else {
id = (item['id'] as string) || ''
amount = (item['amount'] as number) || 0
balance = (item['balance_after'] as number) || 0
type = (item['type'] as string) || 'consume'
remark = (item['description'] as string) || ''
createdAt = (item['created_at'] as string) || ''
id = (item['id'] as string) ?? ''
amount = (item['amount'] as number) ?? 0
balance = (item['balance_after'] as number) ?? 0
type = (item['type'] as string) ?? 'consume'
remark = (item['description'] as string) ?? ''
createdAt = (item['created_at'] as string) ?? ''
}
mappedData.push({
@@ -355,7 +355,7 @@ const loadTransactions = async (loadMore: boolean = false) => {
// 获取当前用户ID
const getCurrentUserId = (): string => {
const userStore = uni.getStorageSync('userInfo')
return userStore?.id || ''
return userStore?.getString('id') ?? ''
}
// 获取交易图标
@@ -369,7 +369,8 @@ const getTransactionIcon = (type: string): string => {
income: '💰',
expense: '📤'
}
return icons[type] || '💰'
const icon = icons[type]
return icon != null ? icon : '💰'
}
// 获取交易标题
@@ -383,7 +384,8 @@ const getTransactionTitle = (type: string): string => {
income: '收入',
expense: '支出'
}
return titles[type] || '交易'
const title = titles[type]
return title != null ? title : '交易'
}
// 格式化时间
@@ -531,7 +533,7 @@ const goBack = () => {
.wallet-page {
display: flex;
flex-direction: column;
height: 100vh;
flex: 1; /* Fixed 100vh */
background-color: #f5f5f5;
}
@@ -595,13 +597,14 @@ const goBack = () => {
.dashboard-container {
flex-direction: row; /* 横向排列 */
align-items: flex-start;
gap: 20px;
max-width: 100%;
/* gap: 20px; REMOVED */
/* max-width: 100%; REMOVED */
}
.dashboard-main {
width: 400px; /* 左侧固定宽度 */
flex-shrink: 0;
margin-right: 20px; /* REPLACED gap */
}
.dashboard-side {
@@ -632,7 +635,7 @@ const goBack = () => {
}
.balance-label {
display: block;
/* display: block; REMOVED */
font-size: 14px;
opacity: 0.9;
margin-bottom: 10px;
@@ -640,7 +643,7 @@ const goBack = () => {
}
.balance-value {
display: block;
/* display: block; REMOVED */
font-size: 36px;
font-weight: bold;
margin-bottom: 20px;
@@ -650,7 +653,7 @@ const goBack = () => {
.balance-actions {
display: flex;
flex-direction: row;
gap: 20px;
/* gap: 20px; REMOVED */
}
.action-btn {
@@ -665,6 +668,7 @@ const goBack = () => {
.action-btn.recharge {
background-color: #ffffff;
color: #667eea;
margin-right: 20px; /* REPLACED gap */
}
.action-btn.withdraw {
@@ -688,14 +692,14 @@ const goBack = () => {
}
.stat-label {
display: block;
/* display: block; REMOVED */
font-size: 12px;
color: #666666;
margin-bottom: 8px;
}
.stat-value {
display: block;
/* display: block; REMOVED */
font-size: 16px;
font-weight: bold;
color: #333333;
@@ -752,9 +756,7 @@ const goBack = () => {
}
.filter-tabs {
display: flex;
flex-direction: row;
gap: 15px;
/* gap: 15px; REMOVED */
}
.filter-tab {
@@ -762,22 +764,17 @@ const goBack = () => {
color: #666666;
padding: 5px 0;
position: relative;
margin-right: 15px; /* REPLACED gap */
border-bottom: 2px solid transparent; /* Prepare for active state */
}
.filter-tab.active {
color: #007aff;
font-weight: bold;
border-bottom: 2px solid #007aff; /* REPLACED ::after */
}
.filter-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #007aff;
}
/* ::after removed */
.empty-transactions {
display: flex;
@@ -899,7 +896,10 @@ const goBack = () => {
}
.tip-title {
display: block;
/* display: block; REMOVED */
font-size: 16px;
font-weight: bold;
/* display: block; REMOVED */
font-size: 16px;
font-weight: bold;
color: #333333;
@@ -907,11 +907,11 @@ const goBack = () => {
}
.tip-item {
display: block;
/* display: block; REMOVED */
margin-bottom: 8px;
font-size: 12px;
color: #666666;
line-height: 1.6;
margin-bottom: 8px;
line-height: 1.6;
}
.tip-item:last-child {
@@ -974,7 +974,7 @@ const goBack = () => {
}
.amount-label {
display: block;
/* display: block; REMOVED */
font-size: 14px;
color: #333333;
margin-bottom: 10px;
@@ -1007,7 +1007,7 @@ const goBack = () => {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
/* gap: 10px; REMOVED */
margin-bottom: 15px;
}
@@ -1017,6 +1017,8 @@ const goBack = () => {
border-radius: 15px;
font-size: 14px;
color: #333333;
margin-right: 10px; /* REPLACED gap */
margin-bottom: 10px; /* REPLACED gap */
}
.quick-amount.active {
@@ -1026,7 +1028,7 @@ const goBack = () => {
}
.recharge-tip {
display: block;
/* display: block; REMOVED */
font-size: 12px;
color: #999999;
}
@@ -1034,7 +1036,7 @@ const goBack = () => {
.popup-footer {
display: flex;
flex-direction: row;
gap: 15px;
/* gap: 15px; REMOVED */
}
.cancel-btn,
@@ -1050,6 +1052,7 @@ const goBack = () => {
.cancel-btn {
background-color: #f5f5f5;
color: #666666;
margin-right: 15px; /* REPLACED gap */
}
.confirm-btn {

View File

@@ -117,14 +117,19 @@ const loadData = async () => {
let cardNum = ''
if (item instanceof UTSJSONObject) {
id = item.getString('id') || ''
bankName = item.getString('bank_name') || ''
cardNum = item.getString('card_number') || ''
id = item.getString('id') ?? ''
bankName = item.getString('bank_name') ?? ''
cardNum = item.getString('card_number') ?? ''
} else {
const m = item as Map<string, any>
id = (m.get('id') as string) || ''
bankName = (m.get('bank_name') as string) || ''
cardNum = (m.get('card_number') as string) || ''
const idVal = m.get('id')
id = idVal != null ? (idVal as string) : ''
const nameVal = m.get('bank_name')
bankName = nameVal != null ? (nameVal as string) : ''
const numVal = m.get('card_number')
cardNum = numVal != null ? (numVal as string) : ''
}
if (id != '') {
@@ -207,7 +212,7 @@ const submitWithdraw = async () => {
<style scoped>
.page-container {
background-color: #f5f5f5;
min-height: 100vh;
flex: 1; /* Fixed 100vh issue */
padding: 20px;
}
.card {
@@ -230,7 +235,14 @@ const submitWithdraw = async () => {
.bank-info {
display: flex;
align-items: center;
gap: 10px;
/* gap removed */
}
.bank-name {
margin-right: 10px;
font-weight: bold;
}
.card-type {
margin-right: 10px;
}
.placeholder {
color: #999;
@@ -242,7 +254,7 @@ const submitWithdraw = async () => {
font-size: 14px;
color: #666;
margin-bottom: 10px;
display: block;
/* display: block removed */
}
.input-wrapper {
display: flex;
@@ -329,6 +341,6 @@ const submitWithdraw = async () => {
padding: 15px 0;
text-align: center;
color: #5785e5;
font-weight: 500;
font-weight: bold;
}
</style>