Merge remote-tracking branch 'origin/cyh666666/consumer'
This commit is contained in:
9
.hbuilderx/launch.json
Normal file
9
.hbuilderx/launch.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"version" : "1.0",
|
||||
"configurations" : [
|
||||
{
|
||||
"playground" : "standard",
|
||||
"type" : "uni-app:app-android"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
18
components/supadb/aksupainstance - 副本.uts
Normal file
18
components/supadb/aksupainstance - 副本.uts
Normal 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
|
||||
@@ -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
14
main - 副本.uts
Normal 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 }
|
||||
}
|
||||
16
main.uts
16
main.uts
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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
118
pages - 副本 (2).json
Normal 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
213
pages - 副本.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
275
pages.json
275
pages.json
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
360
pages/mall/consumer/address-edit.uvue
Normal file
360
pages/mall/consumer/address-edit.uvue
Normal 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>
|
||||
307
pages/mall/consumer/address-list.uvue
Normal file
307
pages/mall/consumer/address-list.uvue
Normal 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>
|
||||
918
pages/mall/consumer/address.uvue
Normal file
918
pages/mall/consumer/address.uvue
Normal 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>
|
||||
293
pages/mall/consumer/apply-refund.uvue
Normal file
293
pages/mall/consumer/apply-refund.uvue
Normal 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>
|
||||
810
pages/mall/consumer/cart - 副本.uvue
Normal file
810
pages/mall/consumer/cart - 副本.uvue
Normal 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>
|
||||
1141
pages/mall/consumer/cart.uvue
Normal file
1141
pages/mall/consumer/cart.uvue
Normal file
File diff suppressed because it is too large
Load Diff
131
pages/mall/consumer/category - 副本.uvue
Normal file
131
pages/mall/consumer/category - 副本.uvue
Normal 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>
|
||||
1419
pages/mall/consumer/category.uvue
Normal file
1419
pages/mall/consumer/category.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1159
pages/mall/consumer/categorygood.uvue
Normal file
1159
pages/mall/consumer/categorygood.uvue
Normal file
File diff suppressed because it is too large
Load Diff
593
pages/mall/consumer/chat.uvue
Normal file
593
pages/mall/consumer/chat.uvue
Normal 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>
|
||||
1716
pages/mall/consumer/checkout.uvue
Normal file
1716
pages/mall/consumer/checkout.uvue
Normal file
File diff suppressed because it is too large
Load Diff
149
pages/mall/consumer/coupons.uvue
Normal file
149
pages/mall/consumer/coupons.uvue
Normal 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>
|
||||
269
pages/mall/consumer/favorites.uvue
Normal file
269
pages/mall/consumer/favorites.uvue
Normal 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>
|
||||
580
pages/mall/consumer/footprint.uvue
Normal file
580
pages/mall/consumer/footprint.uvue
Normal 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>
|
||||
1923
pages/mall/consumer/index - 副本.uvue
Normal file
1923
pages/mall/consumer/index - 副本.uvue
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1540
pages/mall/consumer/indexback.uvue
Normal file
1540
pages/mall/consumer/indexback.uvue
Normal file
File diff suppressed because it is too large
Load Diff
2113
pages/mall/consumer/indexgood.uvue
Normal file
2113
pages/mall/consumer/indexgood.uvue
Normal file
File diff suppressed because it is too large
Load Diff
204
pages/mall/consumer/logistics.uvue
Normal file
204
pages/mall/consumer/logistics.uvue
Normal 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>
|
||||
634
pages/mall/consumer/messages - 副本.uvue
Normal file
634
pages/mall/consumer/messages - 副本.uvue
Normal 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>
|
||||
1330
pages/mall/consumer/messages.uvue
Normal file
1330
pages/mall/consumer/messages.uvue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
1094
pages/mall/consumer/orders.uvue
Normal file
1094
pages/mall/consumer/orders.uvue
Normal file
File diff suppressed because it is too large
Load Diff
996
pages/mall/consumer/ordersgood.uvue
Normal file
996
pages/mall/consumer/ordersgood.uvue
Normal 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>
|
||||
1050
pages/mall/consumer/orderss.uvue
Normal file
1050
pages/mall/consumer/orderss.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1046
pages/mall/consumer/ordersx.uvue
Normal file
1046
pages/mall/consumer/ordersx.uvue
Normal file
File diff suppressed because it is too large
Load Diff
190
pages/mall/consumer/payment-success.uvue
Normal file
190
pages/mall/consumer/payment-success.uvue
Normal 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>
|
||||
988
pages/mall/consumer/payment.uvue
Normal file
988
pages/mall/consumer/payment.uvue
Normal 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>
|
||||
@@ -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 {
|
||||
|
||||
909
pages/mall/consumer/profile - 副本.uvue
Normal file
909
pages/mall/consumer/profile - 副本.uvue
Normal 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>
|
||||
@@ -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 {
|
||||
|
||||
163
pages/mall/consumer/refund-review.uvue
Normal file
163
pages/mall/consumer/refund-review.uvue
Normal 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>
|
||||
858
pages/mall/consumer/refund.uvue
Normal file
858
pages/mall/consumer/refund.uvue
Normal 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>
|
||||
775
pages/mall/consumer/review.uvue
Normal file
775
pages/mall/consumer/review.uvue
Normal 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>
|
||||
1233
pages/mall/consumer/search.uvue
Normal file
1233
pages/mall/consumer/search.uvue
Normal file
File diff suppressed because it is too large
Load Diff
702
pages/mall/consumer/settings - 副本.uvue
Normal file
702
pages/mall/consumer/settings - 副本.uvue
Normal 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>
|
||||
747
pages/mall/consumer/settings.uvue
Normal file
747
pages/mall/consumer/settings.uvue
Normal 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>
|
||||
375
pages/mall/consumer/shop-detail.uvue
Normal file
375
pages/mall/consumer/shop-detail.uvue
Normal 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>
|
||||
950
pages/mall/consumer/wallet - 副本.uvue
Normal file
950
pages/mall/consumer/wallet - 副本.uvue
Normal 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>
|
||||
998
pages/mall/consumer/wallet.uvue
Normal file
998
pages/mall/consumer/wallet.uvue
Normal 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>
|
||||
@@ -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('*', {})
|
||||
|
||||
0
static/images/default-banner.png
Normal file
0
static/images/default-banner.png
Normal file
0
static/images/default-product.png
Normal file
0
static/images/default-product.png
Normal file
365
types/mall-types - 副本.uts
Normal file
365
types/mall-types - 副本.uts
Normal 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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
1
unpackage/cache/.app-android/tsc/app-android/.tsbuildInfo
vendored
Normal file
1
unpackage/cache/.app-android/tsc/app-android/.tsbuildInfo
vendored
Normal file
File diff suppressed because one or more lines are too long
271
utils/mock-category-data.uts
Normal file
271
utils/mock-category-data.uts
Normal 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: '解热镇痛'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user