consumer模块完成度95%,准备部署消费者端测试

This commit is contained in:
cyh666666
2026-03-05 08:45:00 +08:00
parent cceb556c62
commit 7f7f723d93
1043 changed files with 53958 additions and 3445 deletions

View File

@@ -1,52 +1,70 @@
<template>
<template>
<view class="page-container">
<scroll-view class="address-edit-page" direction="vertical">
<view class="form-group">
<view class="form-item">
<text class="label">收货人</text>
<input class="input" v-model="formData.name" placeholder="请填写收货人姓名" />
</view>
<view class="form-item">
<text class="label">手机号码</text>
<input class="input" v-model="formData.phone" type="number" maxlength="11" placeholder="请填写手机号码" />
</view>
<view class="form-item">
<text class="label">所在地区</text>
<input class="input" v-model="regionString" placeholder="省市区县、乡镇等" />
</view>
<view class="form-item">
<text class="label">详细地址</text>
<input class="input" v-model="formData.detail" placeholder="街道、楼牌号等" />
</view>
</view>
<view class="form-group">
<view class="form-item">
<text class="label">智能填写</text>
<textarea class="smart-textarea" v-model="smartInput" placeholder="粘贴姓名+电话+地址,自动识别填充" @input="parseSmartInput" maxlength="200"></textarea>
<text class="smart-tip">示例:张三 13800138000 北京市朝阳区三里屯SOHO A座</text>
</view>
<view class="form-item">
<text class="label">标签</text>
<view class="tags-container">
<text
v-for="tag in tags"
:key="tag"
class="tag-item"
:class="{ active: formData.label === tag }"
@click="selectTag(tag)"
>{{ tag }}</text>
<scroll-view class="address-edit-scroll" scroll-y="true">
<view class="address-edit-content">
<!-- 基础信息组 -->
<view class="form-group">
<view class="form-item">
<text class="label">收货人</text>
<input class="input" v-model="formData.name" placeholder="请填写收货人姓名" placeholder-class="placeholder" />
</view>
<view class="form-item">
<text class="label">手机号码</text>
<input class="input" v-model="formData.phone" type="number" maxlength="11" placeholder="请填写手机号码" placeholder-class="placeholder" />
</view>
<view class="form-item">
<text class="label">所在地区</text>
<input class="input" v-model="regionString" placeholder="省市区县、乡镇等" placeholder-class="placeholder" />
<text class="arrow-icon"></text>
</view>
<view class="form-item detail-item">
<text class="label">详细地址</text>
<textarea class="textarea" v-model="formData.detail" placeholder="街道、楼牌号等" placeholder-class="placeholder" maxlength="100"></textarea>
</view>
</view>
<!-- 标签与默认设置组 -->
<view class="form-group">
<view class="form-item label-section">
<text class="label">地址标签</text>
<view class="tags-container">
<view
v-for="tag in tags"
:key="tag"
class="tag-item"
:class="{ active: formData.label === tag }"
@click="selectTag(tag)"
>
<text class="tag-text" :class="{ 'tag-text-active': formData.label === tag }">{{ tag }}</text>
</view>
</view>
</view>
<view class="form-item switch-item">
<view class="switch-label-group">
<text class="label">设为默认地址</text>
<text class="sub-label">下单时优先使用该地址</text>
</view>
<switch :checked="formData.isDefault" color="#ff5000" @change="onSwitchChange" />
</view>
</view>
<!-- 智能识别组 -->
<view class="form-group smart-group">
<view class="smart-header">
<text class="smart-title">智能填写</text>
<text class="smart-clear" v-if="smartInput" @click="smartInput = ''">清空</text>
</view>
<textarea class="smart-textarea" v-model="smartInput" placeholder="粘贴整段地址,自动识别姓名、电话、地址" @input="parseSmartInput" maxlength="200"></textarea>
<view class="smart-footer">
<text class="smart-tip">示例张三13800138000北京市朝阳区...</text>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="footer-actions">
<button class="save-btn" @click="saveAddress">保存地址</button>
<button v-if="isEdit" class="delete-btn" @click="deleteAddress">删除此地址</button>
</view>
</view>
<view class="form-item switch-item">
<text class="label">设为默认收货地址</text>
<switch :checked="formData.isDefault" color="#ff5000" @change="onSwitchChange" />
</view>
</view>
<view class="footer-btn">
<button class="save-btn" @click="saveAddress">保存</button>
<button v-if="isEdit" class="delete-btn" @click="deleteAddress">删除收货地址</button>
</view>
</scroll-view>
</view>
@@ -371,35 +389,54 @@ const deleteAddress = () => {
<style>
.page-container {
flex: 1;
background-color: #f8f8f8;
display: flex;
flex-direction: column;
}
.address-edit-page {
flex: 1; /* Replace min-height: 100vh */
.address-edit-scroll {
flex: 1;
}
.address-edit-content {
padding: 12px;
padding-bottom: 40px;
}
.form-group {
background-color: white;
margin-bottom: 15px;
padding: 0 15px;
background-color: #ffffff;
border-radius: 12px;
padding: 0 16px;
margin-bottom: 12px;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.02);
}
.form-item {
display: flex;
flex-direction: row;
align-items: center;
border-bottom: 1px solid #f5f5f5;
padding: 15px 0;
padding: 18px 0;
border-bottom: 1px solid #f9f9f9;
}
.form-item:last-child {
border-bottom: none;
}
.detail-item {
align-items: flex-start;
flex-direction: column;
}
.detail-item .label {
margin-bottom: 12px;
}
.label {
width: 80px;
font-size: 15px;
color: #333;
font-weight: 500;
}
.input {
@@ -408,54 +445,152 @@ const deleteAddress = () => {
color: #333;
}
.switch-item {
justify-content: space-between;
.textarea {
width: 100%;
height: 60px;
font-size: 15px;
line-height: 1.5;
color: #333;
}
.placeholder {
color: #ccc;
font-size: 15px;
}
.arrow-icon {
font-size: 18px;
color: #ccc;
margin-left: 8px;
}
/* 标签选择 */
.label-section {
align-items: flex-start;
flex-direction: column;
}
.label-section .label {
margin-bottom: 16px;
}
.tags-container {
flex: 1;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.tag-item {
font-size: 12px;
color: #666;
border: 1px solid #ddd;
padding: 4px 12px;
border-radius: 15px;
margin-right: 10px;
padding: 6px 18px;
background-color: #f7f7f7;
border-radius: 20px;
margin-right: 12px;
border: 1px solid transparent;
}
.tag-item.active {
background-color: #ff5000;
color: white;
background-color: #fff1eb;
border-color: #ff5000;
}
.footer-btn {
margin-top: 30px;
padding: 0 15px;
.tag-text {
font-size: 13px;
color: #666;
}
.tag-text-active {
color: #ff5000;
font-weight: bold;
}
/* 开关项 */
.switch-item {
justify-content: space-between;
}
.switch-label-group {
display: flex;
flex-direction: column;
}
.sub-label {
font-size: 12px;
color: #999;
margin-top: 4px;
}
/* 智能填写 */
.smart-group {
padding: 16px;
}
.smart-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.smart-title {
font-size: 14px;
color: #333;
font-weight: bold;
}
.smart-clear {
font-size: 12px;
color: #007aff;
}
.smart-textarea {
width: 100%;
height: 80px;
background-color: #f9f9f9;
border-radius: 8px;
padding: 12px;
font-size: 13px;
line-height: 1.6;
color: #666;
}
.smart-footer {
margin-top: 8px;
}
.smart-tip {
font-size: 11px;
color: #999;
}
/* 底部按钮 */
.footer-actions {
margin-top: 32px;
display: flex;
flex-direction: column;
}
.save-btn {
background-color: #ff5000;
color: white;
border-radius: 25px;
color: #ffffff;
height: 48px;
line-height: 48px;
font-size: 16px;
height: 44px;
line-height: 44px;
font-weight: bold;
border-radius: 24px;
border: none;
margin-bottom: 15px;
margin-bottom: 16px;
box-shadow: 0 8rpx 20rpx rgba(255, 80, 0, 0.2);
}
.delete-btn {
background-color: white;
color: #333;
border-radius: 25px;
background-color: #ffffff;
color: #ee0a24;
height: 48px;
line-height: 48px;
font-size: 16px;
height: 44px;
line-height: 44px;
border: 1px solid #ddd;
border-radius: 24px;
border: 1px solid #f0f0f0;
}
</style>

View File

@@ -0,0 +1,463 @@
<template>
<view class="page-container">
<scroll-view class="address-edit-page" direction="vertical">
<view class="form-group">
<view class="form-item">
<text class="label">鏀惰揣浜?/text>
<input class="input" v-model="formData.name" placeholder="璇峰~鍐欐敹璐т汉濮撳悕" />
</view>
<view class="form-item">
<text class="label">鎵嬫満鍙风爜</text>
<input class="input" v-model="formData.phone" type="number" maxlength="11" placeholder="璇峰~鍐欐墜鏈哄彿鐮? />
</view>
<view class="form-item">
<text class="label">鎵?鍦ㄥ湴鍖?/text>
<input class="input" v-model="regionString" placeholder="鐪佸競鍖哄幙銆佷埂闀囩瓑" />
</view>
<view class="form-item">
<text class="label">璇︾粏鍦板潃</text>
<input class="input" v-model="formData.detail" placeholder="琛楅亾銆佹ゼ鐗屽彿绛? />
</view>
</view>
<view class="form-group">
<view class="form-item">
<text class="label">鏅鸿兘濉啓</text>
<textarea class="smart-textarea" v-model="smartInput" placeholder="绮樿创濮撳悕+鐢佃瘽+鍦板潃锛岃嚜鍔ㄨ瘑鍒~鍏? @input="parseSmartInput" maxlength="200"></textarea>
<text class="smart-tip">绀轰緥锛氬紶涓?13800138000 鍖椾含甯傛湞闃冲尯涓夐噷灞疭OHO A搴?/text>
</view>
<view class="form-item">
<text class="label">鏍囩</text>
<view class="tags-container">
<text
v-for="tag in tags"
:key="tag"
class="tag-item"
:class="{ active: formData.label === tag }"
@click="selectTag(tag)"
>{{ tag }}</text>
</view>
</view>
<view class="form-item switch-item">
<text class="label">璁句负榛樿鏀惰揣鍦板潃</text>
<switch :checked="formData.isDefault" color="#ff5000" @change="onSwitchChange" />
</view>
</view>
<view class="footer-btn">
<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, AddAddressParams, UpdateAddressParams } from '@/utils/supabaseService.uts'
type Address = {
id: string
name: string
phone: string
province: string
city: string
district: string
detail: string
isDefault: boolean
label?: string
}
const isEdit = ref(false)
const addressId = ref('')
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 AddressForm)
const loadAddress = async (id: string) => {
try {
// 浠嶴upabase鍔犺浇鍦板潃璇︽儏
const address = await supabaseService.getAddressById(id)
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 ?? ''
regionString.value = `${address.province} ${address.city} ${address.district}`.trim()
} else {
// 濡傛灉Supabase娌℃湁鎵惧埌锛屽皾璇曚粠鏈湴瀛樺偍鍔犺浇
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses != null) {
const addresses = JSON.parse(storedAddresses as string) as Address[]
const localAddress = addresses.find(item => item.id === id)
if (localAddress != null) {
formData.name = localAddress.name
formData.phone = localAddress.phone
formData.detail = localAddress.detail
formData.isDefault = localAddress.isDefault
formData.label = localAddress.label ?? ''
regionString.value = `${localAddress.province} ${localAddress.city} ${localAddress.district}`.trim()
}
}
}
} catch (error) {
console.error('鍔犺浇鍦板潃璇︽儏澶辫触:', error)
// 澶辫触鏃朵粠鏈湴瀛樺偍鍔犺浇
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses != null) {
try {
const addresses = JSON.parse(storedAddresses as string) as Address[]
const address = addresses.find(item => item.id === id)
if (address != null) {
formData.name = address.name
formData.phone = address.phone
formData.detail = address.detail
formData.isDefault = address.isDefault
formData.label = address.label ?? ''
regionString.value = `${address.province} ${address.city} ${address.district}`.trim()
}
} catch (e) {
console.error('瑙f瀽鏈湴鍦板潃鏁版嵁澶辫触', e)
}
}
}
}
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 = ''
} else {
formData.label = tag
}
}
const onSwitchChange = (e: UniSwitchChangeEvent) => {
formData.isDefault = e.detail.value
}
const saveAddress = async () => {
if (formData.name == '') {
uni.showToast({ title: '璇峰~鍐欐敹璐т汉', icon: 'none' })
return
}
if (formData.phone == '') {
uni.showToast({ title: '璇峰~鍐欐墜鏈哄彿鐮?, icon: 'none' })
return
}
if (regionString.value == '') {
uni.showToast({ title: '璇峰~鍐欐墍鍦ㄥ湴鍖?, icon: 'none' })
return
}
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 addressData = {
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 AddAddressParams
let success = false
if (isEdit.value) {
// 鏇存柊鍦板潃
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)
}
if (success) {
// 鍚屾椂鏇存柊鏈湴瀛樺偍浣滀负缂撳瓨
const storedAddresses = uni.getStorageSync('addresses')
let addresses: Address[] = []
if (storedAddresses != null) {
try {
addresses = JSON.parse(storedAddresses as string) as Address[]
} catch (e) {
addresses = []
}
}
// 濡傛灉璁句负榛樿锛屽彇娑堝叾浠栭粯璁?
if (formData.isDefault) {
addresses.forEach(item => {
item.isDefault = false
})
}
if (isEdit.value) {
const index = addresses.findIndex(item => item.id === addressId.value)
if (index !== -1) {
addresses[index] = {
...addresses[index],
name: formData.name,
phone: formData.phone,
province: province,
city: city,
district: district,
detail: formData.detail,
isDefault: formData.isDefault,
label: formData.label
}
}
} else {
const newAddress: Address = {
id: `addr_${Date.now()}`, // 涓存椂ID锛屽疄闄呯敱Supabase鐢熸垚
name: formData.name,
phone: formData.phone,
province: province,
city: city,
district: district,
detail: formData.detail,
isDefault: formData.isDefault,
label: formData.label
}
addresses.push(newAddress)
}
uni.setStorageSync('addresses', JSON.stringify(addresses))
uni.showToast({
title: '淇濆瓨鎴愬姛',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
console.error('淇濆瓨鍦板潃澶辫触')
uni.showToast({
title: '淇濆瓨澶辫触',
icon: 'none'
})
}
}
const parseSmartInput = () => {
const input = smartInput.value.trim()
if (input == '') return
// 鎻愬彇鎵嬫満鍙?
const phoneRegex = /(1[3-9]\d{9})/
const phoneMatch = input.match(phoneRegex)
if (phoneMatch != null) {
formData.phone = phoneMatch[0] ?? ''
}
// 鎻愬彇濮撳悕锛堝彇绗竴涓?-4浣嶄腑鏂囷級
const nameRegex = /([\u4e00-\u9fa5]{2,4})/
const nameMatch = input.match(nameRegex)
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, '')
addrText = addrText.replace(/[锛?;锛沑s]+/g, ' ').trim()
// 瑙f瀽鐪佸競鍖?
const pattern1 = /^(.*?鐪??(.*?甯??(.*?[鍖哄幙])?(.*)$/
const m = addrText.match(pattern1)
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 = () => {
uni.showModal({
title: '鎻愮ず',
content: '纭畾瑕佸垹闄よ鍦板潃鍚楋紵',
success: (res: UniShowModalResult) => {
if (res.confirm) {
// 璋冪敤Supabase鏈嶅姟鍒犻櫎鍦板潃
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('瑙f瀽鏈湴鍦板潃鏁版嵁澶辫触', e)
}
}
uni.showToast({
title: '鍒犻櫎鎴愬姛',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
console.error('鍒犻櫎鍦板潃澶辫触')
uni.showToast({
title: '鍒犻櫎澶辫触',
icon: 'none'
})
}
})
}
}
})
}
</script>
<style>
.page-container {
flex: 1;
display: flex;
flex-direction: column;
}
.address-edit-page {
flex: 1; /* Replace min-height: 100vh */
}
.form-group {
background-color: white;
margin-bottom: 15px;
padding: 0 15px;
}
.form-item {
display: flex;
align-items: center;
border-bottom: 1px solid #f5f5f5;
padding: 15px 0;
}
.form-item:last-child {
border-bottom: none;
}
.label {
width: 80px;
font-size: 15px;
color: #333;
}
.input {
flex: 1;
font-size: 15px;
color: #333;
}
.switch-item {
justify-content: space-between;
}
.tags-container {
flex: 1;
display: flex;
flex-wrap: wrap;
}
.tag-item {
font-size: 12px;
color: #666;
border: 1px solid #ddd;
padding: 4px 12px;
border-radius: 15px;
margin-right: 10px;
}
.tag-item.active {
background-color: #ff5000;
color: white;
border-color: #ff5000;
}
.footer-btn {
margin-top: 30px;
padding: 0 15px;
}
.save-btn {
background-color: #ff5000;
color: white;
border-radius: 25px;
font-size: 16px;
height: 44px;
line-height: 44px;
border: none;
margin-bottom: 15px;
}
.delete-btn {
background-color: white;
color: #333;
border-radius: 25px;
font-size: 16px;
height: 44px;
line-height: 44px;
border: 1px solid #ddd;
}
</style>

View File

@@ -1,9 +1,9 @@
<template>
<template>
<view class="address-list-page">
<view class="address-list">
<view v-if="addresses.length === 0" class="empty-state">
<text class="empty-icon">📍</text>
<text class="empty-text">暂无收货地址</text>
<text class="empty-icon">馃搷</text>
<text class="empty-text">鏆傛棤鏀惰揣鍦板潃</text>
</view>
<view v-else v-for="(item, index) in addresses" :key="item.id" class="address-item" @click="selectAddress(item)">
@@ -11,24 +11,24 @@
<view class="item-header">
<text class="user-name">{{ item.name }}</text>
<text class="user-phone">{{ item.phone }}</text>
<text v-if="item.isDefault" class="default-tag">默认</text>
<text v-if="item.isDefault" class="default-tag">榛樿</text>
<text v-if="item.label" class="label-tag">{{ item.label }}</text>
</view>
<text class="address-text">{{ getFullAddress(item) }}</text>
</view>
<view class="item-actions">
<view class="action-item" @click.stop="editAddress(item.id)">
<text class="action-icon">📝</text>
<text class="action-icon">馃摑</text>
</view>
<view class="action-item" @click.stop="deleteAddress(item.id)">
<text class="action-icon"><EFBFBD></text>
<text class="action-icon">锟斤笍</text>
</view>
</view>
</view>
</view>
<view class="footer-btn">
<button class="add-btn" @click="addAddress">新建收货地址</button>
<button class="add-btn" @click="addAddress">鏂板缓鏀惰揣鍦板潃</button>
</view>
</view>
</template>
@@ -81,18 +81,18 @@ const loadAddresses = () => {
addresses.value = []
}
} else {
// 初始Mock数据
// 鍒濆Mock鏁版嵁
addresses.value = [
{
id: 'addr_001',
name: '张三',
name: '寮犱笁',
phone: '13800138000',
province: '北京市',
city: '北京市',
district: '朝阳区',
detail: '三里屯SOHO A座',
province: '鍖椾含甯?,
city: '鍖椾含甯?,
district: '鏈濋槼鍖?,
detail: '涓夐噷灞疭OHO A搴?,
isDefault: true,
label: '公司'
label: '鍏徃'
}
]
uni.setStorageSync('addresses', JSON.stringify(addresses.value))
@@ -109,11 +109,11 @@ const addAddress = () => {
})
}
// 删除地址
// 鍒犻櫎鍦板潃
const deleteAddress = (id: string) => {
uni.showModal({
title: '提示',
content: '确定要删除该地址吗?',
title: '鎻愮ず',
content: '纭畾瑕佸垹闄よ鍦板潃鍚楋紵',
success: (res) => {
if (res.confirm) {
const index = addresses.value.findIndex(addr => addr.id === id)
@@ -121,7 +121,7 @@ const deleteAddress = (id: string) => {
addresses.value.splice(index, 1)
uni.setStorageSync('addresses', JSON.stringify(addresses.value))
uni.showToast({
title: '删除成功',
title: '鍒犻櫎鎴愬姛',
icon: 'success'
})
}
@@ -160,7 +160,7 @@ const selectAddress = (item: Address) => {
padding: 10px;
border-left: 1px solid #f0f0f0;
display: flex;
flex-direction: column; /* 竖向排列图标 */
flex-direction: column; /* 绔栧悜鎺掑垪鍥炬爣 */
justify-content: center;
align-items: center;
gap: 15px;
@@ -176,7 +176,7 @@ const selectAddress = (item: Address) => {
padding-bottom: calc(10px + env(safe-area-inset-bottom));
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
display: flex;
justify-content: center; /* 居中显示 */
justify-content: center; /* 灞呬腑鏄剧ず */
align-items: center;
}
@@ -188,11 +188,11 @@ const selectAddress = (item: Address) => {
height: 44px;
line-height: 44px;
border: none;
width: 100%; /* 默认占满 */
width: 100%; /* 榛樿鍗犳弧 */
max-width: 100%;
}
/* 响应式布局优化 */
/* 鍝嶅簲寮忓竷灞€浼樺寲 */
@media screen and (min-width: 768px) {
.address-list {
max-width: 800px;
@@ -209,11 +209,13 @@ 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 {
width: 300px; /* 桌面端限制宽度 */
width: 300px; /* 妗岄潰绔檺鍒跺搴?*/
}
}
</style>

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="address-list-page">
<view class="address-list">
<view v-if="addresses.length === 0" class="empty-state">
@@ -180,21 +180,113 @@ const selectAddress = (item: Address) => {
</script>
<style>
.address-list-page {
background-color: #f8f8f8;
min-height: 100vh;
padding: 12px;
padding-bottom: 100px;
}
.address-list {
display: flex;
flex-direction: column;
}
.address-item {
background-color: #ffffff;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
display: flex;
flex-direction: row;
align-items: center;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.03);
}
.item-content {
flex: 1;
display: flex;
flex-direction: column;
margin-right: 12px;
}
.item-header {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 8px;
flex-wrap: wrap;
}
.user-name {
font-size: 16px;
font-weight: bold;
color: #333;
margin-right: 12px;
}
.user-phone {
font-size: 14px;
color: #666;
margin-right: 12px;
}
.default-tag {
background-color: #fff1eb;
color: #ff5000;
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
margin-right: 6px;
border: 1px solid #ff5000;
}
.label-tag {
background-color: #eef5ff;
color: #007aff;
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
border: 1px solid #007aff;
}
.address-text {
font-size: 14px;
color: #333;
line-height: 1.5;
lines: 2;
text-overflow: ellipsis;
}
.item-actions {
padding: 10px;
padding-left: 16px;
border-left: 1px solid #f0f0f0;
display: flex;
flex-direction: column; /* 向排列图标 */
justify-content: center;
flex-direction: row; /* 改为横向排列图标更符合习惯 */
align-items: center;
}
.action-item {
margin-bottom: 15px;
padding: 8px;
}
.action-item:last-child {
margin-bottom: 0px;
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 100px;
}
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-text {
font-size: 14px;
color: #999;
}
.footer-btn {
@@ -248,3 +340,4 @@ const selectAddress = (item: Address) => {
}
}
</style>

View File

@@ -1,4 +1,4 @@
<!-- 地址管理页面 -->
<!-- 地址管理页面 -->
<template>
<view class="address-page">
<!-- 顶部栏 -->
@@ -920,3 +920,4 @@ const cancelNewAddress = () => {
border: none;
}
</style>

View File

@@ -1,14 +1,14 @@
<template>
<template>
<view class="apply-refund-page">
<view class="section">
<view class="section-title">退款类型</view>
<radio-group @change="handleTypeChange" class="type-group">
<label class="type-item">
<radio value="1" :checked="refundType === 1" color="#ff4444" class="type-radio" />
<radio value="1" :checked="refundType === 1" color="#ff5000" class="type-radio" />
<text>仅退款</text>
</label>
<label class="type-item">
<radio value="2" :checked="refundType === 2" color="#ff4444" class="type-radio" />
<radio value="2" :checked="refundType === 2" color="#ff5000" class="type-radio" />
<text>退货退款</text>
</label>
</radio-group>
@@ -133,20 +133,29 @@ const handleReasonChange = (e: any) => {
}
const submitRefund = async () => {
console.log('=== 提交退款 ===')
console.log('refundReason:', refundReason.value)
console.log('refundAmount:', refundAmount.value)
console.log('maxAmount:', maxAmount.value)
if (refundReason.value == '') {
uni.showToast({ title: '请选择退款原因', icon: 'none' })
return
}
const amount = parseFloat(refundAmount.value)
console.log('解析后金额:', amount)
if (isNaN(amount) || amount <= 0 || amount > maxAmount.value) {
uni.showToast({ title: '请输入有效的退款金额', icon: 'none' })
return
}
submitting.value = true
uni.showLoading({ title: '提交中...' })
try {
console.log('调用 createRefund, orderId:', orderId.value)
const result = await supabaseService.createRefund({
order_id: orderId.value,
refund_type: refundType.value,
@@ -155,13 +164,13 @@ const submitRefund = async () => {
description: description.value
})
console.log('createRefund 结果:', JSON.stringify(result))
uni.hideLoading()
if (result.success) {
uni.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => {
// Go back to orders listing focused on refund type?
// or stay here? User probably wants to see list.
// Since profile redirects "Refunds" to orders list, let's go there.
uni.navigateBack()
}, 1500)
} else {
@@ -169,6 +178,7 @@ const submitRefund = async () => {
}
} catch (err) {
console.error('提交退款失败', err)
uni.hideLoading()
uni.showToast({ title: '提交异常', icon: 'none' })
} finally {
submitting.value = false
@@ -278,10 +288,11 @@ const submitRefund = async () => {
}
.submit-btn {
background-color: #ff4444;
background-color: #ff5000;
color: #ffffff;
border-radius: 22px;
font-size: 16px;
font-weight: bold;
}
</style>

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="add-card-page">
<view class="form-container">
<view class="form-item">

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="bank-cards-page">
<view class="card-list">
<view v-for="card in cards" :key="card.id" class="card-item" :class="getCardClass(card.bank_name)">

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
<!-- pages/mall/consumer/cart.uvue -->
<!-- pages/mall/consumer/cart.uvue -->
<template>
<view class="cart-page">
<!-- 智能顶部导航栏 - 与消息页保持一致 -->
@@ -118,6 +118,10 @@
<view v-if="recommendProducts.length > 0" class="recommend-section">
<view class="section-header">
<text class="section-title">猜你喜欢</text>
<view class="refresh-btn" @click="refreshRecommend">
<text class="refresh-icon">🔄</text>
<text class="refresh-text">换一批</text>
</view>
</view>
<view class="recommend-list">
<view
@@ -281,6 +285,35 @@ onMounted(() => {
initPage()
})
const refreshRecommend = async () => {
try {
// 使用 searchProducts 获取销售额排序的 6 个产品
const hotResp = await supabaseService.searchProducts('', 1, 6, 'sales')
const recommends = hotResp.data
if (recommends.length > 0) {
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 ?? p.market_price ?? 0,
image: p.main_image_url ?? p.image_url ?? '/static/images/default-product.png',
skuId: '',
merchant_id: p.merchant_id ?? ''
}
})
uni.showToast({
title: '已更新推荐',
icon: 'none'
})
}
} catch (error) {
console.error('刷新推荐失败:', error)
}
}
// 加载数据
const loadCartData = async () => {
loading.value = true
@@ -746,9 +779,9 @@ const goToCheckout = () => {
top: 0;
left: 0;
right: 0;
background-color: #4CAF50;
background-color: #ff5000;
z-index: 1000;
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
box-shadow: 0 2px 12px rgba(255, 80, 0, 0.15);
display: flex;
flex-direction: column;
justify-content: flex-start;
@@ -943,10 +976,9 @@ const goToCheckout = () => {
background-color: #ff5000;
color: white;
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
text-align: center;
line-height: 18px;
font-size: 12px;
}
.unselected-icon {
@@ -1012,29 +1044,30 @@ const goToCheckout = () => {
.quantity-control {
display: flex;
flex-direction: row; /* 显式设置横向排列 */
flex-direction: row;
align-items: center;
background-color: #f5f5f5;
border-radius: 12px;
overflow: hidden;
height: 24px;
height: 28px;
}
.quantity-btn {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #666;
width: 28px;
height: 28px;
text-align: center;
line-height: 28px;
font-size: 16px;
color: #333;
background-color: #eee;
}
.quantity-value {
width: 30px;
min-width: 36px;
text-align: center;
font-size: 13px;
line-height: 24px;
font-size: 14px;
line-height: 28px;
color: #333;
}
/* 推荐商品 */
@@ -1047,6 +1080,27 @@ const goToCheckout = () => {
.section-header {
margin-bottom: 15px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.refresh-btn {
display: flex;
flex-direction: row;
align-items: center;
padding: 4px 8px;
}
.refresh-icon {
font-size: 14px;
margin-right: 4px;
}
.refresh-text {
font-size: 12px;
color: #999;
}
.section-title {
@@ -1504,3 +1558,4 @@ const goToCheckout = () => {
}
}
</style>

View File

@@ -0,0 +1,25 @@
<!-- pages/mall/consumer/cart.uvue -->
<template>
<view class="cart-page">
<!-- 鏅鸿兘椤堕儴瀵艰埅鏍?- 涓庢秷鎭〉淇濇寔涓€鑷?-->
<view class="smart-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-container">
<text class="nav-title">璐墿杞?/text>
<view class="nav-actions">
<view class="action-btn" @click="toggleManageMode">
<text class="action-icon">{{ isManageMode ? '鉁? : '鈿欙笍' }}</text>
<text class="action-text">{{ isManageMode ? '瀹屾垚' : '绠$悊' }}</text>
</view>
</view>
</view>
</view>
<!-- 瀵艰埅鏍忓崰浣嶇 - 闇€瑕佸寘鍚玸tatusBarHeight + 瀵艰埅鏍忛珮搴?4px -->
<view class="navbar-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 璐墿杞﹀唴瀹?-->
<scroll-view
:scroll-y="true"
class="cart-content"
:show-scrollbar="false"
:enhanced="true"

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +1,36 @@
<template>
<template>
<view class="category-page">
<!-- 顶部搜索栏 -->
<!-- 椤堕儴鎼滅储鏍?-->
<view class="search-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="search-container">
<view class="search-box" @click="navigateToSearch" :style="{ height: '30px' }">
<!-- 模拟输入框 -->
<text class="search-placeholder">请输入药品名称、症状或品牌</text>
<!-- 妯℃嫙杈撳叆妗?-->
<text class="search-placeholder">璇疯緭鍏ヨ嵂鍝佸悕绉般€佺棁鐘舵垨鍝佺墝</text>
<!-- 扫码图标 -->
<!-- 鎵爜鍥炬爣 -->
<view class="nav-icon-btn" @click.stop="onScan">
<text class="nav-icon">🔳</text>
<text class="nav-icon">馃敵</text>
</view>
<!-- 相机图标 -->
<!-- 鐩告満鍥炬爣 -->
<view class="nav-camera-btn" @click.stop="onCamera">
<text class="nav-camera-icon">📷</text>
<text class="nav-camera-icon">馃摲</text>
</view>
<!-- 搜索按钮 -->
<!-- 鎼滅储鎸夐挳 -->
<view class="nav-inner-search-btn" :style="{ height: '22px' }">
<text class="nav-inner-search-text">搜索</text>
<text class="nav-inner-search-text">鎼滅储</text>
</view>
</view>
</view>
</view>
<!-- 导航栏占位 - 需要包含statusBarHeight + 搜索框高度44px -->
<!-- 瀵艰埅鏍忓崰浣?- 闇€瑕佸寘鍚玸tatusBarHeight + 鎼滅储妗嗛珮搴?4px -->
<view class="navbar-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 分类内容区 -->
<!-- 鍒嗙被鍐呭鍖?-->
<view class="category-content">
<!-- 左侧一级分类 -->
<!-- 宸︿晶涓€绾у垎绫?-->
<scroll-view :scroll-y="true" class="primary-category">
<view
v-for="item in primaryCategories"
@@ -44,20 +44,20 @@
</view>
</scroll-view>
<!-- 右侧商品列表 -->
<!-- 鍙充晶鍟嗗搧鍒楄〃 -->
<scroll-view
:scroll-y="true"
class="product-content"
@scrolltolower="loadMore"
:lower-threshold="50"
>
<!-- 分类标题 -->
<!-- 鍒嗙被鏍囬 -->
<view class="category-header">
<text class="category-title">{{ currentCategoryName }}</text>
<text class="category-desc">{{ currentCategoryDesc }}</text>
</view>
<!-- 商品网格 -->
<!-- 鍟嗗搧缃戞牸 -->
<view v-if="productList.length > 0" class="product-grid">
<view
v-for="product in productList"
@@ -72,7 +72,7 @@
/>
<text class="product-name" :lines="2">{{ product.name }}</text>
<view class="product-bottom">
<text class="product-price">¥{{ product.base_price ?? product.price ?? 0 }}</text>
<text class="product-price">{{ product.base_price ?? product.price ?? 0 }}</text>
<view class="product-add-btn" @click.stop="addToCart(product)">
<text class="add-icon">+</text>
</view>
@@ -80,16 +80,16 @@
</view>
</view>
<!-- 空状态 -->
<!-- 绌虹姸鎬?-->
<view v-else class="empty-state">
<text class="empty-icon">💊</text>
<text class="empty-text">暂无相关药品</text>
<text class="empty-desc">该分类下暂无商品,敬请期待</text>
<text class="empty-icon">馃拪</text>
<text class="empty-text">鏆傛棤鐩稿叧鑽搧</text>
<text class="empty-desc">璇ュ垎绫讳笅鏆傛棤鍟嗗搧锛屾暚璇锋湡寰?/text>
</view>
<!-- 加载更多提示 -->
<!-- 鍔犺浇鏇村鎻愮ず -->
<view v-if="hasMore" class="load-more">
<text class="load-text">上拉加载更多</text>
<text class="load-text">涓婃媺鍔犺浇鏇村</text>
</view>
</scroll-view>
</view>
@@ -109,38 +109,38 @@ type LocalCategory = {
color: string
}
// 响应式数据
// 鍝嶅簲寮忔暟鎹?
const statusBarHeight = ref(0)
const headerHeight = ref(44) // 默认头部高度
const headerHeight = ref(44) // 榛樿澶撮儴楂樺害
const primaryCategories = ref<LocalCategory[]>([])
const productList = ref<Product[]>([])
const activePrimary = ref<string>('')
const cartCount = ref(3)
const hasMore = ref(true)
const hasLoadedFromParams = ref(false) // 标记是否已通过参数加载
const hasLoadedFromParams = ref(false) // 鏍囪鏄惁宸查€氳繃鍙傛暟鍔犺浇
const currentPage = ref(1)
const loading = ref(false)
// 获取当前分类信息
// 鑾峰彇褰撳墠鍒嗙被淇℃伅
const currentCategoryName = ref('')
const currentCategoryDesc = ref('')
// 页面参数
// 椤甸潰鍙傛暟
const pageParams = ref<any>({})
// 加载商品数据
// 鍔犺浇鍟嗗搧鏁版嵁
async function loadProducts(): Promise<void> {
if (loading.value) return
if (activePrimary.value == '') {
console.warn('activePrimary为空,无法加载商品')
console.warn('activePrimary涓虹┖锛屾棤娉曞姞杞藉晢鍝?)
return
}
loading.value = true
try {
console.log('开始加载商品,分类ID:', activePrimary.value, '页码:', currentPage.value)
console.log('寮€濮嬪姞杞藉晢鍝侊紝鍒嗙被ID:', activePrimary.value, '椤电爜:', currentPage.value)
const response = await supabaseService.getProductsByCategory(activePrimary.value, currentPage.value)
console.log('商品加载结果:', {
console.log('鍟嗗搧鍔犺浇缁撴灉:', {
dataCount: response.data.length,
total: response.total,
hasmore: response.hasmore,
@@ -155,16 +155,16 @@ async function loadProducts(): Promise<void> {
hasMore.value = response.hasmore
// 更新当前分类信息
// 鏇存柊褰撳墠鍒嗙被淇℃伅
const category = primaryCategories.value.find((cat: LocalCategory): boolean => cat.id === activePrimary.value)
if (category != null) {
currentCategoryName.value = category.name
currentCategoryDesc.value = category.description
}
console.log('商品列表加载完成,当前总数量:', productList.value.length)
console.log('鍟嗗搧鍒楄〃鍔犺浇瀹屾垚锛屽綋鍓嶆€绘暟閲?', productList.value.length)
} catch (error) {
console.error('加载商品数据失败:', error)
console.error('鍔犺浇鍟嗗搧鏁版嵁澶辫触:', error)
if (currentPage.value === 1) {
productList.value = []
}
@@ -176,22 +176,22 @@ async function loadProducts(): Promise<void> {
async function loadCategories(): Promise<void> {
try {
const categoriesData = await supabaseService.getCategories()
console.log('加载分类数据成功,数量:', categoriesData.length)
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('健康')) {
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 icon = catObj.getString('icon') ?? catObj.getString('icon_url') ?? '馃摝'
const color = catObj.getString('color') ?? '#4CAF50'
categories.push({
id,
@@ -204,36 +204,36 @@ async function loadCategories(): Promise<void> {
if (categories.length > 0) {
primaryCategories.value = categories
// 如果没有通过参数设置分类,则设置默认选中一个分类
// 濡傛灉娌℃湁閫氳繃鍙傛暟璁剧疆鍒嗙被锛屽垯璁剧疆榛樿閫変腑涓€涓垎绫?
if (activePrimary.value == '') {
// 优先查找"厨具"相关的分类作为默认
const defaultCategory = categories.find((c: LocalCategory): boolean => c.name.includes('厨具')) ?? categories[0]
// 浼樺厛鏌ユ壘"鍘ㄥ叿"鐩稿叧鐨勫垎绫讳綔涓洪粯璁?
const defaultCategory = categories.find((c: LocalCategory): boolean => c.name.includes('鍘ㄥ叿')) ?? categories[0]
activePrimary.value = defaultCategory.id
console.log('设置默认分类为:', defaultCategory.name, 'ID:', defaultCategory.id)
console.log('璁剧疆榛樿鍒嗙被涓?', defaultCategory.name, 'ID:', defaultCategory.id)
currentCategoryName.value = defaultCategory.name
currentCategoryDesc.value = defaultCategory.description
} else {
// 如果已经选中了分类可能来自Storage更新显示信息
// 濡傛灉宸茬粡閫変腑浜嗗垎绫伙紙鍙兘鏉ヨ嚜Storage锛夛紝鏇存柊鏄剧ず淇℃伅
const current = categories.find((c: LocalCategory): boolean => c.id == activePrimary.value)
if (current != null) {
currentCategoryName.value = current.name
currentCategoryDesc.value = current.description
// 如果此时没有商品列表(且没有正在加载),可能需要加载
// 濡傛灉姝ゆ椂娌℃湁鍟嗗搧鍒楄〃锛堜笖娌℃湁姝e湪鍔犺浇锛夛紝鍙兘闇€瑕佸姞杞?
if (productList.value.length === 0 && !loading.value) {
loadProducts()
}
}
}
} else {
console.warn('从Supabase获取的分类数据为空')
console.warn('浠嶴upabase鑾峰彇鐨勫垎绫绘暟鎹负绌?)
}
} catch (error) {
console.error('加载分类数据失败:', error)
console.error('鍔犺浇鍒嗙被鏁版嵁澶辫触:', error)
}
}
// 加载更多
// 鍔犺浇鏇村
function loadMore(): void {
if (hasMore.value && !loading.value) {
currentPage.value++
@@ -241,72 +241,72 @@ function loadMore(): void {
}
}
// 选择一级分类
// 閫夋嫨涓€绾у垎绫?
async function selectPrimaryCategory(categoryId: string): Promise<void> {
console.log('=== selectPrimaryCategory函数开始执行 ===')
console.log('传入的categoryId:', categoryId)
console.log('当前时间:', Date.now())
console.log('=== selectPrimaryCategory鍑芥暟寮€濮嬫墽琛?===')
console.log('浼犲叆鐨刢ategoryId:', categoryId)
console.log('褰撳墠鏃堕棿:', Date.now())
// 验证categoryId是否有效
// 楠岃瘉categoryId鏄惁鏈夋晥
if (categoryId == '') {
console.error('categoryId为空,尝试使用第一个分类')
console.error('categoryId涓虹┖锛屽皾璇曚娇鐢ㄧ涓€涓垎绫?)
if (primaryCategories.value.length > 0) {
categoryId = primaryCategories.value[0].id
} else {
console.error('没有可用的分类')
console.error('娌℃湁鍙敤鐨勫垎绫?)
return
}
}
console.log('验证后的categoryId:', categoryId)
console.log('当前activePrimary的值:', activePrimary.value)
console.log('楠岃瘉鍚庣殑categoryId:', categoryId)
console.log('褰撳墠activePrimary鐨勫€?', activePrimary.value)
// 更新活动分类
// 鏇存柊娲诲姩鍒嗙被
activePrimary.value = categoryId
console.log('更新后的activePrimary:', activePrimary.value)
console.log('鏇存柊鍚庣殑activePrimary:', activePrimary.value)
// 更新当前分类信息
// 鏇存柊褰撳墠鍒嗙被淇℃伅
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)
console.log('鉁?鎵惧埌鍒嗙被:', category.name, '鎻忚堪:', category.description)
} else {
console.error('❌ 未找到分类ID:', categoryId, ',使用第一个分类')
// 如果找不到对应的分类,使用第一个分类
console.error('鉂?鏈壘鍒板垎绫籌D:', categoryId, '锛屼娇鐢ㄧ涓€涓垎绫?)
// 濡傛灉鎵句笉鍒板搴旂殑鍒嗙被锛屼娇鐢ㄧ涓€涓垎绫?
if (primaryCategories.value.length > 0) {
const firstCategory = primaryCategories.value[0]
currentCategoryName.value = firstCategory.name
currentCategoryDesc.value = firstCategory.description
activePrimary.value = firstCategory.id
categoryId = firstCategory.id
console.log('使用默认分类:', firstCategory.name)
console.log('浣跨敤榛樿鍒嗙被:', firstCategory.name)
}
}
console.log('准备加载商品数据...')
console.log('鍑嗗鍔犺浇鍟嗗搧鏁版嵁...')
// 重置分页并加载
// 閲嶇疆鍒嗛〉骞跺姞杞?
currentPage.value = 1
hasMore.value = true
await loadProducts()
console.log('✅ 加载商品数据成功')
console.log('分类:', categoryId)
console.log('商品数量:', productList.value.length)
console.log('商品列表:', productList.value)
console.log('鉁?鍔犺浇鍟嗗搧鏁版嵁鎴愬姛')
console.log('鍒嗙被:', categoryId)
console.log('鍟嗗搧鏁伴噺:', productList.value.length)
console.log('鍟嗗搧鍒楄〃:', productList.value)
// 验证数据是否已正确更新
console.log('数据更新验证:')
// 楠岃瘉鏁版嵁鏄惁宸叉纭洿鏂?
console.log('鏁版嵁鏇存柊楠岃瘉:')
console.log('activePrimary:', activePrimary.value)
console.log('currentCategoryName:', currentCategoryName.value)
console.log('currentCategoryDesc:', currentCategoryDesc.value)
console.log('productList长度:', productList.value.length)
console.log('productList闀垮害:', productList.value.length)
console.log('=== selectPrimaryCategory函数执行完成 ===')
console.log('=== selectPrimaryCategory鍑芥暟鎵ц瀹屾垚 ===')
}
// 生命周期
// 鐢熷懡鍛ㄦ湡
onMounted(() => {
loadCategories().then(() => {
setTimeout(() => {
@@ -317,160 +317,160 @@ onMounted(() => {
})
})
// 页面加载时处理参数 - 这是处理分类切换的主要入口
// 椤甸潰鍔犺浇鏃跺鐞嗗弬鏁?- 杩欐槸澶勭悊鍒嗙被鍒囨崲鐨勪富瑕佸叆鍙?
onLoad((options: any) => {
// 获取系统状态栏高度
// 鑾峰彇绯荤粺鐘舵€佹爮楂樺害
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight
console.log('=== category页面onLoad被调用 ===')
console.log('页面加载时间:', Date.now())
console.log('传入的options参数:', options)
console.log('当前活动分类:', activePrimary.value)
console.log('=== category椤甸潰onLoad琚皟鐢?===')
console.log('椤甸潰鍔犺浇鏃堕棿:', Date.now())
console.log('浼犲叆鐨刼ptions鍙傛暟:', options)
console.log('褰撳墠娲诲姩鍒嗙被:', activePrimary.value)
let categoryId = ''
let categoryName = ''
// 首先检查传入的options参数
// 棣栧厛妫€鏌ヤ紶鍏ョ殑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)
console.log('鉁?onLoad涓壘鍒板垎绫诲弬鏁?', categoryId, categoryName)
}
// 如果options中没有尝试从getCurrentPages()获取
// 濡傛灉options涓病鏈夛紝灏濊瘯浠巊etCurrentPages()鑾峰彇
if (categoryId == '') {
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
const rawPageOptions = currentPage.options ?? {}
console.log('从getCurrentPages()获取参数:', rawPageOptions)
console.log('浠巊etCurrentPages()鑾峰彇鍙傛暟:', 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)
console.log('鉁?浠巊etCurrentPages()鎵惧埌鍒嗙被鍙傛暟:', categoryId, categoryName)
}
}
}
// 如果有找到分类ID则选中对应的分类
// 濡傛灉鏈夋壘鍒板垎绫籌D锛屽垯閫変腑瀵瑰簲鐨勫垎绫?
if (categoryId != '') {
hasLoadedFromParams.value = true
console.log('✅ 准备选中分类:', categoryId)
console.log('分类名称:', categoryName ?? '未指定')
console.log('鉁?鍑嗗閫変腑鍒嗙被:', categoryId)
console.log('鍒嗙被鍚嶇О:', categoryName ?? '鏈寚瀹?)
// 检查是否需要更新分类
// 妫€鏌ユ槸鍚﹂渶瑕佹洿鏂板垎绫?
if (activePrimary.value !== categoryId) {
console.log('当前分类:', activePrimary.value, '与目标分类:', categoryId, '不同,需要更新')
console.log('准备调用selectPrimaryCategory函数...')
console.log('褰撳墠鍒嗙被:', activePrimary.value, '涓庣洰鏍囧垎绫?', categoryId, '涓嶅悓锛岄渶瑕佹洿鏂?)
console.log('鍑嗗璋冪敤selectPrimaryCategory鍑芥暟...')
selectPrimaryCategory(categoryId)
} else {
console.log('当前分类已经是目标分类,但可能用户想要刷新页面')
console.log('当前分类:', activePrimary.value, '目标分类:', categoryId)
// 即使分类相同,也重新加载数据,确保数据是最新的
// 添加一个小的延迟,确保页面完全显示后再更新数据
console.log('褰撳墠鍒嗙被宸茬粡鏄洰鏍囧垎绫伙紝浣嗗彲鑳界敤鎴锋兂瑕佸埛鏂伴〉闈?)
console.log('褰撳墠鍒嗙被:', activePrimary.value, '鐩爣鍒嗙被:', categoryId)
// 鍗充娇鍒嗙被鐩稿悓锛屼篃閲嶆柊鍔犺浇鏁版嵁锛岀‘淇濇暟鎹槸鏈€鏂扮殑
// 娣诲姞涓€涓皬鐨勫欢杩燂紝纭繚椤甸潰瀹屽叏鏄剧ず鍚庡啀鏇存柊鏁版嵁
setTimeout(() => {
selectPrimaryCategory(categoryId)
}, 100)
}
} else {
console.log('⚠️ onLoad中未找到分类参数,将使用从数据库加载的第一个分类')
// 不再使用硬编码的默认分类loadCategories 会设置第一个分类
console.log('鈿狅笍 onLoad涓湭鎵惧埌鍒嗙被鍙傛暟锛屽皢浣跨敤浠庢暟鎹簱鍔犺浇鐨勭涓€涓垎绫?)
// 涓嶅啀浣跨敤纭紪鐮佺殑榛樿鍒嗙被锛宭oadCategories 浼氳缃涓€涓垎绫?
}
console.log('=== category页面onLoad执行完成 ===')
console.log('=== category椤甸潰onLoad鎵ц瀹屾垚 ===')
})
// 页面显示时也检查参数,确保从其他页面返回时能正确显示
// 椤甸潰鏄剧ず鏃朵篃妫€鏌ュ弬鏁帮紝纭繚浠庡叾浠栭〉闈㈣繑鍥炴椂鑳芥纭樉绀?
onShow(() => {
console.log('=== category页面onShow被调用 ===')
console.log('页面显示时间:', Date.now())
console.log('当前活动分类:', activePrimary.value)
console.log('=== category椤甸潰onShow琚皟鐢?===')
console.log('椤甸潰鏄剧ず鏃堕棿:', Date.now())
console.log('褰撳墠娲诲姩鍒嗙被:', activePrimary.value)
// 1. 优先检查 Storage 中的参数 (由首页传入)
// 1. 浼樺厛妫€鏌?Storage 涓殑鍙傛暟 (鐢遍椤典紶鍏?
const storageCategoryId = (uni.getStorageSync('selectedCategory') as string) ?? ''
if (storageCategoryId !== '') {
console.log('onShow中找到Storage分类参数:', storageCategoryId)
console.log('鉁?onShow涓壘鍒癝torage鍒嗙被鍙傛暟:', storageCategoryId)
hasLoadedFromParams.value = true
// 清除Storage,防止下次误读
// 娓呴櫎Storage锛岄槻姝笅娆¤璇?
uni.removeStorageSync('selectedCategory')
if (activePrimary.value !== storageCategoryId) {
selectPrimaryCategory(storageCategoryId)
}
// 如果分类还没加载完这里设置了ID等loadCategories完成后会自动匹配信息
// 濡傛灉鍒嗙被杩樻病鍔犺浇瀹岋紝杩欓噷璁剧疆浜咺D锛岀瓑loadCategories瀹屾垚鍚庝細鑷姩鍖归厤淇℃伅
return
}
// 在onShow中,我们也需要检查是否有新的参数
// 因为当从主页再次点击分类跳转过来时,可能不会触发onLoad
// 而是触发onShow
// 鍦╫nShow涓紝鎴戜滑涔熼渶瑕佹鏌ユ槸鍚︽湁鏂扮殑鍙傛暟
// 鍥犱负褰撲粠涓婚〉鍐嶆鐐瑰嚮鍒嗙被璺宠浆杩囨潵鏃讹紝鍙兘涓嶄細瑙﹀彂onLoad
// 鑰屾槸瑙﹀彂onShow
// 获取当前页面实例和参数
// 鑾峰彇褰撳墠椤甸潰瀹炰緥鍜屽弬鏁?
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
const rawPageOptions = currentPage.options ?? {}
console.log('onShow中获取参数:', rawPageOptions)
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') ?? '')
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函数...')
console.log('褰撳墠鍒嗙被:', activePrimary.value, '涓庣洰鏍囧垎绫?', categoryId, '涓嶅悓锛岄渶瑕佹洿鏂?)
console.log('鍑嗗璋冪敤selectPrimaryCategory鍑芥暟...')
selectPrimaryCategory(categoryId)
} else {
console.log('当前分类已经是目标分类,但可能用户想要刷新页面')
console.log('当前分类:', activePrimary.value, '目标分类:', categoryId)
// 即使分类相同,也重新加载数据,确保数据是最新的
// 添加一个小的延迟,确保页面完全显示后再更新数据
console.log('褰撳墠鍒嗙被宸茬粡鏄洰鏍囧垎绫伙紝浣嗗彲鑳界敤鎴锋兂瑕佸埛鏂伴〉闈?)
console.log('褰撳墠鍒嗙被:', activePrimary.value, '鐩爣鍒嗙被:', categoryId)
// 鍗充娇鍒嗙被鐩稿悓锛屼篃閲嶆柊鍔犺浇鏁版嵁锛岀‘淇濇暟鎹槸鏈€鏂扮殑
// 娣诲姞涓€涓皬鐨勫欢杩燂紝纭繚椤甸潰瀹屽叏鏄剧ず鍚庡啀鏇存柊鏁版嵁
setTimeout(() => {
selectPrimaryCategory(categoryId)
}, 100)
}
} else {
console.log('⚠️ onShow中未找到分类参数')
console.log('鈿狅笍 onShow涓湭鎵惧埌鍒嗙被鍙傛暟')
}
}
console.log('=== category页面onShow执行完成 ===')
console.log('=== category椤甸潰onShow鎵ц瀹屾垚 ===')
})
// 添加到购物车
// 娣诲姞鍒拌喘鐗╄溅
async function addToCart(product: Product): Promise<void> {
uni.showLoading({ title: '检查商品...' })
uni.showLoading({ title: '妫€鏌ュ晢鍝?..' })
try {
const pid = (product.id ?? '').toString()
const merchantId = product.merchant_id ?? ''
if (pid === '') {
uni.hideLoading()
uni.showToast({ title: '商品无效', icon: 'none' })
uni.showToast({ title: '鍟嗗搧鏃犳晥', icon: 'none' })
return
}
// 检查商品是否有SKU
// 妫€鏌ュ晢鍝佹槸鍚︽湁SKU
const skus = await supabaseService.getProductSkus(pid)
uni.hideLoading()
if (skus.length > 0) {
// 有规格,提示并跳转到商品详情页选择规格
// 鏈夎鏍硷紝鎻愮ず骞惰烦杞埌鍟嗗搧璇︽儏椤甸€夋嫨瑙勬牸
uni.showToast({
title: '请选择规格',
title: '璇烽€夋嫨瑙勬牸',
icon: 'none'
})
setTimeout(() => {
@@ -479,31 +479,31 @@ async function addToCart(product: Product): Promise<void> {
})
}, 500)
} else {
// 无规格,直接加入购物车
uni.showLoading({ title: '添加中...' })
// 鏃犺鏍硷紝鐩存帴鍔犲叆璐墿杞?
uni.showLoading({ title: '娣诲姞涓?..' })
const success = await supabaseService.addToCart(pid, 1, '', merchantId)
uni.hideLoading()
if (success) {
uni.showToast({
title: '已添加到购物车',
title: '宸叉坊鍔犲埌璐墿杞?,
icon: 'success'
})
cartCount.value++
} else {
uni.showToast({
title: '添加失败,请先登录',
title: '娣诲姞澶辫触锛岃鍏堢櫥褰?,
icon: 'none'
})
}
}
} catch (e) {
console.error('添加到购物车异常', e)
console.error('娣诲姞鍒拌喘鐗╄溅寮傚父', e)
uni.hideLoading()
uni.showToast({ title: '操作失败', icon: 'none' })
uni.showToast({ title: '鎿嶄綔澶辫触', icon: 'none' })
}
}
// 导航函数
// 瀵艰埅鍑芥暟
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 {
@@ -519,43 +519,43 @@ function navigateToProduct(product: Product): void {
})
}
// 相机功能
// 鐩告満鍔熻兘
function onCamera(): void {
uni.chooseImage({
count: 1,
sourceType: ['camera'],
success: (res) => {
console.log('相机拍摄成功:', res.tempFilePaths[0])
console.log('鐩告満鎷嶆憚鎴愬姛:', res.tempFilePaths[0])
uni.showToast({
title: '已拍摄,正在识别...',
title: '宸叉媿鎽勶紝姝e湪璇嗗埆...',
icon: 'loading'
})
// 这里可以添加后续的识别逻辑
// 杩欓噷鍙互娣诲姞鍚庣画鐨勮瘑鍒€昏緫
setTimeout(() => {
uni.showToast({
title: '识别成功',
title: '璇嗗埆鎴愬姛',
icon: 'success'
})
}, 1000)
},
fail: (err) => {
console.error('相机调用失败:', err)
console.error('鐩告満璋冪敤澶辫触:', err)
}
})
}
// 扫码功能
// 鎵爜鍔熻兘
function onScan(): void {
uni.scanCode({
success: (res) => {
console.log('扫码成功:', res)
console.log('鎵爜鎴愬姛:', res)
uni.showToast({
title: '扫码成功: ' + res.result,
title: '鎵爜鎴愬姛: ' + res.result,
icon: 'none'
})
},
fail: (err) => {
console.error('扫码失败:', err)
console.error('鎵爜澶辫触:', err)
}
})
}
@@ -571,7 +571,7 @@ function onScan(): void {
flex-direction: column;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif;
}
/* 搜索栏 */
/* 鎼滅储鏍?*/
.search-bar {
position: fixed;
top: 0;
@@ -582,13 +582,13 @@ function onScan(): void {
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
}
/* 导航栏占位 */
/* 瀵艰埅鏍忓崰浣?*/
.navbar-placeholder {
flex-shrink: 0;
}
/* 搜索栏 */
/* 导航栏搜索框容器内边距调整 */
/* 鎼滅储鏍?*/
/* 瀵艰埅鏍忔悳绱㈡瀹瑰櫒鍐呰竟璺濊皟鏁?*/
.search-container {
height: 44px;
padding: 0 16px;
@@ -601,12 +601,12 @@ function onScan(): void {
width: 100%;
}
/* 搜索框 hover 效果 */
/* 鎼滅储妗?hover 鏁堟灉 */
.search-box:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 导航栏搜索框容器内边距调整 */
/* 瀵艰埅鏍忔悳绱㈡瀹瑰櫒鍐呰竟璺濊皟鏁?*/
.search-box {
flex: 1;
max-width: 600px;
@@ -631,7 +631,7 @@ function onScan(): void {
}
.nav-inner-search-text {
font-size: 12px; /* 字体稍微变小 */
font-size: 12px; /* 瀛椾綋绋嶅井鍙樺皬 */
color: #ffffff;
font-weight: normal;
}
@@ -664,7 +664,7 @@ function onScan(): void {
border-right-width: 1px;
border-right-style: solid;
border-right-color: #ddd;
border-right: 1px solid #ddd; /* 修复UVUE样式 */
border-right: 1px solid #ddd; /* 淇UVUE鏍峰紡 */
margin-right: 8px;
}
@@ -672,23 +672,23 @@ function onScan(): void {
font-size: 20px;
}
/* 搜索按钮高度微调 */
/* 鎼滅储鎸夐挳楂樺害寰皟 */
.nav-inner-search-btn {
padding: 0 12px; /* 减小内边距 */
background-color: #87CEEB; /* 天空蓝 */
padding: 0 12px; /* 鍑忓皬鍐呰竟璺?*/
background-color: #87CEEB; /* 澶╃┖钃?*/
border-radius: 16px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
height: 24px; /* 随搜索框高度减小而减小 */
height: 24px; /* 闅忔悳绱㈡楂樺害鍑忓皬鑰屽噺灏?*/
}
.cart-badge {
position: absolute;
top: -5px;
right: -5px;
background: #FF5722;
background: #ff5000;
color: white;
font-size: 10px;
min-width: 18px;
@@ -701,7 +701,7 @@ function onScan(): void {
border: 2px solid #4CAF50;
}
/* 分类内容区 */
/* 鍒嗙被鍐呭鍖?*/
.category-content {
flex: 1;
display: flex;
@@ -714,7 +714,7 @@ function onScan(): void {
overflow: hidden;
}
/* 左侧一级分类 */
/* 宸︿晶涓€绾у垎绫?*/
.primary-category {
width: 100px;
flex: 0 0 100px;
@@ -741,7 +741,7 @@ function onScan(): void {
}
.primary-item:hover {
transform: translateY(-2px); /* 悬停时向上浮动 */
transform: translateY(-2px); /* 鎮仠鏃跺悜涓婃诞鍔?*/
}
.primary-item.active {
@@ -752,7 +752,7 @@ function onScan(): void {
.primary-icon {
font-size: 24px;
margin-bottom: 6px;
margin-right: 0; /* 移除右边距 */
margin-right: 0; /* 绉婚櫎鍙宠竟璺?*/
text-align: center;
/* display: block; removed for uniapp-x support */
}
@@ -763,7 +763,7 @@ function onScan(): void {
/* display: block; removed for uniapp-x support */
}
/* 右侧内容区 */
/* 鍙充晶鍐呭鍖?*/
.product-content {
flex: 1;
padding: 0;
@@ -790,7 +790,7 @@ function onScan(): void {
color: #666;
}
/* 商品网格 */
/* 鍟嗗搧缃戞牸 */
.product-grid {
display: flex;
flex-direction: row;
@@ -896,7 +896,7 @@ function onScan(): void {
margin-right: 6px; /* gap replacement */
}
/* 空状态 */
/* 绌虹姸鎬?*/
.empty-state {
display: flex;
flex-direction: column;
@@ -924,7 +924,7 @@ function onScan(): void {
color: #666;
}
/* 加载更多 */
/* 鍔犺浇鏇村 */
.load-more {
text-align: center;
padding: 20px 0;
@@ -939,9 +939,9 @@ function onScan(): void {
border-radius: 20px;
}
/* ===== 响应式设计 ===== */
/* ===== 鍝嶅簲寮忚璁?===== */
/* 小屏手机 (小于414px) */
/* 灏忓睆鎵嬫満 (灏忎簬414px) */
@media screen and (max-width: 414px) {
.search-container {
padding: 0 12px;
@@ -1051,7 +1051,7 @@ function onScan(): void {
}
}
/* 中屏手机/小平板 (415px-768px) */
/* 涓睆鎵嬫満/灏忓钩鏉?(415px-768px) */
@media screen and (min-width: 415px) and (max-width: 768px) {
.search-container {
padding: 0 16px;
@@ -1075,7 +1075,7 @@ function onScan(): void {
}
}
/* 平板设备 (769px-1024px) */
/* 骞虫澘璁惧 (769px-1024px) */
@media screen and (min-width: 769px) and (max-width: 1024px) {
.search-container {
padding: 0 16px;
@@ -1109,7 +1109,7 @@ function onScan(): void {
}
}
/* 桌面端 (1025px以上) */
/* 妗岄潰绔?(1025px浠ヤ笂) */
@media screen and (min-width: 1025px) {
.search-container {
padding: 0 16px;
@@ -1188,7 +1188,7 @@ function onScan(): void {
}
}
/* 大桌面端 (1400px以上) */
/* 澶ф闈㈢ (1400px浠ヤ笂) */
@media screen and (min-width: 1400px) {
.category-content {
max-width: 1600px;
@@ -1267,3 +1267,5 @@ function onScan(): void {
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +1,36 @@
<template>
<template>
<view class="category-page">
<!-- 顶部搜索栏 -->
<!-- 椤堕儴鎼滅储鏍?-->
<view class="search-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="search-container">
<view class="search-box" @click="navigateToSearch" :style="{ height: '30px' }">
<!-- 模拟输入框 -->
<text class="search-placeholder">请输入药品名称、症状或品牌</text>
<!-- 妯℃嫙杈撳叆妗?-->
<text class="search-placeholder">璇疯緭鍏ヨ嵂鍝佸悕绉般€佺棁鐘舵垨鍝佺墝</text>
<!-- 扫码图标 -->
<!-- 鎵爜鍥炬爣 -->
<view class="nav-icon-btn" @click.stop="onScan">
<text class="nav-icon">🔳</text>
<text class="nav-icon">馃敵</text>
</view>
<!-- 相机图标 -->
<!-- 鐩告満鍥炬爣 -->
<view class="nav-camera-btn" @click.stop="onCamera">
<text class="nav-camera-icon">📷</text>
<text class="nav-camera-icon">馃摲</text>
</view>
<!-- 搜索按钮 -->
<!-- 鎼滅储鎸夐挳 -->
<view class="nav-inner-search-btn" :style="{ height: '22px' }">
<text class="nav-inner-search-text">搜索</text>
<text class="nav-inner-search-text">鎼滅储</text>
</view>
</view>
</view>
</view>
<!-- 导航栏占位 - 需要包含statusBarHeight + 搜索框高度44px -->
<!-- 瀵艰埅鏍忓崰浣?- 闇€瑕佸寘鍚玸tatusBarHeight + 鎼滅储妗嗛珮搴?4px -->
<view class="navbar-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 分类内容区 -->
<!-- 鍒嗙被鍐呭鍖?-->
<view class="category-content">
<!-- 左侧一级分类 -->
<!-- 宸︿晶涓€绾у垎绫?-->
<scroll-view scroll-y class="primary-category">
<view
v-for="item in primaryCategories"
@@ -44,20 +44,20 @@
</view>
</scroll-view>
<!-- 右侧商品列表 -->
<!-- 鍙充晶鍟嗗搧鍒楄〃 -->
<scroll-view
scroll-y
class="product-content"
@scrolltolower="loadMore"
:lower-threshold="50"
>
<!-- 分类标题 -->
<!-- 鍒嗙被鏍囬 -->
<view class="category-header">
<text class="category-title">{{ currentCategoryName }}</text>
<text class="category-desc">{{ currentCategoryDesc }}</text>
</view>
<!-- 商品网格 -->
<!-- 鍟嗗搧缃戞牸 -->
<view v-if="productList.length > 0" class="product-grid">
<view
v-for="product in productList"
@@ -72,7 +72,7 @@
/>
<text class="product-name" :lines="2">{{ product.name }}</text>
<view class="product-bottom">
<text class="product-price">¥{{ product.base_price ?? product.price ?? 0 }}</text>
<text class="product-price">{{ product.base_price ?? product.price ?? 0 }}</text>
<view class="product-add-btn" @click.stop="addToCart(product)">
<text class="add-icon">+</text>
</view>
@@ -80,16 +80,16 @@
</view>
</view>
<!-- 空状态 -->
<!-- 绌虹姸鎬?-->
<view v-else class="empty-state">
<text class="empty-icon">💊</text>
<text class="empty-text">暂无相关药品</text>
<text class="empty-desc">该分类下暂无商品,敬请期待</text>
<text class="empty-icon">馃拪</text>
<text class="empty-text">鏆傛棤鐩稿叧鑽搧</text>
<text class="empty-desc">璇ュ垎绫讳笅鏆傛棤鍟嗗搧锛屾暚璇锋湡寰?/text>
</view>
<!-- 加载更多提示 -->
<!-- 鍔犺浇鏇村鎻愮ず -->
<view v-if="hasMore" class="load-more">
<text class="load-text">上拉加载更多</text>
<text class="load-text">涓婃媺鍔犺浇鏇村</text>
</view>
</scroll-view>
</view>
@@ -109,38 +109,38 @@ type LocalCategory = {
color: string
}
// 响应式数据
// 鍝嶅簲寮忔暟鎹?
const statusBarHeight = ref(0)
const headerHeight = ref(44) // 默认头部高度
const headerHeight = ref(44) // 榛樿澶撮儴楂樺害
const primaryCategories = ref<LocalCategory[]>([])
const productList = ref<Product[]>([])
const activePrimary = ref<string>('')
const cartCount = ref(3)
const hasMore = ref(true)
const hasLoadedFromParams = ref(false) // 标记是否已通过参数加载
const hasLoadedFromParams = ref(false) // 鏍囪鏄惁宸查€氳繃鍙傛暟鍔犺浇
const currentPage = ref(1)
const loading = ref(false)
// 获取当前分类信息
// 鑾峰彇褰撳墠鍒嗙被淇℃伅
const currentCategoryName = ref('')
const currentCategoryDesc = ref('')
// 页面参数
// 椤甸潰鍙傛暟
const pageParams = ref<any>({})
// 加载商品数据
// 鍔犺浇鍟嗗搧鏁版嵁
async function loadProducts(): Promise<void> {
if (loading.value) return
if (activePrimary.value == '') {
console.warn('activePrimary为空,无法加载商品')
console.warn('activePrimary涓虹┖锛屾棤娉曞姞杞藉晢鍝?)
return
}
loading.value = true
try {
console.log('开始加载商品,分类ID:', activePrimary.value, '页码:', currentPage.value)
console.log('寮€濮嬪姞杞藉晢鍝侊紝鍒嗙被ID:', activePrimary.value, '椤电爜:', currentPage.value)
const response = await supabaseService.getProductsByCategory(activePrimary.value, currentPage.value)
console.log('商品加载结果:', {
console.log('鍟嗗搧鍔犺浇缁撴灉:', {
dataCount: response.data.length,
total: response.total,
hasmore: response.hasmore,
@@ -155,16 +155,16 @@ async function loadProducts(): Promise<void> {
hasMore.value = response.hasmore
// 更新当前分类信息
// 鏇存柊褰撳墠鍒嗙被淇℃伅
const category = primaryCategories.value.find((cat: LocalCategory): boolean => cat.id === activePrimary.value)
if (category != null) {
currentCategoryName.value = category.name
currentCategoryDesc.value = category.description
}
console.log('商品列表加载完成,当前总数量:', productList.value.length)
console.log('鍟嗗搧鍒楄〃鍔犺浇瀹屾垚锛屽綋鍓嶆€绘暟閲?', productList.value.length)
} catch (error) {
console.error('加载商品数据失败:', error)
console.error('鍔犺浇鍟嗗搧鏁版嵁澶辫触:', error)
if (currentPage.value === 1) {
productList.value = []
}
@@ -176,22 +176,22 @@ async function loadProducts(): Promise<void> {
async function loadCategories(): Promise<void> {
try {
const categoriesData = await supabaseService.getCategories()
console.log('加载分类数据成功,数量:', categoriesData.length)
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('健康')) {
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 icon = catObj.getString('icon') ?? catObj.getString('icon_url') ?? '馃摝'
const color = catObj.getString('color') ?? '#4CAF50'
categories.push({
id,
@@ -204,36 +204,36 @@ async function loadCategories(): Promise<void> {
if (categories.length > 0) {
primaryCategories.value = categories
// 如果没有通过参数设置分类,则设置默认选中一个分类
// 濡傛灉娌℃湁閫氳繃鍙傛暟璁剧疆鍒嗙被锛屽垯璁剧疆榛樿閫変腑涓€涓垎绫?
if (activePrimary.value == '') {
// 优先查找"厨具"相关的分类作为默认
const defaultCategory = categories.find((c: LocalCategory): boolean => c.name.includes('厨具')) ?? categories[0]
// 浼樺厛鏌ユ壘"鍘ㄥ叿"鐩稿叧鐨勫垎绫讳綔涓洪粯璁?
const defaultCategory = categories.find((c: LocalCategory): boolean => c.name.includes('鍘ㄥ叿')) ?? categories[0]
activePrimary.value = defaultCategory.id
console.log('设置默认分类为:', defaultCategory.name, 'ID:', defaultCategory.id)
console.log('璁剧疆榛樿鍒嗙被涓?', defaultCategory.name, 'ID:', defaultCategory.id)
currentCategoryName.value = defaultCategory.name
currentCategoryDesc.value = defaultCategory.description
} else {
// 如果已经选中了分类可能来自Storage更新显示信息
// 濡傛灉宸茬粡閫変腑浜嗗垎绫伙紙鍙兘鏉ヨ嚜Storage锛夛紝鏇存柊鏄剧ず淇℃伅
const current = categories.find((c: LocalCategory): boolean => c.id == activePrimary.value)
if (current != null) {
currentCategoryName.value = current.name
currentCategoryDesc.value = current.description
// 如果此时没有商品列表(且没有正在加载),可能需要加载
// 濡傛灉姝ゆ椂娌℃湁鍟嗗搧鍒楄〃锛堜笖娌℃湁姝e湪鍔犺浇锛夛紝鍙兘闇€瑕佸姞杞?
if (productList.value.length === 0 && !loading.value) {
loadProducts()
}
}
}
} else {
console.warn('从Supabase获取的分类数据为空')
console.warn('浠嶴upabase鑾峰彇鐨勫垎绫绘暟鎹负绌?)
}
} catch (error) {
console.error('加载分类数据失败:', error)
console.error('鍔犺浇鍒嗙被鏁版嵁澶辫触:', error)
}
}
// 加载更多
// 鍔犺浇鏇村
function loadMore(): void {
if (hasMore.value && !loading.value) {
currentPage.value++
@@ -241,72 +241,72 @@ function loadMore(): void {
}
}
// 选择一级分类
// 閫夋嫨涓€绾у垎绫?
async function selectPrimaryCategory(categoryId: string): Promise<void> {
console.log('=== selectPrimaryCategory函数开始执行 ===')
console.log('传入的categoryId:', categoryId)
console.log('当前时间:', Date.now())
console.log('=== selectPrimaryCategory鍑芥暟寮€濮嬫墽琛?===')
console.log('浼犲叆鐨刢ategoryId:', categoryId)
console.log('褰撳墠鏃堕棿:', Date.now())
// 验证categoryId是否有效
// 楠岃瘉categoryId鏄惁鏈夋晥
if (categoryId == '') {
console.error('categoryId为空,尝试使用第一个分类')
console.error('categoryId涓虹┖锛屽皾璇曚娇鐢ㄧ涓€涓垎绫?)
if (primaryCategories.value.length > 0) {
categoryId = primaryCategories.value[0].id
} else {
console.error('没有可用的分类')
console.error('娌℃湁鍙敤鐨勫垎绫?)
return
}
}
console.log('验证后的categoryId:', categoryId)
console.log('当前activePrimary的值:', activePrimary.value)
console.log('楠岃瘉鍚庣殑categoryId:', categoryId)
console.log('褰撳墠activePrimary鐨勫€?', activePrimary.value)
// 更新活动分类
// 鏇存柊娲诲姩鍒嗙被
activePrimary.value = categoryId
console.log('更新后的activePrimary:', activePrimary.value)
console.log('鏇存柊鍚庣殑activePrimary:', activePrimary.value)
// 更新当前分类信息
// 鏇存柊褰撳墠鍒嗙被淇℃伅
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)
console.log('鉁?鎵惧埌鍒嗙被:', category.name, '鎻忚堪:', category.description)
} else {
console.error('❌ 未找到分类ID:', categoryId, ',使用第一个分类')
// 如果找不到对应的分类,使用第一个分类
console.error('鉂?鏈壘鍒板垎绫籌D:', categoryId, '锛屼娇鐢ㄧ涓€涓垎绫?)
// 濡傛灉鎵句笉鍒板搴旂殑鍒嗙被锛屼娇鐢ㄧ涓€涓垎绫?
if (primaryCategories.value.length > 0) {
const firstCategory = primaryCategories.value[0]
currentCategoryName.value = firstCategory.name
currentCategoryDesc.value = firstCategory.description
activePrimary.value = firstCategory.id
categoryId = firstCategory.id
console.log('使用默认分类:', firstCategory.name)
console.log('浣跨敤榛樿鍒嗙被:', firstCategory.name)
}
}
console.log('准备加载商品数据...')
console.log('鍑嗗鍔犺浇鍟嗗搧鏁版嵁...')
// 重置分页并加载
// 閲嶇疆鍒嗛〉骞跺姞杞?
currentPage.value = 1
hasMore.value = true
await loadProducts()
console.log('✅ 加载商品数据成功')
console.log('分类:', categoryId)
console.log('商品数量:', productList.value.length)
console.log('商品列表:', productList.value)
console.log('鉁?鍔犺浇鍟嗗搧鏁版嵁鎴愬姛')
console.log('鍒嗙被:', categoryId)
console.log('鍟嗗搧鏁伴噺:', productList.value.length)
console.log('鍟嗗搧鍒楄〃:', productList.value)
// 验证数据是否已正确更新
console.log('数据更新验证:')
// 楠岃瘉鏁版嵁鏄惁宸叉纭洿鏂?
console.log('鏁版嵁鏇存柊楠岃瘉:')
console.log('activePrimary:', activePrimary.value)
console.log('currentCategoryName:', currentCategoryName.value)
console.log('currentCategoryDesc:', currentCategoryDesc.value)
console.log('productList长度:', productList.value.length)
console.log('productList闀垮害:', productList.value.length)
console.log('=== selectPrimaryCategory函数执行完成 ===')
console.log('=== selectPrimaryCategory鍑芥暟鎵ц瀹屾垚 ===')
}
// 生命周期
// 鐢熷懡鍛ㄦ湡
onMounted(() => {
loadCategories().then(() => {
setTimeout(() => {
@@ -317,160 +317,160 @@ onMounted(() => {
})
})
// 页面加载时处理参数 - 这是处理分类切换的主要入口
// 椤甸潰鍔犺浇鏃跺鐞嗗弬鏁?- 杩欐槸澶勭悊鍒嗙被鍒囨崲鐨勪富瑕佸叆鍙?
onLoad((options: any) => {
// 获取系统状态栏高度
// 鑾峰彇绯荤粺鐘舵€佹爮楂樺害
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight
console.log('=== category页面onLoad被调用 ===')
console.log('页面加载时间:', Date.now())
console.log('传入的options参数:', options)
console.log('当前活动分类:', activePrimary.value)
console.log('=== category椤甸潰onLoad琚皟鐢?===')
console.log('椤甸潰鍔犺浇鏃堕棿:', Date.now())
console.log('浼犲叆鐨刼ptions鍙傛暟:', options)
console.log('褰撳墠娲诲姩鍒嗙被:', activePrimary.value)
let categoryId = ''
let categoryName = ''
// 首先检查传入的options参数
// 棣栧厛妫€鏌ヤ紶鍏ョ殑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)
console.log('鉁?onLoad涓壘鍒板垎绫诲弬鏁?', categoryId, categoryName)
}
// 如果options中没有尝试从getCurrentPages()获取
// 濡傛灉options涓病鏈夛紝灏濊瘯浠巊etCurrentPages()鑾峰彇
if (categoryId == '') {
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
const rawPageOptions = currentPage.options ?? {}
console.log('从getCurrentPages()获取参数:', rawPageOptions)
console.log('浠巊etCurrentPages()鑾峰彇鍙傛暟:', 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)
console.log('鉁?浠巊etCurrentPages()鎵惧埌鍒嗙被鍙傛暟:', categoryId, categoryName)
}
}
}
// 如果有找到分类ID则选中对应的分类
// 濡傛灉鏈夋壘鍒板垎绫籌D锛屽垯閫変腑瀵瑰簲鐨勫垎绫?
if (categoryId != '') {
hasLoadedFromParams.value = true
console.log('✅ 准备选中分类:', categoryId)
console.log('分类名称:', categoryName ?? '未指定')
console.log('鉁?鍑嗗閫変腑鍒嗙被:', categoryId)
console.log('鍒嗙被鍚嶇О:', categoryName ?? '鏈寚瀹?)
// 检查是否需要更新分类
// 妫€鏌ユ槸鍚﹂渶瑕佹洿鏂板垎绫?
if (activePrimary.value !== categoryId) {
console.log('当前分类:', activePrimary.value, '与目标分类:', categoryId, '不同,需要更新')
console.log('准备调用selectPrimaryCategory函数...')
console.log('褰撳墠鍒嗙被:', activePrimary.value, '涓庣洰鏍囧垎绫?', categoryId, '涓嶅悓锛岄渶瑕佹洿鏂?)
console.log('鍑嗗璋冪敤selectPrimaryCategory鍑芥暟...')
selectPrimaryCategory(categoryId)
} else {
console.log('当前分类已经是目标分类,但可能用户想要刷新页面')
console.log('当前分类:', activePrimary.value, '目标分类:', categoryId)
// 即使分类相同,也重新加载数据,确保数据是最新的
// 添加一个小的延迟,确保页面完全显示后再更新数据
console.log('褰撳墠鍒嗙被宸茬粡鏄洰鏍囧垎绫伙紝浣嗗彲鑳界敤鎴锋兂瑕佸埛鏂伴〉闈?)
console.log('褰撳墠鍒嗙被:', activePrimary.value, '鐩爣鍒嗙被:', categoryId)
// 鍗充娇鍒嗙被鐩稿悓锛屼篃閲嶆柊鍔犺浇鏁版嵁锛岀‘淇濇暟鎹槸鏈€鏂扮殑
// 娣诲姞涓€涓皬鐨勫欢杩燂紝纭繚椤甸潰瀹屽叏鏄剧ず鍚庡啀鏇存柊鏁版嵁
setTimeout(() => {
selectPrimaryCategory(categoryId)
}, 100)
}
} else {
console.log('⚠️ onLoad中未找到分类参数,将使用从数据库加载的第一个分类')
// 不再使用硬编码的默认分类loadCategories 会设置第一个分类
console.log('鈿狅笍 onLoad涓湭鎵惧埌鍒嗙被鍙傛暟锛屽皢浣跨敤浠庢暟鎹簱鍔犺浇鐨勭涓€涓垎绫?)
// 涓嶅啀浣跨敤纭紪鐮佺殑榛樿鍒嗙被锛宭oadCategories 浼氳缃涓€涓垎绫?
}
console.log('=== category页面onLoad执行完成 ===')
console.log('=== category椤甸潰onLoad鎵ц瀹屾垚 ===')
})
// 页面显示时也检查参数,确保从其他页面返回时能正确显示
// 椤甸潰鏄剧ず鏃朵篃妫€鏌ュ弬鏁帮紝纭繚浠庡叾浠栭〉闈㈣繑鍥炴椂鑳芥纭樉绀?
onShow(() => {
console.log('=== category页面onShow被调用 ===')
console.log('页面显示时间:', Date.now())
console.log('当前活动分类:', activePrimary.value)
console.log('=== category椤甸潰onShow琚皟鐢?===')
console.log('椤甸潰鏄剧ず鏃堕棿:', Date.now())
console.log('褰撳墠娲诲姩鍒嗙被:', activePrimary.value)
// 1. 优先检查 Storage 中的参数 (由首页传入)
// 1. 浼樺厛妫€鏌?Storage 涓殑鍙傛暟 (鐢遍椤典紶鍏?
const storageCategoryId = (uni.getStorageSync('selectedCategory') as string) ?? ''
if (storageCategoryId !== '') {
console.log('onShow中找到Storage分类参数:', storageCategoryId)
console.log('鉁?onShow涓壘鍒癝torage鍒嗙被鍙傛暟:', storageCategoryId)
hasLoadedFromParams.value = true
// 清除Storage,防止下次误读
// 娓呴櫎Storage锛岄槻姝笅娆¤璇?
uni.removeStorageSync('selectedCategory')
if (activePrimary.value !== storageCategoryId) {
selectPrimaryCategory(storageCategoryId)
}
// 如果分类还没加载完这里设置了ID等loadCategories完成后会自动匹配信息
// 濡傛灉鍒嗙被杩樻病鍔犺浇瀹岋紝杩欓噷璁剧疆浜咺D锛岀瓑loadCategories瀹屾垚鍚庝細鑷姩鍖归厤淇℃伅
return
}
// 在onShow中,我们也需要检查是否有新的参数
// 因为当从主页再次点击分类跳转过来时,可能不会触发onLoad
// 而是触发onShow
// 鍦╫nShow涓紝鎴戜滑涔熼渶瑕佹鏌ユ槸鍚︽湁鏂扮殑鍙傛暟
// 鍥犱负褰撲粠涓婚〉鍐嶆鐐瑰嚮鍒嗙被璺宠浆杩囨潵鏃讹紝鍙兘涓嶄細瑙﹀彂onLoad
// 鑰屾槸瑙﹀彂onShow
// 获取当前页面实例和参数
// 鑾峰彇褰撳墠椤甸潰瀹炰緥鍜屽弬鏁?
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
const rawPageOptions = currentPage.options ?? {}
console.log('onShow中获取参数:', rawPageOptions)
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') ?? '')
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函数...')
console.log('褰撳墠鍒嗙被:', activePrimary.value, '涓庣洰鏍囧垎绫?', categoryId, '涓嶅悓锛岄渶瑕佹洿鏂?)
console.log('鍑嗗璋冪敤selectPrimaryCategory鍑芥暟...')
selectPrimaryCategory(categoryId)
} else {
console.log('当前分类已经是目标分类,但可能用户想要刷新页面')
console.log('当前分类:', activePrimary.value, '目标分类:', categoryId)
// 即使分类相同,也重新加载数据,确保数据是最新的
// 添加一个小的延迟,确保页面完全显示后再更新数据
console.log('褰撳墠鍒嗙被宸茬粡鏄洰鏍囧垎绫伙紝浣嗗彲鑳界敤鎴锋兂瑕佸埛鏂伴〉闈?)
console.log('褰撳墠鍒嗙被:', activePrimary.value, '鐩爣鍒嗙被:', categoryId)
// 鍗充娇鍒嗙被鐩稿悓锛屼篃閲嶆柊鍔犺浇鏁版嵁锛岀‘淇濇暟鎹槸鏈€鏂扮殑
// 娣诲姞涓€涓皬鐨勫欢杩燂紝纭繚椤甸潰瀹屽叏鏄剧ず鍚庡啀鏇存柊鏁版嵁
setTimeout(() => {
selectPrimaryCategory(categoryId)
}, 100)
}
} else {
console.log('⚠️ onShow中未找到分类参数')
console.log('鈿狅笍 onShow涓湭鎵惧埌鍒嗙被鍙傛暟')
}
}
console.log('=== category页面onShow执行完成 ===')
console.log('=== category椤甸潰onShow鎵ц瀹屾垚 ===')
})
// 添加到购物车
// 娣诲姞鍒拌喘鐗╄溅
async function addToCart(product: Product): Promise<void> {
uni.showLoading({ title: '检查商品...' })
uni.showLoading({ title: '妫€鏌ュ晢鍝?..' })
try {
const pid = (product.id ?? '').toString()
const merchantId = product.merchant_id ?? ''
if (pid === '') {
uni.hideLoading()
uni.showToast({ title: '商品无效', icon: 'none' })
uni.showToast({ title: '鍟嗗搧鏃犳晥', icon: 'none' })
return
}
// 检查商品是否有SKU
// 妫€鏌ュ晢鍝佹槸鍚︽湁SKU
const skus = await supabaseService.getProductSkus(pid)
uni.hideLoading()
if (skus.length > 0) {
// 有规格,提示并跳转到商品详情页选择规格
// 鏈夎鏍硷紝鎻愮ず骞惰烦杞埌鍟嗗搧璇︽儏椤甸€夋嫨瑙勬牸
uni.showToast({
title: '请选择规格',
title: '璇烽€夋嫨瑙勬牸',
icon: 'none'
})
setTimeout(() => {
@@ -479,31 +479,31 @@ async function addToCart(product: Product): Promise<void> {
})
}, 500)
} else {
// 无规格,直接加入购物车
uni.showLoading({ title: '添加中...' })
// 鏃犺鏍硷紝鐩存帴鍔犲叆璐墿杞?
uni.showLoading({ title: '娣诲姞涓?..' })
const success = await supabaseService.addToCart(pid, 1, '', merchantId)
uni.hideLoading()
if (success) {
uni.showToast({
title: '已添加到购物车',
title: '宸叉坊鍔犲埌璐墿杞?,
icon: 'success'
})
cartCount.value++
} else {
uni.showToast({
title: '添加失败,请先登录',
title: '娣诲姞澶辫触锛岃鍏堢櫥褰?,
icon: 'none'
})
}
}
} catch (e) {
console.error('添加到购物车异常', e)
console.error('娣诲姞鍒拌喘鐗╄溅寮傚父', e)
uni.hideLoading()
uni.showToast({ title: '操作失败', icon: 'none' })
uni.showToast({ title: '鎿嶄綔澶辫触', icon: 'none' })
}
}
// 导航函数
// 瀵艰埅鍑芥暟
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 {
@@ -519,43 +519,43 @@ function navigateToProduct(product: Product): void {
})
}
// 相机功能
// 鐩告満鍔熻兘
function onCamera(): void {
uni.chooseImage({
count: 1,
sourceType: ['camera'],
success: (res) => {
console.log('相机拍摄成功:', res.tempFilePaths[0])
console.log('鐩告満鎷嶆憚鎴愬姛:', res.tempFilePaths[0])
uni.showToast({
title: '已拍摄,正在识别...',
title: '宸叉媿鎽勶紝姝e湪璇嗗埆...',
icon: 'loading'
})
// 这里可以添加后续的识别逻辑
// 杩欓噷鍙互娣诲姞鍚庣画鐨勮瘑鍒€昏緫
setTimeout(() => {
uni.showToast({
title: '识别成功',
title: '璇嗗埆鎴愬姛',
icon: 'success'
})
}, 1000)
},
fail: (err) => {
console.error('相机调用失败:', err)
console.error('鐩告満璋冪敤澶辫触:', err)
}
})
}
// 扫码功能
// 鎵爜鍔熻兘
function onScan(): void {
uni.scanCode({
success: (res) => {
console.log('扫码成功:', res)
console.log('鎵爜鎴愬姛:', res)
uni.showToast({
title: '扫码成功: ' + res.result,
title: '鎵爜鎴愬姛: ' + res.result,
icon: 'none'
})
},
fail: (err) => {
console.error('扫码失败:', err)
console.error('鎵爜澶辫触:', err)
}
})
}
@@ -571,7 +571,7 @@ function onScan(): void {
flex-direction: column;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif;
}
/* 搜索栏 */
/* 鎼滅储鏍?*/
.search-bar {
position: fixed;
top: 0;
@@ -582,13 +582,13 @@ function onScan(): void {
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
}
/* 导航栏占位 */
/* 瀵艰埅鏍忓崰浣?*/
.navbar-placeholder {
flex-shrink: 0;
}
/* 搜索栏 */
/* 导航栏搜索框容器内边距调整 */
/* 鎼滅储鏍?*/
/* 瀵艰埅鏍忔悳绱㈡瀹瑰櫒鍐呰竟璺濊皟鏁?*/
.search-container {
height: 44px;
padding: 0 16px;
@@ -601,12 +601,12 @@ function onScan(): void {
width: 100%;
}
/* 搜索框 hover 效果 */
/* 鎼滅储妗?hover 鏁堟灉 */
.search-box:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 导航栏搜索框容器内边距调整 */
/* 瀵艰埅鏍忔悳绱㈡瀹瑰櫒鍐呰竟璺濊皟鏁?*/
.search-box {
flex: 1;
max-width: 600px;
@@ -631,7 +631,7 @@ function onScan(): void {
}
.nav-inner-search-text {
font-size: 12px; /* 字体稍微变小 */
font-size: 12px; /* 瀛椾綋绋嶅井鍙樺皬 */
color: #ffffff;
font-weight: normal;
}
@@ -664,7 +664,7 @@ function onScan(): void {
border-right-width: 1px;
border-right-style: solid;
border-right-color: #ddd;
border-right: 1px solid #ddd; /* 修复UVUE样式 */
border-right: 1px solid #ddd; /* 淇UVUE鏍峰紡 */
margin-right: 8px;
}
@@ -672,23 +672,23 @@ function onScan(): void {
font-size: 20px;
}
/* 搜索按钮高度微调 */
/* 鎼滅储鎸夐挳楂樺害寰皟 */
.nav-inner-search-btn {
padding: 0 12px; /* 减小内边距 */
background-color: #87CEEB; /* 天空蓝 */
padding: 0 12px; /* 鍑忓皬鍐呰竟璺?*/
background-color: #87CEEB; /* 澶╃┖钃?*/
border-radius: 16px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
height: 24px; /* 随搜索框高度减小而减小 */
height: 24px; /* 闅忔悳绱㈡楂樺害鍑忓皬鑰屽噺灏?*/
}
.cart-badge {
position: absolute;
top: -5px;
right: -5px;
background: #FF5722;
background: #ff5000;
color: white;
font-size: 10px;
min-width: 18px;
@@ -701,7 +701,7 @@ function onScan(): void {
border: 2px solid #4CAF50;
}
/* 分类内容区 */
/* 鍒嗙被鍐呭鍖?*/
.category-content {
flex: 1;
height: 0px;
@@ -715,10 +715,10 @@ function onScan(): void {
overflow: hidden;
}
/* 左侧一级分类 */
/* 宸︿晶涓€绾у垎绫?*/
.primary-category {
width: 120px;
height: 100%; /* 占满父容器高度 */
height: 100%; /* 鍗犳弧鐖跺鍣ㄩ珮搴?*/
margin-right: 20px; /* gap replacement */
background: white;
border-radius: 12px;
@@ -729,7 +729,7 @@ function onScan(): void {
.primary-item {
display: flex;
flex-direction: column; /* 图标和文字垂直排列 */
flex-direction: column; /* 鍥炬爣鍜屾枃瀛楀瀭鐩存帓鍒?*/
align-items: center;
justify-content: center;
padding: 12px 8px;
@@ -742,7 +742,7 @@ function onScan(): void {
}
.primary-item:hover {
transform: translateY(-2px); /* 悬停时向上浮动 */
transform: translateY(-2px); /* 鎮仠鏃跺悜涓婃诞鍔?*/
}
.primary-item.active {
@@ -753,7 +753,7 @@ function onScan(): void {
.primary-icon {
font-size: 24px;
margin-bottom: 6px;
margin-right: 0; /* 移除右边距 */
margin-right: 0; /* 绉婚櫎鍙宠竟璺?*/
text-align: center;
/* display: block; removed for uniapp-x support */
}
@@ -764,11 +764,11 @@ function onScan(): void {
/* display: block; removed for uniapp-x support */
}
/* 右侧内容区 */
/* 鍙充晶鍐呭鍖?*/
.product-content {
flex: 1;
height: 100%; /* 占满父容器高度 */
padding: 0; /* 移除内边距,交给内部元素 */
height: 100%; /* 鍗犳弧鐖跺鍣ㄩ珮搴?*/
padding: 0; /* 绉婚櫎鍐呰竟璺濓紝浜ょ粰鍐呴儴鍏冪礌 */
}
.category-header {
@@ -792,7 +792,7 @@ function onScan(): void {
color: #666;
}
/* 商品网格 */
/* 鍟嗗搧缃戞牸 */
.product-grid {
display: flex;
flex-direction: row;
@@ -898,7 +898,7 @@ function onScan(): void {
margin-right: 6px; /* gap replacement */
}
/* 空状态 */
/* 绌虹姸鎬?*/
.empty-state {
display: flex;
flex-direction: column;
@@ -926,7 +926,7 @@ function onScan(): void {
color: #666;
}
/* 加载更多 */
/* 鍔犺浇鏇村 */
.load-more {
text-align: center;
padding: 20px 0;
@@ -941,9 +941,9 @@ function onScan(): void {
border-radius: 20px;
}
/* ===== 响应式设计 ===== */
/* ===== 鍝嶅簲寮忚璁?===== */
/* 小屏手机 (小于414px) */
/* 灏忓睆鎵嬫満 (灏忎簬414px) */
@media screen and (max-width: 414px) {
.search-container {
padding: 0 12px;
@@ -961,19 +961,19 @@ function onScan(): void {
}
.primary-category {
width: 80px; /* 减小宽度 */
/* display: flex; 移除flex布局,保持默认 */
/* flex-wrap: wrap; 移除换行 */
width: 80px; /* 鍑忓皬瀹藉害 */
/* display: flex; 绉婚櫎flex甯冨眬锛屼繚鎸侀粯璁?*/
/* flex-wrap: wrap; 绉婚櫎鎹㈣ */
padding: 8px 0;
margin-right: 10px; /* Gap replacement */
}
.primary-item {
/* width: calc(25% - 8px); 移除百分比宽度 */
width: auto; /* 恢复自动宽度 */
/* width: calc(25% - 8px); 绉婚櫎鐧惧垎姣斿搴?*/
width: auto; /* 鎭㈠鑷姩瀹藉害 */
margin: 4px;
padding: 8px 4px;
/* text-align: center; 已经在通用样式中设置 */
/* text-align: center; 宸茬粡鍦ㄩ€氱敤鏍峰紡涓缃?*/
}
.primary-icon {
@@ -989,7 +989,7 @@ function onScan(): void {
.product-grid {
/* grid-template-columns: repeat(2, 1fr); REMOVED */
/* gap: 8px; REMOVED */
padding: 0 4px 20px 4px; /* 增加底部内边距 */
padding: 0 4px 20px 4px; /* 澧炲姞搴曢儴鍐呰竟璺?*/
}
.product-card {
@@ -997,12 +997,12 @@ function onScan(): void {
margin: 1%;
}
/* 手机端商品卡片极简模式 - 仿照主页样式 */
/* 鎵嬫満绔晢鍝佸崱鐗囨瀬绠€妯″紡 - 浠跨収涓婚〉鏍峰紡 */
.product-spec,
.manufacturer,
.original-price,
.sales-info,
.product-badge { /* 分类页也隐藏角标,保持整洁 */
.product-badge { /* 鍒嗙被椤典篃闅愯棌瑙掓爣锛屼繚鎸佹暣娲?*/
display: none;
}
@@ -1011,7 +1011,7 @@ function onScan(): void {
}
.product-image {
height: 100px; /* 由于分类页右侧空间更窄,图片高度设得更小一点 */
height: 100px; /* 鐢变簬鍒嗙被椤靛彸渚х┖闂存洿绐勶紝鍥剧墖楂樺害璁惧緱鏇村皬涓€鐐?*/
}
.product-name {
@@ -1043,7 +1043,7 @@ function onScan(): void {
}
.product-meta {
display: none; /* 隐藏整个元数据行 */
display: none; /* 闅愯棌鏁翠釜鍏冩暟鎹 */
}
.search-container {
@@ -1060,7 +1060,7 @@ function onScan(): void {
}
}
/* 中屏手机/小平板 (415px-768px) */
/* 涓睆鎵嬫満/灏忓钩鏉?(415px-768px) */
@media screen and (min-width: 415px) and (max-width: 768px) {
.search-container {
padding: 0 16px;
@@ -1073,7 +1073,7 @@ function onScan(): void {
}
}
/* 平板设备 (769px-1024px) */
/* 骞虫澘璁惧 (769px-1024px) */
@media screen and (min-width: 769px) and (max-width: 1024px) {
.search-container {
padding: 0 16px;
@@ -1086,7 +1086,7 @@ function onScan(): void {
}
}
/* 桌面端 (1025px以上) */
/* 妗岄潰绔?(1025px浠ヤ笂) */
@media screen and (min-width: 1025px) {
.search-container {
padding: 0 16px;
@@ -1163,7 +1163,7 @@ function onScan(): void {
}
}
/* 大桌面端 (1400px以上) */
/* 澶ф闈㈢ (1400px浠ヤ笂) */
@media screen and (min-width: 1400px) {
.category-content {
max-width: 1600px;
@@ -1244,3 +1244,5 @@ function onScan(): void {
}
}
</style>

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="category-page">
<!-- 顶部搜索栏 -->
<view class="search-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
@@ -727,9 +727,9 @@ function onScan(): void {
top: 0;
left: 0;
right: 0;
background-color: #4CAF50;
background-color: #ff5000;
z-index: 1000;
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
box-shadow: 0 2px 12px rgba(255, 80, 0, 0.15);
}
/* 导航栏占位 */
@@ -848,7 +848,7 @@ function onScan(): void {
align-items: center;
justify-content: center;
padding: 0 4px;
border: 2px solid #4CAF50;
border: 2px solid #ff5000;
}
/* 分类内容区 */
@@ -967,7 +967,7 @@ function onScan(): void {
}
.sub-category-item.active {
background: #4CAF50;
background: #ff5000;
}
.sub-category-item.active .sub-category-name {
@@ -1076,7 +1076,7 @@ function onScan(): void {
align-items: center;
justify-content: center;
/* gap: 6px; */
background: #4CAF50;
background: #ff5000;
color: white;
padding: 8px 12px;
border-radius: 8px;
@@ -1107,7 +1107,7 @@ function onScan(): void {
.empty-icon {
font-size: 60px;
color: #4CAF50;
color: #ff5000;
margin-bottom: 15px;
}
@@ -1464,3 +1464,4 @@ function onScan(): void {
}
}
</style>

View File

@@ -1,31 +1,31 @@
<template>
<template>
<view class="category-page">
<!-- 顶部搜索栏 -->
<!-- 椤堕儴鎼滅储鏍?-->
<view class="search-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="search-container">
<view class="search-box" @click="navigateToSearch" :style="{ height: '30px' }">
<!-- 模拟输入框 -->
<text class="search-placeholder">请输入药品名称、症状或品牌</text>
<!-- 妯℃嫙杈撳叆妗?-->
<text class="search-placeholder">璇疯緭鍏ヨ嵂鍝佸悕绉般€佺棁鐘舵垨鍝佺墝</text>
<!-- 扫码图标 -->
<!-- 鎵爜鍥炬爣 -->
<view class="nav-icon-btn" @click.stop="onScan">
<text class="nav-icon">🔳</text>
<text class="nav-icon">馃敵</text>
</view>
<!-- 相机图标 -->
<!-- 鐩告満鍥炬爣 -->
<view class="nav-camera-btn" @click.stop="onCamera">
<text class="nav-camera-icon">📷</text>
<text class="nav-camera-icon">馃摲</text>
</view>
<!-- 搜索按钮 -->
<!-- 鎼滅储鎸夐挳 -->
<view class="nav-inner-search-btn" :style="{ height: '22px' }">
<text class="nav-inner-search-text">搜索</text>
<text class="nav-inner-search-text">鎼滅储</text>
</view>
</view>
</view>
</view>
<!-- 分类内容区 -->
<!-- 鍒嗙被鍐呭鍖?-->
<view
class="category-content"
:style="{
@@ -33,7 +33,7 @@
height: `calc(100vh - ${statusBarHeight + headerHeight + 10}px)`
}"
>
<!-- 左侧一级分类 -->
<!-- 宸︿晶涓€绾у垎绫?-->
<scroll-view scroll-y class="primary-category">
<view
v-for="item in primaryCategories"
@@ -47,15 +47,15 @@
</view>
</scroll-view>
<!-- 右侧商品列表 -->
<!-- 鍙充晶鍟嗗搧鍒楄〃 -->
<scroll-view scroll-y class="product-content">
<!-- 分类标题 -->
<!-- 鍒嗙被鏍囬 -->
<view class="category-header">
<text class="category-title">{{ currentCategoryName }}</text>
<text class="category-desc">{{ currentCategoryDesc }}</text>
</view>
<!-- 商品网格 -->
<!-- 鍟嗗搧缃戞牸 -->
<view v-if="productList.length > 0" class="product-grid">
<view
v-for="product in productList"
@@ -75,34 +75,34 @@
<view class="price-section">
<view class="current-price">
<text class="price-symbol">¥</text>
<text class="price-symbol"></text>
<text class="price-value">{{ product.price }}</text>
</view>
<text class="original-price" v-if="product.originalPrice > product.price">
¥{{ product.originalPrice }}
{{ product.originalPrice }}
</text>
</view>
<view class="product-meta">
<text class="manufacturer">{{ product.manufacturer }}</text>
<view class="sales-info">
<text class="sales-count">已售{{ product.sales }}</text>
<text class="sales-count">宸插敭{{ product.sales }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<!-- 绌虹姸鎬?-->
<view v-else class="empty-state">
<text class="empty-icon">💊</text>
<text class="empty-text">暂无相关药品</text>
<text class="empty-desc">该分类下暂无商品,敬请期待</text>
<text class="empty-icon">馃拪</text>
<text class="empty-text">鏆傛棤鐩稿叧鑽搧</text>
<text class="empty-desc">璇ュ垎绫讳笅鏆傛棤鍟嗗搧锛屾暚璇锋湡寰?/text>
</view>
<!-- 加载更多提示 -->
<!-- 鍔犺浇鏇村鎻愮ず -->
<view v-if="hasMore" class="load-more">
<text class="load-text">上拉加载更多</text>
<text class="load-text">涓婃媺鍔犺浇鏇村</text>
</view>
</scroll-view>
</view>
@@ -114,49 +114,49 @@ import { ref, onMounted } from 'vue'
import supabaseService from '@/utils/supabaseService.uts'
import type { Category, Product } from '@/utils/supabaseService.uts'
// 响应式数据
// 鍝嶅簲寮忔暟鎹?
const statusBarHeight = ref(0)
const headerHeight = ref(44) // 默认头部高度
const headerHeight = ref(44) // 榛樿澶撮儴楂樺害
const primaryCategories = ref<Category[]>([])
const productList = ref<Product[]>([])
const activePrimary = ref<string>('')
const cartCount = ref(3)
const hasMore = ref(true)
// 获取当前分类信息
// 鑾峰彇褰撳墠鍒嗙被淇℃伅
const currentCategoryName = ref('')
const currentCategoryDesc = ref('')
// 页面参数
// 椤甸潰鍙傛暟
const pageParams = ref<any>({})
// 生命周期
// 鐢熷懡鍛ㄦ湡
onMounted(async() => {
await loadCategories()
await loadProducts()
})
// 添加加载分类的方法
// 娣诲姞鍔犺浇鍒嗙被鐨勬柟娉?
const loadCategories = async () => {
const categories = await supabaseService.getCategories()
if (categories.length > 0) {
primaryCategories.value = categories
// 设置默认选中第一个分类
// 璁剧疆榛樿閫変腑绗竴涓垎绫?
if (!activePrimary.value && categories[0]) {
activePrimary.value = categories[0].id
}
}
}
// 加载商品数据
// 鍔犺浇鍟嗗搧鏁版嵁
const loadProducts = async () => {
if (activePrimary.value) {
const response = await supabaseService.getProductsByCategory(activePrimary.value)
productList.value = response.data
hasMore.value = response.hasmore
// 更新当前分类信息
// 鏇存柊褰撳墠鍒嗙被淇℃伅
const category = primaryCategories.value.find(cat => cat.id === activePrimary.value)
if (category) {
currentCategoryName.value = category.name
@@ -165,130 +165,130 @@ const loadProducts = async () => {
}
}
// 页面加载时处理参数 - 这是处理分类切换的主要入口
// 椤甸潰鍔犺浇鏃跺鐞嗗弬鏁?- 杩欐槸澶勭悊鍒嗙被鍒囨崲鐨勪富瑕佸叆鍙?
onLoad((options: any) => {
console.log('=== category页面onLoad被调用 ===')
console.log('页面加载时间:', Date.now())
console.log('传入的options参数:', options)
console.log('当前活动分类:', activePrimary.value)
console.log('=== category椤甸潰onLoad琚皟鐢?===')
console.log('椤甸潰鍔犺浇鏃堕棿:', Date.now())
console.log('浼犲叆鐨刼ptions鍙傛暟:', options)
console.log('褰撳墠娲诲姩鍒嗙被:', activePrimary.value)
let categoryId = ''
let categoryName = ''
// 首先检查传入的options参数
// 棣栧厛妫€鏌ヤ紶鍏ョ殑options鍙傛暟
if (options && options.categoryId) {
categoryId = options.categoryId
categoryName = options.name || ''
console.log('onLoad中找到分类参数:', categoryId, categoryName)
console.log('鉁?onLoad涓壘鍒板垎绫诲弬鏁?', categoryId, categoryName)
}
// 如果options中没有尝试从getCurrentPages()获取
// 濡傛灉options涓病鏈夛紝灏濊瘯浠巊etCurrentPages()鑾峰彇
if (!categoryId) {
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
const pageOptions = currentPage.options || {}
console.log('从getCurrentPages()获取参数:', pageOptions)
console.log('浠巊etCurrentPages()鑾峰彇鍙傛暟:', pageOptions)
if (pageOptions.categoryId) {
categoryId = pageOptions.categoryId
categoryName = pageOptions.name || ''
console.log('✅ 从getCurrentPages()找到分类参数:', categoryId, categoryName)
console.log('鉁?浠巊etCurrentPages()鎵惧埌鍒嗙被鍙傛暟:', categoryId, categoryName)
}
}
}
// 如果有找到分类ID则选中对应的分类
// 濡傛灉鏈夋壘鍒板垎绫籌D锛屽垯閫変腑瀵瑰簲鐨勫垎绫?
if (categoryId) {
console.log('✅ 准备选中分类:', categoryId)
console.log('分类名称:', categoryName || '未指定')
console.log('鉁?鍑嗗閫変腑鍒嗙被:', categoryId)
console.log('鍒嗙被鍚嶇О:', categoryName || '鏈寚瀹?)
// 检查是否需要更新分类
// 妫€鏌ユ槸鍚﹂渶瑕佹洿鏂板垎绫?
if (activePrimary.value !== categoryId) {
console.log('当前分类:', activePrimary.value, '与目标分类:', categoryId, '不同,需要更新')
console.log('准备调用selectPrimaryCategory函数...')
console.log('褰撳墠鍒嗙被:', activePrimary.value, '涓庣洰鏍囧垎绫?', categoryId, '涓嶅悓锛岄渶瑕佹洿鏂?)
console.log('鍑嗗璋冪敤selectPrimaryCategory鍑芥暟...')
selectPrimaryCategory(categoryId)
} else {
console.log('当前分类已经是目标分类,但可能用户想要刷新页面')
console.log('当前分类:', activePrimary.value, '目标分类:', categoryId)
// 即使分类相同,也重新加载数据,确保数据是最新的
// 添加一个小的延迟,确保页面完全显示后再更新数据
console.log('褰撳墠鍒嗙被宸茬粡鏄洰鏍囧垎绫伙紝浣嗗彲鑳界敤鎴锋兂瑕佸埛鏂伴〉闈?)
console.log('褰撳墠鍒嗙被:', activePrimary.value, '鐩爣鍒嗙被:', categoryId)
// 鍗充娇鍒嗙被鐩稿悓锛屼篃閲嶆柊鍔犺浇鏁版嵁锛岀‘淇濇暟鎹槸鏈€鏂扮殑
// 娣诲姞涓€涓皬鐨勫欢杩燂紝纭繚椤甸潰瀹屽叏鏄剧ず鍚庡啀鏇存柊鏁版嵁
setTimeout(() => {
selectPrimaryCategory(categoryId)
}, 100)
}
} else {
console.log('⚠️ onLoad中未找到分类参数,将使用从数据库加载的第一个分类')
// 不再使用硬编码的默认分类loadCategories 会设置第一个分类
console.log('鈿狅笍 onLoad涓湭鎵惧埌鍒嗙被鍙傛暟锛屽皢浣跨敤浠庢暟鎹簱鍔犺浇鐨勭涓€涓垎绫?)
// 涓嶅啀浣跨敤纭紪鐮佺殑榛樿鍒嗙被锛宭oadCategories 浼氳缃涓€涓垎绫?
}
console.log('=== category页面onLoad执行完成 ===')
console.log('=== category椤甸潰onLoad鎵ц瀹屾垚 ===')
})
// 页面显示时也检查参数,确保从其他页面返回时能正确显示
// 椤甸潰鏄剧ず鏃朵篃妫€鏌ュ弬鏁帮紝纭繚浠庡叾浠栭〉闈㈣繑鍥炴椂鑳芥纭樉绀?
onShow(() => {
console.log('=== category页面onShow被调用 ===')
console.log('页面显示时间:', Date.now())
console.log('当前活动分类:', activePrimary.value)
console.log('=== category椤甸潰onShow琚皟鐢?===')
console.log('椤甸潰鏄剧ず鏃堕棿:', Date.now())
console.log('褰撳墠娲诲姩鍒嗙被:', activePrimary.value)
// 在onShow中,我们也需要检查是否有新的参数
// 因为当从主页再次点击分类跳转过来时,可能不会触发onLoad
// 而是触发onShow
// 鍦╫nShow涓紝鎴戜滑涔熼渶瑕佹鏌ユ槸鍚︽湁鏂扮殑鍙傛暟
// 鍥犱负褰撲粠涓婚〉鍐嶆鐐瑰嚮鍒嗙被璺宠浆杩囨潵鏃讹紝鍙兘涓嶄細瑙﹀彂onLoad
// 鑰屾槸瑙﹀彂onShow
// 获取当前页面实例和参数
// 鑾峰彇褰撳墠椤甸潰瀹炰緥鍜屽弬鏁?
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
const pageOptions = currentPage.options || {}
console.log('onShow中获取参数:', pageOptions)
console.log('onShow涓幏鍙栧弬鏁?', pageOptions)
// 检查是否有分类参数
// 妫€鏌ユ槸鍚︽湁鍒嗙被鍙傛暟
if (pageOptions.categoryId) {
const categoryId = pageOptions.categoryId
const categoryName = pageOptions.name || ''
console.log('onShow中找到分类参数:', categoryId, categoryName)
console.log('URL中的时间戳参数:', pageOptions.timestamp)
console.log('URL中的随机参数:', pageOptions.random)
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函数...')
console.log('褰撳墠鍒嗙被:', activePrimary.value, '涓庣洰鏍囧垎绫?', categoryId, '涓嶅悓锛岄渶瑕佹洿鏂?)
console.log('鍑嗗璋冪敤selectPrimaryCategory鍑芥暟...')
selectPrimaryCategory(categoryId)
} else {
console.log('当前分类已经是目标分类,但可能用户想要刷新页面')
console.log('当前分类:', activePrimary.value, '目标分类:', categoryId)
// 即使分类相同,也重新加载数据,确保数据是最新的
// 添加一个小的延迟,确保页面完全显示后再更新数据
console.log('褰撳墠鍒嗙被宸茬粡鏄洰鏍囧垎绫伙紝浣嗗彲鑳界敤鎴锋兂瑕佸埛鏂伴〉闈?)
console.log('褰撳墠鍒嗙被:', activePrimary.value, '鐩爣鍒嗙被:', categoryId)
// 鍗充娇鍒嗙被鐩稿悓锛屼篃閲嶆柊鍔犺浇鏁版嵁锛岀‘淇濇暟鎹槸鏈€鏂扮殑
// 娣诲姞涓€涓皬鐨勫欢杩燂紝纭繚椤甸潰瀹屽叏鏄剧ず鍚庡啀鏇存柊鏁版嵁
setTimeout(() => {
selectPrimaryCategory(categoryId)
}, 100)
}
} else {
console.log('⚠️ onShow中未找到分类参数')
console.log('尝试从URL中解析参数...')
console.log('鈿狅笍 onShow涓湭鎵惧埌鍒嗙被鍙傛暟')
console.log('灏濊瘯浠嶶RL涓В鏋愬弬鏁?..')
// 尝试从当前页面的URL中解析参数
// 灏濊瘯浠庡綋鍓嶉〉闈㈢殑URL涓В鏋愬弬鏁?
const currentUrl = currentPage.route || ''
console.log('当前页面路由:', currentUrl)
console.log('褰撳墠椤甸潰璺敱:', currentUrl)
// 如果URL中有查询参数尝试解析
// 濡傛灉URL涓湁鏌ヨ鍙傛暟锛屽皾璇曡В鏋?
if (currentPage.$page && currentPage.$page.fullPath) {
const fullPath = currentPage.$page.fullPath
console.log('完整路径:', fullPath)
console.log('瀹屾暣璺緞:', fullPath)
// 尝试解析查询参数
// 灏濊瘯瑙f瀽鏌ヨ鍙傛暟
const queryIndex = fullPath.indexOf('?')
if (queryIndex > -1) {
const queryString = fullPath.substring(queryIndex + 1)
console.log('查询字符串:', queryString)
console.log('鏌ヨ瀛楃涓?', queryString)
// 简单解析查询参数
// 绠€鍗曡В鏋愭煡璇㈠弬鏁?
const params = new URLSearchParams(queryString)
const urlCategoryId = params.get('categoryId')
if (urlCategoryId) {
console.log('✅ 从URL解析到分类参数:', urlCategoryId)
console.log('鉁?浠嶶RL瑙瀽鍒板垎绫诲弬鏁?', urlCategoryId)
selectPrimaryCategory(urlCategoryId)
}
}
@@ -296,78 +296,78 @@ onShow(() => {
}
}
console.log('=== category页面onShow执行完成 ===')
console.log('=== category椤甸潰onShow鎵ц瀹屾垚 ===')
})
// 选择一级分类
// 閫夋嫨涓€绾у垎绫?
const selectPrimaryCategory = async (categoryId: string) => {
console.log('=== selectPrimaryCategory函数开始执行 ===')
console.log('传入的categoryId:', categoryId)
console.log('当前时间:', Date.now())
console.log('=== selectPrimaryCategory鍑芥暟寮€濮嬫墽琛?===')
console.log('浼犲叆鐨刢ategoryId:', categoryId)
console.log('褰撳墠鏃堕棿:', Date.now())
// 验证categoryId是否有效
// 楠岃瘉categoryId鏄惁鏈夋晥
if (!categoryId) {
console.error('categoryId为空,尝试使用第一个分类')
console.error('categoryId涓虹┖锛屽皾璇曚娇鐢ㄧ涓€涓垎绫?)
if (primaryCategories.value.length > 0) {
categoryId = primaryCategories.value[0].id
} else {
console.error('没有可用的分类')
console.error('娌℃湁鍙敤鐨勫垎绫?)
return
}
}
console.log('验证后的categoryId:', categoryId)
console.log('当前activePrimary的值:', activePrimary.value)
console.log('楠岃瘉鍚庣殑categoryId:', categoryId)
console.log('褰撳墠activePrimary鐨勫€?', activePrimary.value)
// 更新活动分类
// 鏇存柊娲诲姩鍒嗙被
activePrimary.value = categoryId
console.log('更新后的activePrimary:', activePrimary.value)
console.log('鏇存柊鍚庣殑activePrimary:', activePrimary.value)
// 更新当前分类信息
// 鏇存柊褰撳墠鍒嗙被淇℃伅
const category = primaryCategories.value.find(cat => cat.id === categoryId)
if (category) {
currentCategoryName.value = category.name
currentCategoryDesc.value = category.description
console.log('✅ 找到分类:', category.name, '描述:', category.description)
console.log('鉁?鎵惧埌鍒嗙被:', category.name, '鎻忚堪:', category.description)
} else {
console.error('❌ 未找到分类ID:', categoryId, ',使用第一个分类')
// 如果找不到对应的分类,使用第一个分类
console.error('鉂?鏈壘鍒板垎绫籌D:', categoryId, '锛屼娇鐢ㄧ涓€涓垎绫?)
// 濡傛灉鎵句笉鍒板搴旂殑鍒嗙被锛屼娇鐢ㄧ涓€涓垎绫?
if (primaryCategories.value.length > 0) {
const firstCategory = primaryCategories.value[0]
currentCategoryName.value = firstCategory.name
currentCategoryDesc.value = firstCategory.description
activePrimary.value = firstCategory.id
categoryId = firstCategory.id
console.log('使用默认分类:', firstCategory.name)
console.log('浣跨敤榛樿鍒嗙被:', firstCategory.name)
}
}
console.log('准备加载商品数据...')
console.log('鍑嗗鍔犺浇鍟嗗搧鏁版嵁...')
// 加载对应商品 - 使用 Supabase 服务
// 鍔犺浇瀵瑰簲鍟嗗搧 - 浣跨敤 Supabase 鏈嶅姟
const response = await supabaseService.getProductsByCategory(categoryId)
productList.value = response.data
hasMore.value = response.hasmore
console.log('✅ 加载商品数据成功')
console.log('分类:', categoryId)
console.log('商品数量:', response.data.length)
console.log('商品列表:', response.data)
console.log('鉁?鍔犺浇鍟嗗搧鏁版嵁鎴愬姛')
console.log('鍒嗙被:', categoryId)
console.log('鍟嗗搧鏁伴噺:', response.data.length)
console.log('鍟嗗搧鍒楄〃:', response.data)
// 验证数据是否已正确更新
console.log('数据更新验证:')
// 楠岃瘉鏁版嵁鏄惁宸叉纭洿鏂?
console.log('鏁版嵁鏇存柊楠岃瘉:')
console.log('activePrimary:', activePrimary.value)
console.log('currentCategoryName:', currentCategoryName.value)
console.log('currentCategoryDesc:', currentCategoryDesc.value)
console.log('productList长度:', productList.value.length)
console.log('productList闀垮害:', productList.value.length)
console.log('=== selectPrimaryCategory函数执行完成 ===')
console.log('=== selectPrimaryCategory鍑芥暟鎵ц瀹屾垚 ===')
}
// 添加到购物车
// 娣诲姞鍒拌喘鐗╄溅
const addToCart = (product: any) => {
// 获取现有购物车数据
// 鑾峰彇鐜版湁璐墿杞︽暟鎹?
const cartData = uni.getStorageSync('cart')
let cartItems: any[] = []
@@ -375,41 +375,41 @@ const addToCart = (product: any) => {
try {
cartItems = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
console.error('瑙f瀽璐墿杞︽暟鎹け璐?, e)
}
}
// 检查商品是否已存在
// 妫€鏌ュ晢鍝佹槸鍚﹀凡瀛樺湪
const existingItem = cartItems.find((item: any) => item.id === product.id)
if (existingItem) {
existingItem.quantity++
} else {
// 添加新商品
// 娣诲姞鏂板晢鍝?
cartItems.push({
id: product.id,
shopId: product.shopId || 'shop_default',
shopName: product.shopName || product.manufacturer || '自营店铺',
shopName: product.shopName || product.manufacturer || '鑷惀搴楅摵',
name: product.name,
price: product.price,
image: product.image,
spec: product.specification || '默认规格',
spec: product.specification || '榛樿瑙勬牸',
quantity: 1,
selected: true
})
}
// 保存回存储
// 淇濆瓨鍥炲瓨鍌?
uni.setStorageSync('cart', JSON.stringify(cartItems))
uni.showToast({
title: '已添加到购物车',
title: '宸叉坊鍔犲埌璐墿杞?,
icon: 'success'
})
cartCount.value++
}
// 导航函数
// 瀵艰埅鍑芥暟
const navigateToSearch = () => uni.navigateTo({ url: '/pages/mall/consumer/search' })
const navigateToCart = () => uni.navigateTo({ url: '/pages/medicine/cart' })
const navigateToProduct = (product: any) => {
@@ -418,43 +418,43 @@ const navigateToProduct = (product: any) => {
})
}
// 相机功能
// 鐩告満鍔熻兘
const onCamera = () => {
uni.chooseImage({
count: 1,
sourceType: ['camera'],
success: (res) => {
console.log('相机拍摄成功:', res.tempFilePaths[0])
console.log('鐩告満鎷嶆憚鎴愬姛:', res.tempFilePaths[0])
uni.showToast({
title: '已拍摄,正在识别...',
title: '宸叉媿鎽勶紝姝e湪璇嗗埆...',
icon: 'loading'
})
// 这里可以添加后续的识别逻辑
// 杩欓噷鍙互娣诲姞鍚庣画鐨勮瘑鍒€昏緫
setTimeout(() => {
uni.showToast({
title: '识别成功',
title: '璇嗗埆鎴愬姛',
icon: 'success'
})
}, 1000)
},
fail: (err) => {
console.error('相机调用失败:', err)
console.error('鐩告満璋冪敤澶辫触:', err)
}
})
}
// 扫码功能
// 鎵爜鍔熻兘
const onScan = () => {
uni.scanCode({
success: (res) => {
console.log('扫码成功:', res)
console.log('鎵爜鎴愬姛:', res)
uni.showToast({
title: '扫码成功: ' + res.result,
title: '鎵爜鎴愬姛: ' + res.result,
icon: 'none'
})
},
fail: (err) => {
console.error('扫码失败:', err)
console.error('鎵爜澶辫触:', err)
}
})
}
@@ -469,7 +469,7 @@ const onScan = () => {
flex-direction: column;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif;
}
/* 搜索栏 */
/* 鎼滅储鏍?*/
.search-bar {
position: fixed;
top: 0;
@@ -480,10 +480,10 @@ const onScan = () => {
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
}
/* 搜索栏 */
/* 导航栏搜索框容器内边距调整 */
/* 鎼滅储鏍?*/
/* 瀵艰埅鏍忔悳绱㈡瀹瑰櫒鍐呰竟璺濊皟鏁?*/
.search-container {
height: 44px; /* 调整为与消息页一致的高度 */
height: 44px; /* 璋冩暣涓轰笌娑堟伅椤典竴鑷寸殑楂樺害 */
padding: 0 16px;
display: flex;
align-items: center;
@@ -493,12 +493,12 @@ const onScan = () => {
width: 100%;
}
/* 搜索框 hover 效果 */
/* 鎼滅储妗?hover 鏁堟灉 */
.search-box:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 导航栏搜索框容器内边距调整 */
/* 瀵艰埅鏍忔悳绱㈡瀹瑰櫒鍐呰竟璺濊皟鏁?*/
.search-box {
flex: 1;
max-width: 600px;
@@ -506,12 +506,12 @@ const onScan = () => {
border-radius: 20px;
padding: 0 4px 0 12px;
display: flex;
flex-direction: row; /* UVUE 显式设置 row */
flex-direction: row; /* UVUE 鏄惧紡璁剧疆 row */
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
height: 32px; /* 减小高度与顶部高度44px适配略小于顶部高度 */
height: 32px; /* 鍑忓皬楂樺害锛屼笌椤堕儴楂樺害44px閫傞厤锛岀暐灏忎簬椤堕儴楂樺害 */
}
.search-placeholder {
@@ -524,7 +524,7 @@ const onScan = () => {
}
.nav-inner-search-text {
font-size: 12px; /* 字体稍微变小 */
font-size: 12px; /* 瀛椾綋绋嶅井鍙樺皬 */
color: #ffffff;
font-weight: 500;
}
@@ -557,7 +557,7 @@ const onScan = () => {
border-right-width: 1px;
border-right-style: solid;
border-right-color: #ddd;
border-right: 1px solid #ddd; /* 修复UVUE样式 */
border-right: 1px solid #ddd; /* 淇UVUE鏍峰紡 */
margin-right: 8px;
}
@@ -565,23 +565,23 @@ const onScan = () => {
font-size: 20px;
}
/* 搜索按钮高度微调 */
/* 鎼滅储鎸夐挳楂樺害寰皟 */
.nav-inner-search-btn {
padding: 0 12px; /* 减小内边距 */
background-color: #87CEEB; /* 天空蓝 */
padding: 0 12px; /* 鍑忓皬鍐呰竟璺?*/
background-color: #87CEEB; /* 澶╃┖钃?*/
border-radius: 16px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
height: 24px; /* 随搜索框高度减小而减小 */
height: 24px; /* 闅忔悳绱㈡楂樺害鍑忓皬鑰屽噺灏?*/
}
.cart-badge {
position: absolute;
top: -5px;
right: -5px;
background: #FF5722;
background: #ff5000;
color: white;
font-size: 10px;
min-width: 18px;
@@ -594,36 +594,36 @@ const onScan = () => {
border: 2px solid #4CAF50;
}
/* 分类内容区 */
/* 鍒嗙被鍐呭鍖?*/
.category-content {
display: flex;
flex-direction: row; /* 强制水平排列 */
/* margin-top: 44px; 已通过 style 动态绑定 */
flex-direction: row; /* 寮哄埗姘村钩鎺掑垪 */
/* margin-top: 44px; 宸查€氳繃 style 鍔ㄦ€佺粦瀹?*/
padding: 0 16px;
max-width: 1400px;
margin-left: auto;
margin-right: auto;
width: 100%;
gap: 20px;
/* height: calc(100vh - 44px); 已通过 style 动态绑定 */
overflow: hidden; /* 防止整体滚动 */
/* height: calc(100vh - 44px); 宸查€氳繃 style 鍔ㄦ€佺粦瀹?*/
overflow: hidden; /* 闃叉鏁翠綋婊氬姩 */
}
/* 左侧一级分类 */
/* 宸︿晶涓€绾у垎绫?*/
.primary-category {
width: 120px;
height: 100%; /* 占满父容器高度 */
height: 100%; /* 鍗犳弧鐖跺鍣ㄩ珮搴?*/
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; /* 允许内部滚动 */
overflow-y: auto; /* 鍏佽鍐呴儴婊氬姩 */
}
.primary-item {
display: flex;
flex-direction: column; /* 图标和文字垂直排列 */
flex-direction: column; /* 鍥炬爣鍜屾枃瀛楀瀭鐩存帓鍒?*/
align-items: center;
justify-content: center;
padding: 12px 8px;
@@ -636,7 +636,7 @@ const onScan = () => {
}
.primary-item:hover {
transform: translateY(-2px); /* 悬停时向上浮动 */
transform: translateY(-2px); /* 鎮仠鏃跺悜涓婃诞鍔?*/
}
.primary-item.active {
@@ -647,7 +647,7 @@ const onScan = () => {
.primary-icon {
font-size: 24px;
margin-bottom: 6px;
margin-right: 0; /* 移除右边距 */
margin-right: 0; /* 绉婚櫎鍙宠竟璺?*/
text-align: center;
display: block;
}
@@ -658,12 +658,12 @@ const onScan = () => {
display: block;
}
/* 右侧内容区 */
/* 鍙充晶鍐呭鍖?*/
.product-content {
flex: 1;
height: 100%; /* 占满父容器高度 */
padding: 0; /* 移除内边距,交给内部元素 */
overflow-y: auto; /* 允许内部滚动 */
height: 100%; /* 鍗犳弧鐖跺鍣ㄩ珮搴?*/
padding: 0; /* 绉婚櫎鍐呰竟璺濓紝浜ょ粰鍐呴儴鍏冪礌 */
overflow-y: auto; /* 鍏佽鍐呴儴婊氬姩 */
}
.category-header {
@@ -687,7 +687,7 @@ const onScan = () => {
color: #666;
}
/* 商品网格 */
/* 鍟嗗搧缃戞牸 */
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
@@ -713,7 +713,7 @@ const onScan = () => {
position: absolute;
top: 12px;
left: 12px;
background: #FF5722;
background: #ff5000;
color: white;
font-size: 11px;
padding: 4px 12px;
@@ -763,13 +763,13 @@ const onScan = () => {
.price-symbol {
font-size: 14px;
color: #FF5722;
color: #ff5000;
}
.price-value {
font-size: 20px;
font-weight: bold;
color: #FF5722;
color: #ff5000;
margin-left: 2px;
}
@@ -822,7 +822,7 @@ const onScan = () => {
font-size: 14px;
}
/* 空状态 */
/* 绌虹姸鎬?*/
.empty-state {
display: flex;
flex-direction: column;
@@ -850,7 +850,7 @@ const onScan = () => {
color: #666;
}
/* 加载更多 */
/* 鍔犺浇鏇村 */
.load-more {
text-align: center;
padding: 20px 0;
@@ -865,29 +865,29 @@ const onScan = () => {
border-radius: 20px;
}
/* ===== 响应式设计 ===== */
/* ===== 鍝嶅簲寮忚璁?===== */
/* 小屏手机 (小于414px) */
/* 灏忓睆鎵嬫満 (灏忎簬414px) */
@media screen and (max-width: 414px) {
.category-content {
/* flex-direction: column; 移除这一行,保持 row 布局 */
/* flex-direction: column; 绉婚櫎杩欎竴琛岋紝淇濇寔 row 甯冨眬 */
padding: 0 8px;
gap: 10px;
}
.primary-category {
width: 80px; /* 减小宽度 */
/* display: flex; 移除flex布局,保持默认 */
/* flex-wrap: wrap; 移除换行 */
width: 80px; /* 鍑忓皬瀹藉害 */
/* display: flex; 绉婚櫎flex甯冨眬锛屼繚鎸侀粯璁?*/
/* flex-wrap: wrap; 绉婚櫎鎹㈣ */
padding: 8px 0;
}
.primary-item {
/* width: calc(25% - 8px); 移除百分比宽度 */
width: auto; /* 恢复自动宽度 */
/* width: calc(25% - 8px); 绉婚櫎鐧惧垎姣斿搴?*/
width: auto; /* 鎭㈠鑷姩瀹藉害 */
margin: 4px;
padding: 8px 4px;
/* text-align: center; 已经在通用样式中设置 */
/* text-align: center; 宸茬粡鍦ㄩ€氱敤鏍峰紡涓缃?*/
}
.primary-icon {
@@ -901,17 +901,17 @@ const onScan = () => {
}
.product-grid {
grid-template-columns: repeat(2, 1fr); /* 改为双列显示 */
grid-template-columns: repeat(2, 1fr); /* 鏀逛负鍙屽垪鏄剧ず */
gap: 8px;
padding: 0 4px 20px 4px; /* 增加底部内边距 */
padding: 0 4px 20px 4px; /* 澧炲姞搴曢儴鍐呰竟璺?*/
}
/* 手机端商品卡片极简模式 - 仿照主页样式 */
/* 鎵嬫満绔晢鍝佸崱鐗囨瀬绠€妯″紡 - 浠跨収涓婚〉鏍峰紡 */
.product-spec,
.manufacturer,
.original-price,
.sales-info,
.product-badge { /* 分类页也隐藏角标,保持整洁 */
.product-badge { /* 鍒嗙被椤典篃闅愯棌瑙掓爣锛屼繚鎸佹暣娲?*/
display: none;
}
@@ -920,7 +920,7 @@ const onScan = () => {
}
.product-image {
height: 100px; /* 由于分类页右侧空间更窄,图片高度设得更小一点 */
height: 100px; /* 鐢变簬鍒嗙被椤靛彸渚х┖闂存洿绐勶紝鍥剧墖楂樺害璁惧緱鏇村皬涓€鐐?*/
}
.product-name {
@@ -951,7 +951,7 @@ const onScan = () => {
}
.product-meta {
display: none; /* 隐藏整个元数据行 */
display: none; /* 闅愯棌鏁翠釜鍏冩暟鎹 */
}
.search-container {
@@ -968,7 +968,7 @@ const onScan = () => {
}
}
/* 中屏手机/小平板 (415px-768px) */
/* 涓睆鎵嬫満/灏忓钩鏉?(415px-768px) */
@media screen and (min-width: 415px) and (max-width: 768px) {
.product-grid {
grid-template-columns: repeat(2, 1fr);
@@ -976,7 +976,7 @@ const onScan = () => {
}
}
/* 平板设备 (769px-1024px) */
/* 骞虫澘璁惧 (769px-1024px) */
@media screen and (min-width: 769px) and (max-width: 1024px) {
.product-grid {
grid-template-columns: repeat(3, 1fr);
@@ -984,7 +984,7 @@ const onScan = () => {
}
}
/* 桌面端 (1025px以上) */
/* 妗岄潰绔?(1025px浠ヤ笂) */
@media screen and (min-width: 1025px) {
.category-content {
gap: 30px;
@@ -1054,7 +1054,7 @@ const onScan = () => {
}
}
/* 大桌面端 (1400px以上) */
/* 澶ф闈㈢ (1400px浠ヤ笂) */
@media screen and (min-width: 1400px) {
.category-content {
max-width: 1600px;
@@ -1129,3 +1129,4 @@ const onScan = () => {
}
}
</style>

View File

@@ -1,17 +1,17 @@
<!-- pages/mall/consumer/chat.uvue -->
<!-- pages/mall/consumer/chat.uvue -->
<template>
<view class="chat-page">
<!-- 聊天头部 -->
<view class="chat-header" :style="{ paddingTop: navPaddingTop }">
<view class="header-back" @click="goBack">
<text class="back-icon"></text>
<text class="back-icon">🔙</text>
</view>
<view class="header-info">
<text class="chat-title">{{ headerTitle }}</text>
<text class="chat-status">在线</text>
</view>
<view class="header-actions">
<text class="action-icon" @click="showMoreActions"></text>
<text class="action-icon" @click="showMoreActions"></text>
</view>
</view>
@@ -78,7 +78,7 @@
</view>
</scroll-view>
<!-- 聊天输入 -->
<!-- 聊天输入 -->
<view class="chat-input">
<view class="input-tools">
<text class="tool-icon" @click="showEmojiPicker">😊</text>
@@ -129,11 +129,11 @@ import type { AkSupaRealtimeChannel } from '@/components/supadb/aksupa.uts'
import { getCurrentUser } from '@/utils/store.uts'
type UiChatMessage = {
id: string
viewId: string
type: string
content: string
time: string
id: string
viewId: string
type: string
content: string
time: string
}
// 响应式数据
@@ -151,7 +151,7 @@ const isInitialLoading = ref<boolean>(true)
let realtimeChannel: AkSupaRealtimeChannel | null = null
// 模拟表情列表
const emojiList = ['😊', '😂', '🤣', '😍', '😘', '🥰', '😭', '😡', '👍', '👏', '🙏', '🎉', '❤️', '🔥', '']
const emojiList = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
function scrollToBottom() : void {
if (messages.value.length === 0) return
@@ -161,13 +161,13 @@ function scrollToBottom() : void {
const targetId = 'msg-' + lastMsg.id
// 关键点:在 UVue 安卓端,直接连续赋值可能被合并。
// 我们先清 ID然后在下一帧赋值确保 scroll-view 监听到变化。
// 我们先清 ID然后在下一帧赋值确保 scroll-view 监听到变化。
scrollToView.value = ''
// 增加多次尝试,确保在 DOM 彻底完成渲染(包含由于高度计算引起的多次排版)后定位。
setTimeout(() => {
scrollToView.value = targetId
console.log('[scrollToBottom] 发起第一次滚动定位:', targetId)
console.log('[scrollToBottom] 发起第一次滚动定位', targetId)
// 二次校准:针对长消息或图片导致的高度变化
setTimeout(() => {
@@ -184,98 +184,98 @@ 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}`
return ${hours}:
}
function setupRealtimeSubscription(): void {
console.log('开始建立聊天实时订阅...')
console.log('当前用户ID:', currentUserId.value, '商家ID:', merchantId.value)
realtimeChannel = supa.channel('chat-messages-' + Date.now().toString())
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'ml_chat_messages'
}, (payload: any) => {
console.log('=== 收到实时订阅回调 ===')
const payloadObj = (payload instanceof UTSJSONObject) ? (payload as UTSJSONObject) : (JSON.parse(JSON.stringify(payload ?? {})) as UTSJSONObject)
const newMsgAny = payloadObj.get('new')
if (newMsgAny == null) {
console.log('newMsgAny 为空,跳过')
return
}
const newMsg = (newMsgAny instanceof UTSJSONObject) ? (newMsgAny as UTSJSONObject) : (JSON.parse(JSON.stringify(newMsgAny)) as UTSJSONObject)
console.log('收到新消息:', newMsg)
console.log('开始建立聊天实时订阅...')
console.log('当前用户ID:', currentUserId.value, '商家ID:', merchantId.value)
const senderId = newMsg.getString('sender_id') ?? ''
const receiverId = newMsg.getString('receiver_id') ?? ''
const msgId = newMsg.getString('id') ?? ''
const content = newMsg.getString('content') ?? ''
console.log('=== 消息详情 ===')
console.log('消息ID:', msgId)
console.log('发送者ID:', senderId)
console.log('接收者ID:', receiverId)
console.log('当前用户ID:', currentUserId.value)
console.log('商家ID:', merchantId.value)
console.log('消息内容:', content)
realtimeChannel = supa.channel('chat-messages-' + Date.now().toString())
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'ml_chat_messages'
}, (payload: any) => {
console.log('=== 收到实时订阅回调 ===')
const payloadObj = (payload instanceof UTSJSONObject) ? (payload as UTSJSONObject) : (JSON.parse(JSON.stringify(payload ?? {})) as UTSJSONObject)
const newMsgAny = payloadObj.get('new')
if (newMsgAny == null) {
console.log('newMsgAny 为空,跳过')
return
}
const newMsg = (newMsgAny instanceof UTSJSONObject) ? (newMsgAny as UTSJSONObject) : (JSON.parse(JSON.stringify(newMsgAny)) as UTSJSONObject)
console.log('收到新消息', newMsg)
// 检查消息是否已经在列表中(避免重复)
for (let i = 0; i < messages.value.length; i++) {
if (messages.value[i].id == msgId) {
console.log('消息已存在,跳过')
return
}
}
const senderId = newMsg.getString('sender_id') ?? ''
const receiverId = newMsg.getString('receiver_id') ?? ''
const msgId = newMsg.getString('id') ?? ''
const content = newMsg.getString('content') ?? ''
// 判断消息类型
const isMyMessage = (senderId == currentUserId.value)
const isForMe = (receiverId == currentUserId.value)
const isRelatedToCurrentChat = (senderId == merchantId.value || receiverId == merchantId.value)
console.log('=== 条件判断 ===')
console.log('isMyMessage:', isMyMessage)
console.log('isForMe:', isForMe)
console.log('isRelatedToCurrentChat:', isRelatedToCurrentChat)
console.log('=== 消息详情 ===')
console.log('消息ID:', msgId)
console.log('发送者ID:', senderId)
console.log('接收者ID:', receiverId)
console.log('当前用户ID:', currentUserId.value)
console.log('商家ID:', merchantId.value)
console.log('消息内容:', content)
// 如果消息与当前聊天无关,跳过
if (!isRelatedToCurrentChat) {
console.log('消息与当前聊天无关,跳过')
return
}
// 检查消息是否已经在列表中(避免重复)
for (let i = 0; i < messages.value.length; i++) {
if (messages.value[i].id == msgId) {
console.log('消息已存在,跳过')
return
}
}
// 如果是自己发送的消息,或者是发给自己的消息,都显示
if (isMyMessage || isForMe) {
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 isMyMessage = (senderId == currentUserId.value)
const isForMe = (receiverId == currentUserId.value)
const isRelatedToCurrentChat = (senderId == merchantId.value || receiverId == merchantId.value)
// 生成安全的 viewId
const safeViewId = 'msg_' + msgId.replace(/[^a-zA-Z0-9]/g, '_')
console.log('=== 条件判断 ===')
console.log('isMyMessage:', isMyMessage)
console.log('isForMe:', isForMe)
console.log('isRelatedToCurrentChat:', isRelatedToCurrentChat)
const incomingMsg: UiChatMessage = {
id: msgId,
viewId: safeViewId,
type: isMyMessage ? 'sent' : 'received',
content: content,
time: timeStr
}
// 如果消息与当前聊天无关,跳过
if (!isRelatedToCurrentChat) {
console.log('消息与当前聊天无关,跳过')
return
}
console.log('=== 添加新消息到列表 ===')
console.log('消息类型:', incomingMsg.type)
console.log('消息内容:', incomingMsg.content)
messages.value.push(incomingMsg)
scrollToBottom()
} else {
console.log('条件不满足,不添加消息')
}
})
.subscribe((status: string, err: any | null) => {
console.log('订阅状态:', status)
if (err != null) {
console.log('订阅错误:', err)
}
})
// 如果是自己发送的消息,或者是发给自己的消息,都显示
if (isMyMessage || isForMe) {
const createdAt = newMsg.getString('created_at') ?? new Date().toISOString()
const date = new Date(createdAt)
const timeStr = ${date.getHours().toString().padStart(2, '0')}:
// 生成安全从 viewId
const safeViewId = 'msg_' + msgId.replace(/[^a-zA-Z0-9]/g, '_')
const incomingMsg: UiChatMessage = {
id: msgId,
viewId: safeViewId,
type: isMyMessage ? 'sent' : 'received',
content: content,
time: timeStr
}
console.log('=== 添加新消息到列表 ===')
console.log('消息类型:', incomingMsg.type)
console.log('消息内容:', incomingMsg.content)
messages.value.push(incomingMsg)
scrollToBottom()
} else {
console.log('条件不满足,不添加消息')
}
})
.subscribe((status: string, err: any | null) => {
console.log('订阅状态', status)
if (err != null) {
console.log('订阅错误:', err)
}
})
}
async function loadChatHistory(): Promise<void> {
@@ -300,7 +300,7 @@ async function loadChatHistory(): Promise<void> {
for (let i = 0; i < sortedRawMsgs.length; i++) {
const m = sortedRawMsgs[i]
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 timeStr = ${date.getHours().toString().padStart(2, '0')}:
const sender = m.sender_id ?? ''
const msgType = (currentUserId.value != '' && sender == currentUserId.value) ? 'sent' : 'received'
@@ -333,42 +333,42 @@ function onScrollToUpper(e: any): void {
}
async function loadMerchantInfo(): Promise<void> {
if (merchantId.value == '') return
try {
const response = await supa
.from('ml_shops')
.select('shop_logo, shop_name')
.eq('merchant_id', merchantId.value)
.limit(1)
.execute()
if (response.error != null) {
console.error('[loadMerchantInfo] 获取商家信息失败:', response.error)
return
}
const rawData = response.data
if (rawData == null) return
const rawList = rawData as any[]
if (rawList.length == 0) return
const shopData = rawList[0]
const shopObj = JSON.parse(JSON.stringify(shopData)) as UTSJSONObject
const logo = shopObj.getString('shop_logo')
if (logo != null && logo != '') {
merchantAvatar.value = logo
}
const name = shopObj.getString('shop_name')
if (name != null && name != '' && headerTitle.value == '在线客服') {
headerTitle.value = name
}
} catch (e) {
console.error('[loadMerchantInfo] 获取商家信息异常:', e)
}
if (merchantId.value == '') return
try {
const response = await supa
.from('ml_shops')
.select('shop_logo, shop_name')
.eq('merchant_id', merchantId.value)
.limit(1)
.execute()
if (response.error != null) {
console.error('[loadMerchantInfo] 获取商家信息失败:', response.error)
return
}
const rawData = response.data
if (rawData == null) return
const rawList = rawData as any[]
if (rawList.length == 0) return
const shopData = rawList[0]
const shopObj = JSON.parse(JSON.stringify(shopData)) as UTSJSONObject
const logo = shopObj.getString('shop_logo')
if (logo != null && logo != '') {
merchantAvatar.value = logo
}
const name = shopObj.getString('shop_name')
if (name != null && name != '' && headerTitle.value == '在线客服') {
headerTitle.value = name
}
} catch (e) {
console.error('[loadMerchantInfo] 获取商家信息异常:', e)
}
}
// 生命周期
@@ -379,33 +379,33 @@ onLoad((options: any) => {
// 状态栏高度 + 10px 原有顶部内边距
navPaddingTop.value = (statusBarH + 10) + 'px'
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
}
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(() => {
supabaseService.ensureSession().then((uid) => {
if (uid != null) {
currentUserId.value = uid
} else {
getCurrentUser().then((user) => {
if (user != null) {
currentUserId.value = user.id ?? ''
}
})
}
supabaseService.ensureSession().then((uid) => {
if (uid != null) {
currentUserId.value = uid
} else {
getCurrentUser().then((user) => {
if (user != null) {
currentUserId.value = user.id ?? ''
}
})
}
loadMerchantInfo()
loadChatHistory()
setupRealtimeSubscription()
})
loadMerchantInfo()
loadChatHistory()
setupRealtimeSubscription()
})
})
onUnmounted(() => {
@@ -425,7 +425,7 @@ const sendMessage = async () => {
if (merchantId.value != '') {
console.log('[sendMessage] 开始发送消息到:', merchantId.value)
const success = await supabaseService.sendMessage(merchantId.value, content)
console.log('[sendMessage] 发送结果:', success)
console.log('[sendMessage] 发送结果', success)
if (!success) {
uni.showToast({
title: '发送失败',
@@ -687,13 +687,23 @@ const goBack = () => {
white-space: pre-wrap;
}
.message-text {
font-size: 15px;
color: #333;
line-height: 1.4;
margin-bottom: 5px;
word-wrap: break-word;
word-break: break-all;
white-space: pre-wrap;
}
.message-time {
font-size: 11px;
color: #999;
text-align: right;
}
/* 聊天输入 */
/* 聊天输入 */
.chat-input {
background-color: white;
border-top: 1px solid #eee;
@@ -781,3 +791,4 @@ const goBack = () => {
/* 响应式适配 removed for strict uv-app-x compliance */
</style>

View File

@@ -1,4 +1,4 @@
<!-- pages/mall/consumer/chat.uvue -->
<!-- pages/mall/consumer/chat.uvue -->
<template>
<view class="chat-page">
<!-- 聊天头部 -->
@@ -7,11 +7,15 @@
<text class="back-icon"></text>
</view>
<view class="header-info">
<text class="chat-title">{{ headerTitle }}</text>
<text class="chat-status">在线</text>
<view class="header-info-text-wrapper">
<text class="chat-title">{{ headerTitle }}</text>
<text class="chat-status">在线</text>
</view>
</view>
<view class="header-actions">
<text class="action-icon" @click="showMoreActions">⋮</text>
<view class="action-icon" @click="showMoreActions">
<text class="action-icon-text">⋯</text>
</view>
</view>
</view>
@@ -20,7 +24,7 @@
scroll-y="true"
class="chat-content"
:scroll-into-view="scrollToView"
:scroll-with-animation="false"
:scroll-with-animation="true"
:show-scrollbar="false"
upper-threshold="100"
@scrolltoupper="onScrollToUpper"
@@ -53,7 +57,7 @@
/>
<view class="message-content-wrapper">
<text class="sender-name">{{ headerTitle }}</text>
<view class="message-bubble">
<view class="message-bubble received-bubble">
<text class="message-text">{{ message.content }}</text>
<text class="message-time">{{ message.time }}</text>
</view>
@@ -158,25 +162,34 @@ function scrollToBottom() : void {
// 获取最后一条消息的 ID
const lastMsg = messages.value[messages.value.length - 1]
const targetId = 'msg-' + lastMsg.id
const targetId = lastMsg.viewId
// 关键点:在 UVue 安卓端,直接连续赋值可能被合并。
// 我们先清 ID然后在下一帧赋值确保 scroll-view 监听到变化。
// 我们先清 ID然后在下一帧赋值确保 scroll-view 监听到变化。
scrollToView.value = ''
// 增加多次尝试,确保在 DOM 彻底完成渲染(包含由于高度计算引起的多次排版)后定位。
// 延迟更久一点,确保安卓端列表排版彻底完成
setTimeout(() => {
scrollToView.value = targetId
console.log('[scrollToBottom] 发起第一次滚动定位:', targetId)
console.log('[scrollToBottom] 发起滚动定位:', targetId)
// 二次校准:针对长消息或图片导致的高度变化
// 分级校准:针对长消息或渲染抖动导致的高度变化
setTimeout(() => {
scrollToView.value = ''
setTimeout(() => {
scrollToView.value = targetId
console.log('[scrollToBottom] 二次校准完成:', targetId)
console.log('[scrollToBottom] 第一阶段校准:', targetId)
}, 50)
}, 100)
}, 500)
// 最终深度校准(针对首屏数据较多时)
setTimeout(() => {
scrollToView.value = ''
setTimeout(() => {
scrollToView.value = targetId
console.log('[scrollToBottom] 最终校准:', targetId)
}, 50)
}, 1200)
}, 300)
}
@@ -522,7 +535,7 @@ const goBack = () => {
<style>
.chat-page {
width: 100%;
height: 100vh;
flex: 1;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
@@ -552,7 +565,12 @@ const goBack = () => {
.header-info {
flex: 1;
text-align: center;
}
.header-info-text-wrapper {
display: flex;
flex-direction: column;
align-items: center;
}
.chat-title {
@@ -571,7 +589,11 @@ const goBack = () => {
font-size: 20px;
color: #333;
width: 40px;
}
.action-icon-text {
text-align: right;
width: 100%;
}
/* 聊天内容区 */
@@ -591,7 +613,9 @@ const goBack = () => {
/* 系统消息 */
.message-item.system {
text-align: center;
display: flex;
flex-direction: row;
justify-content: center;
margin-bottom: 20px;
}
@@ -601,12 +625,14 @@ const goBack = () => {
background-color: #f0f0f0;
padding: 5px 15px;
border-radius: 15px;
text-align: center;
}
/* 时间分割线 */
.time-divider {
text-align: center;
display: flex;
flex-direction: row;
justify-content: center;
margin: 20px 0;
}
@@ -616,6 +642,7 @@ const goBack = () => {
background-color: #f0f0f0;
padding: 3px 10px;
border-radius: 10px;
text-align: center;
}
/* 消息项 */
@@ -644,7 +671,7 @@ const goBack = () => {
}
.message-content-wrapper {
max-width: 70%;
width: 260px;
display: flex;
flex-direction: column;
}
@@ -655,9 +682,11 @@ const goBack = () => {
border-radius: 12px;
position: relative;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
align-self: flex-start; /* 关键:根据内容宽度自适应,不撑满 */
word-wrap: break-word;
word-break: break-all;
}
.received-bubble {
align-self: flex-start;
border-top-left-radius: 2px;
}
.message-bubble.me {
@@ -666,10 +695,6 @@ const goBack = () => {
border-top-right-radius: 2px;
}
.message-bubble:not(.me) {
border-top-left-radius: 2px;
}
.sender-name {
font-size: 11px;
color: #999;
@@ -682,8 +707,6 @@ const goBack = () => {
color: #333;
line-height: 1.4;
margin-bottom: 5px;
word-wrap: break-word;
word-break: break-all;
white-space: pre-wrap;
}
@@ -781,3 +804,4 @@ const goBack = () => {
/* 响应式适配 removed for strict uv-app-x compliance */
</style>

View File

@@ -1,22 +1,22 @@
<template>
<template>
<view class="chat-page">
<!-- 聊天头部 -->
<!-- 鑱婂ぉ澶撮儴 -->
<view class="chat-header" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="header-content">
<view class="header-back" @click="goBack">
<text class="back-icon"></text>
<text class="back-icon">鈥?/text>
</view>
<view class="header-info">
<text class="chat-title">{{ merchantName || '在线客服' }}</text>
<text class="chat-status">在线</text>
<text class="chat-title">{{ merchantName || '鍦ㄧ嚎瀹㈡湇' }}</text>
<text class="chat-status">鍦ㄧ嚎</text>
</view>
<view class="header-actions">
<text class="action-icon" @click="showMoreActions">⋮</text>
<text class="action-icon" @click="showMoreActions">鈰?/text>
</view>
</view>
</view>
<!-- 聊天内容 -->
<!-- 鑱婂ぉ鍐呭 -->
<scroll-view
scroll-y
class="chat-content"
@@ -24,29 +24,29 @@
scroll-with-animation
@scrolltoupper="loadMoreHistory"
>
<!-- 占位,防止内容被头部遮挡 -->
<!-- 鍗犱綅锛岄槻姝㈠唴瀹硅澶撮儴閬尅 -->
<view :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 聊天消息列表 -->
<!-- 鑱婂ぉ娑堟伅鍒楄〃 -->
<view class="chat-messages">
<!-- 系统提示 -->
<!-- 绯荤粺鎻愮ず -->
<view class="message-item system">
<text class="system-text">已连接到商家,开始聊天吧</text>
<text class="system-text">宸茶繛鎺ュ埌鍟嗗锛屽紑濮嬭亰澶╁惂</text>
</view>
<!-- 消息项 -->
<!-- 娑堟伅椤?-->
<view
v-for="(message, index) in messages"
:key="message.id"
:class="['message-item', message.type]"
:id="'msg-' + message.id"
>
<!-- 时间显示逻辑每5分钟显示一次时间 -->
<!-- 鏃堕棿鏄剧ず閫昏緫锛氭瘡5鍒嗛挓鏄剧ず涓€娆℃椂闂?-->
<view v-if="shouldShowTime(index)" class="time-divider">
<text>{{ formatTime(message.rawTime) }}</text>
</view>
<!-- 对方消息 -->
<!-- 瀵规柟娑堟伅 -->
<view v-if="message.type === 'received'" class="message-wrapper">
<image
class="avatar"
@@ -61,7 +61,7 @@
</view>
</view>
<!-- 我的消息 -->
<!-- 鎴戠殑娑堟伅 -->
<view v-else class="message-wrapper me">
<view class="message-content-wrapper">
<view class="message-bubble me">
@@ -75,24 +75,24 @@
/>
</view>
</view>
<!-- 底部填充,防止被输入框遮挡 -->
<!-- 搴曢儴濉厖锛岄槻姝㈣杈撳叆妗嗛伄鎸?-->
<view style="height: 20px;"></view>
<view id="bottom-anchor" style="height: 1px;"></view>
</view>
</scroll-view>
<!-- 聊天输入区 -->
<!-- 鑱婂ぉ杈撳叆鍖?-->
<view class="chat-input-area">
<view class="input-tools">
<text class="tool-icon" @click="toggleEmoji">😊</text>
<!-- <text class="tool-icon" @click="chooseImage">📷</text> -->
<text class="tool-icon" @click="toggleEmoji">馃槉</text>
<!-- <text class="tool-icon" @click="chooseImage">馃摲</text> -->
</view>
<view class="input-wrapper">
<input
class="message-input"
v-model="inputMessage"
placeholder="请输入消息..."
placeholder="璇疯緭鍏ユ秷鎭?.."
:adjust-position="true"
confirm-type="send"
@confirm="sendMessage"
@@ -102,11 +102,11 @@
:class="{ active: inputMessage.trim().length > 0 }"
@click="sendMessage"
>
发送
鍙戦€?
</button>
</view>
<!-- 表情选择器 (简化版) -->
<!-- 琛ㄦ儏閫夋嫨鍣?(绠€鍖栫増) -->
<scroll-view scroll-y v-if="showEmoji" class="emoji-picker">
<view class="emoji-grid">
<text
@@ -131,13 +131,13 @@ import type { ChatMessage } from '@/utils/supabaseService.uts'
import supa from '@/components/supadb/aksupainstance.uts'
// import { getCurrentUser } from '@/utils/store.uts'
// 界面状态
// 鐣岄潰鐘舵€?
const statusBarHeight = ref(0)
const scrollToView = ref('')
const showEmoji = ref(false)
const inputMessage = ref('')
// 业务数据
// 涓氬姟鏁版嵁
const merchantId = ref('')
const merchantName = ref('')
const merchantLogo = ref('')
@@ -145,38 +145,38 @@ const userAvatar = ref('')
const currentUserId = ref('')
const messages = ref<any[]>([])
const emojiList = ['😊', '😂', '👍', '👌', '❤️', '🌹', '🙏', '🎉', '😡', '😭', '🤔', '👋', '🤝', '💊', '🏥']
const emojiList = ['馃槉', '馃槀', '馃憤', '馃憣', '鉂わ笍', '馃尮', '馃檹', '馃帀', '馃槨', '馃槶', '馃', '馃憢', '馃', '馃拪', '馃彞']
let realtimeChannel: any | null = null
onLoad((options: any) => {
// 获取状态栏高度
// 鑾峰彇鐘舵€佹爮楂樺害
const sys = uni.getSystemInfoSync()
statusBarHeight.value = sys.statusBarHeight ?? 0
// 获取参数
// 鑾峰彇鍙傛暟
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') ?? '商家'
merchantName.value = optObj.getString('merchantName') ?? '鍟嗗'
merchantLogo.value = optObj.getString('merchantLogo') ?? ''
console.log('开始聊天,商家ID:', merchantId.value)
console.log('寮€濮嬭亰澶╋紝鍟嗗ID:', merchantId.value)
} else {
merchantName.value = '平台客服'
merchantName.value = '骞冲彴瀹㈡湇'
}
// 获取当前用户
// 鑾峰彇褰撳墠鐢ㄦ埛
const uid = supabaseService.getCurrentUserId()
if (uid != null) {
currentUserId.value = uid
// 简单获取一下头像实际应该从Profile获取
// 绠€鍗曡幏鍙栦竴涓嬪ご鍍忥紝瀹為檯搴旇浠嶱rofile鑾峰彇
userAvatar.value = 'https://picsum.photos/100'
}
// 加载历史消息
// 鍔犺浇鍘嗗彶娑堟伅
loadHistory()
// 开启实时订阅
// 寮€鍚疄鏃惰闃?
startRealtimeSubscription()
})
@@ -186,15 +186,15 @@ onUnmounted(() => {
}
})
// 加载历史记录
// 鍔犺浇鍘嗗彶璁板綍
const loadHistory = async () => {
let rawMsgs: ChatMessage[] = []
if (merchantId.value) {
// 获取与特定商家的聊天
// 鑾峰彇涓庣壒瀹氬晢瀹剁殑鑱婂ぉ
rawMsgs = await supabaseService.getChatMessages(merchantId.value)
} else {
// 获取所有(比如客服)
// 鑾峰彇鎵€鏈夛紙姣斿瀹㈡湇锛?
rawMsgs = await supabaseService.getUserChatMessages()
}
@@ -205,14 +205,14 @@ const loadHistory = async () => {
}
const loadMoreHistory = () => {
// TODO: 实现下拉加载更多历史
// TODO: 瀹炵幇涓嬫媺鍔犺浇鏇村鍘嗗彶
}
// 开启实时订阅
// 寮€鍚疄鏃惰闃?
const startRealtimeSubscription = () => {
if (currentUserId.value == '') return
console.log('开启消息监听...')
console.log('寮€鍚秷鎭洃鍚?..')
const filterObj = ({
event: 'INSERT',
schema: 'public',
@@ -223,12 +223,12 @@ const startRealtimeSubscription = () => {
'postgres_changes',
filterObj,
(payload: any) => {
console.log('收到新消息:', payload)
console.log('鏀跺埌鏂版秷鎭?', payload)
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)
// 只有来自当前聊天的商家的消息才显示,或者如果是全局客服模式
// 鍙湁鏉ヨ嚜褰撳墠鑱婂ぉ鐨勫晢瀹剁殑娑堟伅鎵嶆樉绀猴紝鎴栬€呭鏋滄槸鍏ㄥ眬瀹㈡湇妯″紡
const senderId = newMsg.getString('sender_id') ?? ''
if (senderId === merchantId.value || merchantId.value == '') {
const formatted = formatMessage({
@@ -237,14 +237,14 @@ const startRealtimeSubscription = () => {
msg_type: newMsg.getString('msg_type') ?? '',
sender_id: senderId,
receiver_id: newMsg.getString('receiver_id') ?? '',
is_from_user: false, // 收到的一定不是自己发的
is_from_user: false, // 鏀跺埌鐨勪竴瀹氫笉鏄嚜宸卞彂鐨?
created_at: newMsg.getString('created_at') ?? ''
} as ChatMessage)
messages.value.push(formatted)
scrollToBottom()
// 震动提示
// 闇囧姩鎻愮ず
uni.vibrateShort({})
}
}
@@ -252,11 +252,11 @@ const startRealtimeSubscription = () => {
.subscribe()
}
// 格式化消息
// 鏍煎紡鍖栨秷鎭?
const formatMessage = (m: ChatMessage): any => {
// 如果 sender_id 是自己,就是 'sent',否则 'received'
// 注意:数据库字段 is_from_user 有时可能只是标记是否由C端用户发起
// 最准确的是对比 id
// 濡傛灉 sender_id 鏄嚜宸憋紝灏辨槸 'sent'锛屽惁鍒?'received'
// 娉ㄦ剰锛氭暟鎹簱瀛楁 is_from_user 鏈夋椂鍙兘鍙槸鏍囪鏄惁鐢盋绔敤鎴峰彂璧凤紝
// 鏈€鍑嗙‘鐨勬槸瀵规瘮 id
let isMe = false
if (currentUserId.value) {
isMe = m.sender_id === currentUserId.value
@@ -277,7 +277,7 @@ const sendMessage = async () => {
const text = inputMessage.value.trim()
if (text == '') return
// 乐观更新 UI
// 涔愯鏇存柊 UI
const tempId = 'temp_' + Date.now()
const tempMsg = {
id: tempId,
@@ -291,21 +291,21 @@ const sendMessage = async () => {
scrollToBottom()
showEmoji.value = false
// 发送请求
// 注意:如果 merchantId 为空sendChatMessage 第二个参数传 null会变成无主消息
// 鍙戦€佽姹?
// 娉ㄦ剰锛氬鏋?merchantId 涓虹┖锛宻endChatMessage 绗簩涓弬鏁颁紶 null锛屼細鍙樻垚鏃犱富娑堟伅
const success = await supabaseService.sendChatMessage(text, merchantId.value ? merchantId.value : null)
if (!success) {
uni.showToast({ title: '发送失败', icon: 'none' })
// 这里可以加一个重试按钮或移除消息
uni.showToast({ title: '鍙戦€佸け璐?, icon: 'none' })
// 杩欓噷鍙互鍔犱竴涓噸璇曟寜閽垨绉婚櫎娑堟伅
}
}
const scrollToBottom = () => {
// 延时滚动以确保视图更新
// 寤舵椂婊氬姩浠ョ‘淇濊鍥炬洿鏂?
setTimeout(() => {
scrollToView.value = 'bottom-anchor'
// Hack: 重置再设置以强制触发
// Hack: 閲嶇疆鍐嶈缃互寮哄埗瑙﹀彂
setTimeout(() => {
scrollToView.value = 'msg-' + (messages.value.length > 0 ? messages.value[messages.value.length-1].id : '')
}, 50)
@@ -320,13 +320,13 @@ const formatTime = (isoString: string): string => {
const date = new Date(isoString)
const now = new Date()
// 如果是今天,显示 HH:mm
// 濡傛灉鏄粖澶╋紝鏄剧ず HH:mm
if (date.toDateString() === now.toDateString()) {
const h = date.getHours().toString().padStart(2, '0')
const m = date.getMinutes().toString().padStart(2, '0')
return `${h}:${m}`
}
// 否则显示 MM-DD HH:mm
// 鍚﹀垯鏄剧ず MM-DD HH:mm
const mo = (date.getMonth() + 1).toString().padStart(2, '0')
const d = date.getDate().toString().padStart(2, '0')
const h = date.getHours().toString().padStart(2, '0')
@@ -340,7 +340,7 @@ const shouldShowTime = (index: number): boolean => {
const curr = messages.value[index]
const t1 = new Date(prev.rawTime).getTime()
const t2 = new Date(curr.rawTime).getTime()
// 间隔超过5分钟(300000ms)显示时间
// 闂撮殧瓒呰繃5鍒嗛挓(300000ms)鏄剧ず鏃堕棿
return (t2 - t1) > 300000
}
@@ -357,10 +357,10 @@ const insertEmoji = (emoji: string) => {
const showMoreActions = () => {
uni.showActionSheet({
itemList: ['清空记录', '投诉商家'],
itemList: ['娓呯┖璁板綍', '鎶曡瘔鍟嗗'],
success: (res) => {
if (res.tapIndex === 0) {
messages.value = [] // 仅本地清空
messages.value = [] // 浠呮湰鍦版竻绌?
}
}
})
@@ -620,3 +620,4 @@ const showMoreActions = () => {
}
</style>

View File

@@ -1,4 +1,4 @@
<!-- 结算页面 -->
<!-- 结算页面 -->
<template>
<view class="checkout-page">
<scroll-view class="checkout-content" scroll-y>
@@ -137,7 +137,7 @@
<text class="popup-close" @click="showAddressPopup = false">×</text>
</view>
<scroll-view class="address-list-container" scroll-y>
<scroll-view class="address-list-container" scroll-y="true" :scroll-with-animation="true">
<!-- 登录提示 -->
<view v-if="isLoggedIn == false" class="login-prompt" @click="goToLogin">
<text class="login-prompt-icon">🔒</text>
@@ -210,53 +210,63 @@
<view class="address-form-popup" @click.stop>
<view class="form-header">
<text class="form-title">新建收货地址</text>
<text class="form-close" @click="cancelNewAddress">×</text>
<view class="form-close-btn" @click="cancelNewAddress">
<text class="form-close-icon">✕</text>
</view>
</view>
<scroll-view class="form-content" scroll-y>
<view class="form-item">
<text class="form-label">收货人</text>
<input class="form-input" v-model="newAddress.recipient_name"
placeholder="请输入收货人姓名" />
</view>
<view class="form-item">
<text class="form-label">手机号</text>
<input class="form-input" v-model="newAddress.phone"
placeholder="请输入手机号码" type="number" />
</view>
<view class="form-item">
<text class="form-label">智能填写地址</text>
<textarea class="form-textarea smart-address-input"
v-model="smartAddressInput"
placeholder="粘贴如北京市朝阳区三里屯SOHO A座 张三 13800138000"
@input="parseSmartAddress"
maxlength="200" />
<text class="smart-tip">自动识别:地址+姓名+电话(支持粘贴文本)</text>
</view>
<view class="form-item">
<text class="form-label">所在地区</text>
<view class="region-inputs">
<input class="form-input region-input form-input-readonly" v-model="newAddress.province"
placeholder="省" readonly />
<input class="form-input region-input form-input-readonly" v-model="newAddress.city"
placeholder="市" readonly />
<input class="form-input region-input form-input-readonly" v-model="newAddress.district"
placeholder="区/县" readonly />
<scroll-view class="form-content" scroll-y="true">
<view class="form-section">
<view class="form-item">
<text class="form-label">收货人</text>
<input class="form-input" v-model="newAddress.recipient_name"
placeholder="请输入收货人姓名" />
</view>
<view class="form-item">
<text class="form-label">手机号</text>
<input class="form-input" v-model="newAddress.phone"
placeholder="请输入手机号码" type="number" />
</view>
</view>
<view class="form-item">
<text class="form-label">详细地址</text>
<textarea class="form-textarea" v-model="newAddress.detail"
placeholder="街道、小区、楼栋、门牌号等"
maxlength="100" />
<view class="form-section">
<view class="form-item">
<view class="label-row">
<text class="form-label">智能填写</text>
<text class="smart-tag">识别姓名/电话/地址</text>
</view>
<textarea class="form-textarea smart-address-input"
v-model="smartAddressInput"
placeholder="粘贴收货信息文本,自动拆分字段"
@input="parseSmartAddress"
maxlength="200" />
</view>
</view>
<view class="form-section">
<view class="form-item">
<text class="form-label">所在地区</text>
<view class="region-inputs">
<input class="form-input region-input form-input-readonly" v-model="newAddress.province"
placeholder="省" readonly />
<input class="form-input region-input form-input-readonly" v-model="newAddress.city"
placeholder="市" readonly />
<input class="form-input region-input form-input-readonly" v-model="newAddress.district"
placeholder="区/县" readonly />
</view>
</view>
<view class="form-item">
<text class="form-label">详细地址</text>
<textarea class="form-textarea detail-textarea" v-model="newAddress.detail"
placeholder="如街道、楼栋、门牌号等"
maxlength="100" />
</view>
</view>
<view class="form-item checkbox-item">
<view class="checkbox-wrapper" @click="newAddress.is_default = newAddress.is_default ? false : true">
<view class="checkbox-wrapper" @click="newAddress.is_default = !newAddress.is_default">
<view :class="['checkbox', { checked: newAddress.is_default }]">
<text v-if="newAddress.is_default" class="checkbox-check">✓</text>
</view>
@@ -266,8 +276,7 @@
</scroll-view>
<view class="form-buttons">
<button class="form-cancel-btn" @click="cancelNewAddress">取消</button>
<button class="form-submit-btn" @click="saveNewAddress">保存</button>
<button class="form-submit-btn" @click="saveNewAddress">保存并使用</button>
</view>
</view>
</view>
@@ -1511,58 +1520,259 @@ const goToLogin = () => {
/* 弹窗样式 */
.address-popup-mask, .address-form-mask, .confirm-popup-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); z-index: 9998; }
.address-popup-mask { display: flex; align-items: flex-end; justify-content: center; }
.address-form-mask, .confirm-popup-mask { display: flex; align-items: center; justify-content: center; }
.address-popup { background-color: #ffffff; width: 100%; height: 500px; border-radius: 20px 20px 0 0; display: flex; flex-direction: column; }
.address-form-popup { background-color: #ffffff; width: 90%; max-width: 500px; height: 600px; border-radius: 12px; display: flex; flex-direction: column; }
.address-form-mask, .confirm-popup-mask { display: flex; align-items: center; justify-content: center; z-index: 10000; }
.address-popup {
background-color: #ffffff;
width: 100%;
border-radius: 20px 20px 0 0;
display: flex;
flex-direction: column;
position: relative;
z-index: 9999;
}
.address-form-popup {
background-color: #f8f8f8;
width: 92%;
max-width: 500px;
height: 85vh;
border-radius: 16px;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
z-index: 10001;
}
.popup-header {
padding: 16px;
border-bottom: 0.5px solid #eee;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: relative;
}
.popup-title { font-size: 17px; font-weight: bold; color: #333333; }
.popup-close { position: absolute; right: 16px; font-size: 20px; color: #999999; padding: 4px; }
.address-list-container {
flex: 1;
width: 100%;
padding: 12px;
box-sizing: border-box;
}
.popup-address-item {
padding: 16px;
margin-bottom: 12px;
background-color: #fff;
border: 1px solid #f0f0f0;
border-radius: 12px;
position: relative;
}
.popup-address-header {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 8px;
}
.popup-address-name { font-size: 15px; font-weight: bold; color: #333333; margin-right: 12px; }
.popup-address-phone { font-size: 14px; color: #666666; margin-right: 8px; }
.popup-default-tag { background-color: #fff0eb; padding: 1px 6px; border-radius: 4px; }
.popup-tag-text { color: #ff5000; font-size: 10px; }
.popup-address-detail { font-size: 13px; color: #666; line-height: 1.4; }
.popup-selected-indicator {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
color: #ff5000;
font-weight: bold;
font-size: 18px;
}
.popup-empty-address { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 20px; }
.popup-empty-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.3; }
.popup-empty-text { font-size: 14px; color: #999; }
.popup-add-address-btn {
background-color: #ff5000;
margin: 12px 16px 30px;
height: 44px;
border-radius: 22px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.popup-btn-icon { color: #ffffff; font-size: 20px; margin-right: 6px; font-weight: 300; }
.popup-btn-text { color: #ffffff; font-size: 15px; font-weight: bold; }
.address-form-popup {
background-color: #f8f8f8;
width: 92%;
max-width: 500px;
height: 85vh;
border-radius: 16px;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
z-index: 10000;
}
.form-header {
padding: 16px;
background-color: #ffffff;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
position: relative;
border-bottom: 0.5px solid #eee;
flex-shrink: 0;
}
.form-title { font-size: 17px; font-weight: bold; color: #333333; }
.form-close-btn { position: absolute; right: 12px; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; }
.form-close-icon { font-size: 18px; color: #999; }
.form-content {
flex: 1;
height: 100px; /* 进一步减小初始高度,确保安卓端 flex:1 接管 */
min-height: 0;
padding: 12px;
}
.address-form-popup {
background-color: #f8f8f8;
width: 92%;
max-width: 500px;
height: 80vh; /* 稍微压低高度确保不在边缘产生冲突 */
border-radius: 16px;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
z-index: 10001;
}
.form-section {
background-color: #ffffff;
border-radius: 12px;
padding: 0 12px;
margin-bottom: 12px;
}
.form-item {
padding: 16px 0;
border-bottom: 0.5px solid #f5f5f5;
}
.form-item:last-child {
border-bottom: none;
}
.form-label {
font-size: 14px;
color: #333;
margin-bottom: 10px;
display: block;
}
.label-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.smart-tag {
font-size: 11px;
color: #ff5000;
background-color: #fff0eb;
padding: 2px 6px;
border-radius: 4px;
}
.form-input {
width: 100%;
height: 32px;
font-size: 15px;
color: #333;
}
.form-input-readonly { color: #888; }
.region-inputs {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.region-input { flex: 1; text-align: center; }
.form-textarea {
width: 100%;
background-color: #f9f9f9;
border-radius: 8px;
padding: 10px;
font-size: 14px;
color: #333;
box-sizing: border-box;
}
.smart-address-input { height: 80px; }
.detail-textarea { height: 60px; }
.checkbox-item {
background-color: #ffffff;
border-radius: 12px;
padding: 16px;
}
.checkbox-wrapper { display: flex; flex-direction: row; align-items: center; }
.checkbox { width: 18px; height: 18px; border: 1.5px solid #ddd; border-radius: 50%; margin-right: 10px; display: flex; align-items: center; justify-content: center; }
.checkbox.checked { background-color: #ff5000; border-color: #ff5000; }
.checkbox-check { color: #ffffff; font-size: 12px; }
.checkbox-label { font-size: 14px; color: #333; }
.form-buttons {
padding: 12px 16px 30px;
background-color: #ffffff;
flex-shrink: 0;
}
.form-submit-btn {
width: 100%;
background-color: #ff5000;
color: #ffffff;
height: 44px;
line-height: 44px;
border-radius: 22px;
font-size: 16px;
font-weight: bold;
border: none;
}
/* 确认保存弹窗 */
.confirm-popup-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); z-index: 9998; display: flex; align-items: center; justify-content: center; }
.confirm-popup { background-color: #ffffff; width: 80%; max-width: 320px; border-radius: 12px; overflow: hidden; }
.popup-header, .form-header { padding: 20px 15px; border-bottom: 1px solid #e5e5e5; display: flex; align-items: center; justify-content: space-between; }
.popup-title, .form-title { font-size: 18px; font-weight: bold; color: #333333; }
.popup-close, .form-close { font-size: 24px; color: #999999; padding: 5px; }
.address-list-container, .form-content { flex: 1; padding: 15px; }
.popup-address-item { padding: 15px; margin-bottom: 10px; border: 1px solid #e5e5e5; border-radius: 8px; position: relative; }
.popup-address-header { display: flex; align-items: center; margin-bottom: 10px; }
.popup-address-name { font-size: 16px; font-weight: bold; color: #333333; margin-right: 15px; }
.popup-address-phone { font-size: 14px; color: #666666; margin-right: 10px; }
.popup-default-tag { background-color: #ff4757; padding: 2px 8px; border-radius: 10px; }
.popup-tag-text { color: #ffffff; font-size: 12px; }
.popup-address-detail { font-size: 14px; color: #333333; line-height: 1.4; }
.popup-empty-address { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 20px; }
.popup-empty-icon { font-size: 60px; margin-bottom: 15px; }
.popup-empty-text { font-size: 16px; color: #999999; }
.popup-add-address-btn { background-color: #007aff; margin: 15px; padding: 15px; border-radius: 8px; display: flex; align-items: center; justify-content: center; }
.popup-btn-icon { color: #ffffff; font-size: 20px; margin-right: 10px; }
.popup-btn-text { color: #ffffff; font-size: 16px; font-weight: bold; }
.form-item { margin-bottom: 20px; }
.form-label { font-size: 14px; color: #333333; margin-bottom: 8px; }
.form-input { width: 100%; padding: 12px; border: 1px solid #e5e5e5; border-radius: 8px; font-size: 14px; color: #333333; box-sizing: border-box; }
.form-input-readonly { background-color: #f9f9f9; color: #666666; }
.region-inputs { display: flex; justify-content: space-between; }
.region-input { flex: 1; margin-right: 10px; }
.region-input:last-child { margin-right: 0; }
.form-textarea { width: 100%; min-height: 80px; padding: 12px; border: 1px solid #e5e5e5; border-radius: 8px; font-size: 14px; color: #333333; box-sizing: border-box; }
.smart-address-input { min-height: 60px; }
.smart-tip { font-size: 12px; color: #999999; margin-top: 5px; }
.checkbox-item { margin-top: 20px; }
.checkbox-wrapper { display: flex; align-items: center; }
.checkbox { width: 20px; height: 20px; border: 1px solid #e5e5e5; border-radius: 4px; margin-right: 10px; display: flex; align-items: center; justify-content: center; }
.checkbox.checked { background-color: #007aff; border-color: #007aff; }
.checkbox-check { color: #ffffff; font-size: 14px; }
.checkbox-label { font-size: 14px; color: #333333; }
.form-buttons { display: flex; padding: 15px; border-top: 1px solid #e5e5e5; }
.form-cancel-btn { flex: 1; background-color: #f5f5f5; color: #333333; padding: 12px; border-radius: 8px; font-size: 16px; border: none; margin-right: 10px; }
.form-submit-btn { flex: 1; background-color: #007aff; color: #ffffff; padding: 12px; border-radius: 8px; font-size: 16px; border: none; }
.confirm-header { padding: 20px 0 10px; text-align: center; }
.confirm-title { font-size: 18px; font-weight: bold; color: #333333; }
.confirm-content { padding: 0 20px 20px; text-align: center; }
.confirm-message { font-size: 16px; color: #666666; }
.confirm-buttons { display: flex; border-top: 1px solid #e5e5e5; }
.confirm-btn { flex: 1; height: 50px; line-height: 50px; text-align: center; font-size: 16px; background-color: #ffffff; border: none; border-radius: 0; }
.confirm-btn.cancel { color: #666666; border-right: 1px solid #e5e5e5; }
.confirm-btn.confirm { color: #007aff; font-weight: bold; }
</style>
.confirm-header { padding: 24px 0 12px; text-align: center; }
.confirm-title { font-size: 17px; font-weight: bold; color: #333; }
.confirm-content { padding: 0 24px 24px; text-align: center; }
.confirm-message { font-size: 14px; color: #666; line-height: 1.5; }
.confirm-buttons { display: flex; flex-direction: row; border-top: 0.5px solid #eee; }
.confirm-btn { flex: 1; height: 48px; line-height: 48px; text-align: center; font-size: 16px; background-color: #ffffff; border: none; border-radius: 0; }
.confirm-btn.cancel { color: #666; border-right: 0.5px solid #eee; }
.confirm-btn.confirm { color: #ff5000; font-weight: bold; }
</style>

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="coupons-page">
<view class="coupon-list">
<view v-if="coupons.length === 0" class="empty-state">
@@ -159,4 +159,4 @@ const useCoupon = (coupon: Coupon) => {
border-radius: 15px;
line-height: 1.5;
}
</style>
</style>

View File

@@ -4526,10 +4526,38 @@
- 使用单行字符串格式:`'*, table1(*), table2(field1, field2)'`
- 如果关联查询失败,使用替代方案
================================================================================
一百二十七、Android 特有 UI 与交互规范(最新补充)
================================================================================
1. 图标兼容性Emoji 风险)
- **现象**:在 Android 真机上直接使用 Emoji如 🛒、❤️)可能导致显示不全、模糊或样式不统一。
- **规范**:所有功能图标**必须使用本地 PNG 图片**。
- **路径**:底部导航图标放在 `static/tabbar/`,业务小图标放在 `pages/mall/consumer/icons/`。
- **TabBar 特别强调**`pages.json` 中的 `tabBar` 图标路径严禁使用 `.svg`,必须使用 `.png` 以确保安卓稳定性。
2. 布局高度计算修复
- **场景**Chat 页面或带有固定底部的详情页。
- **规范**`scroll-view` 的父级必须是 `flex: column`,且调用 `scroll-view` 时应配合 `flex: 1; height: 0;`。
- **双重滚动校准**Android 初始渲染可能导致滚动不到底。
```uts
setTimeout(() => { this.scrollTop = 99999 }, 200); // 首次快速定位
setTimeout(() => { this.scrollTop = 99999 + Math.random() }, 500); // 二次精准校准
```
3. 空状态展示(去行业化文案)
- **规范**:在数据加载中或为空时,严禁使用行业特定干扰词(如“暂无药品”)。
- **方案**:统一使用“正在加载商品...”或“该分类下暂无商品”,并辅以 `loading-state` 动画,提升加载感知。
4. SKU 解析与展示规范
- **规范**:从数据库返回的 JSON 规格数据(如 `sku_spec_attr`)必须通过格式化函数转换为易读文本。
- **示例**`[{"key":"规格","value":"10mg*7片"}]` -> `规格: 10mg*7片`。
- **技巧**:在 `uts` 中处理 `UTSJSONObject` 时,推荐使用 `getString("value")` 进行容错处理。
================================================================================
文档结束
================================================================================
---
*最后更新2026-03-03*
*最后更新2026-03-04*

View File

@@ -1,304 +1,591 @@
<!-- 收藏页面 -->
<template>
<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>
<text class="empty-text">暂无收藏商品</text>
<button class="go-shopping-btn" @click="goShopping">去逛逛</button>
</view>
<view v-else v-for="(product, index) in favorites" :key="index" class="product-item" @click="goToDetail(product.id)">
<image :src="product.main_image_url" class="product-image" mode="aspectFill" />
<text class="product-name" :lines="2">{{ product.name }}</text>
<view class="product-bottom">
<text class="product-price">¥{{ product.price }}</text>
<view class="product-add-btn" @click.stop="addToCart(product)">
<text class="add-icon">+</text>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="favorites-page">
<view class="favorites-header">
<view v-if="favorites.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="favorites-content" :scroll-y="true">
<view v-if="favorites.length === 0 && !isLoading" class="empty-favorites">
<text class="empty-icon">❤️</text>
<text class="empty-text">暂无收藏商品</text>
<text class="empty-subtext">快去收藏喜欢的商品吧</text>
<button class="go-shopping-btn" @click="goShopping">去逛逛</button>
</view>
<view class="product-group">
<view class="group-items">
<view v-for="(product, index) in favorites" :key="index" class="product-item">
<view v-if="isEditMode" class="item-selector" @click="toggleSelect(product)">
<view :class="['select-icon', { selected: product.selected === true }]">
<text v-if="product.selected === true" class="icon-text">✓</text>
</view>
</view>
<view class="item-content" @click="viewProduct(product)">
<image class="product-image" :src="product.main_image_url" mode="aspectFill" />
<text class="product-name" :lines="2">{{ product.name }}</text>
<view class="product-bottom">
<text class="product-price">¥{{ product.price }}</text>
<view v-if="!isEditMode" class="product-add-btn" @click.stop="addToCart(product)">
<text class="add-icon">+</text>
</view>
</view>
</view>
</view>
</view>
</view>
<view v-if="isLoading" class="loading-more">
<text class="loading-text">加载中...</text>
</view>
<view v-if="!isLoading && favorites.length > 0" class="no-more">
<text class="no-more-text">没有更多了</text>
</view>
</scroll-view>
<view v-if="isEditMode && favorites.length > 0" class="edit-bar">
<view class="select-all" @click="toggleSelectAll">
<view :class="['all-select-icon', { selected: isAllSelected }]">
<text v-if="isAllSelected" class="icon-text">✓</text>
</view>
<text class="select-all-text">全选</text>
</view>
<view class="delete-btn" @click="deleteSelected">
<text class="delete-text">删除({{ selectedCount }})</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { supabaseService } from '@/utils/supabaseService.uts'
import type { Product } from '@/utils/supabaseService.uts'
const favorites = ref<Array<Product>>([])
const addToCart = async (product: Product) => {
uni.showLoading({ title: '检查商品...' })
try {
const merchantId = product.merchant_id ?? product.shop_id ?? ''
// 检查商品是否有SKU
const skus = await supabaseService.getProductSkus(product.id)
uni.hideLoading()
if (skus.length > 0) {
// 有规格,提示并跳转到商品详情页选择规格
uni.showToast({ title: '请选择规格', icon: 'none' })
setTimeout(() => {
uni.navigateTo({
url: '/pages/mall/consumer/product-detail?id=' + product.id
})
}, 500)
} else {
// 无规格,直接加入购物车
uni.showLoading({ title: '添加中...' })
const success = await supabaseService.addToCart(product.id, 1, '', merchantId)
uni.hideLoading()
if (success) {
uni.showToast({ title: '已添加到购物车', icon: 'success' })
} else {
uni.showToast({ title: '添加失败', icon: 'none' })
}
}
} catch (e) {
console.error('添加到购物车异常', e)
uni.hideLoading()
uni.showToast({ title: '操作失败', icon: 'none' })
}
type FavoriteType = {
id: string
name: string
price: number
main_image_url: string
merchant_id: string
selected: boolean
}
const loadFavorites = async () => {
const res = await supabaseService.getFavorites()
const productList: Product[] = []
for (let i = 0; i < res.length; i++) {
const item = res[i]
let prod: any | null = null
let itemObj: UTSJSONObject | null = null
if (item instanceof UTSJSONObject) {
itemObj = item as UTSJSONObject
prod = itemObj.get('ml_products')
} else {
itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
prod = itemObj.get('ml_products')
}
let image = '/static/default-product.png'
let id = ''
let name = '未知商品'
let price = 0
let sales = 0
let merchantId = ''
if (prod != null) {
let prodObj: UTSJSONObject
if (prod instanceof UTSJSONObject) {
prodObj = prod as UTSJSONObject
} else {
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
merchantId = prodObj.getString('merchant_id') ?? ''
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 {
if (itemObj != null) {
id = itemObj.getString('target_id') ?? ''
}
}
const favorites = ref<FavoriteType[]>([])
const isEditMode = ref<boolean>(false)
const isLoading = ref<boolean>(false)
const product: Product = {
id: id,
name: name,
price: price,
category_id: '',
merchant_id: merchantId,
main_image_url: image,
sale_count: sales
} as Product
productList.push(product)
}
favorites.value = productList
const selectedCount = computed((): number => {
return favorites.value.filter((item): Boolean => item.selected === true).length
})
const isAllSelected = computed((): boolean => {
return favorites.value.length > 0 && favorites.value.every((item): Boolean => item.selected === true)
})
const loadFavorites = async () => {
isLoading.value = true
try {
const res = await supabaseService.getFavorites()
console.log('收藏数据加载完成,数量:', res.length)
const productList: FavoriteType[] = []
for (let i = 0; i < res.length; i++) {
const item = res[i]
let prod: any | null = null
let itemObj: UTSJSONObject | null = null
if (item instanceof UTSJSONObject) {
itemObj = item as UTSJSONObject
prod = itemObj.get('ml_products')
} else {
itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
prod = itemObj.get('ml_products')
}
let image = '/static/default-product.png'
let id = ''
let name = '未知商品'
let price = 0
let merchantId = ''
if (prod != null) {
let prodObj: UTSJSONObject
if (prod instanceof UTSJSONObject) {
prodObj = prod as UTSJSONObject
} else {
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
merchantId = prodObj.getString('merchant_id') ?? ''
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 {
if (itemObj != null) {
id = itemObj.getString('target_id') ?? ''
}
}
const product: FavoriteType = {
id: id,
name: name,
price: price,
main_image_url: image,
merchant_id: merchantId,
selected: false
}
productList.push(product)
}
favorites.value = productList
console.log('收藏列表更新完成,数量:', favorites.value.length)
} catch (e) {
console.error('加载收藏失败', e)
} finally {
isLoading.value = false
}
}
const toggleEditMode = () => {
isEditMode.value = !isEditMode.value
for (let i = 0; i < favorites.value.length; i++) {
favorites.value[i].selected = false
}
}
const clearAll = () => {
if (favorites.value.length === 0) return
uni.showModal({
title: '清空收藏',
content: '确定要清空所有收藏商品吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({ title: '清空中...' })
const productIds: string[] = []
for (let i = 0; i < favorites.value.length; i++) {
productIds.push(favorites.value[i].id)
}
let completed = 0
for (let i = 0; i < productIds.length; i++) {
supabaseService.toggleFavorite(productIds[i]).then(() => {
completed++
if (completed === productIds.length) {
uni.hideLoading()
favorites.value = []
uni.showToast({
title: '已清空',
icon: 'success'
})
}
}).catch(() => {
completed++
if (completed === productIds.length) {
uni.hideLoading()
loadFavorites()
uni.showToast({
title: '部分清空失败',
icon: 'none'
})
}
})
}
}
}
})
}
const toggleSelect = (item: FavoriteType) => {
item.selected = !(item.selected === true)
favorites.value = [...favorites.value]
}
const toggleSelectAll = () => {
const newSelectedState = !isAllSelected.value
for (let i = 0; i < favorites.value.length; i++) {
favorites.value[i].selected = newSelectedState
}
favorites.value = [...favorites.value]
}
const deleteSelected = () => {
const selectedItems = favorites.value.filter((item): Boolean => item.selected === true)
if (selectedItems.length === 0) {
uni.showToast({
title: '请选择要删除的商品',
icon: 'none'
})
return
}
uni.showModal({
title: '删除收藏',
content: `确定要删除选中的 ${selectedItems.length} 个商品吗?`,
success: (res) => {
if (res.confirm) {
uni.showLoading({ title: '删除中...' })
let completed = 0
const total = selectedItems.length
for (let i = 0; i < selectedItems.length; i++) {
supabaseService.toggleFavorite(selectedItems[i].id).then(() => {
completed++
if (completed === total) {
uni.hideLoading()
loadFavorites()
uni.showToast({
title: '已删除',
icon: 'success'
})
}
}).catch(() => {
completed++
if (completed === total) {
uni.hideLoading()
loadFavorites()
uni.showToast({
title: '部分删除失败',
icon: 'none'
})
}
})
}
}
}
})
}
const viewProduct = (product: FavoriteType) => {
if (isEditMode.value) {
toggleSelect(product)
return
}
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?id=${product.id}`
})
}
const addToCart = async (product: FavoriteType) => {
uni.showLoading({ title: '检查商品...' })
try {
const merchantId = product.merchant_id ?? ''
const skus = await supabaseService.getProductSkus(product.id)
uni.hideLoading()
if (skus.length > 0) {
uni.showToast({ title: '请选择规格', icon: 'none' })
setTimeout(() => {
uni.navigateTo({
url: '/pages/mall/consumer/product-detail?id=' + product.id
})
}, 500)
} else {
uni.showLoading({ title: '添加中...' })
const success = await supabaseService.addToCart(product.id, 1, '', merchantId)
uni.hideLoading()
if (success) {
uni.showToast({ title: '已添加到购物车', icon: 'success' })
} else {
uni.showToast({ title: '添加失败', icon: 'none' })
}
}
} catch (e) {
console.error('添加到购物车异常', e)
uni.hideLoading()
uni.showToast({ title: '操作失败', icon: 'none' })
}
}
const goShopping = () => {
uni.switchTab({
url: '/pages/mall/consumer/index'
})
}
const goToDetail = (id: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${id}`
})
}
const removeFavorite = (id: string) => {
uni.showModal({
title: '取消收藏',
content: '确定要取消收藏该商品吗?',
success: (res) => {
if (res.confirm) {
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)
}
uni.showToast({
title: '已取消收藏',
icon: 'none'
})
} else {
uni.showToast({
title: '取消失败',
icon: 'none'
})
}
})
}
}
})
uni.switchTab({
url: '/pages/mall/consumer/index'
})
}
onMounted(() => {
loadFavorites()
loadFavorites()
})
</script>
<style>
<style scoped>
.favorites-page {
padding: 15px;
background-color: #f5f5f5;
flex: 1;
display: flex;
flex-direction: column;
flex: 1;
}
.product-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
width: 100%;
.favorites-header {
background-color: #ffffff;
padding: 15px;
border-bottom: 1px solid #e5e5e5;
display: flex;
align-items: center;
justify-content: space-between;
}
.empty-state {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 100px;
.header-actions {
display: flex;
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;
}
.favorites-content {
flex: 1;
height: 0px;
}
.empty-favorites {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
background-color: #ffffff;
}
.empty-icon {
font-size: 60px;
margin-bottom: 20px;
color: #ddd;
font-size: 80px;
margin-bottom: 20px;
}
.empty-text {
font-size: 16px;
color: #999;
margin-bottom: 20px;
font-size: 16px;
color: #666666;
margin-bottom: 10px;
}
.empty-subtext {
font-size: 14px;
color: #999999;
margin-bottom: 30px;
}
.go-shopping-btn {
background-color: #ff5000;
color: white;
padding: 8px 24px;
border-radius: 20px;
font-size: 14px;
border: none;
background-color: #007aff;
color: #ffffff;
padding: 10px 40px;
border-radius: 25px;
font-size: 14px;
border: none;
}
.product-group {
background-color: #ffffff;
margin-bottom: 10px;
padding: 0 10px;
}
.group-items {
padding: 0;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.product-item {
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
overflow: hidden;
width: 48%;
margin-bottom: 12px;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
overflow: hidden;
width: 48%;
margin-bottom: 12px;
position: relative;
}
.item-selector {
position: absolute;
top: 5px;
right: 5px;
z-index: 10;
width: 30px;
height: 30px;
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
}
.select-icon {
width: 20px;
height: 20px;
border: 1px solid #cccccc;
border-radius: 10px;
background-color: rgba(255,255,255,0.5);
display: flex;
align-items: center;
justify-content: center;
}
.select-icon.selected {
background-color: #007aff;
border-color: #007aff;
}
.icon-text {
color: #ffffff;
font-size: 12px;
}
.item-content {
display: flex;
flex-direction: column;
}
.product-image {
width: 100%;
height: 170px;
border-radius: 8px;
margin-bottom: 8px;
background: #f5f5f5;
width: 100%;
height: 170px;
border-radius: 8px;
margin-bottom: 8px;
background: #f5f5f5;
}
.product-name {
font-size: 13px;
color: #333;
margin-bottom: 5px;
line-height: 1.4;
height: 36px;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 8px;
font-size: 13px;
color: #333;
margin-bottom: 5px;
line-height: 1.4;
height: 36px;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 8px;
}
.product-bottom {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 8px 8px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 8px 8px;
}
.product-price {
font-size: 15px;
color: #ff5000;
font-weight: bold;
font-size: 15px;
color: #ff5000;
font-weight: bold;
}
.product-add-btn {
width: 24px;
height: 24px;
background-color: #ff5000;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background-color: #ff5000;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.add-icon {
color: #fff;
font-size: 16px;
font-weight: bold;
color: #fff;
font-size: 16px;
font-weight: bold;
}
/* PC/Tablet Responsive */
@media (min-width: 768px) {
.product-item {
width: 31% !important;
}
.product-item {
width: 32% !important;
}
}
@media (min-width: 1024px) {
.product-item {
width: 15% !important;
}
.product-grid, .header {
max-width: 1200px;
margin: 0 auto;
}
.product-item {
width: 16% !important;
}
.favorites-content, .favorites-header {
max-width: 1200px;
margin: 0 auto;
}
}
.loading-more,
.no-more {
padding: 20px;
text-align: center;
background-color: #ffffff;
}
.loading-text,
.no-more-text {
color: #999999;
font-size: 14px;
}
.edit-bar {
background-color: #ffffff;
border-top: 1px solid #e5e5e5;
padding: 15px;
display: flex;
align-items: center;
justify-content: space-between;
}
.select-all {
display: flex;
align-items: center;
}
.all-select-icon {
width: 20px;
height: 20px;
border: 1px solid #cccccc;
border-radius: 10px;
margin-right: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.all-select-icon.selected {
background-color: #007aff;
border-color: #007aff;
}
.select-all-text {
font-size: 14px;
color: #333333;
}
.delete-btn {
background-color: #ff4757;
padding: 10px 20px;
border-radius: 15px;
}
.delete-text {
color: #ffffff;
font-size: 14px;
font-weight: bold;
}
</style>

View File

@@ -1,4 +1,4 @@
<!-- 足迹页面 -->
<!-- 足迹页面 -->
<template>
<view class="footprint-page">
<view class="footprint-header">
@@ -747,3 +747,4 @@ onMounted(() => {
font-weight: bold;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
<!-- pages/mall/consumer/index.uvue -->
<!-- pages/mall/consumer/index.uvue -->
<template>
<view class="medic-home">
<!-- 智能顶部导航栏 - 添加滚动隐藏效果 -->
@@ -151,7 +151,7 @@
<text class="service-desc">三甲医生在线</text>
</view>
<view class="service-card" @click="navigateToPrescription">
<view class="service-icon" style="background: #4CAF50;">
<view class="service-icon" style="background: #ff5000;">
<text class="service-icon-text">📋</text>
</view>
<text class="service-name">电子处方</text>
@@ -595,7 +595,7 @@ const familyItems = [
name: '消毒酒精',
desc: '环境消毒',
icon: '🧪',
color: '#4CAF50',
color: '#ff5000',
categoryId: 'external'
},
{
@@ -1032,9 +1032,9 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
top: 0;
left: 0;
right: 0;
background-color: #4CAF50;
background-color: #ff5000;
z-index: 1000;
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
box-shadow: 0 2px 12px rgba(255, 80, 0, 0.15);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
@@ -1244,7 +1244,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.tab-pill.active .tab-text {
color: #4CAF50;
color: #ff5000;
font-weight: bold;
}
@@ -1256,7 +1256,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.section-title.active {
color: #4CAF50;
color: #ff5000;
font-size: 20px;
}
@@ -1292,7 +1292,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.category-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: var(--card-color, #4CAF50);
border-color: var(--card-color, #ff5000);
}
/* 二级分类样式 */
@@ -1371,7 +1371,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
width: 44px;
height: 44px;
border-radius: 22px;
background: var(--card-color, #4CAF50);
background: var(--card-color, #ff5000);
display: flex;
align-items: center;
justify-content: center;
@@ -1494,7 +1494,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.news-more {
font-size: 14px;
color: #4CAF50;
color: #ff5000;
/* cursor: pointer; removed for uvue support */
}
@@ -1663,7 +1663,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.section-icon {
font-size: 20px;
color: #4CAF50;
color: #ff5000;
margin-right: 8px; /* Replacement for gap */
}
@@ -1697,9 +1697,9 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.sort-tab.active {
background: #4CAF50;
background: #ff5000;
color: white;
border-color: #4CAF50;
border-color: #ff5000;
}
.sort-tab:hover {
@@ -1707,7 +1707,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.sort-tab.active:hover {
background: #388E3C;
background: #e64a00;
}
/* 产品网格 */
@@ -1786,7 +1786,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
align-items: center;
justify-content: center;
/* gap: 6px; removed */
background: #4CAF50;
background: #ff5000;
color: white;
padding: 8px 12px;
border-radius: 8px;
@@ -1918,9 +1918,9 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.filter-item.active {
background: #4CAF50;
background: #ff5000;
color: white;
border-color: #4CAF50;
border-color: #ff5000;
}
.filter-item:hover {
@@ -1928,7 +1928,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.filter-item.active:hover {
background: #388E3C;
background: #e64a00;
}
.recommend-grid {
@@ -2055,7 +2055,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
width: 36px;
height: 36px;
border-radius: 18px;
background: #4CAF50;
background: #ff5000;
display: flex;
align-items: center;
justify-content: center;
@@ -2064,7 +2064,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.add-to-cart:hover {
background: #388E3C;
background: #e64a00;
transform: scale(1.1);
}
@@ -2142,7 +2142,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
width: 32px;
height: 32px;
border: 3px solid #f0f0f0;
border-top-color: #4CAF50;
border-top-color: #ff5000;
border-radius: 16px;
margin-bottom: 12px;
}
@@ -2642,7 +2642,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.sort-tab.active,
.filter-item.active {
background: #4CAF50;
background: #ff5000;
color: white;
}
@@ -2653,12 +2653,12 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.sort-tab.active:hover,
.filter-item.active:hover {
background: #388E3C;
background: #e64a00;
}
.nav-tool-item {
background: rgba(76, 175, 80, 0.2);
color: #4CAF50;
background: rgba(255, 80, 0, 0.2);
color: #ff5000;
}
.manufacturer,
@@ -2669,3 +2669,4 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="logistics-page">
<view class="logistics-header">
<view class="product-info">
@@ -296,3 +296,4 @@ onMounted(() => {
color: #999;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="messages-page">
<!-- 智能顶部导航栏 - 与主页保持一致 -->
<view class="smart-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
@@ -75,7 +75,7 @@
<view
v-for="message in serviceMessages"
:key="message.id"
:class="['message-item', { unread: !message.read, active: message.active }]"
:class="['message-item', { active: message.active }]"
@click="startChatWithService(message)"
>
<view class="message-icon-wrapper">
@@ -88,6 +88,7 @@
<view v-else class="message-icon-default" :style="{ backgroundColor: message.color }">
<text class="message-icon-text">{{ message.icon }}</text>
</view>
<view v-if="message.unreadCount > 0" class="unread-dot"></view>
<view v-if="message.online" class="online-dot"></view>
</view>
<view class="message-content">
@@ -96,17 +97,11 @@
<text class="message-title">{{ message.title }}</text>
<text v-if="message.role" class="message-role">{{ message.role }}</text>
</view>
<view class="message-header-right">
<text class="message-time">{{ message.time }}</text>
<text v-if="message.unreadCount > 0" class="message-unread-count">{{ message.unreadCount }}</text>
</view>
<text class="message-time">{{ message.time }}</text>
</view>
<view class="message-preview-wrapper">
<text class="message-preview">{{ message.content }}</text>
<text v-if="message.lastMessage" class="last-message">{{ message.lastMessage }}</text>
</view>
<view v-if="message.tags" class="message-tags">
<text v-for="tag in message.tags" :key="tag" class="message-tag">{{ tag }}</text>
<text v-if="message.unreadCount > 0" class="message-unread-count">{{ message.unreadCount > 99 ? '99+' : message.unreadCount }}</text>
</view>
</view>
</view>
@@ -123,19 +118,24 @@
<view
v-for="message in systemMessages"
:key="message.id"
:class="['message-item', { unread: !message.read }]"
class="message-item"
@click="viewSystemMessage(message)"
>
<view class="message-icon-wrapper">
<text class="message-icon">📢</text>
<text class="message-icon-text">📢</text>
<view v-if="!message.read" class="unread-dot"></view>
</view>
<view class="message-content">
<view class="message-header">
<text class="message-title">{{ message.title }}</text>
<view class="message-title-wrapper">
<text class="message-title">{{ message.title }}</text>
<text v-if="message.important" class="important-tag">重要</text>
</view>
<text class="message-time">{{ message.time }}</text>
</view>
<text class="message-preview">{{ message.content }}</text>
<view v-if="message.important" class="important-tag">重要</view>
<view class="message-preview-wrapper">
<text class="message-preview">{{ message.content }}</text>
</view>
</view>
</view>
</view>
@@ -145,22 +145,27 @@
<view
v-for="message in orderMessages"
:key="message.id"
:class="['message-item', { unread: !message.read }]"
class="message-item"
@click="viewOrderMessage(message)"
>
<view class="message-icon-wrapper">
<text class="message-icon">📦</text>
<text class="message-icon-text">📦</text>
<view v-if="!message.read" class="unread-dot"></view>
</view>
<view class="message-content">
<view class="message-header">
<text class="message-title">{{ message.title }}</text>
<view class="message-title-wrapper">
<text class="message-title">{{ message.title }}</text>
<view v-if="message.status" class="order-status-tag" :class="message.status">
{{ message.statusText }}
</view>
</view>
<text class="message-time">{{ message.time }}</text>
</view>
<text class="message-preview">{{ message.content }}</text>
<text class="order-info" v-if="message.order_no">订单号: {{ message.order_no }}</text>
<view v-if="message.status" class="order-status" :class="message.status">
{{ message.statusText }}
<view class="message-preview-wrapper">
<text class="message-preview">{{ message.content }}</text>
</view>
<text class="order-no-text" v-if="message.order_no">订单号: {{ message.order_no }}</text>
</view>
</view>
</view>
@@ -649,9 +654,9 @@ const onRefresh = () => {
top: 0;
left: 0;
right: 0;
background-color: #4CAF50;
background-color: #ff5000;
z-index: 1000;
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
box-shadow: 0 2px 12px rgba(255, 80, 0, 0.15);
display: flex;
flex-direction: column;
justify-content: flex-start;
@@ -758,8 +763,8 @@ const onRefresh = () => {
}
.tab-item.active {
color: #4CAF50;
border-bottom-color: #4CAF50;
color: #ff5000;
border-bottom-color: #ff5000;
font-weight: bold;
}
@@ -826,8 +831,8 @@ const onRefresh = () => {
}
.service-status.online {
background: #E8F5E9;
color: #4CAF50;
background: #FFF3E0;
color: #ff5000;
}
.service-desc {
@@ -885,37 +890,31 @@ const onRefresh = () => {
background-color: white;
border-radius: 12px;
padding: 15px;
margin-bottom: 10px;
margin-bottom: 12px;
display: flex;
align-items: flex-start;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
/* cursor: pointer; removed for uniapp-x support */
flex-direction: row;
align-items: center; /* 居中对齐头像和内容 */
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
transition: opacity 0.2s ease;
}
.message-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
.message-item:active {
opacity: 0.7;
}
.message-item.unread {
background-color: #f0f9f0;
border-left: 3px solid #4CAF50;
}
.message-item.active {
border: 1px solid #4CAF50;
background-color: white; /* 保持白色背景,通过红点示未读 */
}
.message-icon-wrapper {
width: 50px;
height: 50px;
border-radius: 25px;
background-color: #f5f5f5;
width: 48px;
height: 48px;
border-radius: 24px;
background-color: #f8fafc;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
margin-right: 12px;
flex-shrink: 0;
position: relative;
}
@@ -923,30 +922,40 @@ const onRefresh = () => {
.message-icon-default {
width: 100%;
height: 100%;
border-radius: 25px;
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.message-icon-text {
font-size: 24px;
color: white;
font-size: 22px;
}
.message-avatar {
width: 100%;
height: 100%;
border-radius: 25px;
border-radius: 24px;
}
.unread-dot {
position: absolute;
top: 0;
right: 0;
width: 10px;
height: 10px;
background-color: #ff5000;
border-radius: 5px;
border: 1.5px solid white;
}
.online-dot {
position: absolute;
bottom: 2px;
right: 2px;
bottom: 0;
right: 0;
width: 12px;
height: 12px;
background-color: #4CAF50;
background-color: #4cd964;
border-radius: 6px;
border: 2px solid white;
}
@@ -954,81 +963,83 @@ const onRefresh = () => {
.message-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.message-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
align-items: center;
margin-bottom: 4px;
}
.message-title-wrapper {
flex: 1;
min-width: 0;
margin-right: 10px;
display: flex;
flex-direction: row;
align-items: center;
}
.message-title {
font-size: 16px;
color: #333;
font-weight: bold;
/* display: block; REMOVED for uniapp-x support */
margin-bottom: 4px;
color: #1a1a1a;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.message-role {
font-size: 12px;
color: #4CAF50;
background: #E8F5E9;
padding: 2px 8px;
border-radius: 10px;
/* display: inline-block; REMOVED for uniapp-x support */
}
.message-header-right {
display: flex;
flex-direction: column;
align-items: flex-end;
/* gap: 4px; removed for uniapp-x support */
font-size: 10px;
color: #ff5000;
background: #fff0eb;
padding: 1px 6px;
border-radius: 4px;
margin-left: 6px;
flex-shrink: 0;
}
.message-time {
font-size: 12px;
color: #999;
white-space: nowrap;
margin-bottom: 4px; /* replaced gap */
}
.message-unread-count {
font-size: 11px;
color: white;
background: #FF5722;
padding: 2px 6px;
border-radius: 10px;
min-width: 18px;
text-align: center;
font-weight: bold;
margin-left: 10px;
flex-shrink: 0;
}
.message-preview-wrapper {
margin-bottom: 8px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.message-preview {
font-size: 14px;
flex: 1;
font-size: 13px;
color: #666;
line-height: 1.4;
margin-bottom: 4px;
/* 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 */
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 8px;
}
.message-unread-count {
font-size: 10px;
color: white;
background: #ff5000;
padding: 0 5px;
border-radius: 10px;
min-width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
flex-shrink: 0;
}
.last-message {
@@ -1055,8 +1066,8 @@ const onRefresh = () => {
.order-info {
font-size: 12px;
color: #4CAF50;
background-color: #E8F5E9;
color: #ff5000;
background-color: #FFF3E0;
padding: 4px 10px;
border-radius: 4px;
/* display: inline-block; REMOVED for uniapp-x support */
@@ -1085,8 +1096,8 @@ const onRefresh = () => {
}
.order-status.completed {
background: #E8F5E9;
color: #4CAF50;
background: #FFF3E0;
color: #ff5000;
}
.important-tag {
@@ -1155,26 +1166,65 @@ const onRefresh = () => {
line-height: 1.5;
}
.order-no-text {
font-size: 11px;
color: #999;
margin-top: 4px;
}
.order-status-tag {
font-size: 10px;
padding: 1px 6px;
border-radius: 4px;
margin-left: 6px;
flex-shrink: 0;
}
.order-status-tag.shipping {
background: #e3f2fd;
color: #2196f3;
}
.order-status-tag.processing {
background: #fff8e1;
color: #ffb300;
}
.order-status-tag.completed {
background: #fff0eb;
color: #ff5000;
}
.important-tag {
background-color: #ff5000;
color: white;
font-size: 10px;
padding: 1px 6px;
border-radius: 4px;
margin-left: 6px;
flex-shrink: 0;
}
/* 空状态 */
.empty-messages {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
text-align: center;
padding-top: 100px;
}
.empty-icon {
font-size: 80px;
color: #ddd;
margin-bottom: 20px;
font-size: 60px;
margin-bottom: 16px;
opacity: 0.3;
}
.empty-title {
font-size: 18px;
color: #666;
margin-bottom: 10px;
font-size: 16px;
color: #333;
font-weight: bold;
margin-bottom: 8px;
}
.empty-desc {
@@ -1191,7 +1241,7 @@ const onRefresh = () => {
}
.action-button {
background: linear-gradient(135deg, #4CAF50, #2E7D32);
background: linear-gradient(135deg, #ff5000, #e64a00);
color: white;
border: none;
border-radius: 25px;
@@ -1310,7 +1360,7 @@ const onRefresh = () => {
}
.tab-item.active {
color: #4CAF50;
color: #ff5000;
}
.customer-service-info,
@@ -1370,3 +1420,4 @@ const onRefresh = () => {
}
}
</style>

View File

@@ -1,22 +1,22 @@
<template>
<template>
<view class="messages-page">
<!-- 智能顶部导航栏 - 与主页保持一致 -->
<!-- 鏅鸿兘椤堕儴瀵艰埅鏍?- 涓庝富椤典繚鎸佷竴鑷?-->
<view class="smart-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-container">
<text class="nav-title">消息中心</text>
<text class="nav-title">娑堟伅涓績</text>
<view class="nav-actions">
<view class="action-btn" @click="clearAllUnread">
<text class="action-icon">🧹</text>
<text class="action-text">一键已读</text>
<text class="action-icon">馃Ч</text>
<text class="action-text">涓€閿凡璇?/text>
</view>
</view>
</view>
</view>
<!-- 导航栏占位符 -->
<!-- 瀵艰埅鏍忓崰浣嶇 -->
<view class="navbar-placeholder" :style="{ height: (statusBarHeight + 10) + 'px' }"></view>
<!-- 消息分类标签 - 固定在顶部,方便随时切换 -->
<!-- 娑堟伅鍒嗙被鏍囩 - 鍥哄畾鍦ㄩ《閮紝鏂逛究闅忔椂鍒囨崲 -->
<view class="tabs-container">
<view class="message-tabs">
<view
@@ -31,7 +31,7 @@
</view>
</view>
<!-- 消息列表内容区 -->
<!-- 娑堟伅鍒楄〃鍐呭鍖?-->
<scroll-view
scroll-y
class="messages-content"
@@ -41,37 +41,37 @@
:scroll-top="scrollTop"
>
<!-- 客服消息 -->
<!-- 瀹㈡湇娑堟伅 -->
<view v-if="activeTab === 'service'" class="message-section">
<!-- 在线客服卡片 -->
<!-- 鍦ㄧ嚎瀹㈡湇鍗$墖 -->
<view class="customer-service-info">
<view class="service-header">
<text class="service-title">康乐医药在线客服</text>
<text class="service-status online">在线</text>
<text class="service-title">搴蜂箰鍖昏嵂鍦ㄧ嚎瀹㈡湇</text>
<text class="service-status online">鍦ㄧ嚎</text>
</view>
<text class="service-desc">专业医药顾问在线解答,服务时间 9:00-22:00</text>
<text class="service-desc">涓撲笟鍖昏嵂椤鹃棶鍦ㄧ嚎瑙g瓟锛屾湇鍔℃椂闂?9:00-22:00</text>
<view class="service-categories">
<view class="category-item" @click="startQuickService('用药咨询')">
<text class="category-icon">💊</text>
<text class="category-name">用药咨询</text>
<view class="category-item" @click="startQuickService('鐢ㄨ嵂鍜ㄨ')">
<text class="category-icon">馃拪</text>
<text class="category-name">鐢ㄨ嵂鍜ㄨ</text>
</view>
<view class="category-item" @click="startQuickService('处方咨询')">
<text class="category-icon">📋</text>
<text class="category-name">处方咨询</text>
<view class="category-item" @click="startQuickService('澶勬柟鍜ㄨ')">
<text class="category-icon">馃搵</text>
<text class="category-name">澶勬柟鍜ㄨ</text>
</view>
<view class="category-item" @click="startQuickService('副作用咨询')">
<text class="category-icon">⚠️</text>
<text class="category-name">副作用咨询</text>
<view class="category-item" @click="startQuickService('鍓綔鐢ㄥ挩璇?)">
<text class="category-icon">鈿狅笍</text>
<text class="category-name">鍓綔鐢ㄥ挩璇?/text>
</view>
<view class="category-item" @click="startQuickService('药品配送')">
<text class="category-icon">🚚</text>
<text class="category-name">药品配送</text>
<view class="category-item" @click="startQuickService('鑽搧閰嶉€?)">
<text class="category-icon">馃殮</text>
<text class="category-name">鑽搧閰嶉€?/text>
</view>
</view>
</view>
<!-- 客服消息列表 -->
<!-- 瀹㈡湇娑堟伅鍒楄〃 -->
<view
v-for="message in serviceMessages"
:key="message.id"
@@ -111,14 +111,14 @@
</view>
</view>
<!-- 客服系统提示 -->
<!-- 瀹㈡湇绯荤粺鎻愮ず -->
<view class="service-tips">
<text class="tip-icon">💡</text>
<text class="tip-text">温馨提示:请勿相信任何要求转账、付款的信息,谨防诈骗</text>
<text class="tip-icon">馃挕</text>
<text class="tip-text">娓╅Θ鎻愮ず锛氳鍕跨浉淇′换浣曡姹傝浆璐︺€佷粯娆剧殑淇℃伅锛岃皑闃茶瘓楠?/text>
</view>
</view>
<!-- 系统通知 -->
<!-- 绯荤粺閫氱煡 -->
<view v-if="activeTab === 'system'" class="message-section">
<view
v-for="message in systemMessages"
@@ -127,7 +127,7 @@
@click="viewSystemMessage(message)"
>
<view class="message-icon-wrapper">
<text class="message-icon">📢</text>
<text class="message-icon">馃摙</text>
</view>
<view class="message-content">
<view class="message-header">
@@ -135,12 +135,12 @@
<text class="message-time">{{ message.time }}</text>
</view>
<text class="message-preview">{{ message.content }}</text>
<view v-if="message.important" class="important-tag">重要</view>
<view v-if="message.important" class="important-tag">閲嶈</view>
</view>
</view>
</view>
<!-- 订单消息 -->
<!-- 璁㈠崟娑堟伅 -->
<view v-if="activeTab === 'order'" class="message-section">
<view
v-for="message in orderMessages"
@@ -149,7 +149,7 @@
@click="viewOrderMessage(message)"
>
<view class="message-icon-wrapper">
<text class="message-icon">📦</text>
<text class="message-icon">馃摝</text>
</view>
<view class="message-content">
<view class="message-header">
@@ -157,7 +157,7 @@
<text class="message-time">{{ message.time }}</text>
</view>
<text class="message-preview">{{ message.content }}</text>
<text class="order-info" v-if="message.order_no">订单号: {{ message.order_no }}</text>
<text class="order-info" v-if="message.order_no">璁㈠崟鍙? {{ message.order_no }}</text>
<view v-if="message.status" class="order-status" :class="message.status">
{{ message.statusText }}
</view>
@@ -165,7 +165,7 @@
</view>
</view>
<!-- 优惠活动 -->
<!-- 浼樻儬娲诲姩 -->
<view v-if="activeTab === 'promo'" class="message-section">
<view
v-for="message in promoMessages"
@@ -174,7 +174,7 @@
@click="viewPromoMessage(message)"
>
<view class="message-icon-wrapper">
<text class="message-icon">🎁</text>
<text class="message-icon">馃巵</text>
</view>
<view class="message-content">
<view class="message-header">
@@ -183,30 +183,30 @@
</view>
<text class="message-preview">{{ message.content }}</text>
<view v-if="message.coupon" class="coupon-info" @click.stop="claimCoupon(message)">
<text class="coupon-text">{{ message.coupon }}优惠券</text>
<text class="coupon-expiry">有效期至 {{ message.expiry }}</text>
<text class="coupon-action">{{ message.claimed ? '已领取' : '点击领取' }}</text>
<text class="coupon-text">{{ message.coupon }}浼樻儬鍒?/text>
<text class="coupon-expiry">鏈夋晥鏈熻嚦 {{ message.expiry }}</text>
<text class="coupon-action">{{ message.claimed ? '宸查鍙? : '鐐瑰嚮棰嗗彇' }}</text>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<!-- 绌虹姸鎬?-->
<view v-if="!loading && currentMessages.length === 0 && activeTab !== 'service'" class="empty-messages">
<text class="empty-icon">💬</text>
<text class="empty-title">暂无消息</text>
<text class="empty-desc">暂时没有新消息</text>
<text class="empty-icon">馃挰</text>
<text class="empty-title">鏆傛棤娑堟伅</text>
<text class="empty-desc">鏆傛椂娌℃湁鏂版秷鎭?/text>
</view>
<!-- 底部安全区域 -->
<!-- 搴曢儴瀹夊叏鍖哄煙 -->
<view class="safe-area"></view>
</scroll-view>
<!-- 底部固定按钮 -->
<!-- 搴曢儴鍥哄畾鎸夐挳 -->
<view class="floating-action">
<button class="action-button" @click="startNewChat">
<text class="button-icon">✏️</text>
<text class="button-text">新建聊天</text>
<text class="button-icon">鉁忥笍</text>
<text class="button-text">鏂板缓鑱婂ぉ</text>
</button>
</view>
</view>
@@ -216,7 +216,7 @@
import { ref, reactive, computed, onMounted } from 'vue'
import { supabaseService, type Notification, type ChatMessage } from '@/utils/supabaseService.uts'
// 响应式数据
// 鍝嶅簲寮忔暟鎹?
const activeTab = ref<string>('service')
const refreshing = ref<boolean>(false)
const loading = ref<boolean>(false)
@@ -225,32 +225,32 @@ 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)
// 璁$畻婊氬姩鍖哄煙楂樺害锛氬睆骞曢珮搴?- 鐘舵€佹爮 - 瀵艰埅鏍?44) - 鏍囩鏍?42)
const windowHeight = systemInfo.windowHeight
scrollHeight.value = windowHeight - statusBarHeight.value - 44 - 42
}
// 消息分类标签
// 娑堟伅鍒嗙被鏍囩
const messageTabs = reactive([
{ id: 'service', name: '客服消息', unread: 5 },
{ id: 'system', name: '系统通知', unread: 3 },
{ id: 'order', name: '订单消息', unread: 2 },
{ id: 'promo', name: '优惠活动', unread: 2 }
{ id: 'service', name: '瀹㈡湇娑堟伅', unread: 5 },
{ id: 'system', name: '绯荤粺閫氱煡', unread: 3 },
{ id: 'order', name: '璁㈠崟娑堟伅', unread: 2 },
{ id: 'promo', name: '浼樻儬娲诲姩', unread: 2 }
])
// Mock 客服消息数据
// Mock 瀹㈡湇娑堟伅鏁版嵁
const serviceMessages = reactive<any[]>([])
const systemMessages = reactive<any[]>([])
const orderMessages = reactive<any[]>([])
// Mock 优惠活动数据
// Mock 浼樻儬娲诲姩鏁版嵁
const promoMessages = reactive<any[]>([])
// 计算当前显示的消息
// 璁$畻褰撳墠鏄剧ず鐨勬秷鎭?
const currentMessages = computed(() => {
switch (activeTab.value) {
case 'system': return systemMessages
@@ -261,14 +261,14 @@ const currentMessages = computed(() => {
}
})
// 生命周期
// 鐢熷懡鍛ㄦ湡
onMounted(() => {
console.log('Messages Page Mounted')
initPage()
loadMessages()
})
// 简单的日期格式化
// 绠€鍗曠殑鏃ユ湡鏍煎紡鍖?
const formatTime = (isoString: string): string => {
if (!isoString) return ''
try {
@@ -278,22 +278,22 @@ const formatTime = (isoString: string): string => {
}
}
// 加载消息
// 鍔犺浇娑堟伅
const loadMessages = async () => {
loading.value = true
try {
// 清空现有Mock数据
// 娓呯┖鐜版湁Mock鏁版嵁
serviceMessages.length = 0
systemMessages.length = 0
orderMessages.length = 0
promoMessages.length = 0
// 1. 获取通知 (系统、订单、优惠)
// 1. 鑾峰彇閫氱煡 (绯荤粺銆佽鍗曘€佷紭鎯?
const notes = await supabaseService.getUserNotifications()
notes.forEach((note: Notification) => {
// 这里使用 any 类型构建对象,以匹配 reactive 数组的结构
// 杩欓噷浣跨敤 any 绫诲瀷鏋勫缓瀵硅薄锛屼互鍖归厤 reactive 鏁扮粍鐨勭粨鏋?
const item = {
id: note.id,
title: note.title,
@@ -301,10 +301,10 @@ const loadMessages = async () => {
time: formatTime(note.created_at || ''),
read: note.is_read,
type: note.type, // 'system', 'order', 'promotion' => 'promo'
// 默认填充字段以避免渲染报错
// 榛樿濉厖瀛楁浠ラ伩鍏嶆覆鏌撴姤閿?
avatar: note.icon_url,
important: note.type === 'system', // 简单逻辑
coupon: '点击查看',
important: note.type === 'system', // 绠€鍗曢€昏緫
coupon: '鐐瑰嚮鏌ョ湅',
expiry: '',
claimed: false,
order_no: '',
@@ -330,15 +330,15 @@ const loadMessages = async () => {
}
})
// 2. 获取客服消息 (Chat)
// 2. 鑾峰彇瀹㈡湇娑堟伅 (Chat)
const chats = await supabaseService.getUserChatMessages()
if (chats.length > 0) {
// 简单处理:将最新一条显示为"在线客服"会话
// 绠€鍗曞鐞嗭細灏嗘渶鏂颁竴鏉℃樉绀轰负"鍦ㄧ嚎瀹㈡湇"浼氳瘽
const lastMsg = chats[0]
serviceMessages.push({
id: lastMsg.id,
title: '在线客服',
role: '客服专员',
title: '鍦ㄧ嚎瀹㈡湇',
role: '瀹㈡湇涓撳憳',
content: lastMsg.content,
lastMessage: lastMsg.content,
time: formatTime(lastMsg.created_at || ''),
@@ -347,8 +347,8 @@ const loadMessages = async () => {
avatar: '/static/icons/service-avatar.png',
online: true,
unreadCount: chats.filter((m: ChatMessage) => !m.is_read && !m.is_from_user).length,
tags: ['官方客服'],
icon: '👩‍💼',
tags: ['瀹樻柟瀹㈡湇'],
icon: '馃懇鈥嶐煉?,
color: '#2196F3',
important: false,
coupon: '',
@@ -359,21 +359,21 @@ const loadMessages = async () => {
statusText: ''
})
} else {
// 如果没有真实数据,保留一个默认客服入口
// 濡傛灉娌℃湁鐪熷疄鏁版嵁锛屼繚鐣欎竴涓粯璁ゅ鏈嶅叆鍙?
serviceMessages.push({
id: 'default_service',
title: '在线客服',
role: '智能助手',
content: '有问题请随时联系我们',
lastMessage: '欢迎咨询',
time: '刚刚',
title: '鍦ㄧ嚎瀹㈡湇',
role: '鏅鸿兘鍔╂墜',
content: '鏈夐棶棰樿闅忔椂鑱旂郴鎴戜滑',
lastMessage: '娆㈣繋鍜ㄨ',
time: '鍒氬垰',
read: true,
type: 'service',
avatar: '/static/icons/service-avatar.png',
online: true,
unreadCount: 0,
tags: ['自动回复'],
icon: '🤖',
tags: ['鑷姩鍥炲'],
icon: '馃',
color: '#2196F3',
important: false,
coupon: '',
@@ -386,14 +386,14 @@ const loadMessages = async () => {
}
} catch (e) {
console.error('加载消息失败', e)
console.error('鍔犺浇娑堟伅澶辫触', e)
} finally {
updateUnreadCount()
loading.value = false
}
}
// 更新未读数量
// 鏇存柊鏈鏁伴噺
const updateUnreadCount = () => {
let totalUnread = 0
@@ -416,14 +416,14 @@ const updateUnreadCount = () => {
unreadCount.value = totalUnread
}
// 切换标签
// 鍒囨崲鏍囩
const switchTab = (tabId: string) => {
activeTab.value = tabId
// 切换标签时回到顶部,使用微小变化触发滚动更新
// 鍒囨崲鏍囩鏃跺洖鍒伴《閮紝浣跨敤寰皬鍙樺寲瑙﹀彂婊氬姩鏇存柊
scrollTop.value = scrollTop.value === 0 ? 0.01 : 0
}
// 开始与客服聊天
// 寮€濮嬩笌瀹㈡湇鑱婂ぉ
const startChatWithService = (message: any) => {
message.read = true
message.unreadCount = 0
@@ -434,26 +434,26 @@ const startChatWithService = (message: any) => {
})
}
// 快速开始服务
// 蹇€熷紑濮嬫湇鍔?
const startQuickService = (category: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/chat?category=${encodeURIComponent(category)}`
})
}
// 新建聊天
// 鏂板缓鑱婂ぉ
const startNewChat = () => {
uni.showActionSheet({
itemList: ['用药咨询', '处方咨询', '副作用咨询', '药品配送', '其他问题'],
itemList: ['鐢ㄨ嵂鍜ㄨ', '澶勬柟鍜ㄨ', '鍓綔鐢ㄥ挩璇?, '鑽搧閰嶉€?, '鍏朵粬闂'],
success: (res) => {
const categories = ['用药咨询', '处方咨询', '副作用咨询', '药品配送', '其他问题']
const categories = ['鐢ㄨ嵂鍜ㄨ', '澶勬柟鍜ㄨ', '鍓綔鐢ㄥ挩璇?, '鑽搧閰嶉€?, '鍏朵粬闂']
const category = categories[res.tapIndex]
startQuickService(category)
}
})
}
// 查看系统消息
// 鏌ョ湅绯荤粺娑堟伅
const viewSystemMessage = (message: any) => {
message.read = true
updateUnreadCount()
@@ -462,7 +462,7 @@ const viewSystemMessage = (message: any) => {
})
}
// 查看订单消息
// 鏌ョ湅璁㈠崟娑堟伅
const viewOrderMessage = (message: any) => {
message.read = true
updateUnreadCount()
@@ -471,7 +471,7 @@ const viewOrderMessage = (message: any) => {
})
}
// 查看优惠活动
// 鏌ョ湅浼樻儬娲诲姩
const viewPromoMessage = (message: any) => {
message.read = true
updateUnreadCount()
@@ -480,22 +480,22 @@ const viewPromoMessage = (message: any) => {
})
}
// 领取优惠券
// 棰嗗彇浼樻儬鍒?
const claimCoupon = (message: any) => {
if (message.claimed) {
uni.showToast({
title: '您已领取该优惠券',
title: '鎮ㄥ凡棰嗗彇璇ヤ紭鎯犲埜',
icon: 'none'
})
return
}
message.claimed = true
// 保存领取状态到本地存储,供个人页读取
// 淇濆瓨棰嗗彇鐘舵€佸埌鏈湴瀛樺偍锛屼緵涓汉椤佃鍙?
const claimedCouponsCount = uni.getStorageSync('claimedCoupons') || 0
uni.setStorageSync('claimedCoupons', (claimedCouponsCount as number) + 1)
// 保存详细的优惠券信息到 myCoupons 列表
// 淇濆瓨璇︾粏鐨勪紭鎯犲埜淇℃伅鍒?myCoupons 鍒楄〃
const myCoupons = uni.getStorageSync('myCoupons')
let couponsList: any[] = []
if (myCoupons) {
@@ -515,16 +515,16 @@ const claimCoupon = (message: any) => {
uni.setStorageSync('myCoupons', JSON.stringify(couponsList))
uni.showToast({
title: '领取成功',
title: '棰嗗彇鎴愬姛',
icon: 'success'
})
}
// 清除所有未读
// 娓呴櫎鎵€鏈夋湭璇?
const clearAllUnread = () => {
uni.showModal({
title: '确认操作',
content: '确定要标记所有消息为已读吗?',
title: '纭鎿嶄綔',
content: '纭畾瑕佹爣璁版墍鏈夋秷鎭负宸茶鍚楋紵',
success: (res) => {
if (res.confirm) {
serviceMessages.forEach(msg => {
@@ -539,7 +539,7 @@ const clearAllUnread = () => {
unreadCount.value = 0
uni.showToast({
title: '已标记所有消息为已读',
title: '宸叉爣璁版墍鏈夋秷鎭负宸茶',
icon: 'success'
})
}
@@ -547,14 +547,14 @@ const clearAllUnread = () => {
})
}
// 下拉刷新
// 涓嬫媺鍒锋柊
const onRefresh = () => {
refreshing.value = true
setTimeout(() => {
loadMessages()
refreshing.value = false
uni.showToast({
title: '刷新成功',
title: '鍒锋柊鎴愬姛',
icon: 'success'
})
}, 1000)
@@ -562,17 +562,17 @@ const onRefresh = () => {
</script>
<style>
/* 页面结构优化 - 避免双滚动条 */
/* 椤甸潰缁撴瀯浼樺寲 - 閬垮厤鍙屾粴鍔ㄦ潯 */
.messages-page {
width: 100%;
height: 100vh;
background-color: #f8fafc;
display: flex;
flex-direction: column;
overflow: hidden; /* 关键防止body滚动 */
overflow: hidden; /* 鍏抽敭锛氶槻姝ody婊氬姩 */
}
/* 智能导航栏 */
/* 鏅鸿兘瀵艰埅鏍?*/
.smart-navbar {
position: fixed;
top: 0;
@@ -581,42 +581,42 @@ const onRefresh = () => {
background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
z-index: 1000;
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
/* height: 50px; 移除固定高度,由内容决定 */
/* height: 50px; 绉婚櫎鍥哄畾楂樺害锛岀敱鍐呭鍐冲畾 */
display: flex;
flex-direction: row; /* 显式设置行方向 */
flex-direction: row; /* 鏄惧紡璁剧疆琛屾柟鍚?*/
align-items: center;
justify-content: center;
flex-shrink: 0; /* 防止被压缩 */
flex-shrink: 0; /* 闃叉琚帇缂?*/
}
.nav-container {
padding: 0 16px;
display: flex;
flex-direction: row; /* 关键修复UVUE默认是column必须显式设置为row才能横向排列 */
flex-direction: row; /* 鍏抽敭淇锛歎VUE榛樿鏄痗olumn锛屽繀椤绘樉寮忚缃负row鎵嶈兘妯悜鎺掑垪 */
align-items: center;
justify-content: space-between;
width: 100%;
max-width: 1400px;
margin: 0 auto;
height: 44px; /* 调整为标准高度 44px */
height: 44px; /* 璋冩暣涓烘爣鍑嗛珮搴?44px */
}
.nav-title {
font-size: 18px;
font-weight: bold;
color: white;
flex: 1; /* 自适应宽度 */
flex: 1; /* 鑷€傚簲瀹藉害 */
}
.nav-actions {
display: flex;
flex-direction: row; /* 显式设置行方向 */
flex-direction: row; /* 鏄惧紡璁剧疆琛屾柟鍚?*/
align-items: center;
}
.action-btn {
display: flex;
flex-direction: row; /* 显式设置行方向 */
flex-direction: row; /* 鏄惧紡璁剧疆琛屾柟鍚?*/
align-items: center;
background: rgba(255, 255, 255, 0.2);
padding: 4px 12px;
@@ -640,32 +640,32 @@ const onRefresh = () => {
font-weight: 500;
}
/* 导航栏占位符 */
/* 瀵艰埅鏍忓崰浣嶇 */
.navbar-placeholder {
width: 100%;
flex-shrink: 0;
}
/* 消息分类标签容器 */
/* 娑堟伅鍒嗙被鏍囩瀹瑰櫒 */
.tabs-container {
background: white;
padding: 0 10px;
border-bottom: 1px solid #e0e0e0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
z-index: 900;
height: 42px; /* 减小高度,更紧凑 */
height: 42px; /* 鍑忓皬楂樺害锛屾洿绱у噾 */
flex-shrink: 0;
}
.message-tabs {
display: flex;
flex-direction: row; /* 横向排列 */
flex-direction: row; /* 妯悜鎺掑垪 */
height: 100%;
/* overflow-x: auto; 移除自动滚动,改为自适应宽度 */
/* overflow-x: auto; 绉婚櫎鑷姩婊氬姩锛屾敼涓鸿嚜閫傚簲瀹藉害 */
max-width: 1400px;
margin: 0 auto;
/* gap: 4px; removed for uniapp-x support */
justify-content: space-between; /* 均匀分布 */
justify-content: space-between; /* 鍧囧寑鍒嗗竷 */
}
.message-tabs::-webkit-scrollbar {
@@ -675,17 +675,17 @@ const onRefresh = () => {
.tab-item {
padding: 0 4px;
margin: 0 2px;
display: flex; /* 改为 flex 布局 */
flex-direction: row; /* 关键:横向排列 文字和数字 */
align-items: center; /* 垂直居中 */
display: flex; /* 鏀逛负 flex 甯冨眬 */
flex-direction: row; /* 鍏抽敭锛氭í鍚戞帓鍒?鏂囧瓧鍜屾暟瀛?*/
align-items: center; /* 鍨傜洿灞呬腑 */
justify-content: center;
position: relative;
height: 100%;
border-bottom: 3px solid transparent;
transition: all 0.3s ease;
white-space: nowrap; /* 防止换行 */
flex: 1; /* 关键:均分宽度,消除滚动条 */
min-width: 0; /* 允许压缩 */
white-space: nowrap; /* 闃叉鎹㈣ */
flex: 1; /* 鍏抽敭锛氬潎鍒嗗搴︼紝娑堥櫎婊氬姩鏉?*/
min-width: 0; /* 鍏佽鍘嬬缉 */
}
.tab-item.active {
@@ -695,14 +695,14 @@ const onRefresh = () => {
}
.tab-name {
font-size: 14px; /* 微调字体大小适配小屏 */
font-size: 14px; /* 寰皟瀛椾綋澶у皬閫傞厤灏忓睆 */
overflow: hidden;
text-overflow: ellipsis;
}
/* 徽标样式优化 - 放在文字右边 */
/* 寰芥爣鏍峰紡浼樺寲 - 鏀惧湪鏂囧瓧鍙宠竟 */
.tab-badge {
background-color: #FF5722;
background-color: #ff5000;
color: white;
font-size: 10px;
padding: 1px 5px;
@@ -710,24 +710,24 @@ const onRefresh = () => {
min-width: 16px;
text-align: center;
font-weight: bold;
margin-left: 4px; /* 距离文字的间距 */
margin-left: 4px; /* 璺濈鏂囧瓧鐨勯棿璺?*/
display: flex;
align-items: center;
justify-content: center;
height: 16px;
}
/* 消息内容区 */
/* 娑堟伅鍐呭鍖?*/
.messages-content {
flex: 1; /* 关键:自适应剩余高度 */
height: 0; /* 关键强制flex item计算高度 */
flex: 1; /* 鍏抽敭锛氳嚜閫傚簲鍓╀綑楂樺害 */
height: 0; /* 鍏抽敭锛氬己鍒秄lex item璁$畻楂樺害 */
width: 100%;
max-width: 1400px;
margin: 0 auto;
padding-bottom: 20px;
}
/* 客服信息区域 */
/* 瀹㈡湇淇℃伅鍖哄煙 */
.customer-service-info {
background: white;
border-radius: 12px;
@@ -802,7 +802,7 @@ const onRefresh = () => {
font-weight: 500;
}
/* 消息项 */
/* 娑堟伅椤?*/
.message-section {
padding: 10px;
}
@@ -931,7 +931,7 @@ const onRefresh = () => {
.message-unread-count {
font-size: 11px;
color: white;
background: #FF5722;
background: #ff5000;
padding: 2px 6px;
border-radius: 10px;
min-width: 18px;
@@ -1000,7 +1000,7 @@ const onRefresh = () => {
.order-status.processing {
background: #FFF3E0;
color: #FF9800;
color: #ff5000;
}
.order-status.completed {
@@ -1010,7 +1010,7 @@ const onRefresh = () => {
.important-tag {
display: inline-block;
background-color: #FF5722;
background-color: #ff5000;
color: white;
font-size: 11px;
padding: 3px 8px;
@@ -1019,7 +1019,7 @@ const onRefresh = () => {
}
.coupon-info {
background: linear-gradient(135deg, #FF9800, #FF5722);
background: linear-gradient(135deg, #ff5000, #ff5000);
border-radius: 8px;
padding: 10px;
margin-top: 8px;
@@ -1048,7 +1048,7 @@ const onRefresh = () => {
align-self: flex-start;
}
/* 客服系统提示 */
/* 瀹㈡湇绯荤粺鎻愮ず */
.service-tips {
background: #FFF3E0;
border-radius: 10px;
@@ -1061,7 +1061,7 @@ const onRefresh = () => {
.tip-icon {
font-size: 18px;
color: #FF9800;
color: #ff5000;
flex-shrink: 0;
margin-top: 2px;
}
@@ -1072,7 +1072,7 @@ const onRefresh = () => {
line-height: 1.5;
}
/* 空状态 */
/* 绌虹姸鎬?*/
.empty-messages {
display: flex;
flex-direction: column;
@@ -1099,7 +1099,7 @@ const onRefresh = () => {
color: #999;
}
/* 底部浮动按钮 */
/* 搴曢儴娴姩鎸夐挳 */
.floating-action {
position: fixed;
bottom: 20px;
@@ -1133,7 +1133,7 @@ const onRefresh = () => {
height: 80px;
}
/* 响应式适配 */
/* 鍝嶅簲寮忛€傞厤 */
@media screen and (max-width: 320px) {
.tab-name {
font-size: 13px;
@@ -1167,17 +1167,17 @@ const onRefresh = () => {
grid-template-columns: repeat(4, 1fr);
}
/* 平板和桌面端优化标签栏显示 */
/* 骞虫澘鍜屾闈㈢浼樺寲鏍囩鏍忔樉绀?*/
.message-tabs {
justify-content: flex-start; /* 左对齐 */
justify-content: flex-start; /* 宸﹀榻?*/
}
.tab-item {
padding: 0 24px; /* 增加点击区域 */
padding: 0 24px; /* 澧炲姞鐐瑰嚮鍖哄煙 */
}
}
/* 平板设备 */
/* 骞虫澘璁惧 */
@media screen and (min-width: 768px) {
.smart-navbar {
padding: 0 30px;
@@ -1200,7 +1200,7 @@ const onRefresh = () => {
}
}
/* 暗黑模式适配 */
/* 鏆楅粦妯″紡閫傞厤 */
@media (prefers-color-scheme: dark) {
.messages-page {
background-color: #121212;
@@ -1276,3 +1276,4 @@ const onRefresh = () => {
}
}
</style>

View File

@@ -1,4 +1,4 @@
<!-- 消费者端 - 订单详情页 -->
<!-- 消费者端 - 订单详情页 -->
<template>
<view class="order-detail-page">
<scroll-view scroll-y="true" class="scroll-content">
@@ -1240,4 +1240,4 @@ onLoad((options) => {
/* 状态样式 */
.status-4 .status-text { /* Completed */ }
</style>
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,23 @@
<!-- pages/mall/consumer/orders.uvue -->
<!-- pages/mall/consumer/orders.uvue -->
<template>
<view class="orders-page">
<!-- 顶部标题栏 -->
<!-- 椤堕儴鏍囬鏍?-->
<view class="orders-header">
<view class="header-search full-width">
<input
class="search-input"
type="text"
placeholder="搜索订单号或商品名称"
placeholder="鎼滅储璁㈠崟鍙锋垨鍟嗗搧鍚嶇О"
:value="searchKeyword"
@input="onSearchInput"
@confirm="onSearchConfirm"
/>
<text v-if="searchKeyword" class="search-clear" @click="clearSearch">×</text>
<text v-else class="search-icon">🔍</text>
<text v-if="searchKeyword" class="search-clear" @click="clearSearch"></text>
<text v-else class="search-icon">馃攳</text>
</view>
</view>
<!-- 订单状态筛选 -->
<!-- 璁㈠崟鐘舵€佺瓫閫?-->
<view class="order-tabs">
<scroll-view scroll-x class="tab-scroll" :show-scrollbar="false">
<view class="tab-container">
@@ -35,7 +35,7 @@
</scroll-view>
</view>
<!-- 订单列表 -->
<!-- 璁㈠崟鍒楄〃 -->
<scroll-view
scroll-y
class="orders-content"
@@ -44,15 +44,15 @@
@refresherrefresh="onRefresh"
@scrolltolower="loadMore"
>
<!-- 空状态 -->
<!-- 绌虹姸鎬?-->
<view v-if="!loading && orders.length === 0" class="empty-orders">
<text class="empty-icon">📦</text>
<text class="empty-title">暂无订单</text>
<text class="empty-desc">去逛逛,发现心仪的商品</text>
<button class="go-shopping-btn" @click="goShopping">去逛逛</button>
<text class="empty-icon">馃摝</text>
<text class="empty-title">鏆傛棤璁㈠崟</text>
<text class="empty-desc">鍘婚€涢€涳紝鍙戠幇蹇冧华鐨勫晢鍝?/text>
<button class="go-shopping-btn" @click="goShopping">鍘婚€涢€?/button>
</view>
<!-- 订单列表 -->
<!-- 璁㈠崟鍒楄〃 -->
<view v-else class="order-list">
<view
v-for="order in orders"
@@ -60,19 +60,19 @@
class="order-card"
@click="viewOrderDetail(order.id)"
>
<!-- 订单头部:显示店铺名称 -->
<!-- 璁㈠崟澶撮儴锛氭樉绀哄簵閾哄悕绉?-->
<view class="order-card-header">
<view class="shop-info">
<text class="shop-icon">🏪</text>
<text class="shop-name">{{ order.shop_name != null && order.shop_name != '' ? order.shop_name : '自营店铺' }}</text>
<text class="arrow-right"></text>
<text class="shop-icon">馃彧</text>
<text class="shop-name">{{ order.shop_name != null && order.shop_name != '' ? order.shop_name : '鑷惀搴楅摵' }}</text>
<text class="arrow-right">鈥?/text>
</view>
<text :class="['order-status', getStatusClass(order.status)]">
{{ getStatusText(order.status) }}
</text>
</view>
<!-- 订单商品 -->
<!-- 璁㈠崟鍟嗗搧 -->
<view class="order-products">
<view
v-for="product in order.products"
@@ -91,68 +91,68 @@
<text class="product-spec">{{ product.spec }}</text>
</view>
<view class="product-footer">
<text class="product-price">¥{{ product.price }}</text>
<text class="product-price">{{ product.price }}</text>
<text class="product-quantity">x{{ product.quantity }}</text>
</view>
</view>
</view>
</view>
<!-- 订单汇总信息 -->
<!-- 璁㈠崟姹囨€讳俊鎭?-->
<view class="order-summary">
<text class="order-time">{{ formatDate(order.create_time) }}</text>
<view class="summary-right">
<text class="summary-label">共{{ order.products.length }}件商品 实付:</text>
<text class="summary-price">¥{{ order.total_amount }}</text>
<text class="summary-label">鍏眥{ order.products.length }}浠跺晢鍝?瀹炰粯:</text>
<text class="summary-price">{{ order.total_amount }}</text>
</view>
</view>
<!-- 订单操作 -->
<!-- 璁㈠崟鎿嶄綔 -->
<view class="order-actions" @click.stop="">
<view v-if="order.status === 1" class="action-buttons">
<button class="action-btn cancel" @click="cancelOrder(order.id)">取消订单</button>
<button class="action-btn pay" @click="payOrder(order.id)">立即支付</button>
<button class="action-btn cancel" @click="cancelOrder(order.id)">鍙栨秷璁㈠崟</button>
<button class="action-btn pay" @click="payOrder(order.id)">绔嬪嵆鏀粯</button>
</view>
<view v-if="order.status === 2" class="action-buttons">
<button class="action-btn remind" @click="remindShipping(order.id)">提醒发货</button>
<button class="action-btn refund" @click.stop="onApplyRefund(order)">申请售后</button>
<button class="action-btn remind" @click="remindShipping(order.id)">鎻愰啋鍙戣揣</button>
<button class="action-btn refund" @click.stop="onApplyRefund(order)">鐢宠鍞悗</button>
</view>
<view v-if="order.status === 3" class="action-buttons">
<button class="action-btn view" @click="viewLogistics(order.id)">查看物流</button>
<button class="action-btn confirm" @click="confirmReceipt(order.id)">确认收货</button>
<button class="action-btn refund" @click.stop="onApplyRefund(order)">申请售后</button>
<button class="action-btn view" @click="viewLogistics(order.id)">鏌ョ湅鐗╂祦</button>
<button class="action-btn confirm" @click="confirmReceipt(order.id)">纭鏀惰揣</button>
<button class="action-btn refund" @click.stop="onApplyRefund(order)">鐢宠鍞悗</button>
</view>
<view v-if="order.status === 4" class="action-buttons">
<button class="action-btn review" @click="goReview(order)">评价</button>
<button class="action-btn refund" @click.stop="onApplyRefund(order)">申请售后</button>
<button class="action-btn repurchase" @click="repurchase(order)">再次购买</button>
<button class="action-btn review" @click="goReview(order)">璇勪环</button>
<button class="action-btn refund" @click.stop="onApplyRefund(order)">鐢宠鍞悗</button>
<button class="action-btn repurchase" @click="repurchase(order)">鍐嶆璐拱</button>
</view>
<view v-if="order.status === 5" class="action-buttons">
<button class="action-btn view" @click="viewOrderDetail(order.id)">查看详情</button>
<button class="action-btn view" @click="viewOrderDetail(order.id)">鏌ョ湅璇︽儏</button>
</view>
</view>
</view>
</view>
<!-- 加载更多 -->
<!-- 鍔犺浇鏇村 -->
<view v-if="loadingMore" class="loading-more">
<view class="loading-spinner"></view>
<text>加载中...</text>
<text>鍔犺浇涓?..</text>
</view>
<view v-if="!hasMore && orders.length > 0" class="no-more">
<text>没有更多订单了</text>
<text>娌℃湁鏇村璁㈠崟浜?/text>
</view>
<!-- 安全区域 -->
<!-- 瀹夊叏鍖哄煙 -->
<view class="safe-area"></view>
</scroll-view>
<!-- 底部导航 -->
<!-- 搴曢儴瀵艰埅 -->
<view class="tabbar-placeholder"></view>
</view>
</template>
@@ -162,14 +162,14 @@ import { ref, reactive, onMounted, computed } from 'vue'
import { onShow, onLoad, onBackPress } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts'
// 定义标签页类型
// 瀹氫箟鏍囩椤电被鍨?
type OrderTabItem = {
id: string,
name: string,
count: number
}
// 定义订单产品类型
// 瀹氫箟璁㈠崟浜у搧绫诲瀷
type OrderProduct = {
id: string,
name: string,
@@ -179,7 +179,7 @@ type OrderProduct = {
quantity: number
}
// 定义订单类型
// 瀹氫箟璁㈠崟绫诲瀷
type OrderItem = {
id: string,
order_no: string,
@@ -193,7 +193,7 @@ type OrderItem = {
products: OrderProduct[]
}
// 响应式数据
// 鍝嶅簲寮忔暟鎹?
const orders = ref<OrderItem[]>([])
const allOrdersList = ref<OrderItem[]>([]) // Store all fetched orders for client-side filtering
const loading = ref<boolean>(false)
@@ -204,20 +204,20 @@ const page = ref<number>(1)
const activeTab = ref<string>('all')
const searchKeyword = ref<string>('')
// 订单标签页 - 使用 ref 以便整体替换
// 璁㈠崟鏍囩椤?- 浣跨敤 ref 浠ヤ究鏁翠綋鏇挎崲
const orderTabs = ref<OrderTabItem[]>([
{ id: 'all', name: '全部', count: 0 },
{ id: 'pending', name: '待付款', count: 0 },
{ id: 'shipping', name: '待发货', count: 0 },
{ id: 'delivering', name: '待收货', count: 0 },
{ id: 'completed', name: '已完成', count: 0 },
{ id: 'cancelled', name: '已取消', count: 0 }
{ id: 'all', name: '鍏ㄩ儴', count: 0 },
{ id: 'pending', name: '寰呬粯娆?, count: 0 },
{ id: 'shipping', name: '寰呭彂璐?, count: 0 },
{ id: 'delivering', name: '寰呮敹璐?, count: 0 },
{ id: 'completed', name: '宸插畬鎴?, count: 0 },
{ id: 'cancelled', name: '宸插彇娑?, count: 0 }
])
// Removed Mock Data
// 辅助函数:获取状态码
// 杈呭姪鍑芥暟锛氳幏鍙栫姸鎬佺爜
const getStatusByTab = (tabId: string): number => {
if (tabId == 'pending') return 1
if (tabId == 'shipping') return 2
@@ -227,11 +227,11 @@ const getStatusByTab = (tabId: string): number => {
return 0
}
// 格式化规格对象为友好的文本 - 必须在 parseSpecText 之前定义
// 鏍煎紡鍖栬鏍煎璞′负鍙嬪ソ鐨勬枃鏈?- 蹇呴』鍦?parseSpecText 涔嬪墠瀹氫箟
function formatSpecObj(obj: any): string {
if (obj == null) return ''
if (typeof obj !== 'object') {
// 非对象类型直接返回字符串形式
// 闈炲璞$被鍨嬬洿鎺ヨ繑鍥炲瓧绗︿覆褰㈠紡
if (typeof obj === 'string') return obj
if (typeof obj === 'number') return obj.toString()
return ''
@@ -244,14 +244,14 @@ function formatSpecObj(obj: any): string {
const specObj = objParsed as UTSJSONObject
// 使用 JSON.stringify 获取所有键
// 浣跨敤 JSON.stringify 鑾峰彇鎵€鏈夐敭
const specObjStr = JSON.stringify(specObj)
const specObjForKeys = JSON.parse(specObjStr) as UTSJSONObject
// 手动提取键值对
// 鎵嬪姩鎻愬彇閿€煎
const parts: string[] = []
// 尝试获取已知字段
// 灏濊瘯鑾峰彇宸茬煡瀛楁
const colorVal = specObjForKeys.getString('Color')
if (colorVal != null && colorVal != '') {
parts.push('Color: ' + colorVal)
@@ -262,14 +262,14 @@ function formatSpecObj(obj: any): string {
parts.push('Size: ' + sizeVal)
}
const defaultVal = specObjForKeys.getString('默认')
const defaultVal = specObjForKeys.getString('榛樿')
if (defaultVal != null && defaultVal != '') {
parts.push('默认: ' + defaultVal)
parts.push('榛樿: ' + defaultVal)
}
// 如果没有匹配到已知字段,尝试直接显示 JSON
// 濡傛灉娌℃湁鍖归厤鍒板凡鐭ュ瓧娈碉紝灏濊瘯鐩存帴鏄剧ず JSON
if (parts.length === 0) {
// 尝试遍历对象
// 灏濊瘯閬嶅巻瀵硅薄
const objAny = specObjForKeys as any
if (objAny != null) {
return specObjStr.replace(/[{}"]/g, '').replace(/:/g, ': ').replace(/,/g, ' | ')
@@ -282,11 +282,11 @@ function formatSpecObj(obj: any): string {
}
}
// 辅助函数:解析规格文本
// 杈呭姪鍑芥暟锛氳В鏋愯鏍兼枃鏈?
function parseSpecText(specs: any): string {
if (specs == null) return ''
if (typeof specs === 'string') {
// 如果是 JSON 字符串,尝试解析
// 濡傛灉鏄?JSON 瀛楃涓诧紝灏濊瘯瑙f瀽
if (specs.startsWith('{') || specs.startsWith('[')) {
try {
const parsed = JSON.parse(specs)
@@ -298,13 +298,13 @@ function parseSpecText(specs: any): string {
}
return specs
}
// 对于对象类型,格式化显示
// 瀵逛簬瀵硅薄绫诲瀷锛屾牸寮忓寲鏄剧ず
return formatSpecObj(specs)
}
// 辅助函数:更新标签计数
// 杈呭姪鍑芥暟锛氭洿鏂版爣绛捐鏁?
const updateTabsCounts = (allOrders: OrderItem[]) => {
// 计算各状态数量
// 璁$畻鍚勭姸鎬佹暟閲?
const countAll = allOrders.length
const countPending = allOrders.filter((o: OrderItem) => o.status === 1).length
const countShipping = allOrders.filter((o: OrderItem) => o.status === 2).length
@@ -312,7 +312,7 @@ const updateTabsCounts = (allOrders: OrderItem[]) => {
const countCompleted = allOrders.filter((o: OrderItem) => o.status === 4).length
const countCancelled = allOrders.filter((o: OrderItem) => o.status === 5).length
// 更新数组元素
// 鏇存柊鏁扮粍鍏冪礌
orderTabs.value[0].count = countAll
orderTabs.value[1].count = countPending
orderTabs.value[2].count = countShipping
@@ -321,7 +321,7 @@ const updateTabsCounts = (allOrders: OrderItem[]) => {
orderTabs.value[5].count = countCancelled
}
// 辅助函数:按标签筛选订单
// 杈呭姪鍑芥暟锛氭寜鏍囩绛涢€夎鍗?
const filterOrdersByTab = () => {
if (activeTab.value === 'all') {
orders.value = allOrdersList.value
@@ -333,20 +333,20 @@ const filterOrdersByTab = () => {
}
}
// 加载订单数据
// 鍔犺浇璁㈠崟鏁版嵁
const loadOrders = async () => {
loading.value = true
try {
// Fetch all orders from Supabase (status=0)
const fetchedOrders = await supabaseService.getOrders(0)
console.log('[loadOrders] 获取到订单数量:', fetchedOrders.length)
console.log('[loadOrders] 鑾峰彇鍒拌鍗曟暟閲?', fetchedOrders.length)
// Map to View Model
const mappedOrders: OrderItem[] = []
for (let i = 0; i < fetchedOrders.length; i++) {
const order = fetchedOrders[i]
// 使用 JSON 序列化转换
// 浣跨敤 JSON 搴忓垪鍖栬浆鎹?
const orderStr = JSON.stringify(order)
const orderParsed = JSON.parse(orderStr)
if (orderParsed == null) continue
@@ -355,13 +355,13 @@ const loadOrders = async () => {
const itemsRaw = orderObj.get('ml_order_items')
const productsList: OrderProduct[] = []
console.log('[loadOrders] 订单商品数据:', itemsRaw)
console.log('[loadOrders] 璁㈠崟鍟嗗搧鏁版嵁:', itemsRaw)
if (itemsRaw != null) {
// 先检查是否为数组
// 鍏堟鏌ユ槸鍚︿负鏁扮粍
if (Array.isArray(itemsRaw)) {
const items = itemsRaw as any[]
console.log('[loadOrders] 商品数量:', items.length)
console.log('[loadOrders] 鍟嗗搧鏁伴噺:', items.length)
for (let j = 0; j < items.length; j++) {
const item = items[j]
const itemStr = JSON.stringify(item)
@@ -378,11 +378,11 @@ const loadOrders = async () => {
const imageUrl = itemObj.getString('image_url')
const quantity = itemObj.getNumber('quantity')
console.log('[loadOrders] 商品:', productName, '图片:', imageUrl, '规格:', specText)
console.log('[loadOrders] 鍟嗗搧:', productName, '鍥剧墖:', imageUrl, '瑙勬牸:', specText)
const productItem: OrderProduct = {
id: productId ?? '',
name: productName ?? '未知商品',
name: productName ?? '鏈煡鍟嗗搧',
price: price ?? 0,
image: imageUrl ?? '/static/default-product.png',
spec: specText,
@@ -403,8 +403,8 @@ const loadOrders = async () => {
const paidAmount = orderObj.getNumber('paid_amount')
const merchantId = orderObj.getString('merchant_id')
// 从关联查询的 ml_shops 表获取店铺名称
let shopName = '自营店铺'
// 浠庡叧鑱旀煡璇㈢殑 ml_shops 琛ㄨ幏鍙栧簵閾哄悕绉?
let shopName = '鑷惀搴楅摵'
const shopsRaw = orderObj.get('ml_shops')
if (shopsRaw != null) {
const shopStr = JSON.stringify(shopsRaw)
@@ -417,16 +417,16 @@ const loadOrders = async () => {
}
}
} else if (merchantId != null && merchantId != '') {
shopName = '商家店铺'
shopName = '鍟嗗搴楅摵'
}
console.log('[loadOrders] 订单号:', orderNo, '店铺:', shopName, '商品数:', productsList.length)
console.log('[loadOrders] 璁㈠崟鍙?', orderNo, '搴楅摵:', shopName, '鍟嗗搧鏁?', productsList.length)
// 如果没有商品数据,添加一个占位商品
// 濡傛灉娌℃湁鍟嗗搧鏁版嵁锛屾坊鍔犱竴涓崰浣嶅晢鍝?
if (productsList.length === 0) {
const placeholderProduct: OrderProduct = {
id: 'placeholder',
name: '订单商品',
name: '璁㈠崟鍟嗗搧',
price: totalAmount ?? paidAmount ?? 0,
image: '/static/default-product.png',
spec: '',
@@ -450,7 +450,7 @@ const loadOrders = async () => {
mappedOrders.push(mappedOrder)
}
// Sort by created_at desc - 直接使用 OrderItem 类型访问属性
// Sort by created_at desc - 鐩存帴浣跨敤 OrderItem 绫诲瀷璁块棶灞炴€?
mappedOrders.sort((a: OrderItem, b: OrderItem) => {
const timeA = new Date(a.create_time).getTime()
const timeB = new Date(b.create_time).getTime()
@@ -466,14 +466,14 @@ const loadOrders = async () => {
filterOrdersByTab()
} catch (err) {
console.error('加载订单异常:', err)
uni.showToast({ title: '加载订单失败', icon: 'none' })
console.error('鍔犺浇璁㈠崟寮傚父:', err)
uni.showToast({ title: '鍔犺浇璁㈠崟澶辫触', icon: 'none' })
} finally {
loading.value = false
}
}
// 生命周期
// 鐢熷懡鍛ㄦ湡
onLoad((options) => {
if (options == null) return
const statusVal = options['status']
@@ -487,9 +487,9 @@ onLoad((options) => {
if (typeVal != null) {
const type = typeVal 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' // 申请售后默认显示全部
else if (type === 'shipped') activeTab.value = 'delivering' // 鏄犲皠鍒板緟鏀惰揣
else if (type === 'review') activeTab.value = 'completed' // 鏄犲皠鍒板凡瀹屾垚
else if (type === 'refund') activeTab.value = 'all' // 鐢宠鍞悗榛樿鏄剧ず鍏ㄩ儴
}
})
@@ -503,12 +503,12 @@ const formatDate = (isoString: string): string => {
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 之前定义)
// 杈呭姪鍑芥暟锛氳幏鍙栧綋鍓嶈鍗曟暟鎹紙蹇呴』鍦?performSearch 涔嬪墠瀹氫箟锛?
function getCurrentOrderData(): OrderItem[] {
return allOrdersList.value
}
// 搜索执行函数(必须在 onSearchInput 等之前定义)
// 鎼滅储鎵ц鍑芥暟锛堝繀椤诲湪 onSearchInput 绛変箣鍓嶅畾涔夛級
const performSearch = () => {
const keyword = searchKeyword.value.trim().toLowerCase()
if (keyword == '') {
@@ -516,17 +516,17 @@ const performSearch = () => {
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) => {
@@ -542,7 +542,7 @@ const performSearch = () => {
orders.value = filtered
}
// 搜索相关函数
// 鎼滅储鐩稿叧鍑芥暟
const onSearchInput = (e: any) => {
const eObj = e as Record<string, any>
const detail = eObj['detail'] as Record<string, any>
@@ -568,23 +568,23 @@ const formatSpec = (specs: any): string => {
return ''
}
// 切换标签
// 鍒囨崲鏍囩
const switchTab = (tabId: string) => {
activeTab.value = tabId
filterOrdersByTab()
}
// 获取状态文本
// 鑾峰彇鐘舵€佹枃鏈?
const getStatusText = (status: number): string => {
if (status == 1) return '待付款'
if (status == 2) return '待发货'
if (status == 3) return '待收货'
if (status == 4) return '已完成'
if (status == 5) return '已取消'
return '未知状态'
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 => {
if (status == 1) return 'status-pending'
if (status == 2) return 'status-shipping'
@@ -594,41 +594,41 @@ const getStatusClass = (status: number): string => {
return 'status-unknown'
}
// 下拉刷新
// 涓嬫媺鍒锋柊
const onRefresh = () => {
refreshing.value = true
setTimeout(() => {
loadOrders()
refreshing.value = false
uni.showToast({
title: '刷新成功',
title: '鍒锋柊鎴愬姛',
icon: 'success'
})
}, 1000)
}
// 上拉加载更多
// 涓婃媺鍔犺浇鏇村
const loadMore = () => {
if (loadingMore.value || !hasMore.value) return
// 暂未实现分页,直接返回
// 鏆傛湭瀹炵幇鍒嗛〉锛岀洿鎺ヨ繑鍥?
hasMore.value = false
}
// 订单操作函数
// 璁㈠崟鎿嶄綔鍑芥暟
const cancelOrder = (orderId: string) => {
uni.showModal({
title: '确认取消',
content: '确定要取消此订单吗?',
title: '纭鍙栨秷',
content: '纭畾瑕佸彇娑堟璁㈠崟鍚楋紵',
success: (res) => {
if (res.confirm) {
// 这里应该是实际的API调用
// 杩欓噷搴旇鏄疄闄呯殑API璋冪敤
uni.showToast({
title: '订单已取消',
title: '璁㈠崟宸插彇娑?,
icon: 'success'
})
// 更新订单状态
// 鏇存柊璁㈠崟鐘舵€?
const index = orders.value.findIndex((o: any) => {
const obj = o as Record<string, any>
return obj['id'] === orderId
@@ -651,7 +651,7 @@ const payOrder = (orderId: string) => {
const remindShipping = (orderId: string) => {
uni.showToast({
title: '已提醒卖家发货',
title: '宸叉彁閱掑崠瀹跺彂璐?,
icon: 'success'
})
}
@@ -662,7 +662,7 @@ const viewLogistics = (orderId: string) => {
})
}
// goReview 必须在 doConfirmReceipt 之前定义,因为 doConfirmReceipt 会调用它
// goReview 蹇呴』鍦?doConfirmReceipt 涔嬪墠瀹氫箟锛屽洜涓?doConfirmReceipt 浼氳皟鐢ㄥ畠
const goReview = (order: any) => {
const orderObj = order as Record<string, any>
const products = orderObj['products'] as any[]
@@ -678,18 +678,18 @@ const goReview = (order: any) => {
}
const doConfirmReceipt = async (orderId: string) => {
uni.showLoading({ title: '处理中...' })
uni.showLoading({ title: '澶勭悊涓?..' })
try {
const result = await supabaseService.confirmReceipt(orderId)
uni.hideLoading()
if (result.success) {
uni.showToast({
title: '收货成功',
title: '鏀惰揣鎴愬姛',
icon: 'success'
})
// 更新本地状态
// 鏇存柊鏈湴鐘舵€?
const index = orders.value.findIndex((o: any) => {
const obj = o as Record<string, any>
return obj['id'] === orderId
@@ -700,7 +700,7 @@ const doConfirmReceipt = async (orderId: string) => {
orders.value = [...orders.value]
}
// 跳转到评价页面
// 璺宠浆鍒拌瘎浠烽〉闈?
setTimeout(() => {
const order = orders.value.find((o: any) => {
const obj = o as Record<string, any>
@@ -712,14 +712,14 @@ const doConfirmReceipt = async (orderId: string) => {
}, 1000)
} else {
uni.showToast({
title: result.error ?? '确认收货失败',
title: result.error ?? '纭鏀惰揣澶辫触',
icon: 'none'
})
}
} catch (e) {
uni.hideLoading()
uni.showToast({
title: '系统异常',
title: '绯荤粺寮傚父',
icon: 'none'
})
}
@@ -727,8 +727,8 @@ const doConfirmReceipt = async (orderId: string) => {
const confirmReceipt = (orderId: string) => {
uni.showModal({
title: '确认收货',
content: '请确认您已收到商品,且商品无误',
title: '纭鏀惰揣',
content: '璇风‘璁ゆ偍宸叉敹鍒板晢鍝侊紝涓斿晢鍝佹棤璇?,
success: (res) => {
if (res.confirm) {
doConfirmReceipt(orderId)
@@ -739,13 +739,13 @@ const confirmReceipt = (orderId: string) => {
const repurchase = (order: any) => {
uni.showModal({
title: '再次购买',
content: '确定要将这些商品加入购物车吗?',
title: '鍐嶆璐拱',
content: '纭畾瑕佸皢杩欎簺鍟嗗搧鍔犲叆璐墿杞﹀悧锛?,
success: (res) => {
if (res.confirm) {
// 这里应该是实际的API调用
// 杩欓噷搴旇鏄疄闄呯殑API璋冪敤
uni.showToast({
title: '已加入购物车',
title: '宸插姞鍏ヨ喘鐗╄溅',
icon: 'success'
})
}
@@ -767,7 +767,7 @@ const onApplyRefund = (order: any) => {
})
}
// 导航函数
// 瀵艰埅鍑芥暟
const navigateToSearch = () => {
uni.navigateTo({ url: '/pages/mall/consumer/search' })
}
@@ -794,7 +794,7 @@ const goShopping = () => {
overflow: hidden;
}
/* 头部 */
/* 澶撮儴 */
.orders-header {
background-color: white;
padding: 15px;
@@ -856,7 +856,7 @@ const goShopping = () => {
/* cursor: pointer; removed */
}
/* 标签页 */
/* 鏍囩椤?*/
.order-tabs {
background-color: #ffffff;
border-bottom: 1px solid #e5e5e5;
@@ -879,15 +879,15 @@ const goShopping = () => {
}
.tab-item {
/* 移除 flex: 1,改为自适应宽度或固定最小宽度 */
padding: 15px 15px; /* 增加水平内边距 */
/* 绉婚櫎 flex: 1锛屾敼涓鸿嚜閫傚簲瀹藉害鎴栧浐瀹氭渶灏忓搴?*/
padding: 15px 15px; /* 澧炲姞姘村钩鍐呰竟璺?*/
text-align: center;
position: relative;
display: flex;
justify-content: center;
align-items: center;
white-space: nowrap; /* 防止文字换行 */
flex-shrink: 0; /* 防止被压缩 */
white-space: nowrap; /* 闃叉鏂囧瓧鎹㈣ */
flex-shrink: 0; /* 闃叉琚帇缂?*/
}
.tab-item.active {
@@ -930,14 +930,14 @@ const goShopping = () => {
text-align: center;
}
/* 内容区 */
/* 鍐呭鍖?*/
.orders-content {
flex: 1;
height: 0;
width: 100%;
}
/* 空状态 */
/* 绌虹姸鎬?*/
.empty-orders {
display: flex;
flex-direction: column;
@@ -973,7 +973,7 @@ const goShopping = () => {
font-size: 16px;
}
/* 订单列表 */
/* 璁㈠崟鍒楄〃 */
.order-list {
padding: 10px;
}
@@ -986,7 +986,7 @@ const goShopping = () => {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
/* 订单头部 */
/* 璁㈠崟澶撮儴 */
.order-header {
display: flex;
justify-content: space-between;
@@ -1025,14 +1025,14 @@ const goShopping = () => {
color: #999;
}
/* 订单商品 */
/* 璁㈠崟鍟嗗搧 */
.order-products {
padding: 15px;
}
.order-product {
display: flex;
flex-direction: row; /* 显式声明横向排列 */
flex-direction: row; /* 鏄惧紡澹版槑妯悜鎺掑垪 */
margin-bottom: 15px;
width: 100%;
}
@@ -1046,16 +1046,16 @@ const goShopping = () => {
height: 80px;
border-radius: 8px;
margin-right: 12px;
flex-shrink: 0; /* 防止图片被压缩 */
flex-shrink: 0; /* 闃叉鍥剧墖琚帇缂?*/
}
.product-info {
flex: 1; /* 占据右侧剩余所有空间 */
flex: 1; /* 鍗犳嵁鍙充晶鍓╀綑鎵€鏈夌┖闂?*/
display: flex;
flex-direction: column;
justify-content: space-between;
height: 80px;
overflow: hidden; /* 防止文字溢出 */
overflow: hidden; /* 闃叉鏂囧瓧婧㈠嚭 */
}
.product-top-info {
@@ -1098,7 +1098,7 @@ const goShopping = () => {
color: #999;
}
/* 订单信息 */
/* 璁㈠崟淇℃伅 */
.order-info {
padding: 15px;
border-top: 1px solid #f5f5f5;
@@ -1138,7 +1138,7 @@ const goShopping = () => {
font-weight: bold;
}
/* 订单操作 */
/* 璁㈠崟鎿嶄綔 */
.order-actions {
padding: 15px;
}
@@ -1193,7 +1193,7 @@ const goShopping = () => {
border-color: #ff5000;
}
/* 加载更多 */
/* 鍔犺浇鏇村 */
.loading-more {
padding: 20px;
display: flex;
@@ -1218,18 +1218,18 @@ const goShopping = () => {
padding: 20px 0;
}
/* 安全区域 */
/* 瀹夊叏鍖哄煙 */
.safe-area {
height: 20px;
}
/* 底部导航占位 */
/* 搴曢儴瀵艰埅鍗犱綅 */
.tabbar-placeholder {
height: 50px;
background-color: #f5f5f5;
}
/* 响应式适配 */
/* 鍝嶅簲寮忛€傞厤 */
@media screen and (min-width: 768px) {
.order-list {
display: flex;
@@ -1254,7 +1254,7 @@ const goShopping = () => {
}
}
/* 订单卡片新样式 */
/* 璁㈠崟鍗$墖鏂版牱寮?*/
.order-card-header {
padding: 12px 15px;
display: flex;
@@ -1356,3 +1356,5 @@ const goShopping = () => {
}
}
</style>

View File

@@ -1,4 +1,4 @@
<!-- pages/mall/consumer/orders.uvue -->
<!-- pages/mall/consumer/orders.uvue -->
<template>
<view class="orders-page">
<!-- 顶部标题栏 -->
@@ -74,9 +74,12 @@
<text class="shop-name">{{ order.shop_name != null && order.shop_name != '' ? order.shop_name : '自营店铺' }}</text>
<text class="arrow-right"></text>
</view>
<text :class="['order-status', getStatusClass(order.status)]">
{{ getStatusText(order.status) }}
</text>
<view class="status-row">
<text :class="['order-status', getStatusClass(order.status)]">
{{ getStatusText(order.status) }}
</text>
<text class="more-btn" @click.stop="showOrderMenu(order)">⋯</text>
</view>
</view>
<!-- 订单商品 -->
@@ -141,6 +144,16 @@
<view v-if="order.status === 5" class="action-buttons">
<button class="action-btn view" @click="viewOrderDetail(order.id)">查看详情</button>
</view>
<view v-if="order.status === 6" class="action-buttons">
<button class="action-btn view" @click="viewOrderDetail(order.id)">查看详情</button>
<button class="action-btn refund" @click="viewRefundProgress(order.id)">退款进度</button>
</view>
<view v-if="order.status === 7" class="action-buttons">
<button class="action-btn view" @click="viewOrderDetail(order.id)">查看详情</button>
<button class="action-btn repurchase" @click="repurchase(order)">再次购买</button>
</view>
</view>
</view>
</view>
@@ -218,6 +231,7 @@ const orderTabs = ref<OrderTabItem[]>([
{ id: 'shipping', name: '待发货', count: 0 },
{ id: 'delivering', name: '待收货', count: 0 },
{ id: 'completed', name: '已完成', count: 0 },
{ id: 'aftersale', name: '售后', count: 0 },
{ id: 'cancelled', name: '已取消', count: 0 }
])
@@ -234,6 +248,7 @@ const getStatusByTab = (tabId: string): number => {
if (tabId == 'delivering') return 3
if (tabId == 'completed') return 4
if (tabId == 'cancelled') return 5
if (tabId == 'aftersale') return 6
return 0
}
@@ -314,27 +329,31 @@ function parseSpecText(specs: any): string {
// 辅助函数:更新标签计数
const updateTabsCounts = (allOrders: OrderItem[]) => {
// 计算各状态数量
const countAll = allOrders.length
const countPending = allOrders.filter((o: OrderItem) => o.status === 1).length
const countShipping = allOrders.filter((o: OrderItem) => o.status === 2).length
const countDelivering = allOrders.filter((o: OrderItem) => o.status === 3).length
const countCompleted = allOrders.filter((o: OrderItem) => o.status === 4).length
const countCancelled = allOrders.filter((o: OrderItem) => o.status === 5).length
const countAftersale = allOrders.filter((o: OrderItem) => o.status === 6 || o.status === 7).length
// 更新数组元素
orderTabs.value[0].count = countAll
orderTabs.value[1].count = countPending
orderTabs.value[2].count = countShipping
orderTabs.value[3].count = countDelivering
orderTabs.value[4].count = countCompleted
orderTabs.value[5].count = countCancelled
orderTabs.value[5].count = countAftersale
orderTabs.value[6].count = countCancelled
}
// 辅助函数:按标签筛选订单
const filterOrdersByTab = () => {
if (activeTab.value === 'all') {
orders.value = allOrdersList.value
} else if (activeTab.value === 'aftersale') {
orders.value = allOrdersList.value.filter((o: OrderItem) => {
return o.status === 6 || o.status === 7
})
} else {
const targetStatus = getStatusByTab(activeTab.value)
orders.value = allOrdersList.value.filter((o: OrderItem) => {
@@ -489,7 +508,7 @@ onLoad((options) => {
const statusVal = options['status']
if (statusVal != null) {
const status = statusVal as string
if (['all', 'pending', 'shipping', 'delivering', 'completed', 'cancelled'].includes(status)) {
if (['all', 'pending', 'shipping', 'delivering', 'completed', 'aftersale', 'cancelled'].includes(status)) {
activeTab.value = status
}
}
@@ -608,6 +627,47 @@ const getStatusClass = (status: number): string => {
return 'status-unknown'
}
// 联系卖家
const contactSeller = (order: OrderItem) => {
if (order.merchant_id != '') {
uni.navigateTo({
url: `/pages/mall/consumer/chat?merchantId=${order.merchant_id}`
})
} else {
uni.showToast({
title: '暂无卖家联系方式',
icon: 'none'
})
}
}
// 删除订单
const deleteOrder = (orderId: string) => {
uni.showModal({
title: '删除订单',
content: '确定要删除此订单吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({ title: '删除中...' })
supabaseService.deleteOrder(orderId).then(() => {
uni.hideLoading()
uni.showToast({
title: '订单已删除',
icon: 'success'
})
loadOrders()
}).catch(() => {
uni.hideLoading()
uni.showToast({
title: '删除失败',
icon: 'none'
})
})
}
}
})
}
// 下拉刷新
const onRefresh = () => {
refreshing.value = true
@@ -636,22 +696,28 @@ const cancelOrder = (orderId: string) => {
content: '确定要取消此订单吗?',
success: (res) => {
if (res.confirm) {
// 这里应该是实际的API调用
uni.showToast({
title: '订单已取消',
icon: 'success'
uni.showLoading({ title: '取消中...' })
supabaseService.cancelOrder(orderId).then((success) => {
uni.hideLoading()
if (success) {
uni.showToast({
title: '订单已取消',
icon: 'success'
})
loadOrders()
} else {
uni.showToast({
title: '取消失败',
icon: 'none'
})
}
}).catch(() => {
uni.hideLoading()
uni.showToast({
title: '取消失败',
icon: 'none'
})
})
// 更新订单状态
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'] = 5
orders.value = [...orders.value]
}
}
}
})
@@ -663,7 +729,35 @@ const payOrder = (orderId: string) => {
})
}
const remindShipping = (orderId: string) => {
const remindShipping = async (orderId: string) => {
// 基础提醒
uni.showLoading({ title: '正在提醒...' })
try {
// 查找订单中的商家ID
const order = orders.value.find(o => o.id === orderId)
if (order != null) {
const merchantId = order.merchant_id
const orderNo = order.order_no
if (merchantId != '') {
// 向商家发送自动催单消息
const message = `你好,我的订单[${orderNo}]还没有发货,请尽快安排,谢谢。`
const success = await supabaseService.sendChatMessage(message, merchantId)
if (success) {
console.log('催单消息发送成功')
} else {
console.warn('催单消息发送失败,可能是由于网络原因')
}
}
}
} catch (e) {
console.error('提醒发货异常:', e)
} finally {
uni.hideLoading()
}
uni.showToast({
title: '已提醒卖家发货',
icon: 'success'
@@ -752,19 +846,81 @@ const confirmReceipt = (orderId: string) => {
}
const repurchase = (order: any) => {
uni.showModal({
title: '再次购买',
content: '确定要将这些商品加入购物车吗?',
success: (res) => {
if (res.confirm) {
// 这里应该是实际的API调用
uni.showToast({
title: '已加入购物车',
icon: 'success'
})
const orderObj = order as Record<string, any>
const products = orderObj['products'] as any[]
if (products == null || products.length === 0) {
uni.showToast({
title: '订单无商品',
icon: 'none'
})
return
}
uni.showLoading({ title: '处理中...' })
let completed = 0
const total = products.length
let successCount = 0
for (let i = 0; i < products.length; i++) {
const pObj = products[i] as Record<string, any>
const productId = pObj['id'] as string
const merchantId = orderObj['merchant_id'] as string
if (productId != null && productId !== '') {
supabaseService.addToCart(productId, 1, '', merchantId ?? '').then((success) => {
completed++
if (success) successCount++
if (completed === total) {
uni.hideLoading()
if (successCount > 0) {
uni.showToast({
title: `已添加${successCount}件商品`,
icon: 'success'
})
} else {
uni.showToast({
title: '添加失败',
icon: 'none'
})
}
}
}).catch(() => {
completed++
if (completed === total) {
uni.hideLoading()
if (successCount > 0) {
uni.showToast({
title: `已添加${successCount}件商品`,
icon: 'success'
})
} else {
uni.showToast({
title: '添加失败',
icon: 'none'
})
}
}
})
} else {
completed++
if (completed === total) {
uni.hideLoading()
if (successCount > 0) {
uni.showToast({
title: `已添加${successCount}件商品`,
icon: 'success'
})
} else {
uni.showToast({
title: '添加失败',
icon: 'none'
})
}
}
}
})
}
}
const viewOrderDetail = (orderId: string) => {
@@ -781,6 +937,65 @@ const onApplyRefund = (order: any) => {
})
}
const viewRefundProgress = (orderId: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/refund?orderId=${orderId}`
})
}
// 处理订单操作
const handleOrderAction = (order: OrderItem, action: string) => {
if (action === '取消订单') {
cancelOrder(order.id)
} else if (action === '联系卖家') {
contactSeller(order)
} else if (action === '提醒发货') {
remindShipping(order.id)
} else if (action === '申请退款' || action === '申请售后') {
onApplyRefund(order)
} else if (action === '查看物流') {
viewLogistics(order.id)
} else if (action === '确认收货') {
confirmReceipt(order.id)
} else if (action === '再次购买') {
repurchase(order)
} else if (action === '删除订单') {
deleteOrder(order.id)
} else if (action === '退款进度') {
viewRefundProgress(order.id)
}
}
// 显示订单操作菜单
const showOrderMenu = (order: OrderItem) => {
const status = order.status
let actions: string[] = []
if (status === 1) {
actions = ['取消订单', '联系卖家']
} else if (status === 2) {
actions = ['提醒发货', '申请退款', '联系卖家']
} else if (status === 3) {
actions = ['查看物流', '确认收货', '申请退款', '联系卖家']
} else if (status === 4) {
actions = ['申请售后', '再次购买', '联系卖家']
} else if (status === 5) {
actions = ['删除订单', '再次购买', '联系卖家']
} else if (status === 6) {
actions = ['退款进度', '联系卖家']
} else if (status === 7) {
actions = ['再次购买', '联系卖家']
}
uni.showActionSheet({
itemList: actions,
success: (res) => {
const action = actions[res.tapIndex]
handleOrderAction(order, action)
}
})
}
// 导航函数
const navigateToSearch = () => {
uni.navigateTo({ url: '/pages/mall/consumer/search' })
@@ -799,12 +1014,14 @@ const goShopping = () => {
<style>
.orders-page {
width: 100%;
height: 100vh;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
position: relative;
overflow: hidden;
}
@@ -816,9 +1033,8 @@ const goShopping = () => {
align-items: center;
justify-content: center;
border-bottom: 1px solid #eee;
/* position: sticky; removed */
/* top: 0; removed */
z-index: 10;
flex-shrink: 0;
}
.header-search.full-width {
@@ -879,10 +1095,29 @@ const goShopping = () => {
align-items: center;
width: 100%;
z-index: 10;
height: 50px;
flex-shrink: 0;
}
/* 安卓端滚动修复 */
.orders-content {
flex: 1;
width: 100%;
height: 0; /* 关键:强制 flex: 1 生效 */
}
.orders-header {
background-color: white;
padding: 15px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #eee;
flex-shrink: 0;
}
.tab-item-fixed {
padding: 15px 15px;
padding: 0 15px;
text-align: center;
position: relative;
display: flex;
@@ -891,6 +1126,7 @@ const goShopping = () => {
white-space: nowrap;
flex-shrink: 0;
min-width: 60px;
height: 100%;
}
.tab-item-fixed.active {
@@ -958,8 +1194,8 @@ const goShopping = () => {
/* 内容区 */
.orders-content {
flex: 1;
height: 0;
width: 100%;
height: 0; /* 关键:强制让 flex:1 在安卓端生效,防止内容撑开父容器 */
}
/* 空状态 */
@@ -1001,6 +1237,8 @@ const goShopping = () => {
/* 订单列表 */
.order-list {
padding: 10px;
width: 100%;
box-sizing: border-box;
}
.order-card {
@@ -1009,6 +1247,8 @@ const goShopping = () => {
margin-bottom: 10px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
flex-shrink: 0;
box-sizing: border-box;
}
/* 订单头部 */
@@ -1268,15 +1508,20 @@ const goShopping = () => {
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding: 20px;
width: 100%;
max-width: 1200px;
margin: 0 auto;
justify-content: flex-start;
align-content: flex-start;
box-sizing: border-box;
}
.order-card {
width: 48%;
margin: 0 1% 20px 1%;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
flex: none;
box-sizing: border-box;
}
}
@@ -1284,6 +1529,8 @@ const goShopping = () => {
.order-card {
width: 31%;
margin: 0 1% 20px 1%;
flex: none;
box-sizing: border-box;
}
}
@@ -1297,6 +1544,19 @@ const goShopping = () => {
border-bottom: 1px solid #f9f9f9;
}
.status-row {
display: flex;
flex-direction: row;
align-items: center;
}
.more-btn {
font-size: 18px;
color: #999;
margin-left: 8px;
padding: 0 5px;
}
.shop-info {
display: flex;
flex-direction: row;
@@ -1389,3 +1649,4 @@ const goShopping = () => {
}
}
</style>

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="payment-success-page">
<view class="success-content">
<view class="icon-wrapper">
@@ -22,7 +22,6 @@
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts'
const orderId = ref('')
@@ -180,10 +179,16 @@ const goHome = () => {
.btn {
width: 100%;
height: 45px;
line-height: 45px;
text-align: center;
border-radius: 22.5px;
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.primary-btn {
@@ -198,3 +203,4 @@ const goHome = () => {
border: 1px solid #cccccc;
}
</style>

View File

@@ -1,4 +1,4 @@
<!-- 支付页面 -->
<!-- 支付页面 -->
<template>
<view class="payment-page">
<view class="payment-content">
@@ -110,7 +110,6 @@
<script setup lang="uts">
import { ref, onMounted, watch, computed, onUnmounted } from 'vue'
import { onLoad, onBackPress } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts'
type PaymentMethodType = {
@@ -925,3 +924,4 @@ onUnmounted(() => {
color: #333333;
}
</style>

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="points-page">
<view class="points-header">
<view class="points-info">

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
<!-- 消费者端 - 商品详情页 -->
<!-- 消费者端 - 商品详情页 -->
<template>
<view class="product-detail-page">
<scroll-view class="page-scroll" scroll-y="true">
@@ -42,54 +42,54 @@
</view>
<!-- 优惠券入口 (新增) -->
<view class="coupon-entry" @click="showCouponModal" v-if="coupons.length > 0">
<view class="coupon-entry-left">
<text class="coupon-entry-label">优惠</text>
<view class="coupon-tags-row">
<text class="coupon-tag" v-for="(coupon, index) in coupons.slice(0, 2)" :key="index">
{{ coupon.name }}
</text>
</view>
<view class="detail-cell coupon-entry" @click="showCouponModal" v-if="coupons.length > 0">
<text class="cell-label">优惠</text>
<view class="cell-content flex-row">
<text class="coupon-tag" v-for="(coupon, index) in coupons.slice(0, 2)" :key="index">
{{ coupon.name }}
</text>
</view>
<text class="coupon-arrow">领券 ></text>
<text class="cell-arrow">领券 ></text>
</view>
<!-- 商品参数 -->
<view class="params-section" @click="showParamsModal">
<text class="params-title">商品参数</text>
<view class="params-summary">
<text class="params-item" v-if="product.specification">规格: {{ product.specification }}</text>
<text class="params-item" v-if="product.expiry_date">有效期: {{ product.expiry_date }}</text>
<text class="params-item" v-if="product.approval_number">批准文号: {{ product.approval_number }}</text>
<view class="detail-cell params-section" @click="showParamsModal">
<text class="cell-label">参数</text>
<view class="cell-content">
<text class="params-summary-text">{{ getParamsSummary() }}</text>
</view>
<text class="params-arrow">></text>
<text class="cell-arrow">></text>
</view>
<!-- 规格选择 -->
<view class="spec-section" @click="showSpecModal" v-if="productSkus.length > 0">
<text class="spec-title">规格</text>
<text class="spec-selected">{{ selectedSpec ?? '请选择规格' }}</text>
<text class="spec-arrow">></text>
<view class="detail-cell spec-section" @click="showSpecModal" v-if="productSkus.length > 0">
<text class="cell-label">规格</text>
<view class="cell-content">
<text class="spec-selected">{{ selectedSpec ?? '请选择规格' }}</text>
</view>
<text class="cell-arrow">></text>
</view>
<!-- 数量选择 -->
<view class="quantity-section">
<text class="quantity-title">数量</text>
<view class="quantity-selector">
<view class="quantity-btn minus" @click="decreaseQuantity">
<text class="quantity-btn-text">-</text>
</view>
<input class="quantity-input"
type="number"
:value="quantity.toString()"
:min="1"
:max="getMaxQuantity()"
@input="validateQuantity" />
<view class="quantity-btn plus" @click="increaseQuantity">
<text class="quantity-btn-text">+</text>
<view class="detail-cell quantity-section">
<text class="cell-label">数量</text>
<view class="cell-content flex-row align-center justify-between">
<view class="quantity-selector flex-row align-center">
<view class="quantity-btn minus" @click="decreaseQuantity">
<text class="quantity-btn-text">-</text>
</view>
<input class="quantity-input"
type="number"
:value="quantity.toString()"
:min="1"
:max="getMaxQuantity()"
@input="validateQuantity" />
<view class="quantity-btn plus" @click="increaseQuantity">
<text class="quantity-btn-text">+</text>
</view>
</view>
<text class="quantity-stock">库存{{ getAvailableStock() }}件</text>
</view>
<text class="quantity-stock">库存{{ getAvailableStock() }}件</text>
</view>
<!-- 商品详情 -->
@@ -111,23 +111,23 @@
<!-- 底部操作栏 -->
<view class="bottom-actions">
<view class="action-buttons">
<!-- 客服按钮 (新增) -->
<!-- 客服按钮 -->
<view class="action-btn" @click="contactMerchant">
<text class="action-icon">💬</text>
<image src="/static/icons/customer-service.png" class="action-icon-img" />
<text class="action-text">客服</text>
</view>
<view class="action-btn" @click="goToCart">
<text class="action-icon">🛒</text>
<image src="/static/tabbar/cart.png" class="action-icon-img" />
<text class="action-text">购物车</text>
</view>
<view class="action-btn" @click="toggleFavorite">
<text class="action-icon">{{ isFavorite ? '❤️' : '🤍' }}</text>
<image :src="isFavorite ? '/static/icons/favorite.png' : '/static/icons/favorite-active.png'" class="action-icon-img" />
<text class="action-text">{{ isFavorite ? '已收藏' : '收藏' }}</text>
</view>
</view>
<view class="btn-group">
<button class="cart-btn" @click="addToCart">加入购物车</button>
<button class="buy-btn" @click="buyNow">立即购买</button>
<button class="buy-btn" @click="buyNow">立即购买</button>
</view>
</view>
@@ -696,8 +696,17 @@ export default {
getSkuSpecText(sku: ProductSkuType): string {
if (sku.specifications != null) {
const specs = sku.specifications as UTSJSONObject
// 简化处理,直接返回 JSON 字符串
return JSON.stringify(specs)
let specStr = ''
// 在 UTS 中遍历 UTSJSONObject 的推荐方式
for (const key in specs) {
const val = specs[key]
if (val != null) {
specStr += (specStr === '' ? '' : ' ') + val.toString()
}
}
if (specStr !== '') {
return specStr
}
}
return sku.sku_code ?? ''
},
@@ -877,6 +886,15 @@ export default {
hideParamsModal() {
this.showParams = false
},
getParamsSummary(): string {
let summary = ''
if (this.product.specification != null && (this.product.specification as string) != '') summary += '规格 '
if (this.product.expiry_date != null && (this.product.expiry_date as string) != '') summary += '有效期 '
if (this.product.approval_number != null && (this.product.approval_number as string) != '') summary += '批准文号 '
const finalSummary = summary.trim()
return finalSummary != '' ? finalSummary : '查看详情'
}
}
}
@@ -936,7 +954,7 @@ export default {
.current-price {
font-size: 48rpx;
font-weight: bold;
color: #ff4444;
color: #ff5000;
margin-right: 20rpx;
}
@@ -1006,44 +1024,6 @@ export default {
color: #666;
}
/* Coupon Entry Styles */
.coupon-entry {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.coupon-entry-left {
display: flex;
align-items: center;
flex: 1;
}
.coupon-entry-label {
font-size: 30rpx;
color: #333;
width: 120rpx;
font-weight: bold;
}
.coupon-tags-row {
flex: 1;
display: flex;
flex-direction: row;
}
.coupon-tag {
font-size: 20rpx;
color: #ff4444;
border: 1px solid #ff4444;
padding: 2rpx 10rpx;
border-radius: 4rpx;
margin-right: 15rpx;
}
.coupon-arrow {
font-size: 26rpx;
color: #999;
}
/* Modal Popup Styles */
.popup-mask {
position: fixed;
@@ -1100,7 +1080,7 @@ export default {
justify-content: center;
align-items: center;
border-right: 1px dashed #ffccc7;
color: #ff4444;
color: #ff5000;
}
.coupon-amount {
font-size: 40rpx;
@@ -1135,7 +1115,7 @@ export default {
color: #999;
}
.coupon-btn {
background-color: #ff4444;
background-color: #ff5000;
color: #fff;
font-size: 24rpx;
padding: 0 24rpx;
@@ -1145,48 +1125,86 @@ export default {
margin: 0;
}
.spec-section {
.product-description {
background-color: #fff;
padding: 30rpx;
padding-bottom: 140rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
}
.spec-title {
font-size: 30rpx;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
width: 120rpx;
margin-bottom: 20rpx;
}
.spec-selected {
flex: 1;
.description-text {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
.spec-arrow {
/* 统一Cell样式优化 */
.detail-cell {
background-color: #fff;
padding: 32rpx 30rpx;
display: flex;
flex-direction: row;
align-items: center;
border-bottom: 1rpx solid #f8f8f8;
}
.cell-label {
font-size: 28rpx;
color: #999;
width: 90rpx;
flex-shrink: 0;
}
.cell-content {
flex: 1;
margin-left: 10rpx;
}
.cell-arrow {
font-size: 24rpx;
color: #ccc;
margin-left: 10rpx;
}
/* 覆盖具体板块样式 */
.coupon-entry, .params-section, .spec-section {
margin-bottom: 0; /* 连在一起显示 */
}
.quantity-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
margin-bottom: 20rpx; /* 数量选择作为最后一项保留底边距 */
border-bottom: none;
}
.params-summary-text, .spec-selected {
font-size: 28rpx;
color: #333;
}
.flex-row {
display: flex;
flex-direction: row;
}
.align-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.quantity-title {
font-size: 30rpx;
color: #333;
width: 120rpx;
}
/* 数量加减器样式 */
.quantity-selector {
display: flex;
flex-direction: row;
align-items: center;
border: 1rpx solid #e5e5e5;
border-radius: 8rpx;
@@ -1230,26 +1248,6 @@ export default {
color: #666;
}
.product-description {
background-color: #fff;
padding: 30rpx;
padding-bottom: 140rpx;
margin-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.description-text {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
.bottom-actions {
position: fixed;
bottom: 0;
@@ -1280,8 +1278,9 @@ export default {
min-width: 80rpx;
}
.action-icon {
font-size: 40rpx;
.action-icon-img {
width: 44rpx;
height: 44rpx;
margin-bottom: 4rpx;
}
@@ -1308,12 +1307,13 @@ export default {
}
.cart-btn {
background-color: #ffa726;
background-color: #ff5000;
opacity: 0.8;
color: #fff;
}
.buy-btn {
background-color: #ff4444;
background-color: #ff5000;
color: #fff;
}
@@ -1378,7 +1378,7 @@ export default {
.spec-price {
font-size: 26rpx;
color: #ff4444;
color: #ff5000;
margin-right: 20rpx;
}
@@ -1550,3 +1550,6 @@ export default {
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,28 @@
<!-- 消费者端 - 个人中心 -->
<!-- 消费者端 - 个人中心 -->
<template>
<view class="consumer-profile">
<!-- 智能顶部导航栏 - 与消息页保持一致 -->
<view class="smart-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-container">
<!-- 头像 -->
<image
:src="userInfo.avatar_url != '' ? userInfo.avatar_url : '/static/default-avatar.png'"
class="nav-avatar"
@click="editProfile"
/>
<!-- 用户信息横向排列 (名字、积分、余额、优惠券) -->
<view class="nav-user-stats">
<!-- 基础用户信息:头像和昵称 -->
<view class="nav-user-basic" @click="editProfile">
<image
:src="userInfo.avatar_url != '' ? userInfo.avatar_url : '/static/default-avatar.png'"
class="nav-avatar"
/>
<text class="nav-user-name">{{ userInfo.nickname != '' ? userInfo.nickname : userInfo.phone }}</text>
</view>
<!-- 用户资产横向排列 (积分、余额、优惠券) -->
<view class="nav-user-stats">
<view class="nav-stat-item" @click="goToPoints">
<text class="nav-stat-label">积分</text>
<text class="nav-stat-value">{{ userStats.points }}</text>
</view>
<view class="nav-stat-item">
<view class="nav-stat-item" @click="goToWallet">
<text class="nav-stat-label">余额</text>
<text class="nav-stat-value" @click="goToWallet">¥{{ userStats.balance }}</text>
<text class="nav-stat-value">¥{{ userStats.balance }}</text>
</view>
<view class="nav-stat-item" @click="goToCoupons">
@@ -31,7 +31,7 @@
</view>
</view>
<!-- 设置按钮 (右侧) -->
<!-- 设置按钮 (右侧) -->
<view class="nav-actions">
<view class="action-btn" @click="goToSettings">
<text class="action-icon">⚙️</text>
@@ -88,7 +88,10 @@
<!-- 订单状态快捷入口 -->
<view class="order-shortcuts">
<view class="section-title">我的订单</view>
<view class="section-header-row">
<text class="section-title">我的订单</text>
<text class="view-all" @click="goToOrders(currentOrderTab)">查看更多 ></text>
</view>
<view class="order-tabs">
<view class="order-tab" :class="{ active: currentOrderTab === 'all' }" @click="switchOrderTab('all')">
<text class="tab-icon">📋</text>
@@ -117,7 +120,6 @@
<view class="recent-orders">
<view class="section-header">
<text class="section-title">{{ getOrderSectionTitle() }}</text>
<text class="view-all" @click="goToOrders(currentOrderTab)">查看更多 ></text>
</view>
<view v-if="filteredOrders.length === 0" class="empty-orders">
@@ -126,22 +128,36 @@
</view>
<view v-for="order in filteredOrders" :key="order.id" class="order-item" @click="viewOrderDetail(order)">
<view class="order-header">
<text class="order-no">订单号: {{ order.order_no }}</text>
<text class="order-status" :class="getOrderStatusClass(order.status)">{{ getOrderStatusText(order.status) }}</text>
</view>
<view class="order-content">
<image :src="getOrderMainImage(order)" class="order-image" mode="aspectFill" />
<view class="order-info">
<text class="order-title">{{ getOrderTitle(order) }}</text>
<text class="order-amount">¥{{ order.actual_amount }}</text>
<text class="order-time">{{ formatTime(order.created_at) }}</text>
<view class="order-item-header">
<view class="order-shop">
<text class="shop-icon">🏪</text>
<text class="shop-name">{{ getOrderShopName(order) }}</text>
<text class="shop-arrow"></text>
</view>
<view class="status-row">
<text class="order-status-text" :class="getOrderStatusClass(order.status)">{{ getOrderStatusText(order.status) }}</text>
<text class="more-btn" @click.stop="showOrderMenu(order)">⋯</text>
</view>
</view>
<view class="order-actions">
<button v-if="order.status === 1" class="action-btn pay" @click.stop="payOrder(order)">立即支付</button>
<button v-if="order.status === 3" class="action-btn confirm" @click.stop="confirmReceive(order)">确认收货</button>
<button v-if="order.status === 4" class="action-btn review" @click.stop="reviewOrder(order)">评价</button>
<view class="order-item-content">
<image :src="getOrderMainImage(order)" class="order-item-image" mode="aspectFill" />
<view class="order-item-info">
<view class="order-title-row">
<text class="order-item-title">{{ getOrderTitle(order) }}</text>
<text class="order-item-price">¥{{ order.actual_amount }}</text>
</view>
<view class="order-spec-row">
<text class="order-item-spec">{{ getOrderSpec(order) }}</text>
<text class="order-item-num">x{{ getOrderItemCount(order) }}</text>
</view>
<text class="order-item-time">{{ formatDateTime(order.created_at) }}</text>
</view>
</view>
<view class="order-item-actions">
<button v-if="order.status === 1" class="order-action-btn pay" @click.stop="payOrder(order)">立即支付</button>
<button v-if="order.status === 3" class="order-action-btn confirm" @click.stop="confirmReceive(order)">确认收货</button>
<button v-if="order.status === 4" class="order-action-btn review" @click.stop="reviewOrder(order)">评价</button>
<button v-if="order.status === 2 || order.status === 3" class="order-action-btn secondary" @click.stop="viewOrderDetail(order)">查看物流</button>
</view>
</view>
</view>
@@ -290,6 +306,8 @@ type OrderItemType = {
actual_amount: number
created_at: string
ml_order_items: any | null
ml_shops: any | null
items_count: number
}
export default {
@@ -407,13 +425,22 @@ export default {
actualAmount = totalAmount != null ? totalAmount : 0
}
const mlOrderItems = o.get('ml_order_items')
let itemsCount = 0
if (mlOrderItems != null && Array.isArray(mlOrderItems)) {
itemsCount = (mlOrderItems as any[]).length
}
const orderItem: OrderItemType = {
id: o.getString('id') ?? '',
order_no: o.getString('order_no') ?? '',
status: status,
actual_amount: actualAmount,
created_at: o.getString('created_at') ?? '',
ml_order_items: o.get('ml_order_items')
ml_order_items: mlOrderItems,
ml_shops: o.get('ml_shops'),
items_count: itemsCount
}
mappedOrders.push(orderItem)
@@ -629,6 +656,8 @@ export default {
},
getOrderStatusText(status: number): string {
if (status === 6) return '退款中'
if (status === 7) return '已退款'
const statusTexts = ['异常', '待支付', '待发货', '待收货', '已完成', '已取消']
if (status >= 0 && status < statusTexts.length) {
return statusTexts[status]
@@ -637,6 +666,8 @@ export default {
},
getOrderStatusClass(status: number): string {
if (status === 6) return 'refunding'
if (status === 7) return 'refunded'
const statusClasses = ['error', 'pending', 'processing', 'shipping', 'completed', 'cancelled']
if (status >= 0 && status < statusClasses.length) {
return statusClasses[status]
@@ -644,6 +675,242 @@ export default {
return 'error'
},
showOrderMenu(order: OrderItemType) {
const status = order.status
let actions: string[] = []
if (status === 1) {
actions = ['取消订单', '联系卖家']
} else if (status === 2) {
actions = ['提醒发货', '申请退款', '联系卖家']
} else if (status === 3) {
actions = ['查看物流', '确认收货', '申请退款', '联系卖家']
} else if (status === 4) {
actions = ['申请售后', '再次购买', '联系卖家']
} else if (status === 5) {
actions = ['删除订单', '再次购买', '联系卖家']
} else if (status === 6) {
actions = ['退款进度', '联系卖家']
} else if (status === 7) {
actions = ['再次购买', '联系卖家']
}
uni.showActionSheet({
itemList: actions,
success: (res) => {
const action = actions[res.tapIndex]
this.handleOrderAction(order, action)
}
})
},
handleOrderAction(order: OrderItemType, action: string) {
if (action === '取消订单') {
this.cancelOrderAction(order)
} else if (action === '联系卖家') {
this.contactSeller(order)
} else if (action === '提醒发货') {
this.remindShipping(order)
} else if (action === '申请退款' || action === '申请售后') {
this.applyRefund(order)
} else if (action === '查看物流') {
this.viewLogistics(order.id)
} else if (action === '确认收货') {
this.confirmReceive(order)
} else if (action === '再次购买') {
this.repurchase(order)
} else if (action === '删除订单') {
this.deleteOrder(order.id)
} else if (action === '退款进度') {
this.viewRefundProgress(order.id)
}
},
cancelOrderAction(order: OrderItemType) {
uni.showModal({
title: '确认取消',
content: '确定要取消此订单吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({ title: '取消中...' })
supabaseService.cancelOrder(order.id).then(() => {
uni.hideLoading()
uni.showToast({ title: '订单已取消', icon: 'success' })
this.loadOrders()
}).catch(() => {
uni.hideLoading()
uni.showToast({ title: '取消失败', icon: 'none' })
})
}
}
})
},
contactSeller(order: OrderItemType) {
const merchantId = order.ml_shops != null ? this.getMerchantIdFromOrder(order) : ''
if (merchantId !== '') {
uni.navigateTo({
url: `/pages/mall/consumer/chat?merchantId=${merchantId}`
})
} else {
uni.showToast({ title: '暂无卖家联系方式', icon: 'none' })
}
},
getMerchantIdFromOrder(order: OrderItemType): string {
const shopsRaw = order.ml_shops
if (shopsRaw != null) {
const shopStr = JSON.stringify(shopsRaw)
const shopParsed = JSON.parse(shopStr)
if (shopParsed != null) {
const shopObj = shopParsed as UTSJSONObject
return shopObj.getString('merchant_id') ?? ''
}
}
return ''
},
remindShipping(order: OrderItemType) {
uni.showLoading({ title: '提醒中...' })
const merchantId = order.ml_shops != null ? this.getMerchantIdFromOrder(order) : ''
if (merchantId !== '') {
const message = `你好,我的订单[${order.order_no}]还没有发货,请尽快安排,谢谢。`
supabaseService.sendChatMessage(message, merchantId).then(() => {
uni.hideLoading()
uni.showToast({ title: '已提醒卖家发货', icon: 'success' })
}).catch(() => {
uni.hideLoading()
uni.showToast({ title: '提醒失败', icon: 'none' })
})
} else {
uni.hideLoading()
uni.showToast({ title: '无法联系卖家', icon: 'none' })
}
},
applyRefund(order: OrderItemType) {
uni.navigateTo({
url: `/pages/mall/consumer/apply-refund?orderId=${order.id}`
})
},
viewLogistics(orderId: string) {
uni.navigateTo({
url: `/pages/mall/consumer/logistics?orderId=${orderId}`
})
},
repurchase(order: OrderItemType) {
uni.showLoading({ title: '处理中...' })
const itemsRaw = order.ml_order_items
if (itemsRaw == null || (itemsRaw as any[]).length === 0) {
uni.hideLoading()
uni.showToast({ title: '订单无商品', icon: 'none' })
return
}
const items = itemsRaw as any[]
let completed = 0
const total = items.length
let successCount = 0
for (let i = 0; i < items.length; i++) {
const itemStr = JSON.stringify(items[i])
const itemParsed = JSON.parse(itemStr)
if (itemParsed == null) {
completed++
if (completed === total) {
uni.hideLoading()
if (successCount > 0) {
uni.showToast({ title: `已添加${successCount}件商品`, icon: 'success' })
} else {
uni.showToast({ title: '添加失败', icon: 'none' })
}
}
continue
}
const itemObj = itemParsed as UTSJSONObject
const productId = itemObj.getString('product_id') ?? ''
const merchantId = order.ml_shops != null ? this.getMerchantIdFromOrder(order) : ''
if (productId !== '') {
supabaseService.addToCart(productId, 1, '', merchantId).then((success) => {
completed++
if (success) successCount++
if (completed === total) {
uni.hideLoading()
if (successCount > 0) {
uni.showToast({ title: `已添加${successCount}件商品`, icon: 'success' })
} else {
uni.showToast({ title: '添加失败', icon: 'none' })
}
}
}).catch(() => {
completed++
if (completed === total) {
uni.hideLoading()
if (successCount > 0) {
uni.showToast({ title: `已添加${successCount}件商品`, icon: 'success' })
} else {
uni.showToast({ title: '添加失败', icon: 'none' })
}
}
})
} else {
completed++
if (completed === total) {
uni.hideLoading()
if (successCount > 0) {
uni.showToast({ title: `已添加${successCount}件商品`, icon: 'success' })
} else {
uni.showToast({ title: '添加失败', icon: 'none' })
}
}
}
}
},
deleteOrder(orderId: string) {
uni.showModal({
title: '删除订单',
content: '确定要删除此订单吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({ title: '删除中...' })
supabaseService.deleteOrder(orderId).then(() => {
uni.hideLoading()
uni.showToast({ title: '订单已删除', icon: 'success' })
this.loadOrders()
}).catch(() => {
uni.hideLoading()
uni.showToast({ title: '删除失败', icon: 'none' })
})
}
}
})
},
viewRefundProgress(orderId: string) {
uni.navigateTo({
url: `/pages/mall/consumer/refund?orderId=${orderId}`
})
},
getOrderShopName(order: OrderItemType): string {
const shopsRaw = order.ml_shops
if (shopsRaw != null) {
const shopStr = JSON.stringify(shopsRaw)
const shopParsed = JSON.parse(shopStr)
if (shopParsed != null) {
const shopObj = shopParsed as UTSJSONObject
const name = shopObj.getString('shop_name')
if (name != null && name !== '') return name
}
}
return '自营店铺'
},
getOrderMainImage(order: OrderItemType): string {
const itemsRaw = order.ml_order_items
if (itemsRaw == null) return '/static/product1.jpg'
@@ -675,14 +942,80 @@ export default {
const pName = itemObj.getString('product_name')
const name = (pName != null && pName !== '') ? pName : '商品'
if (items.length > 1) {
return name + ' 等' + items.length + '件商品'
}
return name
}
return '精选商品'
},
getOrderSpec(order: OrderItemType): string {
const itemsRaw = order.ml_order_items
if (itemsRaw == null) return ''
const items = itemsRaw as any[]
if (items.length > 0) {
const firstItem = items[0]
const itemStr = JSON.stringify(firstItem)
const itemParsed = JSON.parse(itemStr)
if (itemParsed == null) return ''
const itemObj = itemParsed as UTSJSONObject
const specRaw = itemObj.get('specifications')
if (specRaw == null) return ''
if (typeof specRaw === 'string') {
const specStr = specRaw as string
if (specStr.startsWith('{')) {
try {
const specObj = JSON.parse(specStr) as UTSJSONObject
const parts: string[] = []
const color = specObj.get('Color')
if (color != null) parts.push('颜色: ' + color)
const size = specObj.get('Size')
if (size != null) parts.push('尺码: ' + size)
if (parts.length > 0) return parts.join(' ')
return specStr.replace(/[{}"]/g, '')
} catch (e) {
return specStr
}
}
return specStr
}
return JSON.stringify(specRaw).replace(/[{}"]/g, '')
}
return ''
},
getOrderItemCount(order: OrderItemType): number {
if (order.items_count != null && order.items_count > 0) {
return order.items_count
}
return 1
},
getOrderShopName(order: OrderItemType): string {
const shopsRaw = order.ml_shops
if (shopsRaw != null) {
const shopStr = JSON.stringify(shopsRaw)
const shopParsed = JSON.parse(shopStr)
if (shopParsed != null) {
const shopObj = shopParsed as UTSJSONObject
const name = shopObj.getString('shop_name')
if (name != null && name !== '') return name
}
}
return '自营店铺'
},
formatDateTime(timeStr: string): string {
if (timeStr == null || timeStr === '') return ''
const date = new Date(timeStr)
const y = date.getFullYear()
const m = (date.getMonth() + 1).toString().padStart(2, '0')
const d = date.getDate().toString().padStart(2, '0')
const h = date.getHours().toString().padStart(2, '0')
const i = date.getMinutes().toString().padStart(2, '0')
return `${y}-${m}-${d} ${h}:${i}`
},
formatTime(timeStr: string): string {
const date = new Date(timeStr)
const now = new Date()
@@ -753,11 +1086,21 @@ export default {
content: '确认已收到商品吗?',
success: (res) => {
if (res.confirm) {
uni.showToast({
title: '确认收货成功',
icon: 'success'
uni.showLoading({ title: '处理中...' })
supabaseService.confirmOrderReceived(order.id).then(() => {
uni.hideLoading()
uni.showToast({
title: '确认收货成功',
icon: 'success'
})
this.loadOrders()
}).catch(() => {
uni.hideLoading()
uni.showToast({
title: '操作失败',
icon: 'none'
})
})
this.refreshData()
}
}
})
@@ -891,9 +1234,9 @@ export default {
top: 0;
left: 0;
right: 0;
background-color: #4CAF50;
background-color: #ff5000;
z-index: 1000;
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
box-shadow: 0 2px 12px rgba(255, 80, 0, 0.15);
display: flex;
flex-direction: column;
justify-content: flex-start;
@@ -912,23 +1255,28 @@ export default {
}
/* 导航栏用户信息区域 */
.nav-user-basic {
display: flex;
flex-direction: row;
align-items: center;
flex-shrink: 0;
margin-right: 8px;
}
.nav-user-stats {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start; /* 靠左对齐,紧跟头像 */
margin-right: 12px;
overflow: hidden; /* 防止溢出 */
justify-content: flex-end; /* 将积分、余额、券推向右侧,但在设置按钮之前 */
margin-right: 8px;
}
.nav-user-name {
font-size: 16px;
font-size: 14px;
font-weight: bold;
color: white;
margin-right: 12px;
/* max-width: 30%; REMOVED */
width: 100px; /* Use fixed width approx */
width: 70px; /* 进一步压缩名字宽度 */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -938,31 +1286,31 @@ export default {
display: flex;
flex-direction: row;
align-items: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 2px 8px;
margin-right: 8px;
flex-shrink: 0; /* 防止被压缩 */
background: rgba(255, 255, 255, 0.15); /* 降低透明度更清爽 */
border-radius: 10px;
padding: 2px 6px;
margin-right: 4px; /* 减小间距 */
flex-shrink: 0;
}
.nav-stat-label {
font-size: 11px;
color: rgba(255, 255, 255, 0.9);
margin-right: 4px;
font-size: 10px; /* 调小字体 */
color: rgba(255, 255, 255, 0.85);
margin-right: 2px;
}
.nav-stat-value {
font-size: 12px;
font-size: 11px; /* 调小字体 */
font-weight: bold;
color: white;
}
.nav-avatar {
width: 36px;
height: 36px;
border-radius: 18px;
border: 2px solid rgba(255, 255, 255, 0.8);
margin-right: 12px;
width: 32px; /* 缩小头像 */
height: 32px;
border-radius: 16px;
border: 1.5px solid rgba(255, 255, 255, 0.8);
margin-right: 6px;
flex-shrink: 0;
}
@@ -1010,6 +1358,23 @@ export default {
margin-bottom: 16px;
}
.section-header-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.section-header-row .section-title {
margin-bottom: 0;
}
.view-all {
font-size: 14px;
color: #999;
}
.order-tabs {
display: flex;
flex-direction: row; /* 显式横向排列 */
@@ -1041,12 +1406,12 @@ export default {
/* 选中状态的Tab */
.order-tab.active .tab-icon,
.order-tab.active .tab-text {
color: #4CAF50;
color: #ff5000;
font-weight: bold;
}
.order-tab.active {
background-color: #f0f9f0;
background-color: #fff8f5;
border-radius: 8px;
}
@@ -1054,7 +1419,7 @@ export default {
position: absolute;
top: 0;
right: 10%; /* 调整位置 */
background-color: #ff4444;
background-color: #ff5000;
color: #fff;
font-size: 10px;
padding: 1px 5px;
@@ -1085,114 +1450,200 @@ export default {
}
.order-item {
padding: 25rpx 0;
border-bottom: 1rpx solid #f5f5f5;
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.order-item:last-child {
border-bottom: none;
}
.order-header {
.order-item-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
margin-bottom: 20rpx;
}
.order-no {
font-size: 26rpx;
color: #333;
}
.order-status {
font-size: 24rpx;
padding: 6rpx 12rpx;
border-radius: 10rpx;
color: #fff;
}
.order-status.pending {
background-color: #ffa726;
}
.order-status.processing {
background-color: #2196f3;
}
.order-status.shipping {
background-color: #9c27b0;
}
.order-status.completed {
background-color: #4caf50;
}
.order-content {
.status-row {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 15rpx;
}
.order-image {
width: 100rpx;
height: 100rpx;
border-radius: 8rpx;
margin-right: 20rpx;
.more-btn {
font-size: 18px;
color: #999;
margin-left: 8px;
padding: 0 5px;
}
.order-info {
flex: 1;
.order-shop {
display: flex;
flex-direction: row;
align-items: center;
}
.order-title {
font-size: 26rpx;
color: #333;
margin-bottom: 8rpx;
.shop-icon {
font-size: 32rpx;
margin-right: 8rpx;
}
.order-amount {
.shop-name {
font-size: 28rpx;
color: #ff4444;
font-weight: bold;
margin-bottom: 5rpx;
color: #333;
}
.order-time {
font-size: 22rpx;
.shop-arrow {
font-size: 28rpx;
color: #ccc;
margin-left: 4rpx;
}
.order-status-text {
font-size: 26rpx;
font-weight: bold;
}
.order-status-text.pending {
color: #ff5000;
}
.order-status-text.processing {
color: #ff9500;
}
.order-status-text.shipping {
color: #007aff;
}
.order-status-text.completed {
color: #34c759;
}
.order-status-text.refunding {
color: #ff9500;
}
.order-status-text.refunded {
color: #999;
}
.order-actions {
.order-status-text.cancelled {
color: #999;
}
.order-item-content {
display: flex;
justify-content: flex-end;
/* gap: 15rpx; REMOVED */
flex-direction: row;
margin-bottom: 24rpx;
}
.order-actions .action-btn {
margin-left: 15px; /* Replace gap */
.order-item-image {
width: 140rpx;
height: 140rpx;
border-radius: 12rpx;
margin-right: 20rpx;
background-color: #f8f8f8;
}
.action-btn {
padding: 12rpx 25rpx;
border-radius: 20rpx;
.order-item-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.order-title-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12rpx;
}
.order-item-title {
flex: 1;
font-size: 28rpx;
color: #333;
line-height: 1.4;
margin-right: 20rpx;
lines: 2;
text-overflow: ellipsis;
}
.order-item-price {
font-size: 32rpx;
color: #333;
font-weight: bold;
text-align: right;
}
.order-spec-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-top: 4rpx;
margin-bottom: 8rpx;
}
.order-item-spec {
flex: 1;
font-size: 24rpx;
border: none;
color: #999;
margin-right: 12rpx;
lines: 1;
text-overflow: ellipsis;
}
.action-btn.pay {
background-color: #ff4444;
color: #fff;
.order-item-time {
font-size: 22rpx;
color: #999;
margin-top: auto;
}
.action-btn.confirm {
background-color: #4caf50;
color: #fff;
.order-item-num {
font-size: 24rpx;
color: #999;
}
.action-btn.review {
background-color: #ffa726;
.order-item-actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.order-action-btn {
margin-left: 16rpx;
padding: 10rpx 28rpx;
border-radius: 30rpx;
font-size: 24rpx;
border: 1px solid #ddd;
background-color: #fff;
}
.order-action-btn.pay {
background-color: #ff5000;
color: #fff;
border-color: #ff5000;
}
.order-action-btn.confirm {
background-color: #ff5000;
color: #fff;
border-color: #ff5000;
}
.order-action-btn.review {
color: #ff5000;
border-color: #ff5000;
}
.order-action-btn.secondary {
color: #666;
border-color: #ddd;
}
.service-grid {
@@ -1227,7 +1678,7 @@ export default {
position: absolute;
top: -5rpx;
right: 10rpx;
background-color: #ff4444;
background-color: #ff5000;
color: #fff;
font-size: 18rpx;
padding: 4rpx 6rpx;
@@ -1332,3 +1783,4 @@ export default {
color: #999;
}
</style>

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="red-packets-page">
<view class="tab-header" style="position: fixed; top: 0; left: 0; right: 0; z-index: 10;">
<text

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="review-page">
<view class="header">
<text class="title">服务评价</text>
@@ -160,3 +160,4 @@ const submitReview = () => {
line-height: 50px;
}
</style>

View File

@@ -1,4 +1,4 @@
<!-- 退款页面 -->
<!-- 退款页面 -->
<template>
<view class="refund-page">
<!-- 顶部栏 -->
@@ -663,7 +663,7 @@ const goBack = () => {
}
.status-pending {
background-color: #ffa726;
background-color: #ff5000;
}
.status-processing {
@@ -834,8 +834,8 @@ const goBack = () => {
}
.action-btn.review {
border-color: #ffa726;
color: #ffa726;
border-color: #ff5000;
color: #ff5000;
}
.action-btn.delete {
@@ -871,4 +871,4 @@ const goBack = () => {
font-weight: bold;
border: none;
}
</style>
</style>

View File

@@ -1,4 +1,4 @@
<!-- 评价页面 -->
<!-- 评价页面 -->
<template>
<view class="review-page">
<!-- 顶部栏 -->
@@ -658,7 +658,7 @@ const goBack = (): void => {
}
.star-icon.active {
color: #ffa726;
color: #ff5000;
}
.rating-text {
@@ -850,3 +850,4 @@ const goBack = (): void => {
opacity: 0.6;
}
</style>

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="search-page">
<!-- 搜索头部 -->
<view class="search-header" :style="{ paddingTop: statusBarHeight + 'px' }">
@@ -486,33 +486,23 @@ const performSearch = async (): Promise<void> => {
try {
const prodResp = await supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending)
let shopRespData: Array<any> = []
let shopList: Array<ShopResultType> = []
if (currentPage.value === 1 && activeSort.value === 'default') {
const shopResp = await supabaseService.searchShops(keyword)
if (shopResp.data != null) {
const rawData = shopResp.data
for (let i: number = 0; i < rawData.length; i++) {
shopRespData.push(rawData[i])
if (shopResp.data != null && shopResp.data.length > 0) {
for (let i: number = 0; i < shopResp.data.length; i++) {
const s = shopResp.data[i]
const shopItem: ShopResultType = {
id: s.id ?? '',
name: s.shop_name ?? '',
logo: s.shop_logo ?? '/static/shop_logo_default.png',
productCount: s.product_count ?? 0
}
shopList.push(shopItem)
}
}
}
if (shopRespData.length > 0) {
const shopList: Array<ShopResultType> = []
for (let i: number = 0; i < shopRespData.length; i++) {
const s = shopRespData[i] as UTSJSONObject
const shopItem: ShopResultType = {
id: s.getString('id') ?? '',
name: s.getString('shop_name') ?? '',
logo: s.getString('shop_logo') ?? '/static/shop_logo_default.png',
productCount: s.getNumber('product_count') ?? 0
}
shopList.push(shopItem)
}
searchShopResults.value = shopList
} else {
searchShopResults.value = []
}
searchShopResults.value = shopList
const prodData = prodResp.data != null ? prodResp.data : []
const resultList: Array<SearchResultType> = []
@@ -1344,7 +1334,7 @@ const goBack = () => {
}
.guess-item {
width: 24%; /* 猜你喜欢在平板上显示4列 */
width: 24%;
}
}
@@ -1352,20 +1342,42 @@ const goBack = () => {
@media screen and (min-width: 1024px) {
.results-list {
padding: 0 24px;
max-width: 1200px;
margin: 0 auto;
}
.result-item {
width: 24%;
width: 19%;
}
.guess-item {
width: 16%; /* 猜你喜欢在桌面上显示6列 */
width: 16%;
}
/* 桌面端调整图片高度 */
.product-image {
height: 160px;
}
.guess-grid {
max-width: 1200px;
margin: 0 auto;
}
.main-content {
max-width: 1200px;
margin: 0 auto;
}
}
/* 大屏幕 (1440px以上) */
@media screen and (min-width: 1440px) {
.result-item {
width: 16%;
}
.guess-item {
width: 12%;
}
}
.result-item {
@@ -1518,3 +1530,4 @@ const goBack = () => {
height: 20px;
}
</style>

View File

@@ -1,4 +1,4 @@
<!-- 设置页面 -->
<!-- 设置页面 -->
<template>
<view class="settings-page">
<!-- 顶部栏 -->
@@ -825,3 +825,4 @@ const deleteAccount = () => {
font-size: 14px;
}/* text-decoration: underline; REMOVED */
</style>

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="shop-detail-page">
<scroll-view class="page-scroll" scroll-y="true" @scrolltolower="onScrollToLower" refresher-enabled="true" @refresherrefresh="onRefresherRefresh" :refresher-triggered="isRefresherTriggered">
<!-- 店铺头部信息 -->
@@ -601,8 +601,8 @@ const goToProduct = (id: string) => {
}
.follow-btn {
background-color: #ff4444;
border: 1px solid #ff4444;
background-color: #ff5000;
border: 1px solid #ff5000;
}
.follow-btn .action-text {
@@ -658,7 +658,7 @@ const goToProduct = (id: string) => {
padding: 0 5px;
}
.coupon-amount {
color: #ff4444;
color: #ff5000;
font-weight: bold;
font-size: 18px;
}
@@ -671,7 +671,7 @@ const goToProduct = (id: string) => {
display: flex;
justify-content: center;
align-items: center;
background-color: #ff4444;
background-color: #ff5000;
flex-direction: column;
}
.coupon-btn-label {
@@ -692,7 +692,7 @@ const goToProduct = (id: string) => {
color: #333;
margin-bottom: 15px;
padding-left: 8px;
border-left: 4px solid #ff4444;
border-left: 4px solid #ff5000;
}
.product-grid {
@@ -782,3 +782,4 @@ const goToProduct = (id: string) => {
}
}
</style>

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="followed-shops-page">
<view class="header">
<text class="header-title">我关注的店铺</text>

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="my-subs">
<view class="header">
<text class="title">我的订阅</text>

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="plan-detail">
<view class="header">
<text class="title">订阅方案详情</text>

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="sub-plan-list">
<view class="header">
<text class="title">软件订阅</text>

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="subscribe-checkout">
<view class="header">
<text class="title">确认订阅</text>

View File

@@ -1,84 +1,84 @@
<!-- 钱包页面 -->
<!-- 閽卞寘椤甸潰 -->
<template>
<view class="wallet-page">
<!-- 顶部栏 -->
<!-- 椤堕儴鏍?-->
<view class="wallet-header">
<text class="back-btn" @click="goBack"></text>
<text class="header-title">我的钱包</text>
<text class="more-btn" @click="showMoreActions">···</text>
<text class="back-btn" @click="goBack">鈥?/text>
<text class="header-title">鎴戠殑閽卞寘</text>
<text class="more-btn" @click="showMoreActions">路路路</text>
</view>
<scroll-view class="wallet-content" scroll-y>
<!-- 余额概览 -->
<!-- 浣欓姒傝 -->
<view class="balance-overview">
<text class="balance-label">账户余额</text>
<text class="balance-value">¥{{ balance.toFixed(2) }}</text>
<text class="balance-label">璐︽埛浣欓</text>
<text class="balance-value">{{ balance.toFixed(2) }}</text>
<view class="balance-actions">
<button class="action-btn recharge" @click="recharge">充值</button>
<button class="action-btn withdraw" @click="withdraw">提现</button>
<button class="action-btn recharge" @click="recharge">鍏呭€?/button>
<button class="action-btn withdraw" @click="withdraw">鎻愮幇</button>
</view>
</view>
<!-- 资产统计 -->
<!-- 璧勪骇缁熻 -->
<view class="assets-stats">
<view class="stat-item">
<text class="stat-label">累计充值</text>
<text class="stat-value">¥{{ stats.totalRecharge.toFixed(2) }}</text>
<text class="stat-label">绱鍏呭€?/text>
<text class="stat-value">{{ stats.totalRecharge.toFixed(2) }}</text>
</view>
<view class="stat-item">
<text class="stat-label">累计消费</text>
<text class="stat-value">¥{{ stats.totalConsume.toFixed(2) }}</text>
<text class="stat-label">绱娑堣垂</text>
<text class="stat-value">{{ stats.totalConsume.toFixed(2) }}</text>
</view>
<view class="stat-item">
<text class="stat-label">累计提现</text>
<text class="stat-value">¥{{ stats.totalWithdraw.toFixed(2) }}</text>
<text class="stat-label">绱鎻愮幇</text>
<text class="stat-value">{{ stats.totalWithdraw.toFixed(2) }}</text>
</view>
</view>
<!-- 快捷功能 -->
<!-- 蹇嵎鍔熻兘 -->
<view class="quick-actions">
<view class="action-grid">
<view class="action-item" @click="goToCoupons">
<text class="action-icon">🎫</text>
<text class="action-text">优惠券</text>
<text class="action-icon">馃帿</text>
<text class="action-text">浼樻儬鍒?/text>
</view>
<view class="action-item" @click="goToRedPackets">
<text class="action-icon">🧧</text>
<text class="action-text">红包</text>
<text class="action-icon">馃Ё</text>
<text class="action-text">绾㈠寘</text>
</view>
<view class="action-item" @click="goToPoints">
<text class="action-icon">⭐</text>
<text class="action-text">积分</text>
<text class="action-icon">猸?/text>
<text class="action-text">绉垎</text>
</view>
<view class="action-item" @click="goToBankCards">
<text class="action-icon">💳</text>
<text class="action-text">银行卡</text>
<text class="action-icon">馃挸</text>
<text class="action-text">閾惰鍗?/text>
</view>
</view>
</view>
<!-- 交易记录 -->
<!-- 浜ゆ槗璁板綍 -->
<view class="transactions-section">
<view class="section-header">
<text class="section-title">交易记录</text>
<text class="section-title">浜ゆ槗璁板綍</text>
<view class="filter-tabs">
<text :class="['filter-tab', { active: activeFilter === 'all' }]"
@click="changeFilter('all')">全部</text>
@click="changeFilter('all')">鍏ㄩ儴</text>
<text :class="['filter-tab', { active: activeFilter === 'income' }]"
@click="changeFilter('income')">收入</text>
@click="changeFilter('income')">鏀跺叆</text>
<text :class="['filter-tab', { active: activeFilter === 'expense' }]"
@click="changeFilter('expense')">支出</text>
@click="changeFilter('expense')">鏀嚭</text>
</view>
</view>
<!-- 空状态 -->
<!-- 绌虹姸鎬?-->
<view v-if="transactions.length === 0 && !isLoading" class="empty-transactions">
<text class="empty-icon">💰</text>
<text class="empty-text">暂无交易记录</text>
<text class="empty-subtext">快去使用钱包功能吧</text>
<text class="empty-icon">馃挵</text>
<text class="empty-text">鏆傛棤浜ゆ槗璁板綍</text>
<text class="empty-subtext">蹇幓浣跨敤閽卞寘鍔熻兘鍚?/text>
</view>
<!-- 交易列表 -->
<!-- 浜ゆ槗鍒楄〃 -->
<view class="transactions-list">
<view v-for="transaction in transactions"
:key="transaction.id"
@@ -94,47 +94,47 @@
<view class="transaction-right">
<text :class="['transaction-amount',
{ income: transaction.amount > 0, expense: transaction.amount < 0 }]">
{{ transaction.amount > 0 ? '+' : '' }}¥{{ Math.abs(transaction.amount).toFixed(2) }}
{{ transaction.amount > 0 ? '+' : '' }}{{ Math.abs(transaction.amount).toFixed(2) }}
</text>
<text class="transaction-balance">余额: ¥{{ transaction.current_balance.toFixed(2) }}</text>
<text class="transaction-balance">浣欓: {{ transaction.current_balance.toFixed(2) }}</text>
</view>
</view>
</view>
<!-- 加载更多 -->
<!-- 鍔犺浇鏇村 -->
<view v-if="isLoading" class="loading-more">
<text class="loading-text">加载中...</text>
<text class="loading-text">鍔犺浇涓?..</text>
</view>
<view v-if="!hasMore && transactions.length > 0" class="no-more">
<text class="no-more-text">没有更多记录了</text>
<text class="no-more-text">娌℃湁鏇村璁板綍浜?/text>
</view>
</view>
<!-- 安全提示 -->
<!-- 瀹夊叏鎻愮ず -->
<view class="security-tips">
<text class="tip-title">安全提示</text>
<text class="tip-item">1. 请妥善保管您的支付密码</text>
<text class="tip-item">2. 不要向他人透露您的账户信息</text>
<text class="tip-item">3. 定期修改密码以确保账户安全</text>
<text class="tip-title">瀹夊叏鎻愮ず</text>
<text class="tip-item">1. 璇峰Ε鍠勪繚绠℃偍鐨勬敮浠樺瘑鐮?/text>
<text class="tip-item">2. 涓嶈鍚戜粬浜洪€忛湶鎮ㄧ殑璐︽埛淇℃伅</text>
<text class="tip-item">3. 瀹氭湡淇敼瀵嗙爜浠ョ‘淇濊处鎴峰畨鍏?/text>
</view>
</scroll-view>
<!-- 充值弹窗 -->
<!-- 鍏呭€煎脊绐?-->
<view v-if="showRechargePopup" class="recharge-popup">
<view class="popup-mask" @click="closeRechargePopup"></view>
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">充值</text>
<text class="popup-close" @click="closeRechargePopup">×</text>
<text class="popup-title">鍏呭€?/text>
<text class="popup-close" @click="closeRechargePopup"></text>
</view>
<view class="popup-body">
<text class="amount-label">充值金额</text>
<text class="amount-label">鍏呭€奸噾棰?/text>
<view class="amount-input">
<text class="currency-symbol">¥</text>
<text class="currency-symbol"></text>
<input class="amount-field"
v-model="rechargeAmount"
type="number"
placeholder="请输入充值金额"
placeholder="璇疯緭鍏ュ厖鍊奸噾棰?
focus />
</view>
<view class="quick-amounts">
@@ -142,17 +142,17 @@
:key="amount"
:class="['quick-amount', { active: rechargeAmount === amount.toString() }]"
@click="selectQuickAmount(amount)">
¥{{ amount }}
{{ amount }}
</text>
</view>
<text class="recharge-tip">单笔充值最低10元最高5000元</text>
<text class="recharge-tip">鍗曠瑪鍏呭€兼渶浣?0鍏冿紝鏈€楂?000鍏?/text>
</view>
<view class="popup-footer">
<button class="cancel-btn" @click="closeRechargePopup">取消</button>
<button class="cancel-btn" @click="closeRechargePopup">鍙栨秷</button>
<button class="confirm-btn"
:class="{ disabled: !canRecharge }"
@click="confirmRecharge">
确认充值
纭鍏呭€?
</button>
</view>
</view>
@@ -207,31 +207,31 @@ const showRechargePopup = ref<boolean>(false)
const rechargeAmount = ref<string>('')
const quickAmounts = [50, 100, 200, 500, 1000]
// 计算属性
// 璁$畻灞炴€?
const canRecharge = computed(() => {
const amount = parseFloat(rechargeAmount.value)
return !isNaN(amount) && amount >= 10 && amount <= 5000
})
// 监听过滤器变化
// 鐩戝惉杩囨护鍣ㄥ彉鍖?
watch(activeFilter, () => {
resetTransactions()
loadTransactions()
})
// 生命周期
// 鐢熷懡鍛ㄦ湡
onMounted(() => {
loadWalletData()
})
// 重置交易记录
// 閲嶇疆浜ゆ槗璁板綍
const resetTransactions = () => {
transactions.value = []
currentPage.value = 1
hasMore.value = true
}
// 加载钱包数据
// 鍔犺浇閽卞寘鏁版嵁
const loadWalletData = async () => {
const userId = getCurrentUserId()
if (!userId) {
@@ -247,7 +247,7 @@ const loadWalletData = async () => {
])
}
// 加载余额信息
// 鍔犺浇浣欓淇℃伅
const loadBalance = async () => {
const userId = getCurrentUserId()
if (!userId) return
@@ -260,7 +260,7 @@ const loadBalance = async () => {
.single()
if (error !== null) {
console.error('加载钱包失败:', error)
console.error('鍔犺浇閽卞寘澶辫触:', error)
return
}
@@ -273,11 +273,11 @@ const loadBalance = async () => {
}
}
} catch (err) {
console.error('加载钱包异常:', err)
console.error('鍔犺浇閽卞寘寮傚父:', err)
}
}
// 加载交易记录
// 鍔犺浇浜ゆ槗璁板綍
const loadTransactions = async (loadMore: boolean = false) => {
if (isLoading.value || (!hasMore.value && loadMore)) {
return
@@ -297,20 +297,20 @@ const loadTransactions = async (loadMore: boolean = false) => {
.eq('user_id', userId)
.order('created_at', { ascending: false })
// 根据过滤器筛选
// 鏍规嵁杩囨护鍣ㄧ瓫閫?
if (activeFilter.value === 'income') {
query = query.gt('change_amount', 0)
} else if (activeFilter.value === 'expense') {
query = query.lt('change_amount', 0)
}
// 分页
// 鍒嗛〉
query = query.range((page - 1) * pageSize.value, page * pageSize.value - 1)
const { data, error } = await query
if (error !== null) {
console.error('加载交易记录失败:', error)
console.error('鍔犺浇浜ゆ槗璁板綍澶辫触:', error)
return
}
@@ -326,47 +326,47 @@ const loadTransactions = async (loadMore: boolean = false) => {
hasMore.value = newTransactions.length === pageSize.value
} catch (err) {
console.error('加载交易记录异常:', err)
console.error('鍔犺浇浜ゆ槗璁板綍寮傚父:', err)
} finally {
isLoading.value = false
}
}
// 获取当前用户ID
// 鑾峰彇褰撳墠鐢ㄦ埛ID
const getCurrentUserId = (): string => {
const userStore = uni.getStorageSync('userInfo')
return userStore?.id || ''
}
// 获取交易图标
// 鑾峰彇浜ゆ槗鍥炬爣
const getTransactionIcon = (type: string): string => {
const icons: Record<string, string> = {
recharge: '💳',
consume: '🛒',
withdraw: '🏦',
refund: '🔄',
reward: '🎁',
income: '💰',
expense: '📤'
recharge: '馃挸',
consume: '馃洅',
withdraw: '馃彟',
refund: '馃攧',
reward: '馃巵',
income: '馃挵',
expense: '馃摛'
}
return icons[type] || '💰'
return icons[type] || '馃挵'
}
// 获取交易标题
// 鑾峰彇浜ゆ槗鏍囬
const getTransactionTitle = (type: string): string => {
const titles: Record<string, string> = {
recharge: '账户充值',
consume: '商品消费',
withdraw: '余额提现',
refund: '订单退款',
reward: '活动奖励',
income: '收入',
expense: '支出'
recharge: '璐︽埛鍏呭€?,
consume: '鍟嗗搧娑堣垂',
withdraw: '浣欓鎻愮幇',
refund: '璁㈠崟閫€娆?,
reward: '娲诲姩濂栧姳',
income: '鏀跺叆',
expense: '鏀嚭'
}
return titles[type] || '交易'
return titles[type] || '浜ゆ槗'
}
// 格式化时间
// 鏍煎紡鍖栨椂闂?
const formatTime = (timeStr: string): string => {
const date = new Date(timeStr)
const month = (date.getMonth() + 1).toString().padStart(2, '0')
@@ -376,14 +376,14 @@ const formatTime = (timeStr: string): string => {
return `${month}-${day} ${hours}:${minutes}`
}
// 显示更多操作
// 鏄剧ず鏇村鎿嶄綔
const showMoreActions = () => {
uni.showActionSheet({
itemList: ['交易记录', '安全设置', '帮助中心'],
itemList: ['浜ゆ槗璁板綍', '瀹夊叏璁剧疆', '甯姪涓績'],
success: (res) => {
switch (res.tapIndex) {
case 0:
// 交易记录已经在当前页
// 浜ゆ槗璁板綍宸茬粡鍦ㄥ綋鍓嶉〉
break
case 1:
uni.navigateTo({
@@ -400,72 +400,72 @@ const showMoreActions = () => {
})
}
// 充值
// 鍏呭€?
const recharge = () => {
showRechargePopup.value = true
rechargeAmount.value = ''
}
// 提现
// 鎻愮幇
const withdraw = () => {
uni.navigateTo({
url: '/pages/mall/consumer/withdraw'
})
}
// 跳转到优惠券
// 璺宠浆鍒颁紭鎯犲埜
const goToCoupons = () => {
uni.navigateTo({
url: '/pages/mall/consumer/coupons'
})
}
// 跳转到红包
// 璺宠浆鍒扮孩鍖?
const goToRedPackets = () => {
uni.navigateTo({
url: '/pages/mall/consumer/red-packets'
})
}
// 跳转到积分
// 璺宠浆鍒扮Н鍒?
const goToPoints = () => {
uni.navigateTo({
url: '/pages/mall/consumer/points'
})
}
// 跳转到银行卡
// 璺宠浆鍒伴摱琛屽崱
const goToBankCards = () => {
uni.navigateTo({
url: '/pages/mall/consumer/bank-cards'
})
}
// 切换过滤器
// 鍒囨崲杩囨护鍣?
const changeFilter = (filter: string) => {
activeFilter.value = filter
}
// 加载更多
// 鍔犺浇鏇村
const loadMore = () => {
if (hasMore.value && !isLoading.value) {
loadTransactions(true)
}
}
// 选择快捷金额
// 閫夋嫨蹇嵎閲戦
const selectQuickAmount = (amount: number) => {
rechargeAmount.value = amount.toString()
}
// 确认充值
// 纭鍏呭€?
const confirmRecharge = async () => {
if (!canRecharge.value) return
const amount = parseFloat(rechargeAmount.value)
if (isNaN(amount)) return
// 这里应该跳转到支付页面进行充值
// 杩欓噷搴旇璺宠浆鍒版敮浠橀〉闈㈣繘琛屽厖鍊?
uni.navigateTo({
url: `/pages/mall/consumer/payment?type=recharge&amount=${amount}`
})
@@ -473,13 +473,13 @@ const confirmRecharge = async () => {
closeRechargePopup()
}
// 关闭充值弹窗
// 鍏抽棴鍏呭€煎脊绐?
const closeRechargePopup = () => {
showRechargePopup.value = false
rechargeAmount.value = ''
}
// 返回
// 杩斿洖
const goBack = () => {
uni.navigateBack()
}
@@ -947,4 +947,5 @@ const goBack = () => {
background-color: #cccccc;
opacity: 0.6;
}
</style>
</style>

View File

@@ -1,4 +1,4 @@
<!-- 钱包页面 -->
<!-- 钱包页面 -->
<template>
<view class="wallet-page">
<!-- 顶部栏 -->
@@ -1058,4 +1058,4 @@ const goBack = (): void => {
background-color: #cccccc;
opacity: 0.6;
}
</style>
</style>

View File

@@ -1,82 +1,82 @@
<!-- 钱包页面 -->
<!-- 閽卞寘椤甸潰 -->
<template>
<view class="wallet-page">
<!-- 顶部栏 -->
<!-- 椤堕儴鏍?-->
<!--<view class="wallet-header">
<text class="back-btn" @click="goBack"></text>
<text class="back-btn" @click="goBack">鈥?/text>
</view>-->
<scroll-view class="wallet-content" scroll-y>
<!-- 余额概览 -->
<!-- 浣欓姒傝 -->
<view class="balance-overview">
<text class="balance-label">账户余额</text>
<text class="balance-value">¥{{ balance.toFixed(2) }}</text>
<text class="balance-label">璐︽埛浣欓</text>
<text class="balance-value">{{ balance.toFixed(2) }}</text>
<view class="balance-actions">
<button class="action-btn recharge" @click="recharge">充值</button>
<button class="action-btn withdraw" @click="withdraw">提现</button>
<button class="action-btn recharge" @click="recharge">鍏呭€?/button>
<button class="action-btn withdraw" @click="withdraw">鎻愮幇</button>
</view>
</view>
<!-- 资产统计 -->
<!-- 璧勪骇缁熻 -->
<view class="assets-stats">
<view class="stat-item">
<text class="stat-label">累计充值</text>
<text class="stat-value">¥{{ stats.totalRecharge.toFixed(2) }}</text>
<text class="stat-label">绱鍏呭€?/text>
<text class="stat-value">{{ stats.totalRecharge.toFixed(2) }}</text>
</view>
<view class="stat-item">
<text class="stat-label">累计消费</text>
<text class="stat-value">¥{{ stats.totalConsume.toFixed(2) }}</text>
<text class="stat-label">绱娑堣垂</text>
<text class="stat-value">{{ stats.totalConsume.toFixed(2) }}</text>
</view>
<view class="stat-item">
<text class="stat-label">累计提现</text>
<text class="stat-value">¥{{ stats.totalWithdraw.toFixed(2) }}</text>
<text class="stat-label">绱鎻愮幇</text>
<text class="stat-value">{{ stats.totalWithdraw.toFixed(2) }}</text>
</view>
</view>
<!-- 快捷功能 -->
<!-- 蹇嵎鍔熻兘 -->
<view class="quick-actions">
<view class="action-grid">
<view class="action-item" @click="goToCoupons">
<text class="action-icon">🎫</text>
<text class="action-text">优惠券</text>
<text class="action-icon">馃帿</text>
<text class="action-text">浼樻儬鍒?/text>
</view>
<view class="action-item" @click="goToRedPackets">
<text class="action-icon">🧧</text>
<text class="action-text">红包</text>
<text class="action-icon">馃Ё</text>
<text class="action-text">绾㈠寘</text>
</view>
<view class="action-item" @click="goToPoints">
<text class="action-icon">⭐</text>
<text class="action-text">积分</text>
<text class="action-icon">猸?/text>
<text class="action-text">绉垎</text>
</view>
<view class="action-item" @click="goToBankCards">
<text class="action-icon">💳</text>
<text class="action-text">银行卡</text>
<text class="action-icon">馃挸</text>
<text class="action-text">閾惰鍗?/text>
</view>
</view>
</view>
<!-- 交易记录 -->
<!-- 浜ゆ槗璁板綍 -->
<view class="transactions-section">
<view class="section-header">
<text class="section-title">交易记录</text>
<text class="section-title">浜ゆ槗璁板綍</text>
<view class="filter-tabs">
<text :class="['filter-tab', { active: activeFilter === 'all' }]"
@click="changeFilter('all')">全部</text>
@click="changeFilter('all')">鍏ㄩ儴</text>
<text :class="['filter-tab', { active: activeFilter === 'income' }]"
@click="changeFilter('income')">收入</text>
@click="changeFilter('income')">鏀跺叆</text>
<text :class="['filter-tab', { active: activeFilter === 'expense' }]"
@click="changeFilter('expense')">支出</text>
@click="changeFilter('expense')">鏀嚭</text>
</view>
</view>
<!-- 空状态 -->
<!-- 绌虹姸鎬?-->
<view v-if="transactions.length === 0 && !isLoading" class="empty-transactions">
<text class="empty-icon">💰</text>
<text class="empty-text">暂无交易记录</text>
<text class="empty-subtext">快去使用钱包功能吧</text>
<text class="empty-icon">馃挵</text>
<text class="empty-text">鏆傛棤浜ゆ槗璁板綍</text>
<text class="empty-subtext">蹇幓浣跨敤閽卞寘鍔熻兘鍚?/text>
</view>
<!-- 交易列表 -->
<!-- 浜ゆ槗鍒楄〃 -->
<view class="transactions-list">
<view v-for="transaction in transactions"
:key="transaction.id"
@@ -92,47 +92,47 @@
<view class="transaction-right">
<text :class="['transaction-amount',
{ income: transaction.amount > 0, expense: transaction.amount < 0 }]">
{{ transaction.amount > 0 ? '+' : '' }}¥{{ Math.abs(transaction.amount).toFixed(2) }}
{{ transaction.amount > 0 ? '+' : '' }}{{ Math.abs(transaction.amount).toFixed(2) }}
</text>
<text class="transaction-balance">余额: ¥{{ transaction.current_balance.toFixed(2) }}</text>
<text class="transaction-balance">浣欓: {{ transaction.current_balance.toFixed(2) }}</text>
</view>
</view>
</view>
<!-- 加载更多 -->
<!-- 鍔犺浇鏇村 -->
<view v-if="isLoading" class="loading-more">
<text class="loading-text">加载中...</text>
<text class="loading-text">鍔犺浇涓?..</text>
</view>
<view v-if="!hasMore && transactions.length > 0" class="no-more">
<text class="no-more-text">没有更多记录了</text>
<text class="no-more-text">娌℃湁鏇村璁板綍浜?/text>
</view>
</view>
<!-- 安全提示 -->
<!-- 瀹夊叏鎻愮ず -->
<view class="security-tips">
<text class="tip-title">安全提示</text>
<text class="tip-item">1. 请妥善保管您的支付密码</text>
<text class="tip-item">2. 不要向他人透露您的账户信息</text>
<text class="tip-item">3. 定期修改密码以确保账户安全</text>
<text class="tip-title">瀹夊叏鎻愮ず</text>
<text class="tip-item">1. 璇峰Ε鍠勪繚绠℃偍鐨勬敮浠樺瘑鐮?/text>
<text class="tip-item">2. 涓嶈鍚戜粬浜洪€忛湶鎮ㄧ殑璐︽埛淇℃伅</text>
<text class="tip-item">3. 瀹氭湡淇敼瀵嗙爜浠ョ‘淇濊处鎴峰畨鍏?/text>
</view>
</scroll-view>
<!-- 充值弹窗 -->
<!-- 鍏呭€煎脊绐?-->
<view v-if="showRechargePopup" class="recharge-popup">
<view class="popup-mask" @click="closeRechargePopup"></view>
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">充值</text>
<text class="popup-close" @click="closeRechargePopup">×</text>
<text class="popup-title">鍏呭€?/text>
<text class="popup-close" @click="closeRechargePopup"></text>
</view>
<view class="popup-body">
<text class="amount-label">充值金额</text>
<text class="amount-label">鍏呭€奸噾棰?/text>
<view class="amount-input">
<text class="currency-symbol">¥</text>
<text class="currency-symbol"></text>
<input class="amount-field"
v-model="rechargeAmount"
type="number"
placeholder="请输入充值金额"
placeholder="璇疯緭鍏ュ厖鍊奸噾棰?
focus />
</view>
<view class="quick-amounts">
@@ -140,17 +140,17 @@
:key="amount"
:class="['quick-amount', { active: rechargeAmount === amount.toString() }]"
@click="selectQuickAmount(amount)">
¥{{ amount }}
{{ amount }}
</text>
</view>
<text class="recharge-tip">单笔充值最低10元最高5000元</text>
<text class="recharge-tip">鍗曠瑪鍏呭€兼渶浣?0鍏冿紝鏈€楂?000鍏?/text>
</view>
<view class="popup-footer">
<button class="cancel-btn" @click="closeRechargePopup">取消</button>
<button class="cancel-btn" @click="closeRechargePopup">鍙栨秷</button>
<button class="confirm-btn"
:class="{ disabled: !canRecharge }"
@click="confirmRecharge">
确认充值
纭鍏呭€?
</button>
</view>
</view>
@@ -205,31 +205,31 @@ const showRechargePopup = ref<boolean>(false)
const rechargeAmount = ref<string>('')
const quickAmounts = [50, 100, 200, 500, 1000]
// 计算属性
// 璁$畻灞炴€?
const canRecharge = computed(() => {
const amount = parseFloat(rechargeAmount.value)
return !isNaN(amount) && amount >= 10 && amount <= 5000
})
// 监听过滤器变化
// 鐩戝惉杩囨护鍣ㄥ彉鍖?
watch(activeFilter, () => {
resetTransactions()
loadTransactions()
})
// 生命周期
// 鐢熷懡鍛ㄦ湡
onMounted(() => {
loadWalletData()
})
// 重置交易记录
// 閲嶇疆浜ゆ槗璁板綍
const resetTransactions = () => {
transactions.value = []
currentPage.value = 1
hasMore.value = true
}
// 加载钱包数据
// 鍔犺浇閽卞寘鏁版嵁
const loadWalletData = async () => {
const userId = getCurrentUserId()
if (!userId) {
@@ -245,7 +245,7 @@ const loadWalletData = async () => {
])
}
// 加载余额信息
// 鍔犺浇浣欓淇℃伅
const loadBalance = async () => {
const userId = getCurrentUserId()
if (!userId) return
@@ -258,7 +258,7 @@ const loadBalance = async () => {
.single()
if (error !== null) {
console.error('加载钱包失败:', error)
console.error('鍔犺浇閽卞寘澶辫触:', error)
return
}
@@ -271,11 +271,11 @@ const loadBalance = async () => {
}
}
} catch (err) {
console.error('加载钱包异常:', err)
console.error('鍔犺浇閽卞寘寮傚父:', err)
}
}
// 加载交易记录
// 鍔犺浇浜ゆ槗璁板綍
const loadTransactions = async (loadMore: boolean = false) => {
if (isLoading.value || (!hasMore.value && loadMore)) {
return
@@ -295,20 +295,20 @@ const loadTransactions = async (loadMore: boolean = false) => {
.eq('user_id', userId)
.order('created_at', { ascending: false })
// 根据过滤器筛选
// 鏍规嵁杩囨护鍣ㄧ瓫閫?
if (activeFilter.value === 'income') {
query = query.gt('change_amount', 0)
} else if (activeFilter.value === 'expense') {
query = query.lt('change_amount', 0)
}
// 分页
// 鍒嗛〉
query = query.range((page - 1) * pageSize.value, page * pageSize.value - 1)
const { data, error } = await query
if (error !== null) {
console.error('加载交易记录失败:', error)
console.error('鍔犺浇浜ゆ槗璁板綍澶辫触:', error)
return
}
@@ -324,47 +324,47 @@ const loadTransactions = async (loadMore: boolean = false) => {
hasMore.value = newTransactions.length === pageSize.value
} catch (err) {
console.error('加载交易记录异常:', err)
console.error('鍔犺浇浜ゆ槗璁板綍寮傚父:', err)
} finally {
isLoading.value = false
}
}
// 获取当前用户ID
// 鑾峰彇褰撳墠鐢ㄦ埛ID
const getCurrentUserId = (): string => {
const userStore = uni.getStorageSync('userInfo')
return userStore?.id || ''
}
// 获取交易图标
// 鑾峰彇浜ゆ槗鍥炬爣
const getTransactionIcon = (type: string): string => {
const icons: Record<string, string> = {
recharge: '💳',
consume: '🛒',
withdraw: '🏦',
refund: '🔄',
reward: '🎁',
income: '💰',
expense: '📤'
recharge: '馃挸',
consume: '馃洅',
withdraw: '馃彟',
refund: '馃攧',
reward: '馃巵',
income: '馃挵',
expense: '馃摛'
}
return icons[type] || '💰'
return icons[type] || '馃挵'
}
// 获取交易标题
// 鑾峰彇浜ゆ槗鏍囬
const getTransactionTitle = (type: string): string => {
const titles: Record<string, string> = {
recharge: '账户充值',
consume: '商品消费',
withdraw: '余额提现',
refund: '订单退款',
reward: '活动奖励',
income: '收入',
expense: '支出'
recharge: '璐︽埛鍏呭€?,
consume: '鍟嗗搧娑堣垂',
withdraw: '浣欓鎻愮幇',
refund: '璁㈠崟閫€娆?,
reward: '娲诲姩濂栧姳',
income: '鏀跺叆',
expense: '鏀嚭'
}
return titles[type] || '交易'
return titles[type] || '浜ゆ槗'
}
// 格式化时间
// 鏍煎紡鍖栨椂闂?
const formatTime = (timeStr: string): string => {
const date = new Date(timeStr)
const month = (date.getMonth() + 1).toString().padStart(2, '0')
@@ -374,14 +374,14 @@ const formatTime = (timeStr: string): string => {
return `${month}-${day} ${hours}:${minutes}`
}
// 显示更多操作
// 鏄剧ず鏇村鎿嶄綔
const showMoreActions = () => {
uni.showActionSheet({
itemList: ['交易记录', '安全设置', '帮助中心'],
itemList: ['浜ゆ槗璁板綍', '瀹夊叏璁剧疆', '甯姪涓績'],
success: (res) => {
switch (res.tapIndex) {
case 0:
// 交易记录已经在当前页
// 浜ゆ槗璁板綍宸茬粡鍦ㄥ綋鍓嶉〉
break
case 1:
uni.navigateTo({
@@ -398,72 +398,72 @@ const showMoreActions = () => {
})
}
// 充值
// 鍏呭€?
const recharge = () => {
showRechargePopup.value = true
rechargeAmount.value = ''
}
// 提现
// 鎻愮幇
const withdraw = () => {
uni.navigateTo({
url: '/pages/mall/consumer/withdraw'
})
}
// 跳转到优惠券
// 璺宠浆鍒颁紭鎯犲埜
const goToCoupons = () => {
uni.navigateTo({
url: '/pages/mall/consumer/coupons'
})
}
// 跳转到红包
// 璺宠浆鍒扮孩鍖?
const goToRedPackets = () => {
uni.navigateTo({
url: '/pages/mall/consumer/red-packets'
})
}
// 跳转到积分
// 璺宠浆鍒扮Н鍒?
const goToPoints = () => {
uni.navigateTo({
url: '/pages/mall/consumer/points'
})
}
// 跳转到银行卡
// 璺宠浆鍒伴摱琛屽崱
const goToBankCards = () => {
uni.navigateTo({
url: '/pages/mall/consumer/bank-cards'
})
}
// 切换过滤器
// 鍒囨崲杩囨护鍣?
const changeFilter = (filter: string) => {
activeFilter.value = filter
}
// 加载更多
// 鍔犺浇鏇村
const loadMore = () => {
if (hasMore.value && !isLoading.value) {
loadTransactions(true)
}
}
// 选择快捷金额
// 閫夋嫨蹇嵎閲戦
const selectQuickAmount = (amount: number) => {
rechargeAmount.value = amount.toString()
}
// 确认充值
// 纭鍏呭€?
const confirmRecharge = async () => {
if (!canRecharge.value) return
const amount = parseFloat(rechargeAmount.value)
if (isNaN(amount)) return
// 这里应该跳转到支付页面进行充值
// 杩欓噷搴旇璺宠浆鍒版敮浠橀〉闈㈣繘琛屽厖鍊?
uni.navigateTo({
url: `/pages/mall/consumer/payment?type=recharge&amount=${amount}`
})
@@ -471,20 +471,20 @@ const confirmRecharge = async () => {
closeRechargePopup()
}
// 关闭充值弹窗
// 鍏抽棴鍏呭€煎脊绐?
const closeRechargePopup = () => {
showRechargePopup.value = false
rechargeAmount.value = ''
}
// 返回
// 杩斿洖
const goBack = () => {
uni.navigateBack()
}
</script>
<style scoped>
/* 响应式布局优化 */
/* 鍝嶅簲寮忓竷灞€浼樺寲 */
@media screen and (min-width: 768px) {
.wallet-content {
padding: 20px;
@@ -518,11 +518,11 @@ const goBack = () => {
@media screen and (min-width: 1024px) {
.wallet-page {
flex-direction: row; /* 大屏下改为横向布局 */
flex-direction: row; /* 澶у睆涓嬫敼涓烘í鍚戝竷灞€ */
}
.wallet-header {
display: none; /* 大屏下隐藏顶部栏 */
display: none; /* 澶у睆涓嬮殣钘忛《閮ㄦ爮 */
}
.wallet-content {
@@ -981,4 +981,5 @@ const goBack = () => {
background-color: #cccccc;
opacity: 0.6;
}
</style>
</style>

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="page-container">
<view class="card">
<view class="section-title">提现至</view>
@@ -338,4 +338,4 @@ const submitWithdraw = async () => {
color: #5785e5;
font-weight: bold;
}
</style>
</style>