consumerm模块完成度90%,完善消费者和商家端数据库表,商品、聊天、订单数据对接好了supabase,和商家端对接了聊天功能,安卓端编译通过了css样式,剩余几个页面在处理函数规范问题
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -802,7 +802,7 @@ const goToCheckout = () => {
|
||||
|
||||
.shop-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-right: 4px;
|
||||
/* 自适应宽度,但不超过剩余空间 */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
76
pages/mall/consumer/doc/uts.txt
Normal file
76
pages/mall/consumer/doc/uts.txt
Normal 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
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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; /* 左对齐 */
|
||||
|
||||
@@ -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; /* 垂直居中 */
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user