Merge remote-tracking branch 'origin/cyh666666/consumer'

This commit is contained in:
comlibmb
2026-01-28 09:06:25 +08:00
59 changed files with 34946 additions and 1034 deletions

9
.hbuilderx/launch.json Normal file
View File

@@ -0,0 +1,9 @@
{
"version" : "1.0",
"configurations" : [
{
"playground" : "standard",
"type" : "uni-app:app-android"
}
]
}

View File

@@ -1042,4 +1042,13 @@ function buildSupabaseFilterQuery(filter : UTSJSONObject | null) : string {
return params.join('&');
}
/**
* 创建 Supabase 客户端实例
* @param url 项目 URL
* @param key 项目匿名密钥 (Anon Key)
*/
export function createClient(url : string, key : string) : AkSupa {
return new AkSupa(url, key);
}
export default AkSupa;

View File

@@ -0,0 +1,18 @@
//import AkSupa from './aksupa.uts'
//import { SUPA_URL, SUPA_KEY } from '@/ak/config.uts'
//const supa = new AkSupa(SUPA_URL, SUPA_KEY)
//const supaReady: Promise<boolean> = (async () => {
/// try {
// await supa.signIn('akoo@163.com', 'Hf2152111')
// await supa.signIn('am@163.com', 'kookoo')
// return true
// } catch (err) {
// console.error('Supabase auto sign-in failed', err)
// return false
// }
//})()
//export { supaReady }
//export default supa

View File

@@ -1,3 +1,34 @@
// /components/supadb/aksupainstance.uts
import { createClient } from './aksupa.uts'
// 创建并导出 Supabase 客户端实例
const supabaseUrl = 'https://your-project.supabase.co' // 替换为你的 Supabase URL
const supabaseAnonKey = 'your-anon-key' // 替换为你的匿名密钥
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
// 导出 Supabase 实例就绪状态
export const isSupabaseReady = true
// 如果有其他需要导出的函数,可以这样导出:
export function initializeSupabase(url: string, key: string) {
return createClient(url, key)
}
// 检查连接状态的函数
export function checkConnection() {
return new Promise((resolve) => {
// 模拟连接检查
setTimeout(() => {
resolve(true)
}, 500)
})
}
// 不再使用 supaready 变量,而是提供函数
export async function ensureSupabaseReady() {
return await checkConnection()
}
import AkSupa from './aksupa.uts'
import { SUPA_URL, SUPA_KEY } from '@/ak/config.uts'

14
main - 副本.uts Normal file
View File

@@ -0,0 +1,14 @@
import { createSSRApp } from 'vue'
import App from './App.uvue'
import i18n from '@/uni_modules/i18n/index.uts'
export function createApp() {
const app = createSSRApp(App)
// 注册 i18n 全局属性,使组件可以使用 $t 方法
app.config.globalProperties.$t = (key: string, values?: any, locale?: string): string => {
return i18n.global.t(key, values, locale)
}
return { app }
}

View File

@@ -4,11 +4,15 @@ import App from './App.uvue'
export function createApp() {
const app = createSSRApp(App)
// 简化的$t方法
app.config.globalProperties.$t = (key: string): string => {
return key // 直接返回key不进行翻译
}
// 注册 i18n 全局属性,使组件可以使用 $t 方法
app.config.globalProperties.$t = (key: string, values?: any, locale?: string): string => {
if (!i18n.global) {
console.error('i18n is not initialized')
return key
}
return i18n.global.t(key, values, locale) || key
}
return { app }
}

View File

@@ -1,8 +1,10 @@
{
"name": "mall",
"private": true,
"dependencies": {
"echarts": "^6.0.0"
},
"devDependencies": {
"@dcloudio/types": "^3.4.29"
}
}
}

118
pages - 副本 (2).json Normal file
View File

@@ -0,0 +1,118 @@
{
"pages": [
// 消费者端页面
{
"path": "pages/mall/consumer/index",
"style": {
"navigationBarTitleText": "商城首页",
"enablePullDownRefresh": true
}
},
{
"path": "pages/mall/consumer/category",
"style": {
"navigationBarTitleText": "商品分类"
}
},
{
"path": "pages/mall/consumer/search",
"style": {
"navigationBarTitleText": "搜索商品",
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/consumer/product-detail",
"style": {
"navigationBarTitleText": "商品详情"
}
},
{
"path": "pages/mall/consumer/cart",
"style": {
"navigationBarTitleText": "购物车"
}
},
{
"path": "pages/mall/consumer/orders",
"style": {
"navigationBarTitleText": "我的订单",
"enablePullDownRefresh": true
}
},
{
"path": "pages/mall/consumer/order-detail",
"style": {
"navigationBarTitleText": "订单详情"
}
},
{
"path": "pages/mall/consumer/address",
"style": {
"navigationBarTitleText": "收货地址"
}
},
{
"path": "pages/mall/consumer/address-edit",
"style": {
"navigationBarTitleText": "编辑地址"
}
},
{
"path": "pages/mall/consumer/coupons",
"style": {
"navigationBarTitleText": "我的优惠券"
}
},
{
"path": "pages/mall/consumer/favorites",
"style": {
"navigationBarTitleText": "我的收藏"
}
},
{
"path": "pages/mall/consumer/profile",
"style": {
"navigationBarTitleText": "个人中心"
}
}
],
"tabBar": {
"color": "#999999",
"selectedColor": "#007aff",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/mall/consumer/index",
"text": "首页",
"iconPath": "static/tab-home.png",
"selectedIconPath": "static/tab-home-active.png"
},
{
"pagePath": "pages/mall/consumer/category",
"text": "分类",
"iconPath": "static/tab-category.png",
"selectedIconPath": "static/tab-category-active.png"
},
{
"pagePath": "pages/mall/consumer/cart",
"text": "购物车",
"iconPath": "static/tab-cart.png",
"selectedIconPath": "static/tab-cart-active.png"
},
{
"pagePath": "pages/mall/consumer/orders",
"text": "订单",
"iconPath": "static/tab-order.png",
"selectedIconPath": "static/tab-order-active.png"
},
{
"pagePath": "pages/mall/consumer/profile",
"text": "我的",
"iconPath": "static/tab-profile.png",
"selectedIconPath": "static/tab-profile-active.png"
}
]
}
}

213
pages - 副本.json Normal file
View File

@@ -0,0 +1,213 @@
{
"pages": [
{
"path": "pages/mall/consumer/index",
"style": {
"navigationBarTitleText": "商城首页",
"navigationStyle": "custom"
}
},
{
"path": "pages/user/boot",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/user/login",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/user/register",
"style": {
"navigationBarTitleText": "注册"
}
},
{
"path": "pages/user/forgot-password",
"style": {
"navigationBarTitleText": "忘记密码"
}
},
{
"path": "pages/user/center",
"style": {
"navigationBarTitleText": "用户中心"
}
},
{
"path": "pages/user/profile",
"style": {
"navigationBarTitleText": "个人资料"
}
},
{
"path": "pages/user/terms",
"style": {
"navigationBarTitleText": "用户协议与隐私政策"
}
},
{
"path": "pages/mall/merchant/index",
"style": {
"navigationBarTitleText": "商家中心",
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/delivery/index",
"style": {
"navigationBarTitleText": "配送中心",
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/admin/index",
"style": {
"navigationBarTitleText": "管理后台",
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/service/index",
"style": {
"navigationBarTitleText": "客服工作台",
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/analytics/index",
"style": {
"navigationBarTitleText": "数据分析",
"navigationStyle": "custom"
}
}
],
"subPackages": [
{
"root": "pages/mall",
"pages": [
{
"path": "consumer/product-detail",
"style": {
"navigationBarTitleText": "商品详情"
}
},
{
"path": "consumer/order-detail",
"style": {
"navigationBarTitleText": "订单详情"
}
},
{
"path": "consumer/profile",
"style": {
"navigationBarTitleText": "个人中心"
}
},
{
"path": "consumer/subscription/plan-list",
"style": {
"navigationBarTitleText": "软件订阅"
}
},
{
"path": "consumer/subscription/plan-detail",
"style": {
"navigationBarTitleText": "订阅详情"
}
},
{
"path": "consumer/subscription/subscribe-checkout",
"style": {
"navigationBarTitleText": "确认订阅"
}
},
{
"path": "consumer/subscription/my-subscriptions",
"style": {
"navigationBarTitleText": "我的订阅"
}
},
{
"path": "admin/subscription/plan-management",
"style": {
"navigationBarTitleText": "订阅方案管理"
}
},
{
"path": "admin/subscription/user-subscriptions",
"style": {
"navigationBarTitleText": "用户订阅管理"
}
},
{
"path": "nfc/security/index",
"style": {
"navigationBarTitleText": "安保工作台",
"enablePullDownRefresh": true,
"backgroundColor": "#f8f9fa"
}
}
]
}
],
"tabBar": {
"custom": true,
"color": "#7A7E83",
"selectedColor": "#3cc51f",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/mall/consumer/index",
"iconPath": "static/tab-home.png",
"selectedIconPath": "static/tab-home-current.png",
"text": "首页"
},
{
"pagePath": "pages/mall/consumer/category",
"iconPath": "static/tab-category.png",
"selectedIconPath": "static/tab-category-current.png",
"text": "分类"
},
{
"pagePath": "pages/mall/consumer/cart",
"iconPath": "static/tab-cart.png",
"selectedIconPath": "static/tab-cart-current.png",
"text": "购物车"
},
{
"pagePath": "pages/mall/consumer/profile",
"iconPath": "static/tab-profile.png",
"selectedIconPath": "static/tab-profile-current.png",
"text": "我的"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "mall",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F8F8F8"
},
"condition": {
"current": 0,
"list": [
{
"name": "消费者端首页",
"path": "pages/mall/consumer/index"
},
{
"name": "启动页(登录态判断)",
"path": "pages/user/boot"
},
{
"name": "登录页",
"path": "pages/user/login"
}
]
}
}

View File

@@ -1,66 +1,130 @@
{
"pages": [
{
"path": "pages/mall/consumer/index",
"path": "pages/mall/consumer/index",
"style": {
"navigationBarTitleText": "商城首页",
"navigationBarTitleText": "首页",
"navigationStyle": "custom",
"enablePullDownRefresh": true
}
},
{
"path": "pages/mall/consumer/settings",
"style": {
"navigationBarTitleText": "设置"
}
},
{
"path": "pages/mall/consumer/wallet",
"style": {
"navigationBarTitleText": "我的钱包"
}
},
{
"path": "pages/user/login",
"style": {
"navigationBarTitleText": "用户登录",
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/consumer/category",
"style": {
"navigationBarTitleText": "分类"
}
},
{
"path": "pages/mall/consumer/messages",
"style": {
"navigationBarTitleText": "消息",
"enablePullDownRefresh": true
}
},
{
"path": "pages/mall/consumer/cart",
"style": {
"navigationBarTitleText": "购物车"
}
},
{
"path": "pages/mall/consumer/profile",
"style": {
"navigationBarTitleText": "我的"
}
},
{
"path": "pages/mall/consumer/search",
"style": {
"navigationBarTitleText": "搜索",
"navigationStyle": "custom"
}
},
{
"path": "pages/user/boot",
"path": "pages/mall/consumer/product-detail",
"style": {
"navigationBarTitleText": ""
"navigationBarTitleText": "商品详情"
}
},
{
"path": "pages/user/login",
"path": "pages/mall/consumer/shop-detail",
"style": {
"navigationBarTitleText": "登录"
"navigationBarTitleText": "店铺详情"
}
},
{
"path": "pages/user/register",
"path": "pages/mall/consumer/coupons",
"style": {
"navigationBarTitleText": "注册"
"navigationBarTitleText": "我的优惠券"
}
},
{
"path": "pages/user/forgot-password",
"path": "pages/mall/consumer/favorites",
"style": {
"navigationBarTitleText": "忘记密码"
"navigationBarTitleText": "我的收藏"
}
},
{
"path": "pages/user/center",
"path": "pages/mall/consumer/footprint",
"style": {
"navigationBarTitleText": "用户中心"
"navigationBarTitleText": "我的足迹"
}
},
{
"path": "pages/user/profile",
"path": "pages/mall/consumer/address-list",
"style": {
"navigationBarTitleText": "个人资料"
"navigationBarTitleText": "收货地址"
}
},
{
"path": "pages/user/terms",
"path": "pages/mall/consumer/address-edit",
"style": {
"navigationBarTitleText": "用户协议与隐私政策"
"navigationBarTitleText": "编辑地址"
}
},
{
"path": "pages/mall/merchant/index",
"path": "pages/mall/consumer/checkout",
"style": {
"navigationBarTitleText": "商家中心",
"navigationBarTitleText": "确认订单"
}
},
{
"path": "pages/mall/consumer/payment",
"style": {
"navigationBarTitleText": "收银台"
}
},
{
"path": "pages/mall/consumer/payment-success",
"style": {
"navigationBarTitleText": "支付成功",
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/delivery/index",
"path": "pages/mall/consumer/orders",
"style": {
"navigationBarTitleText": "配送中心",
"navigationStyle": "custom"
"navigationBarTitleText": "我的订单",
"enablePullDownRefresh": true
}
},
{
@@ -162,150 +226,85 @@
}
},
{
"path": "pages/mall/admin/index",
"path": "pages/mall/consumer/order-detail",
"style": {
"navigationBarTitleText": "管理后台",
"navigationStyle": "custom"
"navigationBarTitleText": "订单详情"
}
},
{
"path": "pages/mall/service/index",
"path": "pages/mall/consumer/logistics",
"style": {
"navigationBarTitleText": "客服工作台",
"navigationStyle": "custom"
"navigationBarTitleText": "物流详情"
}
},
{
"path": "pages/mall/analytics/index",
"path": "pages/mall/consumer/review",
"style": {
"navigationBarTitleText": "数据分析",
"navigationBarTitleText": "评价晒单"
}
},
{
"path": "pages/mall/consumer/refund",
"style": {
"navigationBarTitleText": "退款/售后"
}
},
{
"path": "pages/mall/consumer/apply-refund",
"style": {
"navigationBarTitleText": "申请售后"
}
},
{
"path": "pages/mall/consumer/refund-review",
"style": {
"navigationBarTitleText": "服务评价"
}
},
{
"path": "pages/mall/consumer/chat",
"style": {
"navigationBarTitleText": "客服聊天",
"navigationStyle": "custom"
}
}
],
"subPackages": [
{
"root": "pages/mall",
"pages": [
{
"path": "consumer/product-detail",
"style": {
"navigationBarTitleText": "商品详情"
}
},
{
"path": "consumer/order-detail",
"style": {
"navigationBarTitleText": "订单详情"
}
},
{
"path": "consumer/profile",
"style": {
"navigationBarTitleText": "个人中心"
}
},
{
"path": "consumer/subscription/plan-list",
"style": {
"navigationBarTitleText": "软件订阅"
}
},
{
"path": "consumer/subscription/plan-detail",
"style": {
"navigationBarTitleText": "订阅详情"
}
},
{
"path": "consumer/subscription/subscribe-checkout",
"style": {
"navigationBarTitleText": "确认订阅"
}
},
{
"path": "consumer/subscription/my-subscriptions",
"style": {
"navigationBarTitleText": "我的订阅"
}
},
{
"path": "admin/subscription/plan-management",
"style": {
"navigationBarTitleText": "订阅方案管理"
}
},
{
"path": "admin/subscription/user-subscriptions",
"style": {
"navigationBarTitleText": "用户订阅管理"
}
},
{
"path": "nfc/security/index",
"style": {
"navigationBarTitleText": "安保工作台",
"enablePullDownRefresh": true,
"backgroundColor": "#f8f9fa"
}
}
]
}
],
"tabBar": {
"custom": true,
"color": "#7A7E83",
"selectedColor": "#3cc51f",
"borderStyle": "black",
"color": "#999999",
"selectedColor": "#ff5000",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/mall/consumer/index",
"iconPath": "static/tab-home.png",
"selectedIconPath": "static/tab-home-current.png",
"text": "首页"
"text": "首页",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home-active.png"
},
{
"pagePath": "pages/mall/consumer/category",
"iconPath": "static/tab-category.png",
"selectedIconPath": "static/tab-category-current.png",
"text": "分类"
"text": "分类",
"iconPath": "static/tabbar/category.png",
"selectedIconPath": "static/tabbar/category-active.png"
},
{
"pagePath": "pages/mall/consumer/messages",
"text": "消息",
"iconPath": "static/tabbar/messages.png",
"selectedIconPath": "static/tabbar/messages-active.png"
},
{
"pagePath": "pages/mall/consumer/cart",
"iconPath": "static/tab-cart.png",
"selectedIconPath": "static/tab-cart-current.png",
"text": "购物车"
"text": "购物车",
"iconPath": "static/tabbar/cart.png",
"selectedIconPath": "static/tabbar/cart-active.png"
},
{
"pagePath": "pages/mall/consumer/profile",
"iconPath": "static/tab-profile.png",
"selectedIconPath": "static/tab-profile-current.png",
"text": "我的"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "mall",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F8F8F8"
},
"condition": {
"current": 0,
"list": [
{
"name": "消费者端首页",
"path": "pages/mall/consumer/index"
},
{
"name": "启动页(登录态判断)",
"path": "pages/user/boot"
},
{
"name": "登录页",
"path": "pages/user/login"
"text": "我的",
"iconPath": "static/tabbar/profile.png",
"selectedIconPath": "static/tabbar/profile-active.png"
}
]
}
}
}

View File

@@ -0,0 +1,360 @@
<template>
<view class="address-edit-page">
<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>
</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>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
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('')
const formData = reactive({
name: '',
phone: '',
detail: '',
isDefault: false,
label: ''
} as {
name: string
phone: string
detail: string
isDefault: boolean
label: string
})
onLoad((options) => {
if (options['id']) {
isEdit.value = true
addressId.value = options['id'] as string
loadAddress(addressId.value)
}
})
const loadAddress = (id: string) => {
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
const addresses = JSON.parse(storedAddresses as string) as Address[]
const address = addresses.find(item => item.id === id)
if (address) {
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()
}
}
}
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 = () => {
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 storedAddresses = uni.getStorageSync('addresses')
let addresses: Address[] = []
if (storedAddresses) {
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()}`,
name: formData.name,
phone: formData.phone,
province: province,
city: city,
district: district,
detail: formData.detail,
isDefault: formData.isDefault,
label: formData.label
}
// 如果是第一个地址,自动设为默认
if (addresses.length === 0) {
newAddress.isDefault = true
}
addresses.push(newAddress)
}
uni.setStorageSync('addresses', JSON.stringify(addresses))
uni.showToast({
title: '保存成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
const parseSmartInput = () => {
const input = smartInput.value.trim()
if (!input) return
// 提取手机号
const phoneRegex = /(1[3-9]\d{9})/
const phoneMatch = input.match(phoneRegex)
if (phoneMatch) {
formData.phone = phoneMatch[0]
}
// 提取姓名取第一个2-4位中文
const nameRegex = /([\u4e00-\u9fa5]{2,4})/
const nameMatch = input.match(nameRegex)
if (nameMatch) {
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()
// 解析省市区
const pattern1 = /^(.*?省)?(.*?市)?(.*?[区县])?(.*)$/
const m = addrText.match(pattern1)
if (m) {
const [, province, city, district, detail] = m
regionString.value = `${(province||'').trim()} ${(city||'').trim()} ${(district||'').trim()}`.trim()
formData.detail = (detail||'').trim()
} else {
formData.detail = addrText
}
}
const deleteAddress = () => {
uni.showModal({
title: '提示',
content: '确定要删除该地址吗?',
success: (res) => {
if (res.confirm) {
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
let addresses = JSON.parse(storedAddresses as string) as Address[]
addresses = addresses.filter(item => item.id !== addressId.value)
uni.setStorageSync('addresses', JSON.stringify(addresses))
uni.showToast({
title: '删除成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
}
}
})
}
</script>
<style>
.address-edit-page {
min-height: 100vh;
background-color: #f5f5f5;
padding-top: 15px;
}
.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

@@ -0,0 +1,307 @@
<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>
</view>
<view v-else v-for="(item, index) in addresses" :key="item.id" class="address-item" @click="selectAddress(item)">
<view class="item-content">
<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.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>
</view>
<view class="action-item" @click.stop="deleteAddress(item.id)">
<text class="action-icon"><3E></text>
</view>
</view>
</view>
</view>
<view class="footer-btn">
<button class="add-btn" @click="addAddress">新建收货地址</button>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
type Address = {
id: string
name: string
phone: string
province: string
city: string
district: string
detail: string
isDefault: boolean
label?: string
}
const addresses = ref<Address[]>([])
const selectionMode = ref<boolean>(false)
let openerEventChannel: any = null
onShow(() => {
loadAddresses()
})
onMounted(() => {
try {
const ec = uni.getOpenerEventChannel()
openerEventChannel = ec
ec?.on('setSelectMode', (data: any) => {
if (data && typeof data.selectMode === 'boolean') {
selectionMode.value = data.selectMode
}
})
} catch (e) {
// ignore
}
})
const loadAddresses = () => {
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
try {
addresses.value = JSON.parse(storedAddresses as string) as Address[]
} catch (e) {
console.error('Failed to parse addresses', e)
addresses.value = []
}
} else {
// 初始Mock数据
addresses.value = [
{
id: 'addr_001',
name: '张三',
phone: '13800138000',
province: '北京市',
city: '北京市',
district: '朝阳区',
detail: '三里屯SOHO A座',
isDefault: true,
label: '公司'
}
]
uni.setStorageSync('addresses', JSON.stringify(addresses.value))
}
}
const getFullAddress = (item: Address): string => {
return `${item.province}${item.city}${item.district} ${item.detail}`
}
const addAddress = () => {
uni.navigateTo({
url: '/pages/mall/consumer/address-edit'
})
}
// 删除地址
const deleteAddress = (id: string) => {
uni.showModal({
title: '提示',
content: '确定要删除该地址吗?',
success: (res) => {
if (res.confirm) {
const index = addresses.value.findIndex(addr => addr.id === id)
if (index !== -1) {
addresses.value.splice(index, 1)
uni.setStorageSync('addresses', JSON.stringify(addresses.value))
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
}
}
})
}
const editAddress = (id: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/address-edit?id=${id}`
})
}
const selectAddress = (item: Address) => {
if (selectionMode.value && openerEventChannel) {
openerEventChannel.emit('addressSelected', {
id: item.id,
recipient_name: item.name,
phone: item.phone,
province: item.province,
city: item.city,
district: item.district,
detail: item.detail,
is_default: item.isDefault
})
uni.navigateBack()
} else {
editAddress(item.id)
}
}
</script>
<style>
/* 响应式布局优化 */
@media screen and (min-width: 768px) {
.address-list {
max-width: 800px;
margin: 0 auto;
}
.address-list-page {
background-color: #f5f5f5;
}
.footer-btn {
max-width: 800px;
margin: 0 auto;
left: 50%;
transform: translateX(-50%);
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
}
}
.address-list-page {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 80px;
}
.address-list {
padding: 15px;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 100px;
}
.empty-icon {
font-size: 60px;
margin-bottom: 20px;
}
.empty-text {
color: #999;
font-size: 16px;
}
.address-item {
background-color: white;
border-radius: 10px;
padding: 15px;
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.item-content {
flex: 1;
margin-right: 15px;
}
.item-header {
display: flex;
align-items: center;
margin-bottom: 8px;
flex-wrap: wrap;
}
.user-name {
font-size: 16px;
font-weight: bold;
color: #333;
margin-right: 10px;
}
.user-phone {
font-size: 14px;
color: #666;
margin-right: 10px;
}
.default-tag {
background-color: #ff5000;
color: white;
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
margin-right: 5px;
}
.label-tag {
background-color: #e0f2f1;
color: #00796b;
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
}
.address-text {
font-size: 14px;
color: #333;
line-height: 1.4;
}
.item-actions {
padding: 10px;
border-left: 1px solid #f0f0f0;
display: flex;
flex-direction: column; /* 竖向排列图标 */
justify-content: center;
align-items: center;
gap: 15px;
}
.action-item {
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
}
.action-icon {
font-size: 20px;
color: #999;
}
.footer-btn {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: white;
padding: 10px 15px;
padding-bottom: calc(10px + env(safe-area-inset-bottom));
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
}
.add-btn {
background-color: #ff5000;
color: white;
border-radius: 25px;
font-size: 16px;
height: 44px;
line-height: 44px;
border: none;
}
</style>

View File

@@ -0,0 +1,918 @@
<!-- 地址管理页面 -->
<template>
<view class="address-page">
<!-- 顶部栏 -->
<view class="address-header">
<view class="header-title">
<text class="title-text">收货地址</text>
</view>
</view>
<!-- 地址列表 -->
<scroll-view class="address-list" scroll-y>
<!-- 地址为空 -->
<view v-if="addressList.length === 0" class="empty-address">
<text class="empty-icon">📍</text>
<text class="empty-text">暂无收货地址</text>
<text class="empty-subtext">点击下方按钮添加地址</text>
</view>
<!-- 地址项 -->
<view v-for="address in addressList" :key="address.id" class="address-item">
<view class="address-info" @click="selectAddress(address)">
<view class="address-header-row">
<text class="address-name">{{ address.recipient_name }}</text>
<text class="address-phone">{{ address.phone }}</text>
<view v-if="address.is_default" class="default-tag">
<text class="tag-text">默认</text>
</view>
</view>
<view class="address-detail">
<text class="detail-text">{{ getFullAddress(address) }}</text>
</view>
</view>
<view class="address-actions">
<view class="action-item" @click="editAddress(address)">
<text class="action-icon">✏️</text>
<text class="action-text">编辑</text>
</view>
<view class="action-item" @click="deleteAddress(address)">
<text class="action-icon">🗑️</text>
<text class="action-text">删除</text>
</view>
<view v-if="!address.is_default" class="action-item" @click="setDefaultAddress(address)">
<text class="action-icon">⭐</text>
<text class="action-text">设为默认</text>
</view>
</view>
</view>
<!-- 从选择页面返回时的提示 -->
<view v-if="fromSelect && addressList.length > 0" class="select-tip">
<text class="tip-text">请选择收货地址</text>
<text class="tip-subtext">或点击下方添加新地址</text>
</view>
</scroll-view>
<!-- 添加地址按钮 -->
<view class="add-address-btn" @click="showNewAddressForm = true">
<text class="btn-icon">+</text>
<text class="btn-text">添加新地址</text>
</view>
<!-- 新建地址表单弹窗 -->
<view v-if="showNewAddressForm" class="address-form-mask" @click="cancelNewAddress">
<view class="address-form-popup" @click.stop>
<view class="form-header">
<text class="form-title">新建收货地址</text>
<text class="form-close" @click="cancelNewAddress">×</text>
</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="请输入完整地址,系统将自动识别省市区和详细地址"
@blur="parseSmartAddress"
maxlength="200" />
<text class="smart-tip">例如北京市朝阳区三里屯SOHO A座</text>
</view>
<view class="form-item">
<text class="form-label">所在地区</text>
<view class="region-inputs">
<input class="form-input region-input" v-model="newAddress.province"
placeholder="省" readonly />
<input class="form-input region-input" v-model="newAddress.city"
placeholder="市" readonly />
<input class="form-input region-input" v-model="newAddress.district"
placeholder="区/县" readonly />
</view>
</view>
<view class="form-item">
<text class="form-label">详细地址</text>
<textarea class="form-textarea" v-model="newAddress.detail"
placeholder="街道、小区、楼栋、门牌号等"
maxlength="100" />
</view>
<view class="form-item checkbox-item">
<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>
<text class="checkbox-label">设为默认地址</text>
</view>
</view>
</scroll-view>
<view class="form-buttons">
<button class="form-cancel-btn" @click="cancelNewAddress">取消</button>
<button class="form-submit-btn" @click="saveNewAddress">保存</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
type AddressType = {
id: string
user_id: string
recipient_name: string
phone: string
province: string
city: string
district: string
detail: string
postal_code: string | null
is_default: boolean
created_at: string
}
const addressList = ref<Array<AddressType>>([])
const fromSelect = ref<boolean>(false)
const selectCallback = ref<any>(null)
const showNewAddressForm = ref<boolean>(false)
const newAddress = ref<any>({
recipient_name: '',
phone: '',
province: '',
city: '',
district: '',
detail: '',
is_default: false
})
const smartAddressInput = ref<string>('')
// 生命周期
onMounted(() => {
const eventChannel = uni.getEventChannel()
if (eventChannel) {
eventChannel.on('fromSelect', (data: any) => {
fromSelect.value = data.fromSelect || false
selectCallback.value = data.callback
})
}
loadAddresses()
// 监听地址更新事件从checkout页面或其他页面
uni.$on('addressUpdated', (updatedAddressList: any) => {
addressList.value = updatedAddressList
})
})
// 组件卸载时移除事件监听
onUnmounted(() => {
uni.$off('addressUpdated')
})
// 加载地址列表
const loadAddresses = async () => {
const userId = getCurrentUserId()
if (!userId) {
uni.showToast({
title: '请先登录',
icon: 'none'
})
uni.navigateTo({
url: '/pages/user/login'
})
return
}
try {
const { data, error } = await supa
.from('user_addresses')
.select('*')
.eq('user_id', userId)
.order('is_default', { ascending: false })
.order('created_at', { ascending: false })
if (error !== null) {
console.error('加载地址失败:', error)
return
}
addressList.value = data ?? []
} catch (err) {
console.error('加载地址异常:', err)
}
}
// 获取当前用户ID
const getCurrentUserId = (): string | null => {
const userStore = uni.getStorageSync('userInfo')
return userStore?.id || null
}
// 获取完整地址
const getFullAddress = (address: AddressType): string => {
return `${address.province}${address.city}${address.district}${address.detail}`
}
// 选择地址
const selectAddress = (address: AddressType) => {
if (fromSelect.value && selectCallback.value) {
// 返回选择的地址
selectCallback.value(address)
uni.navigateBack()
}
}
// 编辑地址
const editAddress = (address: AddressType) => {
uni.navigateTo({
url: `/pages/mall/consumer/address-edit?id=${address.id}`
})
}
// 删除地址
const deleteAddress = (address: AddressType) => {
uni.showModal({
title: '删除地址',
content: '确定要删除这个收货地址吗?',
success: async (res) => {
if (res.confirm) {
try {
// 如果是默认地址,删除前检查是否还有其他地址
if (address.is_default && addressList.value.length > 1) {
uni.showModal({
title: '提示',
content: '删除默认地址后,系统会自动设置第一个地址为默认地址',
success: async (confirmRes) => {
if (confirmRes.confirm) {
await performDelete(address)
}
}
})
} else {
await performDelete(address)
}
} catch (err) {
console.error('删除地址异常:', err)
}
}
}
})
}
// 执行删除
const performDelete = async (address: AddressType) => {
try {
const { error } = await supa
.from('user_addresses')
.delete()
.eq('id', address.id)
if (error !== null) {
console.error('删除地址失败:', error)
uni.showToast({
title: '删除失败',
icon: 'none'
})
return
}
// 从列表中移除
const index = addressList.value.findIndex(item => item.id === address.id)
if (index !== -1) {
addressList.value.splice(index, 1)
}
// 如果是默认地址被删除,设置第一个地址为默认
if (address.is_default && addressList.value.length > 0) {
const newDefault = addressList.value[0]
await setAsDefault(newDefault)
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
} catch (err) {
console.error('执行删除异常:', err)
}
}
// 设为默认地址
const setDefaultAddress = async (address: AddressType) => {
try {
const userId = getCurrentUserId()
if (!userId) return
// 1. 取消当前所有默认地址
const { error: updateError } = await supa
.from('user_addresses')
.update({ is_default: false })
.eq('user_id', userId)
.eq('is_default', true)
if (updateError !== null) {
console.error('取消默认地址失败:', updateError)
return
}
// 2. 设置新的默认地址
const { error: setError } = await supa
.from('user_addresses')
.update({ is_default: true })
.eq('id', address.id)
if (setError !== null) {
console.error('设置默认地址失败:', setError)
return
}
// 更新本地数据
addressList.value.forEach(item => {
item.is_default = item.id === address.id
})
uni.showToast({
title: '已设为默认地址',
icon: 'success'
})
} catch (err) {
console.error('设置默认地址异常:', err)
}
}
// 设置地址为默认(内部方法)
const setAsDefault = async (address: AddressType) => {
try {
const { error } = await supa
.from('user_addresses')
.update({ is_default: true })
.eq('id', address.id)
if (error !== null) {
console.error('设置默认地址失败:', error)
return
}
address.is_default = true
} catch (err) {
console.error('设置默认地址异常:', err)
}
}
// 解析智能地址
const parseSmartAddress = () => {
const input = smartAddressInput.value.trim()
if (!input) return
// 重置表单
newAddress.value.recipient_name = ''
newAddress.value.phone = ''
newAddress.value.province = ''
newAddress.value.city = ''
newAddress.value.district = ''
newAddress.value.detail = ''
// 尝试匹配手机号码11位数字
const phoneRegex = /(1[3-9]\d{9})/g
const phoneMatches = input.match(phoneRegex)
if (phoneMatches && phoneMatches.length > 0) {
newAddress.value.phone = phoneMatches[0]
}
// 尝试匹配收件人姓名中文姓名2-4个汉字
const nameRegex = /([\u4e00-\u9fa5]{2,4})/g
const nameMatches = input.match(nameRegex)
if (nameMatches && nameMatches.length > 0) {
// 取第一个匹配的中文姓名作为收件人
newAddress.value.recipient_name = nameMatches[0]
}
// 提取地址部分(移除姓名和手机号)
let addressText = input
if (newAddress.value.recipient_name) {
addressText = addressText.replace(newAddress.value.recipient_name, '')
}
if (newAddress.value.phone) {
addressText = addressText.replace(newAddress.value.phone, '')
}
// 清理地址文本(移除多余的空格和标点)
addressText = addressText.replace(/[,;\s]+/g, ' ').trim()
// 地址解析逻辑
const patterns = [
// 匹配格式:省市区详细地址
/^(.*?省)?(.*?市)?(.*?[区县])?(.*)$/,
// 匹配格式:省市详细地址
/^(.*?省)?(.*?市)?(.*)$/
]
for (const pattern of patterns) {
const match = addressText.match(pattern)
if (match) {
const [, province, city, district, detail] = match
if (province) newAddress.value.province = province.replace('省', '').trim()
if (city) newAddress.value.city = city.replace('市', '').trim()
if (district) newAddress.value.district = district.trim()
if (detail) newAddress.value.detail = detail.trim()
// 如果详细地址为空,但还有剩余内容,则作为详细地址
if (!newAddress.value.detail && district && detail) {
newAddress.value.detail = detail.trim()
}
break
}
}
// 如果没有匹配到模式,尝试简单分割
if (!newAddress.value.province && !newAddress.value.city && !newAddress.value.district) {
// 尝试按常见分隔符分割
const parts = addressText.split(/[省市县区]/)
if (parts.length >= 2) {
newAddress.value.province = parts[0] || ''
newAddress.value.city = parts[1] || ''
newAddress.value.detail = parts.slice(2).join('').trim() || addressText
} else {
newAddress.value.detail = addressText
}
}
// 如果地址部分为空,但还有剩余文本,则作为详细地址
if (!newAddress.value.detail && addressText.trim()) {
newAddress.value.detail = addressText.trim()
}
}
// 保存新地址
const saveNewAddress = async () => {
// 验证表单
if (!newAddress.value.recipient_name || !newAddress.value.phone || !newAddress.value.detail) {
uni.showToast({
title: '请填写完整信息',
icon: 'none'
})
return
}
const userId = getCurrentUserId()
if (!userId) {
uni.showToast({
title: '请先登录',
icon: 'none'
})
return
}
try {
const { data, error } = await supa
.from('user_addresses')
.insert({
user_id: userId,
recipient_name: newAddress.value.recipient_name,
phone: newAddress.value.phone,
province: newAddress.value.province,
city: newAddress.value.city,
district: newAddress.value.district,
detail: newAddress.value.detail,
is_default: newAddress.value.is_default,
created_at: new Date().toISOString()
})
.select()
.single()
if (error !== null) {
console.error('保存地址失败:', error)
uni.showToast({
title: '保存失败',
icon: 'none'
})
return
}
// 如果是默认地址,取消其他默认地址
if (newAddress.value.is_default) {
addressList.value.forEach(addr => {
addr.is_default = false
})
// 更新数据库中的其他地址
await supa
.from('user_addresses')
.update({ is_default: false })
.eq('user_id', userId)
.neq('id', data.id)
}
// 添加到列表
addressList.value.unshift(data)
// 发布地址更新事件让checkout页面也能获取到
uni.$emit('addressUpdated', addressList.value)
// 重置表单
resetNewAddressForm()
uni.showToast({
title: '地址保存成功',
icon: 'success'
})
} catch (err) {
console.error('保存地址异常:', err)
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
}
// 重置新建地址表单
const resetNewAddressForm = () => {
showNewAddressForm.value = false
newAddress.value = {
recipient_name: '',
phone: '',
province: '',
city: '',
district: '',
detail: '',
is_default: false
}
smartAddressInput.value = ''
}
// 取消新建地址
const cancelNewAddress = () => {
resetNewAddressForm()
}
</script>
<style scoped>
.address-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.address-header {
background-color: #ffffff;
padding: 15px;
border-bottom: 1px solid #e5e5e5;
}
.header-title {
text-align: center;
}
.title-text {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.address-list {
flex: 1;
padding: 10px;
}
.empty-address {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
background-color: #ffffff;
border-radius: 8px;
}
.empty-icon {
font-size: 80px;
margin-bottom: 20px;
}
.empty-text {
font-size: 16px;
color: #666666;
margin-bottom: 10px;
}
.empty-subtext {
font-size: 14px;
color: #999999;
}
.address-item {
background-color: #ffffff;
margin-bottom: 10px;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.address-info {
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid #f5f5f5;
}
.address-header-row {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.address-name {
font-size: 16px;
font-weight: bold;
color: #333333;
margin-right: 15px;
}
.address-phone {
font-size: 14px;
color: #666666;
margin-right: 10px;
}
.default-tag {
background-color: #ff4757;
padding: 2px 8px;
border-radius: 10px;
}
.tag-text {
color: #ffffff;
font-size: 12px;
}
.address-detail {
font-size: 14px;
color: #333333;
line-height: 1.5;
}
.detail-text {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.address-actions {
display: flex;
justify-content: flex-end;
gap: 20px;
}
.action-item {
display: flex;
align-items: center;
}
.action-icon {
font-size: 16px;
margin-right: 5px;
}
.action-text {
font-size: 14px;
color: #666666;
}
.select-tip {
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
margin-bottom: 10px;
text-align: center;
}
.tip-text {
font-size: 16px;
font-weight: bold;
color: #333333;
margin-bottom: 5px;
display: block;
}
.tip-subtext {
font-size: 14px;
color: #999999;
display: block;
}
.add-address-btn {
background-color: #007aff;
margin: 10px;
padding: 15px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.btn-icon {
color: #ffffff;
font-size: 24px;
margin-right: 10px;
}
.btn-text {
color: #ffffff;
font-size: 16px;
font-weight: bold;
}
/* 新建地址表单弹窗样式 */
.address-form-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1001;
}
.address-form-popup {
background-color: #ffffff;
width: 90%;
max-width: 500px;
max-height: 80vh;
border-radius: 12px;
display: flex;
flex-direction: column;
}
.form-header {
padding: 20px 15px;
border-bottom: 1px solid #e5e5e5;
display: flex;
align-items: center;
justify-content: space-between;
}
.form-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.form-close {
font-size: 24px;
color: #999999;
padding: 5px;
}
.form-content {
flex: 1;
padding: 15px;
max-height: 50vh;
}
.form-item {
margin-bottom: 20px;
}
.form-item:last-child {
margin-bottom: 0;
}
.form-label {
font-size: 14px;
color: #333333;
margin-bottom: 8px;
display: block;
}
.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;
gap: 10px;
}
.region-input {
flex: 1;
}
.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;
display: block;
}
.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;
gap: 10px;
}
.form-cancel-btn {
flex: 1;
background-color: #f5f5f5;
color: #333333;
padding: 12px;
border-radius: 8px;
font-size: 16px;
border: none;
}
.form-submit-btn {
flex: 1;
background-color: #007aff;
color: #ffffff;
padding: 12px;
border-radius: 8px;
font-size: 16px;
border: none;
}
</style>

View File

@@ -0,0 +1,293 @@
<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" />
<text>仅退款</text>
</label>
<label class="type-item">
<radio value="2" :checked="refundType === 2" color="#ff4444" />
<text>退货退款</text>
</label>
</radio-group>
</view>
<view class="section">
<view class="section-title">退款原因</view>
<picker @change="handleReasonChange" :range="reasonList" class="picker">
<view class="picker-content">
<text v-if="refundReason">{{ refundReason }}</text>
<text v-else class="placeholder">请选择退款原因</text>
<text class="arrow">></text>
</view>
</picker>
</view>
<view class="section">
<view class="section-title">退款金额</view>
<view class="amount-input-wrap">
<text class="currency">¥</text>
<input
type="digit"
v-model="refundAmount"
class="amount-input"
:placeholder="`最多可退 ¥${maxAmount}`"
/>
</view>
<text class="amount-tip">最多可退 ¥{{ maxAmount }},含发货邮费 ¥{{ deliveryFee }}</text>
</view>
<view class="section">
<view class="section-title">退款说明</view>
<textarea
v-model="description"
class="desc-input"
placeholder="选填:补充详细的退款说明,有助于商家快速处理"
maxlength="200"
/>
</view>
<view class="submit-bar">
<button class="submit-btn" @click="submitRefund" :loading="submitting">提交申请</button>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
// import supa from '@/components/supadb/aksupainstance.uts'
const orderId = ref('')
const orderItemId = ref('') // Optional, if refunding specific item
const refundType = ref(1) // 1: Only Refund, 2: Return & Refund
const refundReason = ref('')
const refundAmount = ref('')
const description = ref('')
const maxAmount = ref(0)
const deliveryFee = ref(0)
const submitting = ref(false)
const reasonList = [
'多拍/错拍/不想要',
'快递一直未送达',
'未按约定时间发货',
'快递无记录',
'空包裹/少货/错发',
'质量问题',
'其他'
]
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options as any
if (options.orderId) {
orderId.value = options.orderId
loadOrderInfo()
}
})
const loadOrderInfo = async () => {
try {
const { data, error } = await supa
.from('orders')
.select('actual_amount, delivery_fee')
.eq('id', orderId.value)
.single()
if (error == null && data != null) {
maxAmount.value = data['actual_amount'] as number
deliveryFee.value = data['delivery_fee'] as number
refundAmount.value = maxAmount.value.toString()
}
} catch (err) {
console.error('加载订单信息失败', err)
}
}
const handleTypeChange = (e: any) => {
refundType.value = parseInt(e.detail.value)
}
const handleReasonChange = (e: any) => {
const index = e.detail.value as number
refundReason.value = reasonList[index]
}
const submitRefund = async () => {
if (!refundReason.value) {
uni.showToast({ title: '请选择退款原因', icon: 'none' })
return
}
const amount = parseFloat(refundAmount.value)
if (isNaN(amount) || amount <= 0 || amount > maxAmount.value) {
uni.showToast({ title: '请输入有效的退款金额', icon: 'none' })
return
}
submitting.value = true
try {
const userStore = uni.getStorageSync('userInfo')
const userId = userStore?.id
// 1. Create Refund Record
/* const { data, error } = await supa
.from('refunds')
.insert({
user_id: userId,
order_id: orderId.value,
refund_no: 'REF' + Date.now(),
refund_type: refundType.value,
refund_reason: refundReason.value,
refund_amount: amount,
description: description.value,
status: 1, // 待处理
status_history: [{
status: 1,
remark: '用户提交申请',
created_at: new Date().toISOString()
}]
})
if (error != null) throw error */
// MOCK SUBMIT
await new Promise(resolve => setTimeout(resolve, 1000))
// 2. Update Order Status (Optional, e.g. to "After-sales")
// Assuming status 6 is "After-sales/Refund"
/*
await supa
.from('orders')
.update({ status: 6 })
.eq('id', orderId.value)
*/
uni.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => {
uni.redirectTo({
url: '/pages/mall/consumer/refund'
})
}, 1500)
} catch (err) {
console.error('提交退款失败', err)
uni.showToast({ title: '提交失败,请重试', icon: 'none' })
} finally {
submitting.value = false
}
}
</script>
<style scoped>
.apply-refund-page {
min-height: 100vh;
background-color: #f5f5f5;
padding: 15px;
padding-bottom: 80px;
}
.section {
background-color: #ffffff;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 15px;
}
.type-group {
display: flex;
flex-direction: column;
gap: 15px;
}
.type-item {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
}
.picker-content {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
color: #333;
}
.placeholder {
color: #999;
}
.arrow {
color: #ccc;
}
.amount-input-wrap {
display: flex;
align-items: center;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
margin-bottom: 10px;
}
.currency {
font-size: 24px;
font-weight: bold;
color: #333;
margin-right: 10px;
}
.amount-input {
flex: 1;
font-size: 24px;
font-weight: bold;
height: 40px;
}
.amount-tip {
font-size: 12px;
color: #999;
}
.desc-input {
width: 100%;
height: 100px;
font-size: 14px;
background-color: #f9f9f9;
border-radius: 4px;
padding: 10px;
box-sizing: border-box;
}
.submit-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
padding: 15px;
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
}
.submit-btn {
background-color: #ff4444;
color: #ffffff;
border-radius: 22px;
font-size: 16px;
font-weight: bold;
}
</style>

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,131 @@
<!-- 只替换 <style> 部分,其他保持不变 -->
<style>
/* ...(前面的样式如 .category-page, .search-bar 等保持不变)... */
/* 分类内容区 —— 关键修复 */
.category-content {
display: flex;
margin-top: 60px;
padding: 0 16px;
max-width: 1400px;
margin: 60px auto 0 auto; /* 更安全的居中 */
width: 100%;
gap: 20px;
min-height: calc(100vh - 60px); /* 避免内容太短时布局塌陷 */
}
/* 左侧一级分类 */
.primary-category {
width: 120px;
background: white;
border-radius: 12px;
padding: 12px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
flex-shrink: 0;
height: fit-content; /* 防止拉伸 */
max-height: calc(100vh - 80px); /* 避免溢出屏幕 */
overflow-y: auto;
}
/* 右侧商品区域 —— 必须 flex: 1 才能占满剩余空间 */
.product-content {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0; /* 防止 flex 子项溢出 */
background: #f8fafc; /* 与背景一致,或可设为 white */
padding: 16px 0;
border-radius: 12px;
}
/* 商品网格容器需要滚动时也能正常工作 */
.product-content > * {
flex-shrink: 0; /* 防止 header/grid 被压缩 */
}
/* ===== 响应式:小屏手机 (小于等于 414px) ===== */
@media screen and (max-width: 414px) {
.category-content {
flex-direction: column;
padding: 0 12px;
margin-top: 55px; /* 匹配搜索栏高度 */
}
.primary-category {
width: 100%;
display: flex;
flex-wrap: wrap;
padding: 8px;
height: auto;
max-height: none;
overflow: visible;
}
.primary-item {
width: calc(25% - 8px);
margin: 4px;
padding: 10px 8px;
text-align: center;
}
.primary-icon {
margin-right: 0;
margin-bottom: 4px;
font-size: 16px;
}
.primary-name {
font-size: 12px;
}
.product-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.search-container {
padding: 0 12px;
height: 55px;
}
.search-box {
padding: 8px 16px;
}
.category-header {
padding: 0;
}
}
/* 中屏及以上保持网格列数变化,但布局仍是左右 */
@media screen and (min-width: 415px) {
.product-grid {
display: grid;
gap: 20px;
}
}
@media screen and (min-width: 415px) and (max-width: 768px) {
.product-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media screen and (min-width: 769px) and (max-width: 1024px) {
.product-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media screen and (min-width: 1025px) {
.product-grid {
grid-template-columns: repeat(4, 1fr);
}
}
@media screen and (min-width: 1400px) {
.product-grid {
grid-template-columns: repeat(5, 1fr);
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,593 @@
<!-- pages/mall/consumer/chat.uvue -->
<template>
<view class="chat-page">
<!-- 聊天头部 -->
<view class="chat-header">
<view class="header-back" @click="goBack">
<text class="back-icon"></text>
</view>
<view class="header-info">
<text class="chat-title">在线客服</text>
<text class="chat-status">在线</text>
</view>
<view class="header-actions">
<text class="action-icon" @click="showMoreActions">⋮</text>
</view>
</view>
<!-- 聊天内容 -->
<scroll-view
scroll-y
class="chat-content"
:scroll-into-view="scrollToView"
scroll-with-animation
>
<!-- 聊天消息列表 -->
<view class="chat-messages">
<!-- 系统消息 -->
<view class="message-item system">
<text class="system-text">客服 小美 已接入,请描述您的问题</text>
</view>
<!-- 时间分割线 -->
<view class="time-divider">
<text>今天 14:30</text>
</view>
<!-- 消息项 -->
<view
v-for="message in messages"
:key="message.id"
:class="['message-item', message.type]"
:id="'msg-' + message.id"
>
<!-- 对方消息 -->
<view v-if="message.type === 'received'" class="message-wrapper">
<image
class="avatar"
src="https://picsum.photos/40/40?random=1"
mode="aspectFill"
/>
<view class="message-content-wrapper">
<text class="sender-name">客服小美</text>
<view class="message-bubble">
<text class="message-text">{{ message.content }}</text>
<text class="message-time">{{ message.time }}</text>
</view>
</view>
</view>
<!-- 我的消息 -->
<view v-else class="message-wrapper me">
<view class="message-content-wrapper">
<view class="message-bubble me">
<text class="message-text">{{ message.content }}</text>
<text class="message-time">{{ message.time }}</text>
</view>
</view>
<image
class="avatar me"
src="https://picsum.photos/40/40?random=2"
mode="aspectFill"
/>
</view>
</view>
</view>
</scroll-view>
<!-- 聊天输入区 -->
<view class="chat-input">
<view class="input-tools">
<text class="tool-icon" @click="showEmojiPicker">😊</text>
<text class="tool-icon" @click="showImagePicker">📷</text>
<text class="tool-icon" @click="showMoreTools"></text>
</view>
<view class="input-wrapper">
<input
class="message-input"
v-model="inputMessage"
placeholder="请输入消息..."
:focus="inputFocus"
@confirm="sendMessage"
confirm-type="send"
/>
<button
class="send-button"
:class="{ active: inputMessage.trim() }"
@click="sendMessage"
>
发送
</button>
</view>
</view>
<!-- 表情选择器 -->
<view v-if="showEmoji" class="emoji-picker">
<view class="emoji-category">
<text
v-for="emoji in emojiList"
:key="emoji"
class="emoji-item"
@click="insertEmoji(emoji)"
>
{{ emoji }}
</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, onMounted, nextTick } from 'vue'
// 响应式数据
const messages = ref<any[]>([])
const inputMessage = ref<string>('')
const inputFocus = ref<boolean>(false)
const showEmoji = ref<boolean>(false)
const scrollToView = ref<string>('')
// 模拟表情列表
const emojiList = ['😊', '😂', '🤣', '😍', '😘', '🥰', '😭', '😡', '👍', '👏', '🙏', '🎉', '❤️', '🔥', '⭐']
// Mock 聊天记录
const mockMessages = [
{
id: 1,
type: 'received',
content: '您好,欢迎咨询!有什么可以帮助您的吗?',
time: '14:30'
},
{
id: 2,
type: 'sent',
content: '你好,我昨天下的订单一直没有发货',
time: '14:31'
},
{
id: 3,
type: 'received',
content: '请问您的订单号是多少?我帮您查询一下',
time: '14:32'
},
{
id: 4,
type: 'sent',
content: '订单号是202311230001',
time: '14:33'
},
{
id: 5,
type: 'received',
content: '正在为您查询订单状态...',
time: '14:34'
}
]
// 生命周期
onMounted(() => {
loadChatHistory()
// 模拟客服自动回复
setTimeout(() => {
addReceivedMessage('查询到您的订单正在打包中,预计今天下午发货')
}, 3000)
})
// 加载聊天记录
const loadChatHistory = () => {
messages.value = [...mockMessages]
// 滚动到底部
setTimeout(() => {
scrollToBottom()
}, 100)
}
// 发送消息
const sendMessage = () => {
const content = inputMessage.value.trim()
if (!content) return
// 添加发送的消息
const newMessage = {
id: Date.now(),
type: 'sent',
content: content,
time: getCurrentTime()
}
messages.value.push(newMessage)
inputMessage.value = ''
// 滚动到底部
scrollToBottom()
// 模拟客服回复2秒后
setTimeout(() => {
simulateCustomerReply()
}, 2000)
}
// 模拟客服回复
const simulateCustomerReply = () => {
const replies = [
'好的,已为您记录',
'这个问题需要进一步核实',
'我明白了,马上为您处理',
'请稍等,正在为您查询',
'感谢您的反馈'
]
const randomReply = replies[Math.floor(Math.random() * replies.length)]
addReceivedMessage(randomReply)
}
// 添加接收的消息
const addReceivedMessage = (content: string) => {
const newMessage = {
id: Date.now(),
type: 'received',
content: content,
time: getCurrentTime()
}
messages.value.push(newMessage)
scrollToBottom()
}
// 滚动到底部
const scrollToBottom = () => {
nextTick(() => {
if (messages.value.length > 0) {
const lastMsgId = messages.value[messages.value.length - 1].id
scrollToView.value = 'msg-' + lastMsgId
}
})
}
// 获取当前时间
const getCurrentTime = (): string => {
const now = new Date()
const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
return `${hours}:${minutes}`
}
// 插入表情
const insertEmoji = (emoji: string) => {
inputMessage.value += emoji
inputFocus.value = true
}
// 显示表情选择器
const showEmojiPicker = () => {
showEmoji.value = !showEmoji.value
}
// 显示图片选择器
const showImagePicker = () => {
uni.chooseImage({
count: 1,
success: (res) => {
console.log('选择图片:', res.tempFilePaths)
// 这里可以处理图片上传
}
})
}
// 显示更多工具
const showMoreTools = () => {
uni.showActionSheet({
itemList: ['发送位置', '发送文件', '发送语音'],
success: (res) => {
console.log('选择工具:', res.tapIndex)
}
})
}
// 显示更多操作
const showMoreActions = () => {
uni.showActionSheet({
itemList: ['投诉客服', '结束对话', '清除记录'],
success: (res) => {
switch (res.tapIndex) {
case 0:
uni.navigateTo({ url: '/pages/mall/consumer/complaint' })
break
case 1:
uni.showModal({
title: '确认结束',
content: '确定要结束本次对话吗?',
success: (res) => {
if (res.confirm) {
uni.navigateBack()
}
}
})
break
case 2:
uni.showModal({
title: '确认清除',
content: '确定要清除聊天记录吗?',
success: (res) => {
if (res.confirm) {
messages.value = []
}
}
})
break
}
}
})
}
// 返回
const goBack = () => {
uni.navigateBack()
}
</script>
<style>
.chat-page {
width: 100%;
height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
/* 聊天头部 */
.chat-header {
background-color: white;
padding: 10px 15px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #eee;
}
.header-back {
width: 40px;
}
.back-icon {
font-size: 24px;
color: #333;
}
.header-info {
flex: 1;
text-align: center;
}
.chat-title {
font-size: 16px;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 2px;
}
.chat-status {
font-size: 12px;
color: #34c759;
}
.header-actions .action-icon {
font-size: 20px;
color: #333;
width: 40px;
text-align: right;
}
/* 聊天内容区 */
.chat-content {
flex: 1;
padding: 15px;
padding-bottom: 70px; /* 为输入区留出空间 */
}
/* 系统消息 */
.message-item.system {
text-align: center;
margin-bottom: 20px;
}
.system-text {
font-size: 12px;
color: #999;
background-color: #f0f0f0;
padding: 5px 15px;
border-radius: 15px;
display: inline-block;
}
/* 时间分割线 */
.time-divider {
text-align: center;
margin: 20px 0;
}
.time-divider text {
font-size: 12px;
color: #999;
background-color: #f0f0f0;
padding: 3px 10px;
border-radius: 10px;
}
/* 消息项 */
.message-wrapper {
display: flex;
margin-bottom: 15px;
}
.message-wrapper.me {
justify-content: flex-end;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 20px;
margin-right: 10px;
flex-shrink: 0;
}
.avatar.me {
margin-right: 0;
margin-left: 10px;
order: 2;
}
.message-content-wrapper {
max-width: 70%;
}
.sender-name {
font-size: 12px;
color: #999;
margin-bottom: 5px;
display: block;
}
.message-bubble {
background-color: white;
padding: 10px 15px;
border-radius: 18px;
position: relative;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
max-width: 100%;
word-wrap: break-word;
}
.message-bubble.me {
background-color: #95ec69;
border-bottom-right-radius: 4px;
}
.message-bubble:not(.me) {
border-bottom-left-radius: 4px;
}
.message-text {
font-size: 15px;
color: #333;
line-height: 1.4;
display: block;
margin-bottom: 5px;
}
.message-time {
font-size: 11px;
color: #999;
display: block;
text-align: right;
}
/* 聊天输入区 */
.chat-input {
background-color: white;
border-top: 1px solid #eee;
padding: 10px 15px;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
}
.input-tools {
display: flex;
margin-bottom: 10px;
}
.tool-icon {
font-size: 20px;
margin-right: 15px;
color: #666;
}
.input-wrapper {
display: flex;
align-items: center;
}
.message-input {
flex: 1;
background-color: #f5f5f5;
border-radius: 20px;
padding: 10px 15px;
font-size: 15px;
margin-right: 10px;
min-height: 40px;
max-height: 100px;
}
.send-button {
background-color: #ccc;
color: white;
border: none;
border-radius: 20px;
padding: 8px 20px;
font-size: 14px;
min-width: 60px;
transition: all 0.3s ease;
}
.send-button.active {
background-color: #ff5000;
}
/* 表情选择器 */
.emoji-picker {
background-color: white;
border-top: 1px solid #eee;
padding: 10px;
max-height: 200px;
overflow-y: auto;
position: fixed;
bottom: 60px;
left: 0;
right: 0;
z-index: 99;
}
.emoji-category {
display: flex;
flex-wrap: wrap;
}
.emoji-item {
font-size: 24px;
padding: 8px;
width: 45px;
height: 45px;
display: flex;
align-items: center;
justify-content: center;
}
/* 响应式适配 */
@media screen and (max-width: 320px) {
.message-content-wrapper {
max-width: 65%;
}
.message-text {
font-size: 14px;
}
}
@media screen and (min-width: 415px) {
.message-content-wrapper {
max-width: 75%;
}
.chat-input {
padding: 15px 20px;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,149 @@
<template>
<view class="coupons-page">
<view class="coupon-list">
<view v-if="coupons.length === 0" class="empty-state">
<text class="empty-icon">🎫</text>
<text class="empty-text">暂无优惠券</text>
</view>
<view v-else v-for="(coupon, index) in coupons" :key="index" class="coupon-item">
<view class="coupon-left">
<text class="coupon-amount">{{ coupon.amount }}</text>
<text class="coupon-type">优惠券</text>
</view>
<view class="coupon-right">
<text class="coupon-title">{{ coupon.title }}</text>
<text class="coupon-expiry">有效期至: {{ coupon.expiry }}</text>
<button class="use-btn" @click="useCoupon(coupon)">去使用</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
type Coupon = {
title: string
amount: string
expiry: string
id: string
}
const coupons = ref<Coupon[]>([])
onMounted(() => {
loadCoupons()
})
const loadCoupons = () => {
// 从本地存储获取已领取的优惠券详情
// 假设存储格式为 JSON 字符串数组
const storedCoupons = uni.getStorageSync('myCoupons')
if (storedCoupons) {
try {
coupons.value = JSON.parse(storedCoupons as string) as Coupon[]
} catch (e) {
console.error('Failed to parse coupons', e)
coupons.value = []
}
} else {
// 默认空或者是mock一些基础数据如果需要
coupons.value = []
}
}
const useCoupon = (coupon: Coupon) => {
uni.switchTab({
url: '/pages/mall/consumer/index'
})
}
</script>
<style>
.coupons-page {
padding: 15px;
background-color: #f5f5f5;
min-height: 100vh;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 100px;
}
.empty-icon {
font-size: 60px;
margin-bottom: 20px;
}
.empty-text {
font-size: 16px;
color: #999;
}
.coupon-item {
display: flex;
background-color: white;
border-radius: 8px;
margin-bottom: 15px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.coupon-left {
width: 100px;
background: linear-gradient(135deg, #FF9800, #FF5722);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
padding: 15px;
}
.coupon-amount {
font-size: 24px;
font-weight: bold;
}
.coupon-type {
font-size: 12px;
margin-top: 5px;
}
.coupon-right {
flex: 1;
padding: 15px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.coupon-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 8px;
}
.coupon-expiry {
font-size: 12px;
color: #999;
margin-bottom: 10px;
}
.use-btn {
align-self: flex-end;
font-size: 12px;
background-color: #FF5722;
color: white;
padding: 4px 12px;
border-radius: 15px;
line-height: 1.5;
}
</style>

View File

@@ -0,0 +1,269 @@
<template>
<view class="favorites-page">
<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.image" class="product-image" mode="aspectFill" />
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<text class="product-price">¥{{ product.price }}</text>
<view class="product-footer">
<text class="product-sales">已售 {{ product.sales }}</text>
<view class="action-btns">
<view class="cart-btn" @click.stop="addToCart(product)">
<text class="cart-icon">🛒</text>
</view>
<view class="remove-btn" @click.stop="removeFavorite(product.id)">
<text class="remove-icon">🗑️</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
type Product = {
id: string
name: string
price: number
image: string
sales: number
shopId?: string
shopName?: string
}
const favorites = ref<Product[]>([])
onMounted(() => {
loadFavorites()
})
const addToCart = (product: Product) => {
// 获取现有购物车数据
const cartData = uni.getStorageSync('cart')
let cartItems: any[] = []
if (cartData) {
try {
cartItems = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
}
}
// 检查商品是否已存在
const existingItem = cartItems.find((item: any) => item.id === product.id)
if (existingItem) {
existingItem.quantity++
} else {
// 添加新商品
cartItems.push({
id: product.id,
shopId: product.shopId || 'shop_favorite_default',
shopName: product.shopName || '收藏店铺',
name: product.name,
price: product.price,
image: product.image,
spec: '默认规格',
quantity: 1,
selected: true
})
}
// 保存回存储
uni.setStorageSync('cart', JSON.stringify(cartItems))
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
}
const loadFavorites = () => {
// 从本地存储获取收藏列表
const storedFavorites = uni.getStorageSync('favorites')
if (storedFavorites) {
try {
favorites.value = JSON.parse(storedFavorites as string) as Product[]
} catch (e) {
console.error('Failed to parse favorites', e)
favorites.value = []
}
} else {
favorites.value = []
}
}
const goShopping = () => {
uni.switchTab({
url: '/pages/mall/consumer/index'
})
}
const goToDetail = (product: Product) => {
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${product.id}&price=${product.price}&originalPrice=${product.original_price || ''}`
})
}
const removeFavorite = (id: string) => {
uni.showModal({
title: '取消收藏',
content: '确定要取消收藏该商品吗?',
success: (res) => {
if (res.confirm) {
const index = favorites.value.findIndex(item => item.id === id)
if (index !== -1) {
favorites.value.splice(index, 1)
uni.setStorageSync('favorites', JSON.stringify(favorites.value))
uni.showToast({
title: '已取消收藏',
icon: 'none'
})
}
}
}
})
}
</script>
<style>
.favorites-page {
padding: 15px;
background-color: #f5f5f5;
min-height: 100vh;
}
.product-grid {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.empty-state {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 100px;
}
.empty-icon {
font-size: 60px;
margin-bottom: 20px;
color: #ddd;
}
.empty-text {
font-size: 16px;
color: #999;
margin-bottom: 20px;
}
.go-shopping-btn {
background-color: #ff5000;
color: white;
padding: 8px 24px;
border-radius: 20px;
font-size: 14px;
border: none;
}
.product-item {
width: calc(50% - 8px);
background-color: white;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.product-image {
width: 100%;
height: 170px;
background-color: #f5f5f5;
}
.product-info {
padding: 10px;
display: flex;
flex-direction: column;
flex: 1;
}
.product-name {
font-size: 14px;
color: #333;
margin-bottom: 6px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
height: 40px;
line-height: 20px;
}
.product-price {
font-size: 16px;
color: #ff5000;
font-weight: bold;
margin-bottom: 6px;
}
.product-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.product-sales {
font-size: 12px;
color: #999;
}
.action-btns {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.cart-btn, .remove-btn {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.cart-btn {
background-color: #ff5000;
}
.cart-icon {
font-size: 14px;
color: white;
}
.remove-btn {
background-color: #f0f0f0;
}
.remove-icon {
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,580 @@
<!-- 足迹页面 -->
<template>
<view class="footprint-page">
<!-- 顶部栏 -->
<view class="footprint-header">
<view class="header-title">
<text class="title-text">我的足迹</text>
</view>
<view v-if="footprints.length > 0" class="header-actions">
<text class="action-btn" @click="toggleEditMode">{{ isEditMode ? '完成' : '编辑' }}</text>
<text class="action-btn" @click="clearAll">清空</text>
</view>
</view>
<!-- 日期分组 -->
<scroll-view class="footprint-content" scroll-y @scrolltolower="loadMore">
<!-- 空状态 -->
<view v-if="footprints.length === 0 && !isLoading" class="empty-footprints">
<text class="empty-icon">👣</text>
<text class="empty-text">暂无浏览记录</text>
<text class="empty-subtext">快去浏览喜欢的商品吧</text>
<button class="go-shopping-btn" @click="goShopping">去逛逛</button>
</view>
<!-- 按日期分组 -->
<view v-for="(group, date) in groupedFootprints" :key="date" class="date-group">
<view class="group-header">
<text class="group-date">{{ formatGroupDate(date) }}</text>
<text v-if="isEditMode" class="group-select" @click="toggleGroupSelect(date)">
{{ isGroupSelected(date) ? '取消全选' : '全选' }}
</text>
</view>
<view class="group-items">
<view v-for="item in group" :key="item.id" class="footprint-item">
<view v-if="isEditMode" class="item-selector" @click="toggleSelect(item)">
<view :class="['select-icon', { selected: item.selected }]">
<text v-if="item.selected" class="icon-text">✓</text>
</view>
</view>
<view class="item-content" @click="viewProduct(item)">
<image class="product-image" :src="item.image" />
<view class="product-info">
<text class="product-name">{{ item.name }}</text>
<view class="product-price-row">
<text class="current-price">¥{{ item.price }}</text>
<text v-if="item.original_price && item.original_price > item.price"
class="original-price">¥{{ item.original_price }}</text>
</view>
<view class="product-meta">
<text class="sales-text">已售{{ item.sales }}</text>
<text class="time-text">{{ formatTime(item.viewTime) }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="isLoading" class="loading-more">
<text class="loading-text">加载中...</text>
</view>
<view v-if="!hasMore && footprints.length > 0" class="no-more">
<text class="no-more-text">没有更多了</text>
</view>
</scroll-view>
<!-- 编辑操作栏 -->
<view v-if="isEditMode && footprints.length > 0" class="edit-bar">
<view class="select-all" @click="toggleSelectAll">
<view :class="['all-select-icon', { selected: isAllSelected }]">
<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, computed } from 'vue'
type FootprintType = {
id: string
name: string
price: number
original_price?: number
image: string
sales: number
shopId: string
shopName: string
viewTime: number
selected?: boolean
}
const footprints = ref<Array<FootprintType>>([])
const isEditMode = ref<boolean>(false)
const isLoading = ref<boolean>(false)
const hasMore = ref<boolean>(false)
// 计算属性
const selectedCount = computed(() => {
return footprints.value.filter(item => item.selected).length
})
const isAllSelected = computed(() => {
return footprints.value.length > 0 && footprints.value.every(item => item.selected)
})
const groupedFootprints = computed(() => {
const groups: Record<string, FootprintType[]> = {}
footprints.value.forEach(item => {
const date = new Date(item.viewTime).toDateString()
if (!groups[date]) {
groups[date] = []
}
groups[date].push(item)
})
return groups
})
// 生命周期
onMounted(() => {
loadFootprints()
})
// 加载足迹数据
const loadFootprints = (loadMore: boolean = false) => {
isLoading.value = true
// 从本地存储获取足迹数据
const storedFootprints = uni.getStorageSync('footprints')
if (storedFootprints) {
try {
const data = JSON.parse(storedFootprints as string) as any[]
footprints.value = data.map(item => ({
...item,
selected: false
}))
} catch (e) {
console.error('Failed to parse footprints', e)
footprints.value = []
}
} else {
footprints.value = []
}
isLoading.value = false
hasMore.value = false // 本地存储一次性加载完
}
// 格式化日期分组
const formatGroupDate = (dateStr: string): string => {
const date = new Date(dateStr)
const today = new Date()
const yesterday = new Date(today)
yesterday.setDate(yesterday.getDate() - 1)
if (date.toDateString() === today.toDateString()) {
return '今天'
} else if (date.toDateString() === yesterday.toDateString()) {
return '昨天'
} else {
const month = date.getMonth() + 1
const day = date.getDate()
return `${month}月${day}日`
}
}
// 格式化时间
const formatTime = (timestamp: number): string => {
const date = new Date(timestamp)
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
return `${hours}:${minutes}`
}
// 切换编辑模式
const toggleEditMode = () => {
isEditMode.value = !isEditMode.value
// 重置选择状态
footprints.value.forEach(item => {
item.selected = false
})
}
// 清空所有足迹
const clearAll = () => {
if (footprints.value.length === 0) return
uni.showModal({
title: '清空足迹',
content: '确定要清空所有浏览记录吗?',
success: (res) => {
if (res.confirm) {
footprints.value = []
uni.removeStorageSync('footprints')
uni.showToast({
title: '已清空',
icon: 'success'
})
}
}
})
}
// 切换选择状态
const toggleSelect = (item: FootprintType) => {
item.selected = !item.selected
footprints.value = [...footprints.value]
}
// 切换分组全选
const toggleGroupSelect = (dateStr: string) => {
const group = groupedFootprints.value[dateStr]
if (!group) return
const isAllSelected = group.every(item => item.selected)
const newSelectedState = !isAllSelected
group.forEach(item => {
item.selected = newSelectedState
})
footprints.value = [...footprints.value]
}
// 检查组是否全选
const isGroupSelected = (dateStr: string): boolean => {
const group = groupedFootprints.value[dateStr]
if (!group || group.length === 0) return false
return group.every(item => item.selected)
}
// 全选/取消全选
const toggleSelectAll = () => {
const newSelectedState = !isAllSelected.value
footprints.value.forEach(item => {
item.selected = newSelectedState
})
footprints.value = [...footprints.value]
}
// 删除选中项
const deleteSelected = () => {
const selectedItems = footprints.value.filter(item => item.selected)
if (selectedItems.length === 0) {
uni.showToast({
title: '请选择要删除的记录',
icon: 'none'
})
return
}
uni.showModal({
title: '确认删除',
content: `确定要删除选中的${selectedItems.length}条记录吗?`,
success: (res) => {
if (res.confirm) {
// 从列表中移除
footprints.value = footprints.value.filter(item => !item.selected)
// 保存回本地存储
const dataToSave = footprints.value.map(item => {
const { selected, ...rest } = item
return rest
})
uni.setStorageSync('footprints', JSON.stringify(dataToSave))
uni.showToast({
title: '删除成功',
icon: 'success'
})
// 如果删完了,退出编辑模式
if (footprints.value.length === 0) {
isEditMode.value = false
}
}
}
})
}
// 查看商品
const viewProduct = (item: FootprintType) => {
if (isEditMode.value) return
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${item.id}&price=${item.price}&originalPrice=${item.original_price || ''}`
})
}
// 加载更多
const loadMore = () => {
// 本地存储模式下暂不需要加载更多逻辑
}
// 去逛逛
const goShopping = () => {
uni.switchTab({
url: '/pages/mall/consumer/index'
})
}
</script>
<style scoped>
.footprint-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.footprint-header {
background-color: #ffffff;
padding: 15px;
border-bottom: 1px solid #e5e5e5;
display: flex;
align-items: center;
justify-content: space-between;
}
.header-title {
flex: 1;
}
.title-text {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.header-actions {
display: flex;
gap: 20px;
}
.action-btn {
color: #007aff;
font-size: 14px;
padding: 5px;
}
.footprint-content {
flex: 1;
}
.empty-footprints {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
background-color: #ffffff;
}
.empty-icon {
font-size: 80px;
margin-bottom: 20px;
}
.empty-text {
font-size: 16px;
color: #666666;
margin-bottom: 10px;
}
.empty-subtext {
font-size: 14px;
color: #999999;
margin-bottom: 30px;
}
.go-shopping-btn {
background-color: #007aff;
color: #ffffff;
padding: 10px 40px;
border-radius: 25px;
font-size: 14px;
border: none;
}
.date-group {
background-color: #ffffff;
margin-bottom: 10px;
padding: 0 15px;
}
.group-header {
padding: 15px 0;
border-bottom: 1px solid #f5f5f5;
display: flex;
align-items: center;
justify-content: space-between;
}
.group-date {
font-size: 16px;
font-weight: bold;
color: #333333;
}
.group-select {
color: #007aff;
font-size: 14px;
}
.group-items {
padding: 10px 0;
}
.footprint-item {
display: flex;
padding: 15px 0;
border-bottom: 1px solid #f5f5f5;
}
.footprint-item:last-child {
border-bottom: none;
}
.item-selector {
width: 50px;
display: flex;
align-items: center;
justify-content: center;
}
.select-icon {
width: 20px;
height: 20px;
border: 1px solid #cccccc;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.select-icon.selected {
background-color: #007aff;
border-color: #007aff;
}
.icon-text {
color: #ffffff;
font-size: 12px;
}
.item-content {
flex: 1;
display: flex;
}
.product-image {
width: 80px;
height: 80px;
border-radius: 5px;
margin-right: 15px;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.product-name {
font-size: 14px;
color: #333333;
line-height: 1.4;
margin-bottom: 10px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.product-price-row {
display: flex;
align-items: baseline;
margin-bottom: 10px;
}
.current-price {
font-size: 16px;
color: #ff4757;
font-weight: bold;
margin-right: 10px;
}
.original-price {
font-size: 12px;
color: #999999;
text-decoration: line-through;
}
.product-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.sales-text {
font-size: 12px;
color: #999999;
}
.time-text {
font-size: 12px;
color: #666666;
}
.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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,204 @@
<template>
<view class="logistics-page">
<view class="logistics-header">
<view class="product-info">
<image class="product-image" :src="productImage" mode="aspectFill"></image>
<view class="info-right">
<text class="status-text">{{ logisticsStatus }}</text>
<text class="courier-name">{{ courierName }}: {{ trackingNo }}</text>
<text class="phone-text">官方电话: {{ courierPhone }}</text>
</view>
</view>
</view>
<view class="logistics-body">
<view class="track-list">
<view
v-for="(item, index) in trackList"
:key="index"
class="track-item"
:class="{ first: index === 0 }"
>
<view class="node-icon">
<view class="dot"></view>
<view class="line" v-if="index !== trackList.length - 1"></view>
</view>
<view class="node-content">
<text class="track-desc">{{ item.desc }}</text>
<text class="track-time">{{ item.time }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
const orderId = ref('')
const productImage = ref('/static/product1.jpg')
const logisticsStatus = ref('运输中')
const courierName = ref('顺丰速运')
const courierPhone = ref('95338')
const trackingNo = ref('SF1234567890')
type TrackItem = {
desc: string
time: string
}
const trackList = ref<TrackItem[]>([
{
desc: '【深圳市】快件已到达 深圳南山集散中心',
time: '2024-01-26 14:30:00'
},
{
desc: '【广州市】快件已从 广州转运中心 发出,准备发往 深圳南山集散中心',
time: '2024-01-26 09:20:00'
},
{
desc: '【广州市】快件已到达 广州转运中心',
time: '2024-01-25 22:15:00'
},
{
desc: '【杭州市】商家已发货',
time: '2024-01-25 18:00:00'
}
])
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options as any
if (options.orderId) {
orderId.value = options.orderId
// 这里可以根据orderId去请求真实的物流信息
}
})
</script>
<style scoped>
.logistics-page {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 20px;
}
.logistics-header {
background-color: #fff;
padding: 15px;
margin-bottom: 10px;
}
.product-info {
display: flex;
align-items: center;
}
.product-image {
width: 60px;
height: 60px;
border-radius: 4px;
margin-right: 15px;
background-color: #eee;
}
.info-right {
display: flex;
flex-direction: column;
}
.status-text {
font-size: 16px;
color: #ff5000;
font-weight: bold;
margin-bottom: 5px;
}
.courier-name {
font-size: 14px;
color: #333;
margin-bottom: 2px;
}
.phone-text {
font-size: 12px;
color: #999;
}
.logistics-body {
background-color: #fff;
padding: 20px 15px;
}
.track-list {
display: flex;
flex-direction: column;
}
.track-item {
display: flex;
position: relative;
padding-bottom: 25px;
}
.track-item:last-child {
padding-bottom: 0;
}
.node-icon {
width: 20px;
display: flex;
flex-direction: column;
align-items: center;
margin-right: 15px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #ccc;
margin-top: 6px;
z-index: 1;
}
.first .dot {
background-color: #ff5000;
width: 12px;
height: 12px;
margin-top: 4px;
box-shadow: 0 0 0 4px rgba(255, 80, 0, 0.2);
}
.line {
width: 1px;
background-color: #eee;
flex: 1;
margin-top: 2px;
}
.node-content {
flex: 1;
display: flex;
flex-direction: column;
}
.track-desc {
font-size: 14px;
color: #333;
line-height: 1.5;
margin-bottom: 5px;
}
.first .track-desc {
color: #ff5000;
font-weight: bold;
}
.track-time {
font-size: 12px;
color: #999;
}
</style>

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -91,6 +91,7 @@
<button v-if="order.status === 2" class="remind-btn" @click="remindDelivery">提醒发货</button>
<button v-if="order.status === 3" class="confirm-btn" @click="confirmReceive">确认收货</button>
<button v-if="order.status === 4" class="review-btn" @click="goToReview">评价商品</button>
<button v-if="order.status >= 2 && order.status <= 4" class="refund-btn" @click="applyRefund">申请售后</button>
<button v-if="order.status <= 2" class="cancel-btn" @click="cancelOrder">取消订单</button>
<button class="service-btn" @click="contactService">联系客服</button>
</view>
@@ -141,82 +142,83 @@ export default {
}
},
onLoad(options: any) {
const orderId = options.orderId as string
const orderId = options.id || options.orderId as string
if (orderId) {
this.loadOrderDetail(orderId)
}
},
methods: {
loadOrderDetail(orderId: string) {
// 模拟加载订单详情数据
this.order = {
id: orderId,
order_no: 'ORD202401150001',
user_id: 'user_001',
merchant_id: 'merchant_001',
status: 3, // 1:待支付 2:待发货 3:待收货 4:已完成 5:已取消
total_amount: 299.98,
discount_amount: 30.00,
delivery_fee: 8.00,
actual_amount: 277.98,
payment_method: 1, // 1:微信支付 2:支付宝 3:余额
payment_status: 1,
delivery_address: {
name: '张三',
phone: '13800138000',
detail: '北京市朝阳区某某街道某某小区1号楼101室'
},
created_at: '2024-01-15 14:30:00'
// 尝试从本地存储加载订单
const ordersStr = uni.getStorageSync('orders')
let localOrder: any = null
if (ordersStr) {
const orders = JSON.parse(ordersStr as string) as any[]
localOrder = orders.find((o: any) => o.id === orderId)
}
this.orderItems = [
{
id: 'item_001',
order_id: orderId,
product_id: 'product_001',
sku_id: 'sku_001',
product_name: '精选好物商品',
sku_specifications: { color: '红色', size: 'M' },
price: 199.99,
quantity: 1,
total_amount: 199.99,
product_image: '/static/product1.jpg'
},
{
id: 'item_002',
order_id: orderId,
product_id: 'product_002',
sku_id: 'sku_002',
product_name: '优质配件',
sku_specifications: { type: '标准版' },
price: 99.99,
quantity: 1,
total_amount: 99.99,
product_image: '/static/product2.jpg'
}
]
this.merchant = {
id: 'merchant_001',
user_id: 'user_001',
shop_name: '优质好店',
shop_logo: '/static/shop-logo.png',
shop_banner: '/static/shop-banner.png',
shop_description: '专注品质生活',
contact_name: '店主小王',
contact_phone: '13800138000',
shop_status: 1,
rating: 4.8,
total_sales: 15680,
created_at: '2023-06-01'
}
if (this.order.status >= 3) {
this.deliveryInfo = {
courier_name: '李师傅',
courier_phone: '13900139000',
tracking_no: 'YT123456789'
}
if (localOrder) {
// 使用本地存储的数据
this.order = localOrder
// 处理商品项
if (localOrder.items) {
this.orderItems = localOrder.items.map((item: any) => {
return {
...item,
product_image: item.product_image || item.image || '/static/default-product.png',
product_name: item.product_name || item.name,
sku_specifications: item.sku_specifications || item.specifications
}
})
}
// 处理商家信息(模拟,因为本地订单可能没有完整的商家信息)
this.merchant = {
id: localOrder.merchant_id || 'merchant_001',
user_id: 'user_001',
shop_name: localOrder.shopName || '优质好店',
shop_logo: '/static/shop-logo.png',
shop_banner: '/static/shop-banner.png',
shop_description: '专注品质生活',
contact_name: '店主小王',
contact_phone: '13800138000',
shop_status: 1,
rating: 4.8,
total_sales: 15680,
created_at: '2023-06-01'
}
if (this.order.status >= 3) {
this.deliveryInfo = {
courier_name: '李师傅',
courier_phone: '13900139000',
tracking_no: 'YT123456789'
}
}
} else {
// 回退到模拟数据
this.order = {
id: orderId,
order_no: 'ORD202401150001',
user_id: 'user_001',
merchant_id: 'merchant_001',
status: 3, // 1:待支付 2:待发货 3:待收货 4:已完成 5:已取消
total_amount: 299.98,
discount_amount: 30.00,
delivery_fee: 8.00,
actual_amount: 277.98,
payment_method: 1, // 1:微信支付 2:支付宝 3:余额
payment_status: 1,
delivery_address: {
name: '张三',
phone: '13800138000',
detail: '北京市朝阳区某某街道某某小区1号楼101室'
},
created_at: '2024-01-15 14:30:00'
}
// ... (原有模拟数据逻辑保留)
}
},
@@ -310,6 +312,12 @@ export default {
})
},
applyRefund() {
uni.navigateTo({
url: `/pages/mall/consumer/apply-refund?orderId=${this.order.id}`
})
},
cancelOrder() {
uni.showModal({
title: '取消订单',
@@ -578,6 +586,12 @@ export default {
color: #fff;
}
.refund-btn {
background-color: #ffffff;
color: #666;
border: 1px solid #ccc;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666;

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,190 @@
<template>
<view class="payment-success-page">
<view class="success-content">
<view class="icon-wrapper">
<text class="success-icon">✓</text>
</view>
<text class="success-title">支付成功</text>
<text class="success-desc">您的订单已支付成功,我们将尽快为您发货</text>
<view class="order-info" v-if="orderId">
<text class="info-text">订单编号:{{ orderNo }}</text>
<text class="info-text">支付金额:¥{{ amount.toFixed(2) }}</text>
</view>
<view class="action-buttons">
<button class="btn primary-btn" @click="viewOrder">查看订单</button>
<button class="btn secondary-btn" @click="goHome">返回首页</button>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
// import supa from '@/components/supadb/aksupainstance.uts' // 暂时注释掉数据库连接
const orderId = ref('')
const orderNo = ref('')
const amount = ref(0)
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options as any
if (options.orderId) {
orderId.value = options.orderId
orderNo.value = options.orderId // 使用订单ID作为订单号
// 优先使用传递的 amount
if (options.amount) {
amount.value = parseFloat(options.amount)
} else {
// 如果没有传 amount尝试从本地存储查找订单
try {
const ordersStr = uni.getStorageSync('orders')
if (ordersStr) {
const orders = JSON.parse(ordersStr as string) as any[]
const order = orders.find((o: any) => o.id === orderId.value)
if (order) {
amount.value = order.actual_amount || order.total_amount || 0
}
}
} catch (e) {
console.error('读取本地订单失败', e)
}
}
// loadOrderInfo() // 暂时注释掉数据库查询
}
})
// const loadOrderInfo = async () => {
// try {
// const { data, error } = await supa
// .from('orders')
// .select('order_no, actual_amount')
// .eq('id', orderId.value)
// .single()
//
// if (error == null && data != null) {
// orderNo.value = data['order_no'] as string
// amount.value = data['actual_amount'] as number
// }
// } catch (err) {
// console.error('加载订单信息失败', err)
// }
// }
const viewOrder = () => {
uni.navigateTo({
url: '/pages/mall/consumer/orders'
})
}
const goHome = () => {
uni.switchTab({
url: '/pages/mall/consumer/index'
})
}
</script>
<style scoped>
.payment-success-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
background-color: #ffffff;
padding: 0 30px;
}
.success-content {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.icon-wrapper {
width: 80px;
height: 80px;
border-radius: 40px;
background-color: #4cd964;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
box-shadow: 0 4px 10px rgba(76, 217, 100, 0.3);
}
.success-icon {
font-size: 40px;
color: #ffffff;
font-weight: bold;
}
.success-title {
font-size: 24px;
font-weight: bold;
color: #333333;
margin-bottom: 10px;
}
.success-desc {
font-size: 14px;
color: #999999;
text-align: center;
margin-bottom: 30px;
line-height: 1.5;
}
.order-info {
background-color: #f9f9f9;
padding: 15px 20px;
border-radius: 8px;
width: 100%;
margin-bottom: 30px;
display: flex;
flex-direction: column;
align-items: center;
}
.info-text {
font-size: 14px;
color: #666666;
margin-bottom: 5px;
}
.action-buttons {
width: 100%;
display: flex;
flex-direction: column;
gap: 15px;
}
.btn {
width: 100%;
height: 45px;
border-radius: 22.5px;
font-size: 16px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
}
.primary-btn {
background-color: #007aff;
color: #ffffff;
border: none;
}
.secondary-btn {
background-color: #ffffff;
color: #666666;
border: 1px solid #cccccc;
}
</style>

View File

@@ -0,0 +1,988 @@
<!-- 支付页面 -->
<template>
<view class="payment-page">
<view class="payment-content">
<!-- 价格明细 -->
<view class="price-detail-section">
<text class="section-title">价格明细</text>
<view class="price-detail">
<view class="price-row">
<text class="price-label">商品总价</text>
<text class="price-value">¥{{ productAmount.toFixed(2) }}</text>
</view>
<view class="price-row">
<text class="price-label">运费</text>
<text class="price-value">+¥{{ deliveryFee.toFixed(2) }}</text>
</view>
<view v-if="discountAmount > 0" class="price-row">
<text class="price-label">优惠减免</text>
<text class="price-value discount">-¥{{ discountAmount.toFixed(2) }}</text>
</view>
<view class="price-row total">
<text class="price-label">应付金额</text>
<text class="price-value total-price">¥{{ amount.toFixed(2) }}</text>
</view>
</view>
<text class="order-no">订单号: {{ orderNo }}</text>
</view>
<!-- 支付方式 -->
<view class="methods-section">
<text class="section-title">选择支付方式</text>
<view class="method-list">
<view v-for="method in paymentMethods"
:key="method.id"
:class="['method-item', { selected: selectedMethod === method.id }]"
@click="selectMethod(method)">
<view class="method-left">
<text class="method-icon">{{ getMethodIcon(method.id) }}</text>
<view class="method-info">
<text class="method-name">{{ method.name }}</text>
<text class="method-desc">{{ method.description }}</text>
</view>
</view>
<view v-if="selectedMethod === method.id" class="method-selected">
<text class="selected-icon">✓</text>
</view>
</view>
</view>
</view>
<!-- 余额支付 -->
<view v-if="selectedMethod === 'balance' && userBalance > 0" class="balance-section">
<view class="balance-info">
<text class="balance-label">账户余额</text>
<text class="balance-value">¥{{ userBalance.toFixed(2) }}</text>
</view>
<view v-if="userBalance < amount" class="balance-tip">
<text class="tip-text">余额不足,请选择其他支付方式</text>
</view>
</view>
<!-- 密码输入 -->
<view v-if="showPassword" class="password-section">
<text class="password-title">请输入支付密码</text>
<view class="password-input">
<view v-for="(_, index) in 6"
:key="index"
class="password-dot">
<text v-if="password.length > index">●</text>
</view>
</view>
<text class="forgot-password" @click="forgotPassword">忘记密码?</text>
</view>
</view>
<!-- 底部支付按钮 -->
<view class="payment-bottom">
<view class="price-summary">
<text class="summary-label">需支付:</text>
<text class="summary-price">¥{{ amount.toFixed(2) }}</text>
</view>
<button class="pay-btn"
:class="{ disabled: isPaying || (selectedMethod === 'balance' && userBalance < amount) }"
@click="confirmPayment">
<text v-if="!isPaying" class="pay-text">{{ getPayButtonText() }}</text>
<text v-else class="pay-text">支付中...</text>
</button>
</view>
<!-- 密码键盘 -->
<view v-if="showPassword" class="password-keyboard">
<view class="keyboard-grid">
<view v-for="num in 9"
:key="num"
class="keyboard-key"
@click="inputPassword(num.toString())">
<text class="key-text">{{ num }}</text>
</view>
<view class="keyboard-key"></view>
<view class="keyboard-key" @click="inputPassword('0')">
<text class="key-text">0</text>
</view>
<view class="keyboard-key" @click="deletePassword">
<text class="key-text">⌫</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, watch, computed, onUnmounted } from 'vue'
import { onLoad, onBackPress } from '@dcloudio/uni-app'
// import { supabase as supa } from '@/components/supadb/aksupainstance.uts'
type PaymentMethodType = {
id: string
name: string
description: string
icon: string
enabled: boolean
}
const orderId = ref<string>('')
const orderNo = ref<string>('')
const amount = ref<number>(0)
const paymentMethods = ref<Array<PaymentMethodType>>([])
const selectedMethod = ref<string>('wechat')
const userBalance = ref<number>(0)
const isPaying = ref<boolean>(false)
const showPassword = ref<boolean>(false)
const password = ref<string>('')
// 价格相关变量
const productAmount = ref<number>(0) // 商品总价
const deliveryFee = ref<number>(0) // 运费
const discountAmount = ref<number>(0) // 优惠减免
// 生命周期
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options as any
if (options.orderId) {
orderId.value = options.orderId
loadOrderInfo()
}
if (options.amount) {
amount.value = parseFloat(options.amount)
}
// 获取传递的价格详情
if (options.productAmount) {
productAmount.value = parseFloat(options.productAmount)
}
if (options.deliveryFee) {
deliveryFee.value = parseFloat(options.deliveryFee)
}
if (options.discountAmount) {
discountAmount.value = parseFloat(options.discountAmount)
}
// 如果没有传详情,尝试根据总价估算(兼容旧逻辑,但优先使用传参)
if (!options.productAmount && amount.value > 0) {
calculatePriceDetails(amount.value)
}
loadPaymentMethods()
loadUserBalance()
})
// 监听返回操作(包含系统返回键和导航栏返回按钮)
onBackPress((options) => {
// 如果是通过代码主动调用 navigateBack 返回,则允许
if (options.from === 'navigateBack') {
return false
}
// 否则拦截返回,显示确认弹窗
goBack()
return true
})
// 更新本地存储中的订单状态
const updateOrderInStorage = (status: number) => {
try {
// 尝试从 'orders' 读取 (checkout页面写入的key)
const ordersStr = uni.getStorageSync('orders')
let orders: any[] = []
if (ordersStr) {
orders = JSON.parse(ordersStr as string) as any[]
}
const index = orders.findIndex((o: any) => o.id === orderId.value)
if (index !== -1) {
orders[index].status = status
orders[index].payment_status = status === 2 ? 1 : 0 // 2=待发货(已支付), 1=待支付(未支付)
orders[index].updated_at = new Date().toISOString()
// 确保更新的是 'orders' key
uni.setStorageSync('orders', JSON.stringify(orders))
console.log('订单状态已更新到Storage (orders):', orderId.value, status)
} else {
console.warn('在Storage (orders)中未找到订单:', orderId.value)
}
} catch (e) {
console.error('更新订单状态失败', e)
}
}
// 加载订单信息
const loadOrderInfo = async () => {
try {
/* const { data, error } = await supa
.from('orders')
.select('order_no, actual_amount')
.eq('id', orderId.value)
.single()
if (error !== null) {
console.error('加载订单信息失败:', error)
return
}
if (data) {
orderNo.value = data.order_no
amount.value = data.actual_amount || amount.value
} */
// MOCK DATA
orderNo.value = 'ORD_MOCK_' + Date.now()
// Amount already set from options or default
} catch (err) {
console.error('加载订单信息异常:', err)
}
}
// 加载支付方式
const loadPaymentMethods = () => {
paymentMethods.value = [
{
id: 'wechat',
name: '微信支付',
description: '推荐安装微信5.0及以上版本使用',
icon: '💳',
enabled: true
},
{
id: 'alipay',
name: '支付宝',
description: '推荐安装支付宝10.0及以上版本使用',
icon: '💳',
enabled: true
},
{
id: 'balance',
name: '余额支付',
description: '使用账户余额支付',
icon: '💰',
enabled: true
},
{
id: 'bankcard',
name: '银行卡支付',
description: '支持储蓄卡、信用卡',
icon: '💳',
enabled: true
}
]
}
// 加载用户余额
const loadUserBalance = async () => {
const userId = getCurrentUserId()
if (!userId) return
try {
// 这里假设有用户钱包表
/* const { data, error } = await supa
.from('user_wallets')
.select('balance')
.eq('user_id', userId)
.single()
if (error !== null) {
console.error('加载用户余额失败:', error)
return
}
userBalance.value = data?.balance || 0 */
// MOCK BALANCE
userBalance.value = 10000.00
} catch (err) {
console.error('加载用户余额异常:', err)
}
}
// 获取当前用户ID
const getCurrentUserId = (): string => {
const userStore = uni.getStorageSync('userInfo')
return userStore?.id || ''
}
// 获取支付方式图标
const getMethodIcon = (methodId: string): string => {
const icons: Record<string, string> = {
wechat: '💳',
alipay: '💳',
balance: '💰',
bankcard: '💳'
}
return icons[methodId] || '💳'
}
// 选择支付方式
const selectMethod = (method: PaymentMethodType) => {
if (!method.enabled) {
uni.showToast({
title: '该支付方式暂不可用',
icon: 'none'
})
return
}
selectedMethod.value = method.id
showPassword.value = method.id === 'balance' || method.id === 'bankcard'
password.value = '' // 清空密码
}
// 获取支付按钮文本
const getPayButtonText = (): string => {
if (selectedMethod.value === 'balance' && userBalance.value < amount.value) {
return '余额不足'
}
const texts: Record<string, string> = {
wechat: '微信支付',
alipay: '支付宝支付',
balance: '余额支付',
bankcard: '银行卡支付'
}
return texts[selectedMethod.value] || '确认支付'
}
// 减少商品库存
const reduceStock = (orderId: string) => {
try {
// 读取订单
const ordersStr = uni.getStorageSync('orders')
if (!ordersStr) return
const orders = JSON.parse(ordersStr as string) as any[]
const order = orders.find((o: any) => o.id === orderId)
if (!order || !order.items) return
// 读取商品库(这里假设商品库也在本地,实际项目中通常在服务器端处理)
// 模拟:如果有本地商品缓存,则更新
/*
const productsStr = uni.getStorageSync('products')
if (productsStr) {
const products = JSON.parse(productsStr as string) as any[]
let hasChange = false
order.items.forEach((item: any) => {
const product = products.find((p: any) => p.id === item.product_id)
if (product && product.stock >= item.quantity) {
product.stock -= item.quantity
hasChange = true
console.log(`商品 ${product.name} 库存减少 ${item.quantity}, 剩余 ${product.stock}`)
}
})
if (hasChange) {
uni.setStorageSync('products', JSON.stringify(products))
}
}
*/
console.log('模拟扣减库存成功', order.items)
} catch (e) {
console.error('扣减库存失败', e)
}
}
// 确认支付
const confirmPayment = async () => {
if (isPaying.value) return
// 余额支付检查
if (selectedMethod.value === 'balance') {
if (userBalance.value < amount.value) {
uni.showToast({
title: '余额不足',
icon: 'none'
})
return
}
if (!showPassword.value) {
showPassword.value = true
return
}
if (password.value.length !== 6) {
uni.showToast({
title: '请输入6位支付密码',
icon: 'none'
})
return
}
}
isPaying.value = true
try {
// 模拟支付过程
await new Promise(resolve => setTimeout(resolve, 2000))
// 更新订单状态
updateOrderInStorage(2) // 2: 待发货(已支付)
// 扣减库存
reduceStock(orderId.value)
/* const { error } = await supa
.from('orders')
.update({
status: 2, // 待发货
payment_method: getPaymentMethodCode(selectedMethod.value),
payment_status: 1, // 已支付
updated_at: new Date().toISOString()
})
.eq('id', orderId.value)
if (error !== null) {
throw error
}
// 余额支付需要扣减余额
if (selectedMethod.value === 'balance') {
await updateUserBalance(-amount.value)
} */
// 支付成功
uni.showToast({
title: '支付成功',
icon: 'success',
duration: 2000
})
// 发布订单更新事件让profile页面可以刷新数据
uni.$emit('orderUpdated', { orderId: orderId.value, status: 2 }) // 2: 待发货
// 跳转到支付成功页面
setTimeout(() => {
uni.redirectTo({
url: `/pages/mall/consumer/payment-success?orderId=${orderId.value}`
})
}, 1500)
} catch (err) {
console.error('支付失败:', err)
uni.showToast({
title: '支付失败',
icon: 'none'
})
} finally {
isPaying.value = false
}
}
// 获取支付方式代码
const getPaymentMethodCode = (methodId: string): number => {
const codes: Record<string, number> = {
wechat: 1,
alipay: 2,
balance: 3,
bankcard: 4
}
return codes[methodId] || 0
}
// 更新用户余额
const updateUserBalance = async (change: number) => {
const userId = getCurrentUserId()
if (!userId) return
try {
/* const { data: wallet, error: walletError } = await supa
.from('user_wallets')
.select('balance')
.eq('user_id', userId)
.single()
if (walletError !== null) {
console.error('查询钱包失败:', walletError)
return
}
const newBalance = (wallet?.balance || 0) + change
const { error: updateError } = await supa
.from('user_wallets')
.update({ balance: newBalance })
.eq('user_id', userId)
if (updateError !== null) {
console.error('更新余额失败:', updateError)
return
}
// 记录余额变动
const { error: recordError } = await supa
.from('balance_records')
.insert({
user_id: userId,
change_amount: change,
current_balance: newBalance,
change_type: 'order_payment',
related_id: orderId.value,
remark: `订单支付: ${orderNo.value}`
})
if (recordError !== null) {
console.error('记录余额变动失败:', recordError)
}
userBalance.value = newBalance */
} catch (err) {
console.error('更新余额异常:', err)
}
}
// 输入密码
const inputPassword = (num: string) => {
if (password.value.length >= 6) return
password.value += num
}
// 删除密码
const deletePassword = () => {
if (password.value.length > 0) {
password.value = password.value.slice(0, -1)
}
}
// 监听密码输入
watch(password, (newPassword) => {
if (newPassword.length === 6) {
// 自动验证密码
verifyPassword()
}
})
// 验证密码
const verifyPassword = async () => {
// 这里应该验证支付密码,这里简单模拟
const userId = getCurrentUserId()
try {
// 模拟验证
await new Promise(resolve => setTimeout(resolve, 500))
// 假设密码正确
const isCorrect = true
if (isCorrect) {
// 密码正确,继续支付
confirmPayment()
} else {
password.value = ''
uni.showToast({
title: '密码错误',
icon: 'none'
})
}
} catch (err) {
console.error('验证密码异常:', err)
}
}
// 忘记密码
const forgotPassword = () => {
uni.navigateTo({
url: '/pages/user/forgot-password'
})
}
// 计算价格明细
const calculatePriceDetails = (totalAmount: number) => {
// 模拟计算各项费用
// 假设商品总价占总金额的80%运费占10%优惠减免占10%
productAmount.value = totalAmount * 0.8
deliveryFee.value = totalAmount * 0.1
discountAmount.value = totalAmount * 0.1
// 确保总和等于应付金额
const calculatedTotal = productAmount.value + deliveryFee.value - discountAmount.value
if (Math.abs(calculatedTotal - totalAmount) > 0.01) {
// 调整商品总价以匹配应付金额
productAmount.value = totalAmount + discountAmount.value - deliveryFee.value
}
}
// 在组件卸载时移除返回键监听
onUnmounted(() => {
// uni.offBackPress() 在uni-app中不需要手动移除
})
// 返回
const goBack = () => {
uni.showModal({
title: '取消支付',
content: '确定要取消支付吗?取消后订单将保存到待支付订单中',
confirmText: '取消支付',
cancelText: '继续支付',
success: async (res) => {
if (res.confirm) {
// 用户确认取消支付,更新订单状态为待支付
await cancelPayment()
} else {
// 用户选择继续支付,留在当前页面
return
}
}
})
}
// 取消支付,更新订单状态
const cancelPayment = async () => {
try {
// 这里应该调用API更新订单状态为待支付status: 1
// 模拟更新订单状态
/* const { error } = await supa
.from('orders')
.update({
status: 1, // 待支付
updated_at: new Date().toISOString()
})
.eq('id', orderId.value)
if (error !== null) {
console.error('更新订单状态失败:', error)
uni.showToast({
title: '操作失败',
icon: 'none'
})
return
} */
// 更新本地存储
updateOrderInStorage(orderId.value, 1) // 1: 待支付
// 发布订单更新事件让profile页面可以刷新数据
uni.$emit('orderUpdated', { orderId: orderId.value, status: 1 })
uni.showToast({
title: '已保存到待支付订单',
icon: 'success'
})
// 延迟返回,让用户看到提示
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (err) {
console.error('取消支付异常:', err)
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
}
</script>
<style scoped>
.payment-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.payment-header {
background-color: #ffffff;
padding: 15px;
display: flex;
align-items: center;
border-bottom: 1px solid #e5e5e5;
}
.back-btn {
font-size: 24px;
color: #333333;
padding: 5px;
margin-right: 15px;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.payment-content {
flex: 1;
overflow-y: auto;
}
/* 价格明细部分 */
.price-detail-section {
background-color: #ffffff;
padding: 20px 15px;
margin-bottom: 10px;
}
.price-detail {
padding: 15px;
background-color: #f8f9fa;
border-radius: 8px;
margin-bottom: 15px;
}
.price-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
}
.price-row.total {
border-top: 1px solid #e5e5e5;
margin-top: 8px;
padding-top: 15px;
}
.price-label {
font-size: 14px;
color: #666666;
}
.price-value {
font-size: 14px;
color: #333333;
}
.price-value.discount {
color: #4caf50;
}
.price-value.total-price {
font-size: 18px;
color: #ff4757;
font-weight: bold;
}
.order-no {
display: block;
font-size: 12px;
color: #999999;
text-align: center;
}
.methods-section {
background-color: #ffffff;
padding: 20px 15px;
margin-bottom: 10px;
}
.section-title {
display: block;
font-size: 16px;
font-weight: bold;
color: #333333;
margin-bottom: 15px;
}
.method-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.method-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
border: 1px solid #e5e5e5;
border-radius: 8px;
}
.method-item.selected {
border-color: #007aff;
background-color: #f0f8ff;
}
.method-left {
display: flex;
align-items: center;
}
.method-icon {
font-size: 24px;
margin-right: 15px;
}
.method-info {
display: flex;
flex-direction: column;
}
.method-name {
font-size: 16px;
font-weight: bold;
color: #333333;
margin-bottom: 5px;
}
.method-desc {
font-size: 12px;
color: #999999;
}
.method-selected {
width: 24px;
height: 24px;
border-radius: 12px;
background-color: #007aff;
display: flex;
align-items: center;
justify-content: center;
}
.selected-icon {
color: #ffffff;
font-size: 14px;
}
.balance-section {
background-color: #ffffff;
padding: 15px;
margin-bottom: 10px;
}
.balance-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.balance-label {
font-size: 14px;
color: #333333;
}
.balance-value {
font-size: 18px;
color: #ff4757;
font-weight: bold;
}
.balance-tip {
padding: 10px;
background-color: #fff0f0;
border-radius: 5px;
}
.tip-text {
font-size: 12px;
color: #ff4757;
}
.password-section {
background-color: #ffffff;
padding: 30px 15px;
text-align: center;
margin-bottom: 10px;
}
.password-title {
display: block;
font-size: 16px;
color: #333333;
margin-bottom: 30px;
}
.password-input {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
}
.password-dot {
width: 12px;
height: 12px;
border-radius: 6px;
background-color: #333333;
display: flex;
align-items: center;
justify-content: center;
}
.password-dot text {
color: #ffffff;
font-size: 8px;
}
.forgot-password {
color: #007aff;
font-size: 14px;
}
.payment-bottom {
background-color: #ffffff;
border-top: 1px solid #e5e5e5;
padding: 15px;
display: flex;
align-items: center;
justify-content: space-between;
}
.price-summary {
display: flex;
align-items: baseline;
}
.summary-label {
font-size: 14px;
color: #333333;
margin-right: 5px;
}
.summary-price {
font-size: 20px;
color: #ff4757;
font-weight: bold;
}
.pay-btn {
background-color: #007aff;
color: #ffffff;
padding: 0 40px;
height: 45px;
border-radius: 22.5px;
font-size: 16px;
font-weight: bold;
border: none;
}
.pay-btn.disabled {
background-color: #cccccc;
opacity: 0.6;
}
.password-keyboard {
background-color: #ffffff;
border-top: 1px solid #e5e5e5;
padding: 10px;
}
.keyboard-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 1px;
background-color: #e5e5e5;
}
.keyboard-key {
background-color: #ffffff;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.key-text {
font-size: 24px;
color: #333333;
}
</style>

View File

@@ -25,13 +25,13 @@
<view class="shop-info" @click="goToShop">
<image :src="merchant.shop_logo || '/static/default-shop.png'" class="shop-logo" />
<view class="shop-details">
<text class="shop-name">{{ merchant.shop_name }}</text>
<view class="shop-rating">
<text class="rating-text">评分: {{ merchant.rating.toFixed(1) }}</text>
<text class="sales-text">销量: {{ merchant.total_sales }}</text>
<text class="shop-name" @click.stop="goToShop">{{ merchant.shop_name }}</text>
<view class="shop-stats-row">
<text class="rating-text">评分: {{ merchant.rating.toFixed(1) }}</text>
<text class="sales-text">销量: {{ merchant.total_sales }}</text>
</view>
</view>
<text class="enter-shop">进店 ></text>
<text class="enter-shop" @click.stop="goToShop">进店 ></text>
</view>
<!-- 规格选择 -->
@@ -41,6 +41,26 @@
<text class="spec-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"
v-model="quantity"
: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>
<!-- 商品详情 -->
<view class="product-description">
<view class="section-title">商品详情</view>
@@ -50,6 +70,16 @@
<!-- 底部操作栏 -->
<view class="bottom-actions">
<view class="action-buttons">
<view class="action-btn" @click="goToCart">
<text class="action-icon">🛒</text>
<text class="action-text">购物车</text>
</view>
<view class="action-btn" @click="toggleFavorite">
<text class="action-icon">{{ isFavorite ? '❤️' : '🤍' }}</text>
<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>
</view>
@@ -116,49 +146,157 @@ export default {
showSpec: false,
selectedSkuId: '',
selectedSpec: '',
quantity: 1
quantity: 1,
isFavorite: false
}
},
onLoad(options: any) {
const productId = options.productId as string
const productId = options.productId as string || options.id as string
const productPrice = options.price ? parseFloat(options.price) : null
const productOriginalPrice = options.original_price ? parseFloat(options.original_price) : null
const productName = options.name as string
const productImage = options.image as string
if (productId) {
this.loadProductDetail(productId)
this.loadProductDetail(productId, {
price: productPrice,
originalPrice: productOriginalPrice,
name: productName,
image: productImage
})
this.checkFavoriteStatus(productId)
this.saveFootprint(productId)
}
},
computed: {
displayPrice(): number {
if (this.selectedSkuId) {
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
if (sku) return sku.price
}
return this.product.price
}
},
methods: {
loadProductDetail(productId: string) {
// 模拟加载商品详情数据
saveFootprint(productId: string) {
const footprintData = uni.getStorageSync('footprints')
let footprints: any[] = []
if (footprintData) {
try {
footprints = JSON.parse(footprintData as string) as any[]
} catch (e) {
console.error('Failed to parse footprints', e)
}
}
// 移除已存在的相同商品(为了将其移到最新位置)
footprints = footprints.filter(item => item.id !== productId)
// 添加到头部
footprints.unshift({
id: this.product.id,
name: this.product.name,
price: this.product.price,
original_price: this.product.original_price, // 添加原价
image: this.product.images[0],
sales: this.product.sales,
shopId: this.merchant.id,
shopName: this.merchant.shop_name,
viewTime: Date.now()
})
// 限制数量例如最近50条
if (footprints.length > 50) {
footprints = footprints.slice(0, 50)
}
uni.setStorageSync('footprints', JSON.stringify(footprints))
},
loadProductDetail(productId: string, options: any = {}) {
// 根据商品ID生成一个基础价格如果没有传入价格
const generatePriceFromId = (id: string): number => {
// 简单哈希函数将字符串转换为一个在50-500之间的价格
let hash = 0
for (let i = 0; i < id.length; i++) {
hash = (hash << 5) - hash + id.charCodeAt(i)
hash |= 0 // 转换为32位整数
}
// 将哈希值映射到50-500之间
const price = 50 + Math.abs(hash % 450)
// 保留两位小数
return parseFloat(price.toFixed(2))
}
// 优先使用传入的参数否则根据商品ID生成价格
const basePrice = options.price ? parseFloat(options.price) : generatePriceFromId(productId)
// 原价比现价高20%左右
const originalPrice = options.originalPrice ? parseFloat(options.originalPrice) : parseFloat((basePrice * 1.2).toFixed(2))
// 根据商品ID生成不同的商品名称使其更真实
const productNames = [
'高品质运动休闲鞋',
'时尚简约双肩背包',
'多功能智能手环',
'便携式蓝牙音箱',
'全自动雨伞',
'抗菌防螨床上四件套',
'不锈钢保温杯',
'无线充电器',
'高清行车记录仪',
'智能体脂秤'
]
const nameIndex = Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % productNames.length
const productName = options.name ? options.name : productNames[nameIndex]
// 模拟销量和库存,使其更真实
const sales = 1000 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5000
const stock = 50 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 200
this.product = {
id: productId,
merchant_id: 'merchant_001',
category_id: 'cat_001',
name: '精选好物商品',
description: '这是一个高品质的商品,具有优秀的性能和优美的外观设计。',
merchant_id: 'merchant_' + (Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5 + 1).toString().padStart(3, '0'),
category_id: 'cat_' + (Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 10 + 1).toString().padStart(3, '0'),
name: productName,
description: '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。',
images: [
'/static/product1.jpg',
'/static/product2.jpg',
'/static/product3.jpg'
],
price: 199.99,
original_price: 299.99,
stock: 100,
sales: 1256,
price: basePrice,
original_price: originalPrice,
stock: stock,
sales: sales,
status: 1,
created_at: '2024-01-15'
}
// 根据商家ID生成不同的商家信息
const merchantIndex = Math.abs(this.product.merchant_id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5
const shopNames = ['优质好店', '品牌直营店', '官方旗舰店', '专卖店', '精品小店']
const shopDescriptions = [
'专注品质生活',
'品牌官方直营,正品保障',
'厂家直销,价格优惠',
'专注本领域十年老店',
'用心服务每一位顾客'
]
const contactNames = ['店主小王', '店长小李', '经理小张', '客服小赵', '老板小钱']
this.merchant = {
id: 'merchant_001',
user_id: 'user_001',
shop_name: '优质好店',
id: this.product.merchant_id,
user_id: 'user_' + (merchantIndex + 1).toString().padStart(3, '0'),
shop_name: shopNames[merchantIndex],
shop_logo: '/static/shop-logo.png',
shop_banner: '/static/shop-banner.png',
shop_description: '专注品质生活',
contact_name: '店主小王',
contact_phone: '13800138000',
shop_description: shopDescriptions[merchantIndex],
contact_name: contactNames[merchantIndex],
contact_phone: '138' + (10000000 + merchantIndex * 1111111).toString().substring(0, 8),
shop_status: 1,
rating: 4.8,
total_sales: 15680,
rating: 4.5 + (merchantIndex * 0.1),
total_sales: 10000 + merchantIndex * 5000,
created_at: '2023-06-01'
}
@@ -167,13 +305,15 @@ export default {
loadProductSkus(productId: string) {
// 模拟加载商品SKU数据
const basePrice = this.product.price
this.productSkus = [
{
id: 'sku_001',
product_id: productId,
sku_code: 'SKU001',
specifications: { color: '红色', size: 'M' },
price: 199.99,
price: basePrice,
stock: 50,
image_url: '/static/sku1.jpg',
status: 1
@@ -183,7 +323,7 @@ export default {
product_id: productId,
sku_code: 'SKU002',
specifications: { color: '蓝色', size: 'L' },
price: 219.99,
price: parseFloat((basePrice * 1.1).toFixed(2)),
stock: 30,
image_url: '/static/sku2.jpg',
status: 1
@@ -222,6 +362,45 @@ export default {
return
}
// 获取现有购物车数据
const cartData = uni.getStorageSync('cart')
let cartItems: any[] = []
if (cartData) {
try {
cartItems = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
}
}
// 检查商品是否已存在 (同一SKU)
const existingItem = cartItems.find((item: any) => item.id === this.selectedSkuId)
if (existingItem) {
existingItem.quantity += this.quantity
} else {
// 查找SKU信息
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
// 添加新商品
cartItems.push({
id: this.selectedSkuId, // 使用SKU ID作为购物车条目ID
productId: this.product.id,
shopId: this.merchant.id,
shopName: this.merchant.shop_name,
name: this.product.name,
price: sku ? sku.price : this.product.price,
image: (sku && sku.image_url) ? sku.image_url : this.product.images[0],
spec: this.selectedSpec,
quantity: this.quantity,
selected: true
})
}
// 保存回存储
uni.setStorageSync('cart', JSON.stringify(cartItems))
// 模拟添加到购物车
uni.showToast({
title: '已添加到购物车',
@@ -238,9 +417,41 @@ export default {
return
}
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
// 调试:打印价格信息
console.log('立即购买 - 商品价格信息:')
console.log('SKU价格:', sku ? sku.price : '无SKU')
console.log('商品价格:', this.product.price)
console.log('选择的价格:', (sku ? sku.price : this.product.price))
console.log('数量:', this.quantity)
const selectedItem = {
id: this.selectedSkuId,
product_id: this.product.id,
sku_id: this.selectedSkuId,
product_name: this.product.name,
product_image: (sku && sku.image_url) ? sku.image_url : this.product.images[0],
sku_specifications: sku ? sku.specifications : {},
price: Number(parseFloat((sku ? sku.price : this.product.price).toString()).toFixed(2)),
quantity: Number(this.quantity)
}
// 调试:打印最终传递的数据
console.log('立即购买 - 传递的商品数据:', selectedItem)
// 使用Storage传递数据避免EventChannel可能的问题
uni.setStorageSync('checkout_type', 'buy_now')
uni.setStorageSync('checkout_items', JSON.stringify([selectedItem]))
// 跳转到订单确认页
uni.navigateTo({
url: `/pages/mall/consumer/order-confirm?productId=${this.product.id}&skuId=${this.selectedSkuId}&quantity=${this.quantity}`
url: '/pages/mall/consumer/checkout',
success: (res) => {
res.eventChannel.emit('acceptData', {
selectedItems: [selectedItem]
})
}
})
},
@@ -248,6 +459,125 @@ export default {
uni.navigateTo({
url: `/pages/mall/consumer/shop-detail?merchantId=${this.merchant.id}`
})
},
checkFavoriteStatus(id: string) {
const storedFavorites = uni.getStorageSync('favorites')
if (storedFavorites) {
try {
const favorites = JSON.parse(storedFavorites as string) as any[]
this.isFavorite = favorites.some(item => item.id === id)
} catch (e) {
console.error('Failed to parse favorites', e)
}
}
},
toggleFavorite() {
const storedFavorites = uni.getStorageSync('favorites')
let favorites: any[] = []
if (storedFavorites) {
try {
favorites = JSON.parse(storedFavorites as string) as any[]
} catch (e) {
console.error('Failed to parse favorites', e)
}
}
if (this.isFavorite) {
// 取消收藏
favorites = favorites.filter(item => item.id !== this.product.id)
uni.showToast({
title: '已取消收藏',
icon: 'none'
})
} else {
// 添加收藏
favorites.push({
id: this.product.id,
name: this.product.name,
price: this.product.price,
original_price: this.product.original_price, // 保存原价
image: this.product.images[0],
sales: this.product.sales,
shopId: this.merchant.id,
shopName: this.merchant.shop_name
})
uni.showToast({
title: '收藏成功',
icon: 'success'
})
}
uni.setStorageSync('favorites', JSON.stringify(favorites))
this.isFavorite = !this.isFavorite
},
goToHome() {
uni.switchTab({
url: '/pages/mall/consumer/home'
})
},
goToCart() {
uni.switchTab({
url: '/pages/mall/consumer/cart'
})
},
// 数量选择相关方法
decreaseQuantity() {
if (this.quantity > 1) {
this.quantity--
}
},
increaseQuantity() {
const maxQuantity = this.getMaxQuantity()
if (this.quantity < maxQuantity) {
this.quantity++
} else {
uni.showToast({
title: `最多只能购买${maxQuantity}件`,
icon: 'none'
})
}
},
validateQuantity() {
// 确保数量是数字
let num = parseInt(this.quantity)
if (isNaN(num)) {
num = 1
}
// 限制在1和最大库存之间
const maxQuantity = this.getMaxQuantity()
if (num < 1) {
num = 1
} else if (num > maxQuantity) {
num = maxQuantity
uni.showToast({
title: `最多只能购买${maxQuantity}件`,
icon: 'none'
})
}
this.quantity = num
},
getMaxQuantity() {
// 如果有选择SKU使用SKU的库存否则使用商品总库存
if (this.selectedSkuId) {
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
if (sku) return sku.stock
}
return this.product.stock
},
getAvailableStock() {
return this.getMaxQuantity()
}
}
}
@@ -328,6 +658,7 @@ export default {
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
flex-direction: row; /* 显式横向排列 */
align-items: center;
}
@@ -340,6 +671,9 @@ export default {
.shop-details {
flex: 1;
display: flex;
flex-direction: column; /* 内部信息保持纵向,或者根据需要改为横向 */
justify-content: center;
}
.shop-name {
@@ -349,8 +683,10 @@ export default {
margin-bottom: 10rpx;
}
.shop-rating {
.shop-stats-row {
display: flex;
flex-direction: row; /* 显式横向排列 */
align-items: center;
}
.rating-text, .sales-text {
@@ -389,6 +725,66 @@ export default {
color: #999;
}
.quantity-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.quantity-title {
font-size: 30rpx;
color: #333;
width: 120rpx;
}
.quantity-selector {
display: flex;
align-items: center;
border: 1rpx solid #e5e5e5;
border-radius: 8rpx;
overflow: hidden;
}
.quantity-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
}
.quantity-btn.minus {
border-right: 1rpx solid #e5e5e5;
}
.quantity-btn.plus {
border-left: 1rpx solid #e5e5e5;
}
.quantity-btn-text {
font-size: 28rpx;
color: #333;
}
.quantity-input {
width: 80rpx;
height: 60rpx;
text-align: center;
font-size: 28rpx;
color: #333;
border: none;
background-color: #fff;
}
.quantity-stock {
font-size: 24rpx;
color: #666;
}
.product-description {
background-color: #fff;
padding: 30rpx;
@@ -414,21 +810,55 @@ export default {
left: 0;
right: 0;
background-color: #fff;
padding: 20rpx 30rpx;
padding: 10rpx 20rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: row; /* 显式设置横向排列 */
align-items: center;
justify-content: space-between;
}
.action-buttons {
display: flex;
gap: 20rpx;
flex-direction: row; /* 显式设置横向排列 */
align-items: center;
margin-right: 20rpx;
}
.action-btn {
display: flex;
flex-direction: column; /* 图标文字保持纵向 */
align-items: center;
justify-content: center;
margin-right: 20rpx;
min-width: 80rpx;
}
.action-icon {
font-size: 40rpx;
margin-bottom: 4rpx;
}
.action-text {
font-size: 20rpx;
color: #666;
}
.btn-group {
flex: 1;
display: flex;
flex-direction: row; /* 显式设置横向排列 */
align-items: center;
}
.cart-btn, .buy-btn {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
font-size: 28rpx;
height: 72rpx;
line-height: 72rpx;
border-radius: 36rpx;
font-size: 26rpx;
border: none;
margin: 0 10rpx;
}
.cart-btn {

View File

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

View File

@@ -1,66 +1,136 @@
<!-- 消费者端 - 个人中心 -->
<template>
<view class="consumer-profile">
<!-- 用户信息头部 -->
<view class="profile-header">
<image :src="userInfo.avatar_url || '/static/default-avatar.png'" class="user-avatar" @click="editProfile" />
<view class="user-info">
<text class="user-name">{{ userInfo.nickname || userInfo.phone }}</text>
<text class="user-level">{{ getUserLevel() }}</text>
<view class="user-stats">
<text class="stat-item">积分: {{ userStats.points }}</text>
<text class="stat-item">余额: ¥{{ userStats.balance }}</text>
<!-- 智能顶部导航栏 - 与消息页保持一致 -->
<view class="smart-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-container">
<!-- 头像 -->
<image
:src="userInfo.avatar_url || '/static/default-avatar.png'"
class="nav-avatar"
@click="editProfile"
/>
<!-- 用户信息横向排列 (名字、积分、余额、优惠券) -->
<view class="nav-user-stats">
<text class="nav-user-name">{{ userInfo.nickname || userInfo.phone }}</text>
<view class="nav-stat-item">
<text class="nav-stat-label">积分</text>
<text class="nav-stat-value">{{ userStats.points }}</text>
</view>
<view class="nav-stat-item">
<text class="nav-stat-label">余额</text>
<text class="nav-stat-value" @click="goToWallet">¥{{ userStats.balance }}</text>
</view>
<view class="nav-stat-item" @click="goToCoupons">
<text class="nav-stat-label">券</text>
<text class="nav-stat-value">{{ serviceCounts.coupons }}</text>
</view>
</view>
<!-- 设置按钮 (右侧) -->
<view class="nav-actions">
<view class="action-btn" @click="goToSettings">
<text class="action-icon">⚙️</text>
</view>
</view>
</view>
</view>
<!-- 导航栏占位符 - 恢复 -->
<view :style="{ height: (statusBarHeight + 10) + 'px' }"></view>
<!-- 我的服务 (移到订单上方) -->
<view class="my-services" style="margin-top: 10px;">
<view class="section-title">我的服务</view>
<view class="service-grid">
<view class="service-item" @click="goToCoupons">
<text class="service-icon">🎫</text>
<text class="service-text">优惠券</text>
<text v-if="serviceCounts.coupons > 0" class="service-badge">{{ serviceCounts.coupons }}</text>
</view>
<view class="service-item" @click="goToAddress">
<text class="service-icon">📍</text>
<text class="service-text">收货地址</text>
</view>
<view class="service-item" @click="goToFavorites">
<text class="service-icon">❤️</text>
<text class="service-text">我的收藏</text>
<text v-if="serviceCounts.favorites > 0" class="service-badge">{{ serviceCounts.favorites }}</text>
</view>
<view class="service-item" @click="goToFootprint">
<text class="service-icon">👣</text>
<text class="service-text">浏览足迹</text>
</view>
<view class="service-item" @click="goToRefund">
<text class="service-icon">🔄</text>
<text class="service-text">退款/售后</text>
</view>
<view class="service-item" @click="contactService">
<text class="service-icon">💬</text>
<text class="service-text">在线客服</text>
</view>
<view class="service-item" @click="goToMySubscriptions">
<text class="service-icon">🧩</text>
<text class="service-text">我的订阅</text>
</view>
<view class="service-item" @click="goToSubscriptions">
<text class="service-icon">📱</text>
<text class="service-text">软件订阅</text>
</view>
</view>
<view class="settings-icon" @click="goToSettings">⚙️</view>
</view>
<!-- 订单状态快捷入口 -->
<view class="order-shortcuts">
<view class="section-title">我的订单</view>
<view class="order-tabs">
<view class="order-tab" @click="goToOrders('all')">
<view class="order-tab" :class="{ active: currentOrderTab === 'all' }" @click="switchOrderTab('all')">
<text class="tab-icon">📋</text>
<text class="tab-text">全部订单</text>
<text class="tab-text">全部</text>
<text v-if="orderCounts.total > 0" class="tab-badge">{{ orderCounts.total }}</text>
</view>
<view class="order-tab" @click="goToOrders('pending')">
<view class="order-tab" :class="{ active: currentOrderTab === 'pending' }" @click="switchOrderTab('pending')">
<text class="tab-icon">💰</text>
<text class="tab-text">待支付</text>
<text v-if="orderCounts.pending > 0" class="tab-badge">{{ orderCounts.pending }}</text>
</view>
<view class="order-tab" @click="goToOrders('shipped')">
<view class="order-tab" :class="{ active: currentOrderTab === 'toship' }" @click="switchOrderTab('toship')">
<text class="tab-icon">🚚</text>
<text class="tab-text">待发货</text>
<text v-if="orderCounts.toship > 0" class="tab-badge">{{ orderCounts.toship }}</text>
</view>
<view class="order-tab" :class="{ active: currentOrderTab === 'shipped' }" @click="switchOrderTab('shipped')">
<text class="tab-icon">📦</text>
<text class="tab-text">待收货</text>
<text v-if="orderCounts.shipped > 0" class="tab-badge">{{ orderCounts.shipped }}</text>
</view>
<view class="order-tab" @click="goToOrders('completed')">
<text class="tab-icon">⭐</text>
<text class="tab-text">待评价</text>
<text v-if="orderCounts.review > 0" class="tab-badge">{{ orderCounts.review }}</text>
</view>
</view>
</view>
<!-- 最近订单 -->
<!-- 最近订单列表 (根据Tab切换显示) -->
<view class="recent-orders">
<view class="section-header">
<text class="section-title">最近订单</text>
<text class="view-all" @click="goToOrders('all')">查看全部 ></text>
<text class="section-title">{{ getOrderSectionTitle() }}</text>
<text class="view-all" @click="goToOrders(currentOrderTab)">查看更多 ></text>
</view>
<view v-if="recentOrders.length === 0" class="empty-orders">
<text class="empty-text">暂无订单记录</text>
<view v-if="filteredOrders.length === 0" class="empty-orders">
<text class="empty-text">暂无相关订单记录</text>
<button class="start-shopping" @click="goShopping">去逛逛</button>
</view>
<view v-for="order in recentOrders" :key="order.id" class="order-item" @click="viewOrderDetail(order)">
<view 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" />
<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>
@@ -76,7 +146,7 @@
</view>
<!-- 我的服务 -->
<view class="my-services">
<!-- <view class="my-services">
<view class="section-title">我的服务</view>
<view class="service-grid">
<view class="service-item" @click="goToCoupons">
@@ -114,7 +184,7 @@
<text class="service-text">软件订阅</text>
</view>
</view>
</view>
</view> -->
<!-- 消费统计 -->
<view class="consumption-stats">
@@ -147,7 +217,7 @@
</view>
<!-- 账户安全 -->
<view class="account-security">
<!-- <view class="account-security">
<view class="section-title">账户安全</view>
<view class="security-items">
<view class="security-item" @click="changePassword">
@@ -172,12 +242,13 @@
</view>
</view>
</view>
</view>
</view> -->
</view>
</template>
<script>
import { UserType, OrderType } from '@/types/mall-types.uts'
// import { supabase as supa } from '@/components/supadb/aksupainstance.uts'
type UserStatsType = {
points: number
@@ -188,8 +259,8 @@ type UserStatsType = {
type OrderCountsType = {
total: number
pending: number
toship: number
shipped: number
review: number
}
type ServiceCountsType = {
@@ -231,9 +302,9 @@ export default {
orderCounts: {
total: 0,
pending: 0,
toship: 0,
shipped: 0,
review: 0
} as OrderCountsType,
} as any,
serviceCounts: {
coupons: 0,
favorites: 0
@@ -251,16 +322,101 @@ export default {
order_count: 0,
avg_amount: 0,
save_amount: 0
} as ConsumptionStatsType
} as ConsumptionStatsType,
statusBarHeight: 0,
currentOrderTab: 'all' as string, // 当前选中的订单Tab
allOrders: [] as Array<OrderType> // 存储所有订单数据
}
},
onLoad() {
this.initPage()
this.loadUserProfile()
this.loadOrders()
// 监听订单更新事件
uni.$on('orderUpdated', this.handleOrderUpdated)
},
onShow() {
this.refreshData()
},
onUnload() {
// 移除事件监听
uni.$off('orderUpdated', this.handleOrderUpdated)
},
computed: {
// 根据当前Tab筛选订单
filteredOrders(): Array<OrderType> {
if (this.currentOrderTab === 'all') {
return this.allOrders
} else if (this.currentOrderTab === 'pending') {
return this.allOrders.filter((order: OrderType): boolean => order.status === 1)
} else if (this.currentOrderTab === 'toship') {
return this.allOrders.filter((order: OrderType): boolean => order.status === 2)
} else if (this.currentOrderTab === 'shipped') {
return this.allOrders.filter((order: OrderType): boolean => order.status === 3)
} else if (this.currentOrderTab === 'review') {
return this.allOrders.filter((order: OrderType): boolean => order.status === 4)
}
return []
}
},
methods: {
// 加载订单数据
async loadOrders() {
const userStore = uni.getStorageSync('userInfo')
// const userId = userStore?.id
// if (!userId) return
try {
// 从本地存储加载订单数据
const storedOrders = uni.getStorageSync('orders')
let orders: any[] = []
if (storedOrders) {
orders = JSON.parse(storedOrders as string) as any[]
}
this.allOrders = orders
// 按时间倒序
this.allOrders.sort((a: any, b: any) => {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
})
// 过滤最近的订单
this.recentOrders = this.allOrders.slice(0, 5)
// 更新角标统计 (确保状态码一致: 1=待支付, 2=待发货, 3=待收货, 4=待评价)
this.orderCounts = {
total: this.allOrders.length,
pending: this.allOrders.filter((o: any) => o.status === 1).length,
toship: this.allOrders.filter((o: any) => o.status === 2).length, // 修复仅计算状态2为待发货
shipped: this.allOrders.filter((o: any) => o.status === 3).length, // 修复仅计算状态3为待收货
review: this.allOrders.filter((o: any) => o.status === 4).length
}
} catch (e) {
console.error('加载订单异常', e)
}
},
// 切换订单Tab
switchOrderTab(tab: string) {
this.currentOrderTab = tab
},
// 获取当前订单部分标题
getOrderSectionTitle(): string {
const titles: Record<string, string> = {
'all': '全部订单',
'pending': '待支付订单',
'shipped': '待收货订单',
'review': '待评价订单'
}
return titles[this.currentOrderTab] || '我的订单'
},
initPage() {
const systemInfo = uni.getSystemInfoSync()
this.statusBarHeight = systemInfo.statusBarHeight || 0
},
loadUserProfile() {
// 模拟加载用户信息
this.userInfo = {
@@ -364,6 +520,15 @@ export default {
refreshData() {
// 刷新页面数据
this.loadUserProfile()
this.loadOrders()
this.updateCouponCount() // 更新优惠券数量
},
updateCouponCount() {
// 从本地存储读取领取的优惠券数量并叠加到基础数量上
const baseCoupons = 5
const claimedCoupons = uni.getStorageSync('claimedCoupons') || 0
this.serviceCounts.coupons = baseCoupons + (claimedCoupons as number)
},
getUserLevel(): string {
@@ -417,12 +582,20 @@ export default {
})
},
// 跳转设置
goToSettings() {
uni.navigateTo({
url: '/pages/mall/consumer/settings'
})
},
// 跳转钱包
goToWallet() {
uni.navigateTo({
url: '/pages/mall/consumer/wallet'
})
},
goToOrders(type: string) {
uni.navigateTo({
url: `/pages/mall/consumer/orders?type=${type}`
@@ -476,8 +649,9 @@ export default {
},
goToAddress() {
// 暂时跳转到设置页的地址管理
uni.navigateTo({
url: '/pages/mall/consumer/address'
url: '/pages/mall/consumer/address-list'
})
},
@@ -531,6 +705,26 @@ export default {
uni.navigateTo({
url: '/pages/mall/consumer/bind-email'
})
},
// 处理订单更新事件
handleOrderUpdated(data: any) {
// 当收到订单更新事件时,刷新订单数据
console.log('收到订单更新事件:', data)
this.refreshData()
// 显示提示
if (data.status === 1) {
uni.showToast({
title: '订单已保存到待支付',
icon: 'success'
})
} else if (data.status === 2) {
uni.showToast({
title: '支付成功,订单待发货',
icon: 'success'
})
}
}
}
}
@@ -542,115 +736,183 @@ export default {
min-height: 100vh;
}
.profile-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 60rpx 30rpx 40rpx;
/* 智能顶部导航栏 */
.smart-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
z-index: 1000;
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.nav-container {
padding: 0 16px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
max-width: 1400px;
margin: 0 auto;
height: 44px;
}
/* 导航栏用户信息区域 */
.nav-user-stats {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start; /* 靠左对齐,紧跟头像 */
margin-right: 12px;
overflow: hidden; /* 防止溢出 */
}
.nav-user-name {
font-size: 16px;
font-weight: bold;
color: white;
margin-right: 12px;
max-width: 30%; /* 限制名字宽度 */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nav-stat-item {
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; /* 防止被压缩 */
}
.nav-stat-label {
font-size: 11px;
color: rgba(255, 255, 255, 0.9);
margin-right: 4px;
}
.nav-stat-value {
font-size: 12px;
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;
flex-shrink: 0;
}
.nav-actions {
display: flex;
flex-direction: row;
align-items: center;
flex-shrink: 0;
}
.action-btn {
display: flex;
align-items: center;
color: #fff;
justify-content: center;
width: 32px;
height: 32px;
background: rgba(255, 255, 255, 0.2);
border-radius: 16px;
cursor: pointer;
}
.user-avatar {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
margin-right: 30rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
.action-icon {
font-size: 18px;
color: white;
}
.user-info {
flex: 1;
}
.user-name {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 8rpx;
}
.user-level {
font-size: 24rpx;
background-color: rgba(255, 255, 255, 0.2);
padding: 6rpx 12rpx;
border-radius: 12rpx;
margin-bottom: 15rpx;
display: inline-block;
}
.user-stats {
display: flex;
gap: 30rpx;
}
.stat-item {
font-size: 24rpx;
opacity: 0.9;
}
.settings-icon {
font-size: 32rpx;
padding: 10rpx;
/* 导航栏占位符 */
.navbar-placeholder {
width: 100%;
flex-shrink: 0;
}
.order-shortcuts, .recent-orders, .my-services, .consumption-stats, .account-security {
background-color: #fff;
margin-bottom: 20rpx;
padding: 30rpx;
margin: 15px 15px; /* 顶部恢复 margin */
border-radius: 12px; /* 统一圆角 */
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.section-title {
font-size: 32rpx;
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 25rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25rpx;
}
.view-all {
font-size: 24rpx;
color: #007aff;
margin-bottom: 16px;
}
.order-tabs {
display: flex;
flex-direction: row; /* 显式横向排列 */
justify-content: space-between;
width: 100%;
}
.order-tab {
flex: 1;
display: flex;
flex-direction: column;
flex-direction: row; /* 关键:改为横向排列 */
align-items: center;
justify-content: center; /* 居中 */
position: relative;
padding: 8px 0;
}
.tab-icon {
font-size: 40rpx;
margin-bottom: 10rpx;
font-size: 20px;
margin-right: 6px; /* 图标和文字间距 */
margin-bottom: 0; /* 移除底部间距 */
}
.tab-text {
font-size: 24rpx;
color: #666;
font-size: 14px;
color: #333;
}
/* 选中状态的Tab */
.order-tab.active .tab-icon,
.order-tab.active .tab-text {
color: #4CAF50;
font-weight: bold;
}
.order-tab.active {
background-color: #f0f9f0;
border-radius: 8px;
}
.tab-badge {
position: absolute;
top: -8rpx;
right: 20rpx;
top: 0;
right: 10%; /* 调整位置 */
background-color: #ff4444;
color: #fff;
font-size: 20rpx;
padding: 4rpx 8rpx;
border-radius: 10rpx;
min-width: 32rpx;
font-size: 10px;
padding: 1px 5px;
border-radius: 8px;
min-width: 14px;
text-align: center;
line-height: 1.2;
}
.empty-orders {
@@ -782,16 +1044,19 @@ export default {
.service-grid {
display: flex;
flex-wrap: wrap;
gap: 30rpx;
flex-direction: row;
flex-wrap: wrap; /* 允许换行 */
gap: 16px 0; /* 行间距16px列间距由 flex 控制 */
justify-content: flex-start; /* 从左开始排列 */
}
.service-item {
width: 30%;
width: 25%; /* 每行4个 */
display: flex;
flex-direction: column;
align-items: center;
position: relative;
box-sizing: border-box; /* 确保 padding 不影响宽度 */
}
.service-icon {

View File

@@ -0,0 +1,163 @@
<template>
<view class="review-page">
<view class="header">
<text class="title">服务评价</text>
<text class="subtitle">请对本次售后服务进行评价</text>
</view>
<view class="rating-section">
<text class="label">服务评分</text>
<view class="stars">
<text
v-for="i in 5"
:key="i"
class="star"
:class="{ active: i <= rating }"
@click="setRating(i)"
>★</text>
</view>
<text class="rating-text">{{ ratingText }}</text>
</view>
<view class="comment-section">
<textarea
v-model="comment"
class="comment-input"
placeholder="请输入您的评价内容,您的建议是我们改进的动力"
maxlength="200"
/>
<text class="word-count">{{ comment.length }}/200</text>
</view>
<button class="submit-btn" @click="submitReview" :loading="submitting">提交评价</button>
</view>
</template>
<script setup lang="uts">
import { ref, computed } from 'vue'
const rating = ref(5)
const comment = ref('')
const submitting = ref(false)
const ratingText = computed(() => {
const texts = ['非常不满意', '不满意', '一般', '满意', '非常满意']
return texts[rating.value - 1]
})
const setRating = (val: number) => {
rating.value = val
}
const submitReview = () => {
if (submitting.value) return
submitting.value = true
// 模拟提交
setTimeout(() => {
uni.showToast({
title: '评价成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
submitting.value = false
}, 1000)
}
</script>
<style scoped>
.review-page {
min-height: 100vh;
background-color: #ffffff;
padding: 20px;
}
.header {
margin-bottom: 30px;
text-align: center;
}
.title {
font-size: 20px;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 10px;
}
.subtitle {
font-size: 14px;
color: #999;
}
.rating-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 30px;
}
.label {
font-size: 16px;
color: #333;
margin-bottom: 15px;
}
.stars {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.star {
font-size: 32px;
color: #ddd;
transition: color 0.2s;
}
.star.active {
color: #ffca28;
}
.rating-text {
font-size: 14px;
color: #666;
}
.comment-section {
position: relative;
margin-bottom: 30px;
}
.comment-input {
width: 100%;
height: 120px;
background-color: #f5f5f5;
border-radius: 8px;
padding: 15px;
font-size: 14px;
box-sizing: border-box;
}
.word-count {
position: absolute;
bottom: 10px;
right: 10px;
font-size: 12px;
color: #999;
}
.submit-btn {
background-color: #007aff;
color: #ffffff;
border-radius: 25px;
font-size: 16px;
font-weight: bold;
height: 50px;
line-height: 50px;
}
</style>

View File

@@ -0,0 +1,858 @@
<!-- 退款页面 -->
<template>
<view class="refund-page">
<!-- 顶部栏 -->
<view class="refund-header">
<text class="back-btn" @click="goBack"></text>
<text class="header-title">退款/售后</text>
</view>
<!-- 标签页 -->
<view class="refund-tabs">
<view :class="['refund-tab', { active: activeTab === 'all' }]" @click="changeTab('all')">
<text class="tab-text">全部</text>
</view>
<view :class="['refund-tab', { active: activeTab === 'processing' }]" @click="changeTab('processing')">
<text class="tab-text">处理中</text>
<text v-if="tabCounts.processing > 0" class="tab-badge">{{ tabCounts.processing }}</text>
</view>
<view :class="['refund-tab', { active: activeTab === 'completed' }]" @click="changeTab('completed')">
<text class="tab-text">已完成</text>
</view>
</view>
<!-- 售后列表 -->
<scroll-view class="refund-content" scroll-y @scrolltolower="loadMore">
<!-- 空状态 -->
<view v-if="refunds.length === 0 && !isLoading" class="empty-refunds">
<text class="empty-icon">🔄</text>
<text class="empty-text">暂无售后记录</text>
<text class="empty-subtext">您可以在订单详情中申请售后</text>
<button class="go-orders-btn" @click="goToOrders">查看订单</button>
</view>
<!-- 售后项 -->
<view v-for="refund in refunds" :key="refund.id" class="refund-item">
<view class="refund-header">
<text class="refund-no">售后单号: {{ refund.refund_no }}</text>
<text :class="['refund-status', getStatusClass(refund.status)]">
{{ getStatusText(refund.status) }}
</text>
</view>
<view class="order-info">
<text class="order-no">订单号: {{ refund.order?.order_no }}</text>
<text class="order-time">{{ formatTime(refund.order?.created_at) }}</text>
</view>
<view class="product-info" @click="viewOrder(refund.order_id)">
<image class="product-image" :src="getProductImage(refund)" />
<view class="product-details">
<text class="product-name">{{ getProductName(refund) }}</text>
<text v-if="refund.refund_reason" class="refund-reason">原因: {{ refund.refund_reason }}</text>
<view class="refund-amount">
<text class="amount-label">退款金额:</text>
<text class="amount-value">¥{{ refund.refund_amount }}</text>
</view>
</view>
</view>
<!-- 进度时间线 -->
<view v-if="refund.status_history?.length > 0" class="timeline">
<view v-for="(step, index) in getTimelineSteps(refund)"
:key="index"
class="timeline-step">
<view class="step-dot" :class="{ active: step.active, completed: step.completed }"></view>
<view class="step-info">
<text class="step-title">{{ step.title }}</text>
<text class="step-time">{{ step.time }}</text>
<text v-if="step.desc" class="step-desc">{{ step.desc }}</text>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view v-if="refund.status === 1" class="refund-actions">
<button class="action-btn cancel" @click="cancelRefund(refund)">取消申请</button>
<button class="action-btn contact" @click="contactService(refund)">联系客服</button>
</view>
<view v-if="refund.status === 3" class="refund-actions">
<button class="action-btn review" @click="reviewRefund(refund)">评价服务</button>
<button class="action-btn delete" @click="deleteRefund(refund)">删除记录</button>
</view>
</view>
<!-- 加载更多 -->
<view v-if="isLoading" class="loading-more">
<text class="loading-text">加载中...</text>
</view>
<view v-if="!hasMore && refunds.length > 0" class="no-more">
<text class="no-more-text">没有更多了</text>
</view>
</scroll-view>
<!-- 申请售后按钮 -->
<view class="apply-btn-container">
<button class="apply-btn" @click="applyRefund">申请售后</button>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, watch } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
type RefundType = {
id: string
user_id: string
order_id: string
refund_no: string
refund_type: number // 1:仅退款 2:退货退款
refund_reason: string
refund_amount: number
status: number // 1:待处理 2:处理中 3:已完成 4:已取消 5:已拒绝
status_history: Array<{
status: number
remark: string
created_at: string
}> | null
created_at: string
order?: {
id: string
order_no: string
created_at: string
order_items: Array<{
id: string
product_name: string
sku_specifications: any
price: number
quantity: number
product?: {
images: string[]
}
}>
}
}
type TabCountsType = {
processing: number
}
const activeTab = ref<string>('all')
const refunds = ref<Array<RefundType>>([])
const tabCounts = ref<TabCountsType>({
processing: 0
})
const isLoading = ref<boolean>(false)
const currentPage = ref<number>(1)
const pageSize = ref<number>(15)
const hasMore = ref<boolean>(true)
// 监听标签页变化
watch(activeTab, () => {
resetData()
loadRefunds()
})
// 生命周期
onMounted(() => {
loadRefunds()
loadTabCounts()
})
// 重置数据
const resetData = () => {
refunds.value = []
currentPage.value = 1
hasMore.value = true
}
// 加载售后数据
const loadRefunds = async (loadMore: boolean = false) => {
if (isLoading.value || (!hasMore.value && loadMore)) {
return
}
isLoading.value = true
try {
const userId = getCurrentUserId()
if (!userId) {
uni.navigateTo({
url: '/pages/user/login'
})
return
}
const page = loadMore ? currentPage.value + 1 : 1
let query = supa
.from('refunds')
.select(`
*,
order:order_id(
order_no,
created_at,
order_items(
*,
product:product_id(images)
)
)
`)
.eq('user_id', userId)
.order('created_at', { ascending: false })
// 根据标签页过滤
if (activeTab.value === 'processing') {
query = query.in('status', [1, 2]) // 待处理和处理中
} else if (activeTab.value === 'completed') {
query = query.in('status', [3, 4, 5]) // 已完成、已取消、已拒绝
}
// 分页
query = query.range((page - 1) * pageSize.value, page * pageSize.value - 1)
const { data, error } = await query
if (error !== null) {
console.error('加载售后记录失败:', error)
return
}
const newRefunds = data || []
if (loadMore) {
refunds.value.push(...newRefunds)
currentPage.value = page
} else {
refunds.value = newRefunds
currentPage.value = 1
}
hasMore.value = newRefunds.length === pageSize.value
} catch (err) {
console.error('加载售后记录异常:', err)
} finally {
isLoading.value = false
}
}
// 加载标签页计数
const loadTabCounts = async () => {
const userId = getCurrentUserId()
if (!userId) return
try {
const { count, error } = await supa
.from('refunds')
.select('*', { count: 'exact' })
.eq('user_id', userId)
.in('status', [1, 2])
if (error !== null) {
console.error('加载计数失败:', error)
return
}
tabCounts.value.processing = count || 0
} catch (err) {
console.error('加载计数异常:', err)
}
}
// 获取当前用户ID
const getCurrentUserId = (): string => {
const userStore = uni.getStorageSync('userInfo')
return userStore?.id || ''
}
// 获取状态文本
const getStatusText = (status: number): string => {
const statusMap: Record<number, string> = {
1: '待处理',
2: '处理中',
3: '已完成',
4: '已取消',
5: '已拒绝'
}
return statusMap[status] || '未知状态'
}
// 获取状态样式类
const getStatusClass = (status: number): string => {
const classMap: Record<number, string> = {
1: 'status-pending',
2: 'status-processing',
3: 'status-completed',
4: 'status-cancelled',
5: 'status-rejected'
}
return classMap[status] || 'status-unknown'
}
// 获取商品图片
const getProductImage = (refund: RefundType): string => {
const firstItem = refund.order?.order_items?.[0]
if (!firstItem?.product?.images?.[0]) {
return '/static/default-product.png'
}
return firstItem.product.images[0]
}
// 获取商品名称
const getProductName = (refund: RefundType): string => {
const items = refund.order?.order_items || []
if (items.length === 0) return '未知商品'
if (items.length === 1) {
return items[0].product_name
} else {
return `${items[0].product_name}等${items.length}件商品`
}
}
// 格式化时间
const formatTime = (timeStr?: string): string => {
if (!timeStr) return ''
const date = new Date(timeStr)
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
return `${month}-${day}`
}
// 获取时间线步骤
const getTimelineSteps = (refund: RefundType): Array<any> => {
const steps = [
{ status: 0, title: '提交申请', time: refund.created_at },
{ status: 1, title: '商家处理', time: '' },
{ status: 3, title: '退款完成', time: '' }
]
// 如果有状态历史,更新时间和描述
if (refund.status_history) {
refund.status_history.forEach(history => {
if (history.status === 1 || history.status === 2) {
steps[1].time = history.created_at
steps[1].desc = history.remark
} else if (history.status === 3) {
steps[2].time = history.created_at
steps[2].desc = history.remark
}
})
}
// 标记当前状态
return steps.map((step, index) => ({
...step,
active: index === getCurrentStepIndex(refund.status),
completed: index < getCurrentStepIndex(refund.status)
}))
}
// 获取当前步骤索引
const getCurrentStepIndex = (status: number): number => {
switch (status) {
case 1: return 0 // 待处理
case 2: return 1 // 处理中
case 3: return 2 // 已完成
case 4: return 0 // 已取消
case 5: return 1 // 已拒绝
default: return 0
}
}
// 切换标签页
const changeTab = (tab: string) => {
activeTab.value = tab
}
// 加载更多
const loadMore = () => {
if (hasMore.value && !isLoading.value) {
loadRefunds(true)
}
}
// 查看订单
const viewOrder = (orderId: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/order-detail?id=${orderId}`
})
}
// 取消退款申请
const cancelRefund = (refund: RefundType) => {
uni.showModal({
title: '取消申请',
content: '确定要取消这个退款申请吗?',
success: async (res) => {
if (res.confirm) {
try {
const { error } = await supa
.from('refunds')
.update({
status: 4, // 已取消
status_history: [...(refund.status_history || []), {
status: 4,
remark: '用户取消申请',
created_at: new Date().toISOString()
}]
})
.eq('id', refund.id)
if (error !== null) {
throw error
}
refund.status = 4
loadTabCounts() // 重新加载计数
uni.showToast({
title: '已取消',
icon: 'success'
})
} catch (err) {
console.error('取消退款失败:', err)
uni.showToast({
title: '取消失败',
icon: 'none'
})
}
}
}
})
}
// 联系客服
const contactService = (refund: RefundType) => {
uni.navigateTo({
url: `/pages/mall/service/chat?refundId=${refund.id}`
})
}
// 评价服务
const reviewRefund = (refund: RefundType) => {
uni.navigateTo({
url: `/pages/mall/consumer/refund-review?id=${refund.id}`
})
}
// 删除记录
const deleteRefund = (refund: RefundType) => {
uni.showModal({
title: '删除记录',
content: '确定要删除这个售后记录吗?',
success: async (res) => {
if (res.confirm) {
try {
const { error } = await supa
.from('refunds')
.delete()
.eq('id', refund.id)
if (error !== null) {
throw error
}
const index = refunds.value.findIndex(r => r.id === refund.id)
if (index !== -1) {
refunds.value.splice(index, 1)
refunds.value = [...refunds.value]
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
} catch (err) {
console.error('删除记录失败:', err)
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
}
}
})
}
// 申请售后
const applyRefund = () => {
uni.navigateTo({
url: '/pages/mall/consumer/apply-refund'
})
}
// 查看订单
const goToOrders = () => {
uni.switchTab({
url: '/pages/mall/consumer/orders'
})
}
// 返回
const goBack = () => {
uni.navigateBack()
}
</script>
<style scoped>
.refund-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.refund-header {
background-color: #ffffff;
padding: 15px;
display: flex;
align-items: center;
border-bottom: 1px solid #e5e5e5;
}
.back-btn {
font-size: 24px;
color: #333333;
padding: 5px;
margin-right: 15px;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.refund-tabs {
background-color: #ffffff;
display: flex;
border-bottom: 1px solid #e5e5e5;
}
.refund-tab {
flex: 1;
padding: 15px;
text-align: center;
position: relative;
}
.refund-tab.active {
color: #007aff;
}
.refund-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #007aff;
}
.tab-text {
font-size: 16px;
color: #666666;
}
.refund-tab.active .tab-text {
color: #007aff;
font-weight: bold;
}
.tab-badge {
position: absolute;
top: 10px;
right: 20px;
background-color: #ff4757;
color: #ffffff;
font-size: 10px;
padding: 2px 5px;
border-radius: 8px;
min-width: 16px;
text-align: center;
}
.refund-content {
flex: 1;
}
.empty-refunds {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
background-color: #ffffff;
}
.empty-icon {
font-size: 80px;
margin-bottom: 20px;
}
.empty-text {
font-size: 16px;
color: #666666;
margin-bottom: 10px;
}
.empty-subtext {
font-size: 14px;
color: #999999;
margin-bottom: 30px;
}
.go-orders-btn {
background-color: #007aff;
color: #ffffff;
padding: 10px 40px;
border-radius: 25px;
font-size: 14px;
border: none;
}
.refund-item {
background-color: #ffffff;
margin-bottom: 10px;
padding: 15px;
}
.refund-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #f5f5f5;
}
.refund-no {
font-size: 14px;
color: #333333;
}
.refund-status {
font-size: 14px;
padding: 4px 10px;
border-radius: 12px;
color: #ffffff;
}
.status-pending {
background-color: #ffa726;
}
.status-processing {
background-color: #2196f3;
}
.status-completed {
background-color: #4caf50;
}
.status-cancelled {
background-color: #9e9e9e;
}
.status-rejected {
background-color: #f44336;
}
.order-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #f5f5f5;
}
.order-no {
font-size: 13px;
color: #666666;
}
.order-time {
font-size: 12px;
color: #999999;
}
.product-info {
display: flex;
margin-bottom: 15px;
}
.product-image {
width: 60px;
height: 60px;
border-radius: 5px;
margin-right: 15px;
}
.product-details {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.product-name {
font-size: 14px;
color: #333333;
line-height: 1.4;
margin-bottom: 5px;
}
.refund-reason {
font-size: 12px;
color: #666666;
margin-bottom: 8px;
}
.refund-amount {
display: flex;
align-items: baseline;
}
.amount-label {
font-size: 13px;
color: #666666;
margin-right: 5px;
}
.amount-value {
font-size: 16px;
color: #ff4757;
font-weight: bold;
}
.timeline {
padding: 15px 0;
border-top: 1px solid #f5f5f5;
}
.timeline-step {
display: flex;
margin-bottom: 15px;
}
.timeline-step:last-child {
margin-bottom: 0;
}
.step-dot {
width: 12px;
height: 12px;
border-radius: 6px;
border: 2px solid #e5e5e5;
margin-right: 15px;
position: relative;
top: 3px;
}
.step-dot.active {
border-color: #007aff;
background-color: #007aff;
}
.step-dot.completed {
border-color: #4caf50;
background-color: #4caf50;
}
.step-info {
flex: 1;
}
.step-title {
font-size: 14px;
color: #333333;
font-weight: bold;
margin-bottom: 3px;
display: block;
}
.step-time {
font-size: 12px;
color: #999999;
margin-bottom: 3px;
display: block;
}
.step-desc {
font-size: 12px;
color: #666666;
display: block;
}
.refund-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
padding-top: 15px;
border-top: 1px solid #f5f5f5;
}
.action-btn {
padding: 8px 15px;
border-radius: 15px;
font-size: 12px;
border: 1px solid;
background-color: #ffffff;
}
.action-btn.cancel {
border-color: #666666;
color: #666666;
}
.action-btn.contact {
border-color: #007aff;
color: #007aff;
}
.action-btn.review {
border-color: #ffa726;
color: #ffa726;
}
.action-btn.delete {
border-color: #f44336;
color: #f44336;
}
.loading-more,
.no-more {
padding: 20px;
text-align: center;
background-color: #ffffff;
}
.loading-text,
.no-more-text {
color: #999999;
font-size: 14px;
}
.apply-btn-container {
background-color: #ffffff;
padding: 15px;
border-top: 1px solid #e5e5e5;
}
.apply-btn {
background-color: #007aff;
color: #ffffff;
height: 50px;
border-radius: 25px;
font-size: 16px;
font-weight: bold;
border: none;
}
</style>

View File

@@ -0,0 +1,775 @@
<!-- 评价页面 -->
<template>
<view class="review-page">
<!-- 顶部栏 -->
<view class="review-header">
<text class="back-btn" @click="goBack"></text>
<text class="header-title">评价商品</text>
</view>
<scroll-view class="review-content" scroll-y>
<!-- 订单信息 -->
<view class="order-section">
<text class="order-no">订单号: {{ order?.order_no }}</text>
<text class="order-time">下单时间: {{ formatTime(order?.created_at) }}</text>
</view>
<!-- 商品评价 -->
<view class="products-section">
<view v-for="(item, index) in orderItems" :key="item.id" class="product-review">
<view class="product-header">
<image class="product-image" :src="item.product_image || '/static/default-product.png'" />
<view class="product-info">
<text class="product-name">{{ item.product_name }}</text>
<text v-if="item.sku_specifications" class="product-spec">{{ getSpecText(item.sku_specifications) }}</text>
</view>
</view>
<!-- 评分 -->
<view class="rating-section">
<text class="rating-label">评分</text>
<view class="rating-stars">
<text v-for="star in 5"
:key="star"
class="star-icon"
:class="{ active: star <= ratings[index] }"
@click="setRating(index, star)">
</text>
</view>
<text class="rating-text">{{ getRatingText(ratings[index]) }}</text>
</view>
<!-- 评价内容 -->
<view class="content-section">
<textarea class="review-textarea"
v-model="contents[index]"
placeholder="请写下您的使用感受,分享给其他小伙伴吧"
maxlength="500" />
<text class="word-count">{{ contents[index]?.length || 0 }}/500</text>
</view>
<!-- 图片上传 -->
<view class="images-section">
<text class="images-label">上传图片(可选)</text>
<view class="images-grid">
<view v-for="(image, imgIndex) in images[index]"
:key="imgIndex"
class="image-item">
<image class="uploaded-image" :src="image" />
<text class="delete-image" @click="deleteImage(index, imgIndex)">×</text>
</view>
<view v-if="images[index].length < 9"
class="upload-btn"
@click="uploadImage(index)">
<text class="upload-icon">+</text>
<text class="upload-text">添加图片</text>
</view>
</view>
</view>
<!-- 匿名评价 -->
<view class="anonymous-section">
<view class="anonymous-switch">
<text class="switch-label">匿名评价</text>
<switch :checked="anonymous" @change="toggleAnonymous" />
</view>
<text class="anonymous-tip">评价内容对其他用户不可见</text>
</view>
</view>
</view>
<!-- 店铺评价 -->
<view v-if="merchant" class="merchant-section">
<text class="section-title">店铺评价</text>
<view class="merchant-rating">
<text class="rating-item">商品描述相符</text>
<view class="rating-stars small">
<text v-for="star in 5"
:key="star"
class="star-icon"
:class="{ active: star <= merchantRating.description }"
@click="setMerchantRating('description', star)">
</text>
</view>
</view>
<view class="merchant-rating">
<text class="rating-item">物流服务</text>
<view class="rating-stars small">
<text v-for="star in 5"
:key="star"
class="star-icon"
:class="{ active: star <= merchantRating.logistics }"
@click="setMerchantRating('logistics', star)">
</text>
</view>
</view>
<view class="merchant-rating">
<text class="rating-item">服务态度</text>
<view class="rating-stars small">
<text v-for="star in 5"
:key="star"
class="star-icon"
:class="{ active: star <= merchantRating.service }"
@click="setMerchantRating('service', star)">
</text>
</view>
</view>
</view>
<!-- 评价提示 -->
<view class="tips-section">
<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-item">4. 优质评价可获得积分奖励</text>
</view>
</scroll-view>
<!-- 提交按钮 -->
<view class="submit-section">
<button class="submit-btn"
:class="{ disabled: !canSubmit || isSubmitting }"
@click="submitReview">
<text v-if="!isSubmitting" class="submit-text">提交评价</text>
<text v-else class="submit-text">提交中...</text>
</button>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
type OrderItemType = {
id: string
product_id: string
product_name: string
product_image: string
sku_specifications: any
price: number
quantity: number
}
type MerchantType = {
id: string
shop_name: string
rating: number
}
const orderId = ref<string>('')
const order = ref<any>(null)
const orderItems = ref<Array<OrderItemType>>([])
const merchant = ref<MerchantType | null>(null)
const ratings = ref<Array<number>>([])
const contents = ref<Array<string>>([])
const images = ref<Array<Array<string>>>([])
const anonymous = ref<boolean>(false)
const merchantRating = ref({
description: 5,
logistics: 5,
service: 5
})
const isSubmitting = ref<boolean>(false)
// 计算属性
const canSubmit = computed(() => {
// 检查是否所有商品都已评分
if (ratings.value.length === 0) return false
return ratings.value.every(rating => rating > 0)
})
// 生命周期
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options as any
if (options.orderId) {
orderId.value = options.orderId
loadOrderData()
}
})
// 加载订单数据
const loadOrderData = async () => {
try {
const { data: orderData, error: orderError } = await supa
.from('orders')
.select('*')
.eq('id', orderId.value)
.single()
if (orderError !== null) {
console.error('加载订单失败:', orderError)
return
}
order.value = orderData
// 加载订单商品
const { data: itemsData, error: itemsError } = await supa
.from('order_items')
.select(`
*,
product:product_id(images)
`)
.eq('order_id', orderId.value)
if (itemsError !== null) {
console.error('加载订单商品失败:', itemsError)
return
}
orderItems.value = (itemsData || []).map((item: any) => ({
...item,
product_image: item.product?.images?.[0] || '/static/default-product.png'
}))
// 初始化评分和内容数组
const count = orderItems.value.length
ratings.value = new Array(count).fill(5)
contents.value = new Array(count).fill('')
images.value = new Array(count).fill([])
// 加载商家信息
if (order.value.merchant_id) {
const { data: merchantData, error: merchantError } = await supa
.from('merchants')
.select('id, shop_name, rating')
.eq('id', order.value.merchant_id)
.single()
if (!merchantError) {
merchant.value = merchantData
}
}
} catch (err) {
console.error('加载订单数据异常:', err)
}
}
// 格式化时间
const formatTime = (timeStr?: string): string => {
if (!timeStr) return ''
const date = new Date(timeStr)
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
return `${year}-${month}-${day}`
}
// 获取规格文本
const getSpecText = (specs: any): string => {
if (!specs) return ''
if (typeof specs === 'object') {
return Object.keys(specs)
.map(key => `${key}: ${specs[key]}`)
.join('; ')
}
return String(specs)
}
// 获取评分文本
const getRatingText = (rating: number): string => {
const texts = ['非常差', '差', '一般', '好', '非常好']
return texts[rating - 1] || '未评价'
}
// 设置商品评分
const setRating = (index: number, rating: number) => {
ratings.value[index] = rating
ratings.value = [...ratings.value]
}
// 设置商家评分
const setMerchantRating = (type: keyof typeof merchantRating.value, rating: number) => {
merchantRating.value[type] = rating
merchantRating.value = { ...merchantRating.value }
}
// 切换匿名
const toggleAnonymous = (event: any) => {
anonymous.value = event.detail.value
}
// 上传图片
const uploadImage = async (index: number) => {
// 检查图片数量限制
if (images.value[index].length >= 9) {
uni.showToast({
title: '最多上传9张图片',
icon: 'none'
})
return
}
// 使用uni.chooseImage选择图片
uni.chooseImage({
count: 9 - images.value[index].length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFiles = res.tempFilePaths
// 模拟上传过程
uni.showLoading({
title: '上传中...'
})
setTimeout(() => {
// 这里应该调用真实的上传接口
images.value[index].push(...tempFiles)
images.value = [...images.value]
uni.hideLoading()
uni.showToast({
title: '上传成功',
icon: 'success'
})
}, 1000)
}
})
}
// 删除图片
const deleteImage = (index: number, imgIndex: number) => {
images.value[index].splice(imgIndex, 1)
images.value = [...images.value]
}
// 提交评价
const submitReview = async () => {
if (!canSubmit.value || isSubmitting.value) return
isSubmitting.value = true
try {
const userId = getCurrentUserId()
if (!userId) {
uni.showToast({
title: '用户信息错误',
icon: 'none'
})
return
}
// 提交商品评价
/* const productReviews = orderItems.value.map((item, index) => ({
user_id: userId,
product_id: item.product_id,
order_id: orderId.value,
rating: ratings.value[index],
content: contents.value[index] || '',
images: images.value[index],
is_anonymous: anonymous.value,
is_valid: true,
created_at: new Date().toISOString()
}))
const { error: reviewsError } = await supa
.from('product_reviews')
.insert(productReviews)
if (reviewsError !== null) {
throw reviewsError
}
// 提交店铺评价
if (merchant.value) {
const merchantReview = {
user_id: userId,
merchant_id: merchant.value.id,
order_id: orderId.value,
description_rating: merchantRating.value.description,
logistics_rating: merchantRating.value.logistics,
service_rating: merchantRating.value.service,
average_rating: (
merchantRating.value.description +
merchantRating.value.logistics +
merchantRating.value.service
) / 3,
is_anonymous: anonymous.value,
created_at: new Date().toISOString()
}
const { error: merchantError } = await supa
.from('merchant_reviews')
.insert(merchantReview)
if (merchantError !== null) {
console.error('提交店铺评价失败:', merchantError)
}
}
// 更新订单状态为已评价
const { error: orderError } = await supa
.from('orders')
.update({ status: 4 }) // 已完成
.eq('id', orderId.value)
if (orderError !== null) {
console.error('更新订单状态失败:', orderError)
} */
// MOCK SUBMIT
await new Promise(resolve => setTimeout(resolve, 1000))
// 显示成功提示
uni.showToast({
title: '评价成功',
icon: 'success',
duration: 2000
})
// 跳转到评价成功页面
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (err) {
console.error('提交评价失败:', err)
uni.showToast({
title: '提交失败',
icon: 'none'
})
} finally {
isSubmitting.value = false
}
}
// 获取当前用户ID
const getCurrentUserId = (): string => {
const userStore = uni.getStorageSync('userInfo')
return userStore?.id || ''
}
// 返回
const goBack = () => {
uni.navigateBack()
}
</script>
<style scoped>
.review-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.review-header {
background-color: #ffffff;
padding: 15px;
display: flex;
align-items: center;
border-bottom: 1px solid #e5e5e5;
}
.back-btn {
font-size: 24px;
color: #333333;
padding: 5px;
margin-right: 15px;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.review-content {
flex: 1;
}
.order-section {
background-color: #ffffff;
padding: 15px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.order-no {
font-size: 14px;
color: #333333;
}
.order-time {
font-size: 12px;
color: #999999;
}
.products-section {
background-color: #ffffff;
margin-bottom: 10px;
}
.product-review {
padding: 15px;
border-bottom: 1px solid #f5f5f5;
}
.product-review:last-child {
border-bottom: none;
}
.product-header {
display: flex;
margin-bottom: 20px;
}
.product-image {
width: 60px;
height: 60px;
border-radius: 5px;
margin-right: 15px;
}
.product-info {
flex: 1;
}
.product-name {
font-size: 14px;
color: #333333;
line-height: 1.4;
margin-bottom: 5px;
display: block;
}
.product-spec {
font-size: 12px;
color: #999999;
display: block;
}
.rating-section {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.rating-label {
font-size: 14px;
color: #333333;
margin-right: 15px;
}
.rating-stars {
display: flex;
gap: 10px;
}
.rating-stars.small {
gap: 5px;
}
.star-icon {
font-size: 24px;
color: #cccccc;
}
.star-icon.active {
color: #ffa726;
}
.rating-text {
margin-left: 15px;
font-size: 14px;
color: #666666;
}
.content-section {
margin-bottom: 15px;
}
.review-textarea {
width: 100%;
min-height: 80px;
padding: 10px;
border: 1px solid #e5e5e5;
border-radius: 8px;
font-size: 14px;
color: #333333;
line-height: 1.4;
}
.word-count {
display: block;
text-align: right;
font-size: 12px;
color: #999999;
margin-top: 5px;
}
.images-section {
margin-bottom: 15px;
}
.images-label {
display: block;
font-size: 14px;
color: #333333;
margin-bottom: 10px;
}
.images-grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.image-item {
width: 70px;
height: 70px;
border-radius: 5px;
overflow: hidden;
position: relative;
}
.uploaded-image {
width: 100%;
height: 100%;
}
.delete-image {
position: absolute;
top: 2px;
right: 2px;
width: 16px;
height: 16px;
border-radius: 8px;
background-color: rgba(0, 0, 0, 0.5);
color: #ffffff;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.upload-btn {
width: 70px;
height: 70px;
border: 1px dashed #cccccc;
border-radius: 5px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.upload-icon {
font-size: 24px;
color: #999999;
margin-bottom: 5px;
}
.upload-text {
font-size: 10px;
color: #999999;
}
.anonymous-section {
margin-bottom: 15px;
}
.anonymous-switch {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
.switch-label {
font-size: 14px;
color: #333333;
}
.anonymous-tip {
display: block;
font-size: 12px;
color: #999999;
}
.merchant-section {
background-color: #ffffff;
padding: 15px;
margin-bottom: 10px;
}
.section-title {
display: block;
font-size: 16px;
font-weight: bold;
color: #333333;
margin-bottom: 15px;
}
.merchant-rating {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.rating-item {
font-size: 14px;
color: #333333;
}
.tips-section {
background-color: #ffffff;
padding: 15px;
margin-bottom: 10px;
}
.tip-title {
display: block;
font-size: 16px;
font-weight: bold;
color: #333333;
margin-bottom: 10px;
}
.tip-item {
display: block;
font-size: 12px;
color: #666666;
line-height: 1.6;
margin-bottom: 5px;
}
.tip-item:last-child {
margin-bottom: 0;
}
.submit-section {
background-color: #ffffff;
padding: 15px;
border-top: 1px solid #e5e5e5;
}
.submit-btn {
background-color: #007aff;
color: #ffffff;
height: 50px;
border-radius: 25px;
font-size: 16px;
font-weight: bold;
border: none;
}
.submit-btn.disabled {
background-color: #cccccc;
opacity: 0.6;
}
</style>

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -0,0 +1,375 @@
<template>
<view class="shop-detail-page">
<!-- 店铺头部信息 -->
<view class="shop-header">
<image :src="merchant.shop_banner || '/static/default-banner.png'" class="shop-banner" mode="aspectFill" />
<view class="shop-info-card">
<image :src="merchant.shop_logo || '/static/default-shop.png'" class="shop-logo" />
<view class="shop-basic-info">
<text class="shop-name">{{ merchant.shop_name }}</text>
<view class="shop-stats">
<text class="stat-item">⭐ {{ merchant.rating.toFixed(1) }}</text>
<text class="stat-item">销量 {{ merchant.total_sales }}</text>
</view>
</view>
<button class="follow-btn" @click="toggleFollow">{{ isFollowed ? '已关注' : '+ 关注' }}</button>
</view>
<text class="shop-desc">{{ merchant.shop_description || '这家店很懒,什么都没写~' }}</text>
</view>
<!-- 商品列表 -->
<view class="product-section">
<view class="section-title">全部商品</view>
<view class="product-grid">
<view v-for="product in products" :key="product.id" class="product-item" @click="goToProduct(product.id)">
<image :src="product.images[0]" class="product-image" mode="aspectFill" />
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<view class="price-row">
<view class="price-left">
<text class="product-price">¥{{ product.price }}</text>
<text class="product-sales">已售 {{ product.sales }}</text>
</view>
<view class="cart-btn" @click.stop="addToCart(product)">
<text class="cart-icon">🛒</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { MerchantType, ProductType } from '@/types/mall-types.uts'
const merchant = ref<MerchantType>({
id: '',
user_id: '',
shop_name: '',
shop_logo: '',
shop_banner: '',
shop_description: '',
contact_name: '',
contact_phone: '',
shop_status: 0,
rating: 0,
total_sales: 0,
created_at: ''
} as MerchantType)
const products = ref<ProductType[]>([])
const isFollowed = ref(false)
onMounted(() => {
const pages = getCurrentPages()
const options = pages[pages.length - 1].options as any
const merchantId = options['merchantId'] as string
if (merchantId) {
loadShopData(merchantId)
loadShopProducts(merchantId)
}
})
const loadShopData = (id: string) => {
// 模拟加载店铺数据
merchant.value = {
id: id,
user_id: 'user_001',
shop_name: '优质好店',
shop_logo: '/static/shop-logo.png',
shop_banner: '/static/shop-banner.png',
shop_description: '专注品质生活,为您提供最优质的商品和服务。',
contact_name: '店主小王',
contact_phone: '13800138000',
shop_status: 1,
rating: 4.8,
total_sales: 15680,
created_at: '2023-06-01'
}
}
const loadShopProducts = (id: string) => {
// 模拟加载店铺商品列表
products.value = [
{
id: 'prod_001',
merchant_id: id,
category_id: 'cat_001',
name: '精选好物商品 A',
description: '商品描述 A',
images: ['/static/product1.jpg'],
price: 199.99,
original_price: 299.99,
stock: 100,
sales: 1256,
status: 1,
created_at: '2024-01-15'
},
{
id: 'prod_002',
merchant_id: id,
category_id: 'cat_001',
name: '精选好物商品 B',
description: '商品描述 B',
images: ['/static/product2.jpg'],
price: 299.00,
original_price: 399.00,
stock: 50,
sales: 856,
status: 1,
created_at: '2024-01-16'
},
{
id: 'prod_003',
merchant_id: id,
category_id: 'cat_002',
name: '精选好物商品 C',
description: '商品描述 C',
images: ['/static/product3.jpg'],
price: 99.00,
original_price: 129.00,
stock: 200,
sales: 3256,
status: 1,
created_at: '2024-01-17'
}
]
}
const toggleFollow = () => {
isFollowed.value = !isFollowed.value
uni.showToast({
title: isFollowed.value ? '关注成功' : '已取消关注',
icon: 'none'
})
}
const addToCart = (product: ProductType) => {
// 获取现有购物车数据
const cartData = uni.getStorageSync('cart')
let cartItems: any[] = []
if (cartData) {
try {
cartItems = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
}
}
// 检查商品是否已存在
const existingItem = cartItems.find((item: any) => item.productId === product.id)
if (existingItem) {
existingItem.quantity++
} else {
// 添加新商品
cartItems.push({
id: product.id, // 简单使用产品ID作为购物车ID实际可能有规格
productId: product.id,
shopId: merchant.value.id,
shopName: merchant.value.shop_name,
name: product.name,
price: product.price,
image: product.images[0],
spec: '默认规格',
quantity: 1,
selected: true
})
}
// 保存回存储
uni.setStorageSync('cart', JSON.stringify(cartItems))
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
}
const goToProduct = (id: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${id}`
})
}
</script>
<style>
.shop-detail-page {
background-color: #f5f5f5;
min-height: 100vh;
}
.shop-header {
background-color: #fff;
padding-bottom: 20px;
margin-bottom: 10px;
}
.shop-banner {
width: 100%;
height: 150px;
background-color: #eee;
}
.shop-info-card {
display: flex;
flex-direction: row;
align-items: center;
padding: 0 15px;
margin-top: -30px; /* Logo 向上重叠 banner */
position: relative;
z-index: 1;
}
.shop-logo {
width: 60px;
height: 60px;
border-radius: 8px;
border: 2px solid #fff;
background-color: #fff;
margin-right: 12px;
}
.shop-basic-info {
flex: 1;
display: flex;
flex-direction: column;
padding-top: 30px; /* 给 logo 上浮留空间 */
}
.shop-name {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.shop-stats {
display: flex;
flex-direction: row;
}
.stat-item {
font-size: 12px;
color: #666;
margin-right: 12px;
background-color: #f0f0f0;
padding: 2px 6px;
border-radius: 4px;
}
.follow-btn {
font-size: 14px;
background-color: #ff4444;
color: white;
padding: 6px 16px;
border-radius: 20px;
margin-top: 30px; /* 对齐 */
line-height: 1.5;
}
.shop-desc {
display: block;
font-size: 14px;
color: #666;
padding: 10px 15px 0;
line-height: 1.4;
}
.product-section {
padding: 15px;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 15px;
padding-left: 8px;
border-left: 4px solid #ff4444;
}
.product-grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.product-item {
width: calc(50% - 5px);
background-color: white;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
margin-bottom: 10px;
}
.product-image {
width: 100%;
height: 170px;
background-color: #f5f5f5;
}
.product-info {
padding: 10px;
display: flex;
flex-direction: column;
}
.product-name {
font-size: 14px;
color: #333;
margin-bottom: 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
height: 40px;
line-height: 20px;
}
.price-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.price-left {
display: flex;
flex-direction: row;
align-items: baseline;
}
.cart-btn {
width: 24px;
height: 24px;
background-color: #ff4444;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.cart-icon {
font-size: 14px;
color: white;
}
.product-price {
font-size: 16px;
color: #ff4444;
font-weight: bold;
}
.product-sales {
font-size: 12px;
color: #999;
}
</style>

View File

@@ -0,0 +1,950 @@
<!-- 钱包页面 -->
<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>
</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>
<view class="balance-actions">
<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>
</view>
<view class="stat-item">
<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>
</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>
</view>
<view class="action-item" @click="goToRedPackets">
<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>
</view>
<view class="action-item" @click="goToBankCards">
<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>
<view class="filter-tabs">
<text :class="['filter-tab', { active: activeFilter === 'all' }]"
@click="changeFilter('all')">全部</text>
<text :class="['filter-tab', { active: activeFilter === 'income' }]"
@click="changeFilter('income')">收入</text>
<text :class="['filter-tab', { active: activeFilter === 'expense' }]"
@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>
</view>
<!-- 交易列表 -->
<view class="transactions-list">
<view v-for="transaction in transactions"
:key="transaction.id"
class="transaction-item">
<view class="transaction-left">
<text class="transaction-icon">{{ getTransactionIcon(transaction.type) }}</text>
<view class="transaction-info">
<text class="transaction-title">{{ getTransactionTitle(transaction.type) }}</text>
<text class="transaction-time">{{ formatTime(transaction.created_at) }}</text>
<text v-if="transaction.remark" class="transaction-remark">{{ transaction.remark }}</text>
</view>
</view>
<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) }}
</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>
</view>
<view v-if="!hasMore && transactions.length > 0" class="no-more">
<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>
</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>
</view>
<view class="popup-body">
<text class="amount-label">充值金额</text>
<view class="amount-input">
<text class="currency-symbol">¥</text>
<input class="amount-field"
v-model="rechargeAmount"
type="number"
placeholder="请输入充值金额"
focus />
</view>
<view class="quick-amounts">
<text v-for="amount in quickAmounts"
:key="amount"
:class="['quick-amount', { active: rechargeAmount === amount.toString() }]"
@click="selectQuickAmount(amount)">
¥{{ amount }}
</text>
</view>
<text class="recharge-tip">单笔充值最低10元最高5000元</text>
</view>
<view class="popup-footer">
<button class="cancel-btn" @click="closeRechargePopup">取消</button>
<button class="confirm-btn"
:class="{ disabled: !canRecharge }"
@click="confirmRecharge">
确认充值
</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed, watch } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
type WalletType = {
id: string
user_id: string
balance: number
total_recharge: number
total_consume: number
total_withdraw: number
updated_at: string
}
type TransactionType = {
id: string
user_id: string
change_amount: number
current_balance: number
change_type: string // 'recharge' | 'consume' | 'withdraw' | 'refund' | 'reward'
related_id: string | null
remark: string | null
created_at: string
}
type StatsType = {
totalRecharge: number
totalConsume: number
totalWithdraw: number
}
const balance = ref<number>(0)
const stats = ref<StatsType>({
totalRecharge: 0,
totalConsume: 0,
totalWithdraw: 0
})
const transactions = ref<Array<TransactionType>>([])
const activeFilter = ref<string>('all')
const isLoading = ref<boolean>(false)
const currentPage = ref<number>(1)
const pageSize = ref<number>(20)
const hasMore = ref<boolean>(true)
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) {
uni.navigateTo({
url: '/pages/user/login'
})
return
}
await Promise.all([
loadBalance(),
loadTransactions()
])
}
// 加载余额信息
const loadBalance = async () => {
const userId = getCurrentUserId()
if (!userId) return
try {
const { data, error } = await supa
.from('user_wallets')
.select('*')
.eq('user_id', userId)
.single()
if (error !== null) {
console.error('加载钱包失败:', error)
return
}
if (data) {
balance.value = data.balance || 0
stats.value = {
totalRecharge: data.total_recharge || 0,
totalConsume: data.total_consume || 0,
totalWithdraw: data.total_withdraw || 0
}
}
} catch (err) {
console.error('加载钱包异常:', err)
}
}
// 加载交易记录
const loadTransactions = async (loadMore: boolean = false) => {
if (isLoading.value || (!hasMore.value && loadMore)) {
return
}
isLoading.value = true
try {
const userId = getCurrentUserId()
if (!userId) return
const page = loadMore ? currentPage.value + 1 : 1
let query = supa
.from('balance_records')
.select('*')
.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)
return
}
const newTransactions = data || []
if (loadMore) {
transactions.value.push(...newTransactions)
currentPage.value = page
} else {
transactions.value = newTransactions
currentPage.value = 1
}
hasMore.value = newTransactions.length === pageSize.value
} catch (err) {
console.error('加载交易记录异常:', err)
} finally {
isLoading.value = false
}
}
// 获取当前用户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: '📤'
}
return icons[type] || '💰'
}
// 获取交易标题
const getTransactionTitle = (type: string): string => {
const titles: Record<string, string> = {
recharge: '账户充值',
consume: '商品消费',
withdraw: '余额提现',
refund: '订单退款',
reward: '活动奖励',
income: '收入',
expense: '支出'
}
return titles[type] || '交易'
}
// 格式化时间
const formatTime = (timeStr: string): string => {
const date = new Date(timeStr)
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
return `${month}-${day} ${hours}:${minutes}`
}
// 显示更多操作
const showMoreActions = () => {
uni.showActionSheet({
itemList: ['交易记录', '安全设置', '帮助中心'],
success: (res) => {
switch (res.tapIndex) {
case 0:
// 交易记录已经在当前页
break
case 1:
uni.navigateTo({
url: '/pages/mall/consumer/settings'
})
break
case 2:
uni.navigateTo({
url: '/pages/info/help'
})
break
}
}
})
}
// 充值
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}`
})
closeRechargePopup()
}
// 关闭充值弹窗
const closeRechargePopup = () => {
showRechargePopup.value = false
rechargeAmount.value = ''
}
// 返回
const goBack = () => {
uni.navigateBack()
}
</script>
<style scoped>
.wallet-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.wallet-header {
background-color: #ffffff;
padding: 15px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e5e5e5;
}
.back-btn {
font-size: 24px;
color: #333333;
padding: 5px;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.more-btn {
color: #333333;
font-size: 20px;
padding: 5px;
}
.wallet-content {
flex: 1;
}
.balance-overview {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30px 20px;
color: #ffffff;
}
.balance-label {
display: block;
font-size: 14px;
opacity: 0.9;
margin-bottom: 10px;
text-align: center;
}
.balance-value {
display: block;
font-size: 36px;
font-weight: bold;
margin-bottom: 20px;
text-align: center;
}
.balance-actions {
display: flex;
gap: 20px;
}
.action-btn {
flex: 1;
height: 40px;
border-radius: 20px;
font-size: 14px;
font-weight: bold;
border: none;
}
.action-btn.recharge {
background-color: #ffffff;
color: #667eea;
}
.action-btn.withdraw {
background-color: rgba(255, 255, 255, 0.2);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.5);
}
.assets-stats {
background-color: #ffffff;
padding: 20px;
display: flex;
justify-content: space-between;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.stat-item {
flex: 1;
text-align: center;
}
.stat-label {
display: block;
font-size: 12px;
color: #666666;
margin-bottom: 8px;
}
.stat-value {
display: block;
font-size: 16px;
font-weight: bold;
color: #333333;
}
.quick-actions {
background-color: #ffffff;
margin-top: 10px;
padding: 20px;
}
.action-grid {
display: flex;
justify-content: space-between;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
}
.action-icon {
font-size: 28px;
margin-bottom: 8px;
}
.action-text {
font-size: 12px;
color: #666666;
}
.transactions-section {
background-color: #ffffff;
margin-top: 10px;
padding: 15px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #333333;
}
.filter-tabs {
display: flex;
gap: 15px;
}
.filter-tab {
font-size: 14px;
color: #666666;
padding: 5px 0;
position: relative;
}
.filter-tab.active {
color: #007aff;
font-weight: bold;
}
.filter-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #007aff;
}
.empty-transactions {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
}
.empty-icon {
font-size: 60px;
margin-bottom: 20px;
}
.empty-text {
font-size: 16px;
color: #666666;
margin-bottom: 10px;
}
.empty-subtext {
font-size: 14px;
color: #999999;
}
.transactions-list {
display: flex;
flex-direction: column;
}
.transaction-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 15px 0;
border-bottom: 1px solid #f5f5f5;
}
.transaction-item:last-child {
border-bottom: none;
}
.transaction-left {
display: flex;
align-items: flex-start;
}
.transaction-icon {
font-size: 24px;
margin-right: 15px;
}
.transaction-info {
display: flex;
flex-direction: column;
}
.transaction-title {
font-size: 14px;
color: #333333;
font-weight: bold;
margin-bottom: 5px;
}
.transaction-time {
font-size: 12px;
color: #999999;
margin-bottom: 3px;
}
.transaction-remark {
font-size: 12px;
color: #666666;
}
.transaction-right {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.transaction-amount {
font-size: 16px;
font-weight: bold;
margin-bottom: 5px;
}
.transaction-amount.income {
color: #4caf50;
}
.transaction-amount.expense {
color: #333333;
}
.transaction-balance {
font-size: 12px;
color: #999999;
}
.loading-more,
.no-more {
padding: 20px;
text-align: center;
}
.loading-text,
.no-more-text {
color: #999999;
font-size: 14px;
}
.security-tips {
background-color: #ffffff;
margin-top: 10px;
padding: 20px;
}
.tip-title {
display: block;
font-size: 16px;
font-weight: bold;
color: #333333;
margin-bottom: 15px;
}
.tip-item {
display: block;
font-size: 12px;
color: #666666;
line-height: 1.6;
margin-bottom: 8px;
}
.tip-item:last-child {
margin-bottom: 0;
}
.recharge-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
}
.popup-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
}
.popup-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
border-top-left-radius: 15px;
border-top-right-radius: 15px;
padding: 20px;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e5e5e5;
}
.popup-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.popup-close {
font-size: 24px;
color: #999999;
padding: 5px;
}
.popup-body {
margin-bottom: 20px;
}
.amount-label {
display: block;
font-size: 14px;
color: #333333;
margin-bottom: 10px;
}
.amount-input {
display: flex;
align-items: center;
margin-bottom: 20px;
padding: 10px;
border: 1px solid #e5e5e5;
border-radius: 8px;
}
.currency-symbol {
font-size: 20px;
color: #333333;
margin-right: 10px;
}
.amount-field {
flex: 1;
font-size: 24px;
font-weight: bold;
color: #333333;
}
.quick-amounts {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
}
.quick-amount {
padding: 8px 15px;
border: 1px solid #e5e5e5;
border-radius: 15px;
font-size: 14px;
color: #333333;
}
.quick-amount.active {
background-color: #007aff;
color: #ffffff;
border-color: #007aff;
}
.recharge-tip {
display: block;
font-size: 12px;
color: #999999;
}
.popup-footer {
display: flex;
gap: 15px;
}
.cancel-btn,
.confirm-btn {
flex: 1;
height: 45px;
border-radius: 22.5px;
font-size: 16px;
font-weight: bold;
border: none;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666666;
}
.confirm-btn {
background-color: #007aff;
color: #ffffff;
}
.confirm-btn.disabled {
background-color: #cccccc;
opacity: 0.6;
}
</style>

View File

@@ -0,0 +1,998 @@
<!-- 钱包页面 -->
<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>
</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>
<view class="balance-actions">
<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>
</view>
<view class="stat-item">
<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>
</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>
</view>
<view class="action-item" @click="goToRedPackets">
<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>
</view>
<view class="action-item" @click="goToBankCards">
<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>
<view class="filter-tabs">
<text :class="['filter-tab', { active: activeFilter === 'all' }]"
@click="changeFilter('all')">全部</text>
<text :class="['filter-tab', { active: activeFilter === 'income' }]"
@click="changeFilter('income')">收入</text>
<text :class="['filter-tab', { active: activeFilter === 'expense' }]"
@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>
</view>
<!-- 交易列表 -->
<view class="transactions-list">
<view v-for="transaction in transactions"
:key="transaction.id"
class="transaction-item">
<view class="transaction-left">
<text class="transaction-icon">{{ getTransactionIcon(transaction.type) }}</text>
<view class="transaction-info">
<text class="transaction-title">{{ getTransactionTitle(transaction.type) }}</text>
<text class="transaction-time">{{ formatTime(transaction.created_at) }}</text>
<text v-if="transaction.remark" class="transaction-remark">{{ transaction.remark }}</text>
</view>
</view>
<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) }}
</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>
</view>
<view v-if="!hasMore && transactions.length > 0" class="no-more">
<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>
</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>
</view>
<view class="popup-body">
<text class="amount-label">充值金额</text>
<view class="amount-input">
<text class="currency-symbol">¥</text>
<input class="amount-field"
v-model="rechargeAmount"
type="number"
placeholder="请输入充值金额"
focus />
</view>
<view class="quick-amounts">
<text v-for="amount in quickAmounts"
:key="amount"
:class="['quick-amount', { active: rechargeAmount === amount.toString() }]"
@click="selectQuickAmount(amount)">
¥{{ amount }}
</text>
</view>
<text class="recharge-tip">单笔充值最低10元最高5000元</text>
</view>
<view class="popup-footer">
<button class="cancel-btn" @click="closeRechargePopup">取消</button>
<button class="confirm-btn"
:class="{ disabled: !canRecharge }"
@click="confirmRecharge">
确认充值
</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed, watch } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
type WalletType = {
id: string
user_id: string
balance: number
total_recharge: number
total_consume: number
total_withdraw: number
updated_at: string
}
type TransactionType = {
id: string
user_id: string
change_amount: number
current_balance: number
change_type: string // 'recharge' | 'consume' | 'withdraw' | 'refund' | 'reward'
related_id: string | null
remark: string | null
created_at: string
}
type StatsType = {
totalRecharge: number
totalConsume: number
totalWithdraw: number
}
const balance = ref<number>(0)
const stats = ref<StatsType>({
totalRecharge: 0,
totalConsume: 0,
totalWithdraw: 0
})
const transactions = ref<Array<TransactionType>>([])
const activeFilter = ref<string>('all')
const isLoading = ref<boolean>(false)
const currentPage = ref<number>(1)
const pageSize = ref<number>(20)
const hasMore = ref<boolean>(true)
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) {
uni.navigateTo({
url: '/pages/user/login'
})
return
}
await Promise.all([
loadBalance(),
loadTransactions()
])
}
// 加载余额信息
const loadBalance = async () => {
const userId = getCurrentUserId()
if (!userId) return
try {
const { data, error } = await supa
.from('user_wallets')
.select('*')
.eq('user_id', userId)
.single()
if (error !== null) {
console.error('加载钱包失败:', error)
return
}
if (data) {
balance.value = data.balance || 0
stats.value = {
totalRecharge: data.total_recharge || 0,
totalConsume: data.total_consume || 0,
totalWithdraw: data.total_withdraw || 0
}
}
} catch (err) {
console.error('加载钱包异常:', err)
}
}
// 加载交易记录
const loadTransactions = async (loadMore: boolean = false) => {
if (isLoading.value || (!hasMore.value && loadMore)) {
return
}
isLoading.value = true
try {
const userId = getCurrentUserId()
if (!userId) return
const page = loadMore ? currentPage.value + 1 : 1
let query = supa
.from('balance_records')
.select('*')
.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)
return
}
const newTransactions = data || []
if (loadMore) {
transactions.value.push(...newTransactions)
currentPage.value = page
} else {
transactions.value = newTransactions
currentPage.value = 1
}
hasMore.value = newTransactions.length === pageSize.value
} catch (err) {
console.error('加载交易记录异常:', err)
} finally {
isLoading.value = false
}
}
// 获取当前用户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: '📤'
}
return icons[type] || '💰'
}
// 获取交易标题
const getTransactionTitle = (type: string): string => {
const titles: Record<string, string> = {
recharge: '账户充值',
consume: '商品消费',
withdraw: '余额提现',
refund: '订单退款',
reward: '活动奖励',
income: '收入',
expense: '支出'
}
return titles[type] || '交易'
}
// 格式化时间
const formatTime = (timeStr: string): string => {
const date = new Date(timeStr)
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
return `${month}-${day} ${hours}:${minutes}`
}
// 显示更多操作
const showMoreActions = () => {
uni.showActionSheet({
itemList: ['交易记录', '安全设置', '帮助中心'],
success: (res) => {
switch (res.tapIndex) {
case 0:
// 交易记录已经在当前页
break
case 1:
uni.navigateTo({
url: '/pages/mall/consumer/settings'
})
break
case 2:
uni.navigateTo({
url: '/pages/info/help'
})
break
}
}
})
}
// 充值
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}`
})
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;
background-color: #f5f5f5;
}
.balance-overview {
border-radius: 12px;
margin-bottom: 20px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.assets-stats, .quick-actions, .transactions-section, .security-tips {
border-radius: 8px;
margin-bottom: 20px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.popup-content {
width: 400px;
left: 50%;
bottom: 50%;
transform: translate(-50%, 50%);
border-radius: 15px;
}
}
@media screen and (min-width: 1024px) {
.wallet-page {
flex-direction: row; /* 大屏下改为横向布局 */
}
.wallet-header {
display: none; /* 大屏下隐藏顶部栏 */
}
.wallet-content {
width: 100%;
max-width: 1000px;
margin: 0 auto;
}
}
.wallet-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.wallet-header {
background-color: #ffffff;
padding: 15px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e5e5e5;
}
.back-btn {
font-size: 24px;
color: #333333;
padding: 5px;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.more-btn {
color: #333333;
font-size: 20px;
padding: 5px;
}
.wallet-content {
flex: 1;
}
.balance-overview {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30px 20px;
color: #ffffff;
}
.balance-label {
display: block;
font-size: 14px;
opacity: 0.9;
margin-bottom: 10px;
text-align: center;
}
.balance-value {
display: block;
font-size: 36px;
font-weight: bold;
margin-bottom: 20px;
text-align: center;
}
.balance-actions {
display: flex;
gap: 20px;
}
.action-btn {
flex: 1;
height: 40px;
border-radius: 20px;
font-size: 14px;
font-weight: bold;
border: none;
}
.action-btn.recharge {
background-color: #ffffff;
color: #667eea;
}
.action-btn.withdraw {
background-color: rgba(255, 255, 255, 0.2);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.5);
}
.assets-stats {
background-color: #ffffff;
padding: 20px;
display: flex;
justify-content: space-between;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.stat-item {
flex: 1;
text-align: center;
}
.stat-label {
display: block;
font-size: 12px;
color: #666666;
margin-bottom: 8px;
}
.stat-value {
display: block;
font-size: 16px;
font-weight: bold;
color: #333333;
}
.quick-actions {
background-color: #ffffff;
margin-top: 10px;
padding: 20px;
}
.action-grid {
display: flex;
justify-content: space-between;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
}
.action-icon {
font-size: 28px;
margin-bottom: 8px;
}
.action-text {
font-size: 12px;
color: #666666;
}
.transactions-section {
background-color: #ffffff;
margin-top: 10px;
padding: 15px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #333333;
}
.filter-tabs {
display: flex;
gap: 15px;
}
.filter-tab {
font-size: 14px;
color: #666666;
padding: 5px 0;
position: relative;
}
.filter-tab.active {
color: #007aff;
font-weight: bold;
}
.filter-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #007aff;
}
.empty-transactions {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
}
.empty-icon {
font-size: 60px;
margin-bottom: 20px;
}
.empty-text {
font-size: 16px;
color: #666666;
margin-bottom: 10px;
}
.empty-subtext {
font-size: 14px;
color: #999999;
}
.transactions-list {
display: flex;
flex-direction: column;
}
.transaction-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 15px 0;
border-bottom: 1px solid #f5f5f5;
}
.transaction-item:last-child {
border-bottom: none;
}
.transaction-left {
display: flex;
align-items: flex-start;
}
.transaction-icon {
font-size: 24px;
margin-right: 15px;
}
.transaction-info {
display: flex;
flex-direction: column;
}
.transaction-title {
font-size: 14px;
color: #333333;
font-weight: bold;
margin-bottom: 5px;
}
.transaction-time {
font-size: 12px;
color: #999999;
margin-bottom: 3px;
}
.transaction-remark {
font-size: 12px;
color: #666666;
}
.transaction-right {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.transaction-amount {
font-size: 16px;
font-weight: bold;
margin-bottom: 5px;
}
.transaction-amount.income {
color: #4caf50;
}
.transaction-amount.expense {
color: #333333;
}
.transaction-balance {
font-size: 12px;
color: #999999;
}
.loading-more,
.no-more {
padding: 20px;
text-align: center;
}
.loading-text,
.no-more-text {
color: #999999;
font-size: 14px;
}
.security-tips {
background-color: #ffffff;
margin-top: 10px;
padding: 20px;
}
.tip-title {
display: block;
font-size: 16px;
font-weight: bold;
color: #333333;
margin-bottom: 15px;
}
.tip-item {
display: block;
font-size: 12px;
color: #666666;
line-height: 1.6;
margin-bottom: 8px;
}
.tip-item:last-child {
margin-bottom: 0;
}
.recharge-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
}
.popup-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
}
.popup-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
border-top-left-radius: 15px;
border-top-right-radius: 15px;
padding: 20px;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e5e5e5;
}
.popup-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.popup-close {
font-size: 24px;
color: #999999;
padding: 5px;
}
.popup-body {
margin-bottom: 20px;
}
.amount-label {
display: block;
font-size: 14px;
color: #333333;
margin-bottom: 10px;
}
.amount-input {
display: flex;
align-items: center;
margin-bottom: 20px;
padding: 10px;
border: 1px solid #e5e5e5;
border-radius: 8px;
}
.currency-symbol {
font-size: 20px;
color: #333333;
margin-right: 10px;
}
.amount-field {
flex: 1;
font-size: 24px;
font-weight: bold;
color: #333333;
}
.quick-amounts {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
}
.quick-amount {
padding: 8px 15px;
border: 1px solid #e5e5e5;
border-radius: 15px;
font-size: 14px;
color: #333333;
}
.quick-amount.active {
background-color: #007aff;
color: #ffffff;
border-color: #007aff;
}
.recharge-tip {
display: block;
font-size: 12px;
color: #999999;
}
.popup-footer {
display: flex;
gap: 15px;
}
.cancel-btn,
.confirm-btn {
flex: 1;
height: 45px;
border-radius: 22.5px;
font-size: 16px;
font-weight: bold;
border: none;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666666;
}
.confirm-btn {
background-color: #007aff;
color: #ffffff;
}
.confirm-btn.disabled {
background-color: #cccccc;
opacity: 0.6;
}
</style>

View File

@@ -1,4 +1,4 @@
import supa, { supaReady } from '@/components/supadb/aksupainstance.uts'
import { supabase, ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
import type { DeviceInfo, DeviceParams } from './types.uts'
// 服务响应类型
@@ -17,8 +17,8 @@ export class SenseDataService {
*/
static async getDevices(params: DeviceParams): Promise<ServiceResponse<Array<DeviceInfo>>> {
try {
await supaReady
const res = await supa.from(SenseDataService.TABLE_NAME)
await ensureSupabaseReady()
const res = await supabase.from(SenseDataService.TABLE_NAME)
.select('*', {})
.eq('user_id', params.user_id)
.execute()
@@ -46,8 +46,8 @@ export class SenseDataService {
*/
static async bindDevice(deviceData: UTSJSONObject): Promise<ServiceResponse<DeviceInfo>> {
try {
await supaReady
const res = await supa.from(SenseDataService.TABLE_NAME)
await ensureSupabaseReady()
const res = await supabase.from(SenseDataService.TABLE_NAME)
.insert(deviceData)
.select('*', {})
.single()
@@ -75,8 +75,8 @@ export class SenseDataService {
*/
static async unbindDevice(deviceId: string): Promise<ServiceResponse<null>> {
try {
await supaReady
const res = await supa.from(SenseDataService.TABLE_NAME)
await ensureSupabaseReady()
const res = await supabase.from(SenseDataService.TABLE_NAME)
.delete()
.eq('id', deviceId)
.execute()
@@ -102,8 +102,8 @@ export class SenseDataService {
*/
static async updateDevice(deviceId: string, configData: UTSJSONObject): Promise<ServiceResponse<DeviceInfo>> {
try {
await supaReady
const res = await supa.from(SenseDataService.TABLE_NAME)
await ensureSupabaseReady()
const res = await supabase.from(SenseDataService.TABLE_NAME)
.update(configData)
.eq('id', deviceId)
.select('*', {})

View File

View File

View File

@@ -0,0 +1,365 @@
// 电商商城系统类型定义 - UTS Android 兼容
// 用户类型
export type UserType = {
id: string
phone: string
email: string | null
nickname: string | null
avatar_url: string | null
gender: number
user_type: number
status: number
created_at: string
}
// 商城用户扩展信息类型
export type MallUserProfileType = {
id: string
user_id: string
user_type: number
status: number
real_name: string | null
id_card: string | null
credit_score: number
mall_role: string
verification_status: number
verification_data: UTSJSONObject | null
business_license: string | null
shop_category: string | null
service_areas: UTSJSONObject | null
emergency_contact: string | null
preferences: UTSJSONObject | null
created_at: string
updated_at: string
}
// 用户地址类型
export type UserAddressType = {
id: string
user_id: string
receiver_name: string
receiver_phone: string
province: string
city: string
district: string
address_detail: string
postal_code: string | null
is_default: boolean
label: string | null
coordinates: string | null
delivery_instructions: string | null
business_hours: string | null
status: number
created_at: string
updated_at: string
}
// 商家类型
export type MerchantType = {
id: string
user_id: string
shop_name: string
shop_logo: string | null
shop_banner: string | null
shop_description: string | null
contact_name: string
contact_phone: string
shop_status: number
rating: number
total_sales: number
created_at: string
}
// 商品类型
export type ProductType = {
id: string
merchant_id: string
category_id: string
name: string
description: string | null
images: Array<string>
price: number
original_price: number | null
stock: number
sales: number
status: number
created_at: string
}
// 商品SKU类型
export type ProductSkuType = {
id: string
product_id: string
sku_code: string
specifications: UTSJSONObject | null
price: number
stock: number
image_url: string | null
status: number
}
// 购物车类型
export type CartItemType = {
id: string
user_id: string
product_id: string
sku_id: string
quantity: number
selected: boolean
product: ProductType | null
sku: ProductSkuType | null
}
// 订单类型
export type OrderType = {
id: string
order_no: string
user_id: string
merchant_id: string
status: number
total_amount: number
discount_amount: number
delivery_fee: number
actual_amount: number
payment_method: number | null
payment_status: number
delivery_address: UTSJSONObject
created_at: string
}
// 订单商品类型
export type OrderItemType = {
id: string
order_id: string
product_id: string
sku_id: string
product_name: string
sku_specifications: UTSJSONObject | null
price: number
quantity: number
total_amount: number
}
// 配送员类型
export type DeliveryDriverType = {
id: string
user_id: string
real_name: string
id_card: string
driver_license: string | null
vehicle_type: number
vehicle_number: string | null
work_status: number
current_location: UTSJSONObject | null
service_areas: Array<string>
rating: number
total_orders: number
auth_status: number
created_at: string
updated_at: string
}
// 配送任务类型
export type DeliveryTaskType = {
id: string
order_id: string
driver_id: string | null
pickup_address: UTSJSONObject
delivery_address: UTSJSONObject
distance: number | null
estimated_time: number | null
delivery_fee: number
status: number
pickup_time: string | null
delivered_time: string | null
delivery_code: string | null
remark: string | null
created_at: string
updated_at: string
}
// 优惠券模板类型
export type CouponTemplateType = {
id: string
name: string
description: string | null
coupon_type: number
discount_type: number
discount_value: number
min_order_amount: number
max_discount_amount: number | null
total_quantity: number | null
per_user_limit: number
usage_limit: number
merchant_id: string | null
category_ids: Array<string>
product_ids: Array<string>
user_type_limit: number | null
start_time: string
end_time: string
status: number
created_at: string
}
// 用户优惠券类型
export type UserCouponType = {
id: string
user_id: string
template_id: string
coupon_code: string
status: number
used_at: string | null
order_id: string | null
received_at: string
expire_at: string
}
// 分页数据类型
export type PageDataType<T> = {
data: Array<T>
total: number
page: number
pageSize: number
hasMore: boolean
}
// API响应类型
export type ApiResponseType<T> = {
success: boolean
data: T | null
message: string
code: number
}
// 订单状态枚举
export const ORDER_STATUS = {
PENDING_PAYMENT: 1,
PAID: 2,
SHIPPED: 3,
DELIVERED: 4,
COMPLETED: 5,
CANCELLED: 6,
REFUNDING: 7,
REFUNDED: 8
} as const
// 优惠券类型枚举
export const COUPON_TYPE = {
DISCOUNT_AMOUNT: 1, // 满减券
DISCOUNT_PERCENT: 2, // 折扣券
FREE_SHIPPING: 3, // 免运费券
NEWBIE: 4, // 新人券
MEMBER: 5, // 会员券
CATEGORY: 6, // 品类券
MERCHANT: 7, // 商家券
LIMITED_TIME: 8 // 限时券
} as const
// 支付方式枚举
export const PAYMENT_METHOD = {
WECHAT: 1,
ALIPAY: 2,
UNIONPAY: 3,
BALANCE: 4
} as const
// 配送状态枚举
export const DELIVERY_STATUS = {
PENDING: 1,
ASSIGNED: 2,
PICKED_UP: 3,
IN_TRANSIT: 4,
DELIVERED: 5,
FAILED: 6
} as const
// 用户类型枚举
export const MALL_USER_TYPE = {
CONSUMER: 1, // 消费者
MERCHANT: 2, // 商家
DELIVERY: 3, // 配送员
SERVICE: 4, // 客服
ADMIN: 5 // 管理员
} as const
// 用户状态枚举
export const USER_STATUS = {
NORMAL: 1, // 正常
FROZEN: 2, // 冻结
CANCELLED: 3, // 注销
PENDING: 4 // 待审核
} as const
// 认证状态枚举
export const VERIFICATION_STATUS = {
UNVERIFIED: 0, // 未认证
VERIFIED: 1, // 已认证
FAILED: 2 // 认证失败
} as const
// 地址标签枚举
export const ADDRESS_LABEL = {
HOME: 'home', // 家
OFFICE: 'office', // 公司
SCHOOL: 'school', // 学校
OTHER: 'other' // 其他
} as const
// 收藏类型枚举
export const FAVORITE_TYPE = {
PRODUCT: 'product', // 商品
SHOP: 'shop' // 店铺
} as const
// =========================
// 订阅相关类型与枚举
// =========================
// 订阅周期枚举
export const SUBSCRIPTION_PERIOD = {
MONTHLY: 'monthly',
YEARLY: 'yearly'
} as const
// 订阅状态枚举
export const SUBSCRIPTION_STATUS = {
TRIAL: 'trial',
ACTIVE: 'active',
PAST_DUE: 'past_due',
CANCELED: 'canceled',
EXPIRED: 'expired'
} as const
// 软件订阅方案类型
export type SubscriptionPlanType = {
id: string
plan_code: string
name: string
description: string | null
features: UTSJSONObject | null // { featureKey: description }
price: number // 单位:元(或分,取决于后端;前端以显示为准)
currency: string | null // 'CNY' | 'USD' ...
billing_period: keyof typeof SUBSCRIPTION_PERIOD | string // 'monthly' | 'yearly'
trial_days: number | null
is_active: boolean
sort_order?: number | null
created_at?: string
updated_at?: string
}
// 用户订阅记录类型
export type UserSubscriptionType = {
id: string
user_id: string
plan_id: string
status: keyof typeof SUBSCRIPTION_STATUS | string
start_date: string
end_date: string | null
next_billing_date: string | null
auto_renew: boolean
cancel_at_period_end?: boolean | null
metadata?: UTSJSONObject | null
created_at?: string
updated_at?: string
}

View File

@@ -363,3 +363,99 @@ export type UserSubscriptionType = {
created_at?: string
updated_at?: string
}
export interface Product {
id: string
name: string
description: string | null // 允许null
price: number
original_price: number | null // 允许null
images: string[]
// ... 其他属性
}
// types/mall-types.uts
export interface ProductType {
id: string
name: string
description: string
price: number
original_price: number
images: string[]
category_id: string
status: number
stock: number
sales: number
tags: string[]
created_at: string
updated_at: string
}
export interface CategoryType {
id: string
name: string
icon_url: string
parent_id: string | null
is_active: boolean
sort_order: number
}
export interface BannerType {
id: string
title: string
image_url: string
link_url: string
sort_order: number
status: number
}
export interface CouponTemplateType {
id: string
name: string
discount_value: number
min_order_amount: number
start_time: string
end_time: string
status: number
per_user_limit: number
}
export interface CartItemType {
id: string
user_id: string
product_id: string
quantity: number
created_at: string
}
// Mock 数据类型
export interface MockData {
banners: BannerType[]
categories: CategoryType[]
coupons: CouponTemplateType[]
products: ProductType[]
}
// 用户基础信息类型 (兼容 pages/user/types.uts)
export type UserProfile = {
id?: string;
username: string;
email: string;
gender?: string;
birthday?: string;
height_cm?: number;
weight_kg?: number;
bio?: string;
avatar_url?: string;
preferred_language?: string;
role?: string;
school_id?: string;
grade_id?: string;
class_id?: string;
created_at?: string;
updated_at?: string;
}
export type UserStats = {
trainings: number;
points: number;
streak: number;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,271 @@
// 分类页面mock数据模块
// 导出医药分类数据
export const medicineCategories = [
{ id: 'cold', name: '感冒发烧', icon: '🤧', desc: '解热镇痛', color: '#2196F3' },
{ id: 'stomach', name: '肠胃用药', icon: '🤢', desc: '消化系统', color: '#4CAF50' },
{ id: 'pain', name: '止痛消炎', icon: '💊', desc: '镇痛消炎', color: '#F44336' },
{ id: 'skin', name: '皮肤用药', icon: '🤕', desc: '皮肤护理', color: '#9C27B0' },
{ id: 'vitamin', name: '维生素', icon: '🍊', desc: '营养补充', color: '#FF9800' },
{ id: 'chronic', name: '慢性病', icon: '🫀', desc: '长期管理', color: '#795548' },
{ id: 'child', name: '儿童用药', icon: '👶', desc: '儿童专用', color: '#00BCD4' },
{ id: 'external', name: '外用药品', icon: '🧴', desc: '外用制剂', color: '#8BC34A' },
{ id: 'device', name: '医疗器械', icon: '🩺', desc: '医疗设备', color: '#607D8B' },
{ id: 'health', name: '健康食品', icon: '🥗', desc: '保健食品', color: '#FFC107' }
]
// 导出mock商品数据
export const mockProducts = {
cold: [
{
id: 'cold1',
name: '布洛芬缓释胶囊',
specification: '0.3g*24粒',
price: 18.5,
originalPrice: 25.8,
image: '/static/images/default-product.png',
manufacturer: '修正药业',
sales: 2560,
badge: '热销'
},
{
id: 'cold2',
name: '板蓝根颗粒',
specification: '10g*20袋',
price: 22.8,
originalPrice: 29.9,
image: '/static/images/default-product.png',
manufacturer: '白云山',
sales: 1890,
badge: '推荐'
},
{
id: 'cold3',
name: '连花清瘟胶囊',
specification: '0.35g*36粒',
price: 42.8,
originalPrice: 48.0,
image: '/static/images/default-product.png',
manufacturer: '以岭药业',
sales: 3200,
badge: '爆款'
},
{
id: 'cold4',
name: '对乙酰氨基酚片',
specification: '0.5g*12片',
price: 8.9,
originalPrice: 12.0,
image: '/static/images/default-product.png',
manufacturer: '强生制药',
sales: 1420,
badge: '特价'
},
{
id: 'cold5',
name: '感冒清热颗粒',
specification: '3g*10袋',
price: 16.5,
originalPrice: 19.9,
image: '/static/images/default-product.png',
manufacturer: '同仁堂',
sales: 980,
badge: '新品'
},
{
id: 'cold6',
name: '复方氨酚烷胺片',
specification: '10片/盒',
price: 12.8,
originalPrice: 15.0,
image: '/static/images/default-product.png',
manufacturer: '三九医药',
sales: 1650,
badge: '家庭装'
}
],
stomach: [
{
id: 'stomach1',
name: '胃康灵胶囊',
specification: '0.4g*24粒',
price: 32.8,
originalPrice: 38.5,
image: '/static/images/default-product.png',
manufacturer: '三九医药',
sales: 890,
badge: '热销'
},
{
id: 'stomach2',
name: '奥美拉唑肠溶胶囊',
specification: '20mg*14粒',
price: 28.5,
originalPrice: 35.0,
image: '/static/images/default-product.png',
manufacturer: '阿斯利康',
sales: 1250,
badge: '处方药'
},
{
id: 'stomach3',
name: '健胃消食片',
specification: '0.8g*32片',
price: 15.9,
originalPrice: 19.9,
image: '/static/images/default-product.png',
manufacturer: '江中制药',
sales: 2100,
badge: '推荐'
},
{
id: 'stomach4',
name: '蒙脱石散',
specification: '3g*10袋',
price: 18.6,
originalPrice: 22.0,
image: '/static/images/default-product.png',
manufacturer: '益普生',
sales: 1780,
badge: '止泻'
},
{
id: 'stomach5',
name: '多潘立酮片',
specification: '10mg*30片',
price: 22.8,
originalPrice: 26.5,
image: '/static/images/default-product.png',
manufacturer: '西安杨森',
sales: 950,
badge: '促消化'
},
{
id: 'stomach6',
name: '铝碳酸镁咀嚼片',
specification: '0.5g*20片',
price: 25.9,
originalPrice: 29.9,
image: '/static/images/default-product.png',
manufacturer: '拜耳',
sales: 1320,
badge: '护胃'
}
],
pain: [
{
id: 'pain1',
name: '阿莫西林胶囊',
specification: '0.25g*24粒',
price: 28.5,
originalPrice: 35.0,
image: '/static/images/default-product.png',
manufacturer: '华北制药',
sales: 1560,
badge: '处方药'
},
{
id: 'pain2',
name: '双氯芬酸钠缓释片',
specification: '75mg*10片',
price: 19.8,
originalPrice: 24.0,
image: '/static/images/default-product.png',
manufacturer: '诺华制药',
sales: 1280,
badge: '止痛'
},
{
id: 'pain3',
name: '云南白药胶囊',
specification: '0.25g*32粒',
price: 35.9,
originalPrice: 42.0,
image: '/static/images/default-product.png',
manufacturer: '云南白药',
sales: 2350,
badge: '经典'
},
{
id: 'pain4',
name: '塞来昔布胶囊',
specification: '0.2g*10粒',
price: 48.6,
originalPrice: 55.0,
image: '/static/images/default-product.png',
manufacturer: '辉瑞',
sales: 890,
badge: '抗炎'
},
{
id: 'pain5',
name: '布洛芬片',
specification: '0.1g*24片',
price: 12.5,
originalPrice: 15.0,
image: '/static/images/default-product.png',
manufacturer: '中美史克',
sales: 1680,
badge: '经济装'
},
{
id: 'pain6',
name: '头孢克肟胶囊',
specification: '0.1g*6粒',
price: 32.8,
originalPrice: 38.0,
image: '/static/images/default-product.png',
manufacturer: '广州白云山',
sales: 1120,
badge: '抗生素'
}
]
}
// 为其他分类生成默认商品数据
export const generateDefaultProducts = (categoryId: string) => {
const baseProducts = [
{ name: '通用药品1', price: 25.8, manufacturer: '知名药企', sales: 1200 },
{ name: '通用药品2', price: 18.5, manufacturer: '知名药企', sales: 950 },
{ name: '通用药品3', price: 32.0, manufacturer: '知名药企', sales: 1450 },
{ name: '通用药品4', price: 22.8, manufacturer: '知名药企', sales: 880 },
{ name: '通用药品5', price: 28.9, manufacturer: '知名药企', sales: 1100 },
{ name: '通用药品6', price: 19.9, manufacturer: '知名药企', sales: 920 }
]
return baseProducts.map((product, index) => ({
id: `${categoryId}${index + 1}`,
...product,
specification: '规格待定',
originalPrice: product.price * 1.2,
image: '/static/images/default-product.png',
badge: index === 0 ? '热销' : index === 1 ? '推荐' : ''
}))
}
// 获取分类商品数据
export const getCategoryProducts = (categoryId: string) => {
if (mockProducts[categoryId]) {
return mockProducts[categoryId]
} else {
return generateDefaultProducts(categoryId)
}
}
// 获取分类信息
export const getCategoryInfo = (categoryId: string) => {
const category = medicineCategories.find(cat => cat.id === categoryId)
if (category) {
return {
name: category.name,
desc: category.desc
}
} else {
// 返回默认分类信息
return {
name: '感冒发烧',
desc: '解热镇痛'
}
}
}

View File

@@ -1,5 +1,5 @@
import supa, { supaReady } from '@/components/supadb/aksupainstance.uts'
import type { UserProfile } from '@/pages/user/types.uts'
import { supabase, ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
import type { UserProfile } from '@/types/mall-types.uts'
/**
* 确保用户资料存在,如果不存在则创建基础资料
@@ -8,7 +8,7 @@ import type { UserProfile } from '@/pages/user/types.uts'
*/
export async function ensureUserProfile(sessionUser: UTSJSONObject): Promise<UserProfile | null> {
try {
await supaReady
await ensureSupabaseReady()
// 从 sessionUser 中获取用户ID和邮箱
const userId = sessionUser.getString('id')
@@ -20,7 +20,7 @@ export async function ensureUserProfile(sessionUser: UTSJSONObject): Promise<Use
}
// 检查用户是否已存在
const checkRes = await supa.from('ak_users')
const checkRes = await supabase.from('ak_users')
.select('*', {})
.eq('id', userId)
.single()
@@ -30,7 +30,7 @@ export async function ensureUserProfile(sessionUser: UTSJSONObject): Promise<Use
// 用户已存在,返回现有资料
const existingUser = checkRes.data as UTSJSONObject
return {
id: existingUser.getString('id'),
id: existingUser.getString('id') ?? '',
username: existingUser.getString('username') ?? '',
email: existingUser.getString('email') ?? email,
gender: existingUser.getString('gender'),
@@ -40,10 +40,9 @@ export async function ensureUserProfile(sessionUser: UTSJSONObject): Promise<Use
bio: existingUser.getString('bio'),
avatar_url: existingUser.getString('avatar_url'),
preferred_language: existingUser.getString('preferred_language'),
role: existingUser.getString('role'),
school_id: existingUser.getString('school_id'),
grade_id: existingUser.getString('grade_id'),
class_id: existingUser.getString('class_id')
role: existingUser.getString('role') ?? 'consumer',
created_at: existingUser.getString('created_at'),
updated_at: existingUser.getString('updated_at')
} as UserProfile
}
@@ -53,7 +52,7 @@ export async function ensureUserProfile(sessionUser: UTSJSONObject): Promise<Use
newUserData.set('email', email)
newUserData.set('username', email.split('@')[0] ?? 'user') // 默认用户名为邮箱前缀
const insertRes = await supa.from('ak_users')
const insertRes = await supabase.from('ak_users')
.insert(newUserData)
.select('*', {})
.single()
@@ -62,7 +61,7 @@ export async function ensureUserProfile(sessionUser: UTSJSONObject): Promise<Use
if (insertRes.status >= 200 && insertRes.status < 300 && insertRes.data != null) {
const newUser = insertRes.data as UTSJSONObject
return {
id: newUser.getString('id'),
id: newUser.getString('id') ?? '',
username: newUser.getString('username') ?? '',
email: newUser.getString('email') ?? email,
gender: newUser.getString('gender'),
@@ -72,10 +71,9 @@ export async function ensureUserProfile(sessionUser: UTSJSONObject): Promise<Use
bio: newUser.getString('bio'),
avatar_url: newUser.getString('avatar_url'),
preferred_language: newUser.getString('preferred_language'),
role: newUser.getString('role'),
school_id: newUser.getString('school_id'),
grade_id: newUser.getString('grade_id'),
class_id: newUser.getString('class_id')
role: newUser.getString('role') ?? 'consumer',
created_at: newUser.getString('created_at'),
updated_at: newUser.getString('updated_at')
} as UserProfile
} else {
console.error('创建用户资料失败:', insertRes.status)

View File

@@ -1,4 +1,4 @@
import supa, { supaReady } from '@/components/supadb/aksupainstance.uts'
import { supabase as supa, ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
import type { UserProfile, UserStats } from '@/pages/user/types.uts'
import type { DeviceInfo } from '@/pages/sense/types.uts'
import { SenseDataService, type DeviceParams } from '@/pages/sense/senseDataService.uts'
@@ -50,7 +50,7 @@ export const setUserProfile = (profile : UserProfile) => {
// 获取当前用户信息(含补全 profile
export async function getCurrentUser() : Promise<UserProfile | null> {
try {
await supaReady
await ensureSupabaseReady()
} catch (_) {}
const sessionInfo = supa.getSession()