feat: 初次提交我的项目代码
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4,8 +4,8 @@
|
||||
// export const SUPA_KEY: string = 'your-anon-key'
|
||||
|
||||
// 生产环境 - Supabase 云服务
|
||||
export const SUPA_URL: string = 'https://ak3.oulog.com'
|
||||
export const SUPA_KEY: string = 'your-anon-key'
|
||||
//export const SUPA_URL: string = 'https://ak3.oulog.com'
|
||||
//export const SUPA_KEY: string = 'your-anon-key'
|
||||
|
||||
// WebSocket 实时连接
|
||||
export const WS_URL: string = 'wss://ak3.oulog.com/realtime/v1/websocket'
|
||||
|
||||
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,18 +1,31 @@
|
||||
import AkSupa from './aksupa.uts'
|
||||
import { SUPA_URL, SUPA_KEY } from '@/ak/config.uts'
|
||||
// /components/supadb/aksupainstance.uts
|
||||
import { createClient } from './aksupa.uts'
|
||||
|
||||
const supa = new AkSupa(SUPA_URL, SUPA_KEY)
|
||||
// 创建并导出 Supabase 客户端实例
|
||||
const supabaseUrl = 'https://your-project.supabase.co' // 替换为你的 Supabase URL
|
||||
const supabaseAnonKey = 'your-anon-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 const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||
|
||||
export { supaReady }
|
||||
export default supa
|
||||
// 导出 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()
|
||||
}
|
||||
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 }
|
||||
}
|
||||
10
main.uts
10
main.uts
@@ -6,9 +6,13 @@ 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)
|
||||
}
|
||||
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 }
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
204
pages.json
204
pages.json
@@ -3,210 +3,72 @@
|
||||
{
|
||||
"path": "pages/mall/consumer/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "商城首页",
|
||||
"navigationStyle": "custom"
|
||||
"navigationBarTitleText": "首页",
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/boot",
|
||||
"path": "pages/mall/consumer/category",
|
||||
"style": {
|
||||
"navigationBarTitleText": ""
|
||||
"navigationBarTitleText": "分类"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/login",
|
||||
"path": "pages/mall/consumer/messages",
|
||||
"style": {
|
||||
"navigationBarTitleText": "登录"
|
||||
"navigationBarTitleText": "消息",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/register",
|
||||
"path": "pages/mall/consumer/cart",
|
||||
"style": {
|
||||
"navigationBarTitleText": "注册"
|
||||
"navigationBarTitleText": "购物车"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/forgot-password",
|
||||
"path": "pages/mall/consumer/profile",
|
||||
"style": {
|
||||
"navigationBarTitleText": "忘记密码"
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
793
pages/mall/consumer/address-edit.uvue
Normal file
793
pages/mall/consumer/address-edit.uvue
Normal file
@@ -0,0 +1,793 @@
|
||||
<!-- 地址编辑页面 -->
|
||||
<template>
|
||||
<view class="address-edit-page">
|
||||
<!-- 顶部栏 -->
|
||||
<view class="edit-header">
|
||||
<text class="back-btn" @click="goBack">‹</text>
|
||||
<text class="header-title">{{ addressId ? '编辑地址' : '新增地址' }}</text>
|
||||
<text v-if="addressId" class="delete-btn" @click="deleteAddress">删除</text>
|
||||
</view>
|
||||
|
||||
<scroll-view class="edit-content" scroll-y>
|
||||
<!-- 表单 -->
|
||||
<view class="form-section">
|
||||
<view class="form-item">
|
||||
<text class="item-label">收货人</text>
|
||||
<input class="item-input"
|
||||
v-model="formData.recipient_name"
|
||||
placeholder="请输入收货人姓名" />
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="item-label">手机号码</text>
|
||||
<input class="item-input"
|
||||
v-model="formData.phone"
|
||||
placeholder="请输入手机号码"
|
||||
type="number"
|
||||
maxlength="11" />
|
||||
</view>
|
||||
|
||||
<view class="form-item" @click="showRegionPicker">
|
||||
<text class="item-label">所在地区</text>
|
||||
<text class="item-value">{{ regionText || '请选择省市区' }}</text>
|
||||
<text class="item-arrow">›</text>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="item-label">详细地址</text>
|
||||
<textarea class="item-textarea"
|
||||
v-model="formData.detail"
|
||||
placeholder="请输入详细地址,如街道、小区、楼栋号、单元室等"
|
||||
maxlength="100" />
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="item-label">邮政编码</text>
|
||||
<input class="item-input"
|
||||
v-model="formData.postal_code"
|
||||
placeholder="请输入邮政编码"
|
||||
type="number"
|
||||
maxlength="6" />
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="default-switch">
|
||||
<text class="switch-label">设为默认地址</text>
|
||||
<switch :checked="formData.is_default"
|
||||
@change="toggleDefault" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 地址簿 -->
|
||||
<view v-if="addressList.length > 0" class="address-book">
|
||||
<view class="section-title">地址簿</view>
|
||||
<view v-for="address in addressList"
|
||||
:key="address.id"
|
||||
class="book-item"
|
||||
@click="fillFromAddressBook(address)">
|
||||
<view class="book-info">
|
||||
<text class="book-name">{{ address.recipient_name }}</text>
|
||||
<text class="book-phone">{{ address.phone }}</text>
|
||||
</view>
|
||||
<text class="book-address">{{ getFullAddress(address) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<view class="save-btn-container">
|
||||
<button class="save-btn" @click="saveAddress">保存地址</button>
|
||||
</view>
|
||||
|
||||
<!-- 地区选择器 -->
|
||||
<view v-if="showPicker" class="region-picker">
|
||||
<view class="picker-mask" @click="hideRegionPicker"></view>
|
||||
<view class="picker-content">
|
||||
<view class="picker-header">
|
||||
<text class="cancel-btn" @click="hideRegionPicker">取消</text>
|
||||
<text class="picker-title">选择地区</text>
|
||||
<text class="confirm-btn" @click="confirmRegion">确定</text>
|
||||
</view>
|
||||
<picker-view class="picker-view"
|
||||
:value="pickerValue"
|
||||
@change="onPickerChange">
|
||||
<picker-view-column>
|
||||
<view v-for="(province, index) in provinces"
|
||||
:key="index"
|
||||
class="picker-item">
|
||||
{{ province.name }}
|
||||
</view>
|
||||
</picker-view-column>
|
||||
<picker-view-column>
|
||||
<view v-for="(city, index) in cities"
|
||||
:key="index"
|
||||
class="picker-item">
|
||||
{{ city.name }}
|
||||
</view>
|
||||
</picker-view-column>
|
||||
<picker-view-column>
|
||||
<view v-for="(district, index) in districts"
|
||||
:key="index"
|
||||
class="picker-item">
|
||||
{{ district.name }}
|
||||
</view>
|
||||
</picker-view-column>
|
||||
</picker-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
|
||||
}
|
||||
|
||||
type RegionType = {
|
||||
code: string
|
||||
name: string
|
||||
children?: RegionType[]
|
||||
}
|
||||
|
||||
const addressId = ref<string>('')
|
||||
const formData = ref<AddressType>({
|
||||
user_id: '',
|
||||
recipient_name: '',
|
||||
phone: '',
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
detail: '',
|
||||
postal_code: null,
|
||||
is_default: false
|
||||
})
|
||||
const addressList = ref<Array<AddressType>>([])
|
||||
const showPicker = ref<boolean>(false)
|
||||
const provinces = ref<Array<RegionType>>([])
|
||||
const cities = ref<Array<RegionType>>([])
|
||||
const districts = ref<Array<RegionType>>([])
|
||||
const pickerValue = ref<Array<number>>([0, 0, 0])
|
||||
const selectedRegion = ref<{
|
||||
province: RegionType | null
|
||||
city: RegionType | null
|
||||
district: RegionType | null
|
||||
}>({
|
||||
province: null,
|
||||
city: null,
|
||||
district: null
|
||||
})
|
||||
|
||||
// 获取地区文本
|
||||
const regionText = computed(() => {
|
||||
const { province, city, district } = selectedRegion.value
|
||||
if (province && city && district) {
|
||||
return `${province.name} ${city.name} ${district.name}`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadAddresses()
|
||||
loadRegions()
|
||||
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const options = currentPage.options as any
|
||||
|
||||
if (options.id) {
|
||||
addressId.value = options.id
|
||||
loadAddressDetail(options.id)
|
||||
}
|
||||
})
|
||||
|
||||
// 加载地址列表
|
||||
const loadAddresses = async () => {
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) return
|
||||
|
||||
try {
|
||||
const { data, error } = await supa
|
||||
.from('user_addresses')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.order('is_default', { ascending: false })
|
||||
.limit(5)
|
||||
|
||||
if (error !== null) {
|
||||
console.error('加载地址列表失败:', error)
|
||||
return
|
||||
}
|
||||
|
||||
addressList.value = data ?? []
|
||||
} catch (err) {
|
||||
console.error('加载地址列表异常:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载地址详情
|
||||
const loadAddressDetail = async (id: string) => {
|
||||
try {
|
||||
const { data, error } = await supa
|
||||
.from('user_addresses')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single()
|
||||
|
||||
if (error !== null) {
|
||||
console.error('加载地址详情失败:', error)
|
||||
return
|
||||
}
|
||||
|
||||
formData.value = data
|
||||
|
||||
// 设置选中的地区
|
||||
selectedRegion.value = {
|
||||
province: { code: '', name: formData.value.province },
|
||||
city: { code: '', name: formData.value.city },
|
||||
district: { code: '', name: formData.value.district }
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载地址详情异常:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载地区数据
|
||||
const loadRegions = () => {
|
||||
// 这里应该从API加载地区数据,这里使用模拟数据
|
||||
provinces.value = [
|
||||
{ code: '110000', name: '北京市' },
|
||||
{ code: '310000', name: '上海市' },
|
||||
{ code: '440000', name: '广东省' },
|
||||
{ code: '330000', name: '浙江省' },
|
||||
{ code: '320000', name: '江苏省' }
|
||||
]
|
||||
|
||||
// 默认加载第一个省份的城市
|
||||
if (provinces.value.length > 0) {
|
||||
loadCities(provinces.value[0].code)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载城市
|
||||
const loadCities = (provinceCode: string) => {
|
||||
// 模拟城市数据
|
||||
const cityMap: Record<string, RegionType[]> = {
|
||||
'110000': [
|
||||
{ code: '110100', name: '北京市' }
|
||||
],
|
||||
'310000': [
|
||||
{ code: '310100', name: '上海市' }
|
||||
],
|
||||
'440000': [
|
||||
{ code: '440100', name: '广州市' },
|
||||
{ code: '440300', name: '深圳市' },
|
||||
{ code: '440600', name: '佛山市' }
|
||||
],
|
||||
'330000': [
|
||||
{ code: '330100', name: '杭州市' },
|
||||
{ code: '330200', name: '宁波市' },
|
||||
{ code: '330300', name: '温州市' }
|
||||
],
|
||||
'320000': [
|
||||
{ code: '320100', name: '南京市' },
|
||||
{ code: '320200', name: '无锡市' },
|
||||
{ code: '320500', name: '苏州市' }
|
||||
]
|
||||
}
|
||||
|
||||
cities.value = cityMap[provinceCode] || []
|
||||
|
||||
// 加载第一个城市的区县
|
||||
if (cities.value.length > 0) {
|
||||
loadDistricts(cities.value[0].code)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载区县
|
||||
const loadDistricts = (cityCode: string) => {
|
||||
// 模拟区县数据
|
||||
const districtMap: Record<string, RegionType[]> = {
|
||||
'110100': [
|
||||
{ code: '110101', name: '东城区' },
|
||||
{ code: '110102', name: '西城区' },
|
||||
{ code: '110105', name: '朝阳区' },
|
||||
{ code: '110106', name: '丰台区' }
|
||||
],
|
||||
'440100': [
|
||||
{ code: '440103', name: '荔湾区' },
|
||||
{ code: '440104', name: '越秀区' },
|
||||
{ code: '440105', name: '海珠区' },
|
||||
{ code: '440106', name: '天河区' }
|
||||
],
|
||||
'330100': [
|
||||
{ code: '330102', name: '上城区' },
|
||||
{ code: '330103', name: '下城区' },
|
||||
{ code: '330104', name: '江干区' },
|
||||
{ code: '330105', name: '拱墅区' }
|
||||
]
|
||||
}
|
||||
|
||||
districts.value = districtMap[cityCode] || []
|
||||
}
|
||||
|
||||
// 获取当前用户ID
|
||||
const getCurrentUserId = (): string => {
|
||||
const userStore = uni.getStorageSync('userInfo')
|
||||
return userStore?.id || ''
|
||||
}
|
||||
|
||||
// 获取完整地址
|
||||
const getFullAddress = (address: AddressType): string => {
|
||||
return `${address.province}${address.city}${address.district}${address.detail}`
|
||||
}
|
||||
|
||||
// 显示地区选择器
|
||||
const showRegionPicker = () => {
|
||||
showPicker.value = true
|
||||
}
|
||||
|
||||
// 隐藏地区选择器
|
||||
const hideRegionPicker = () => {
|
||||
showPicker.value = false
|
||||
}
|
||||
|
||||
// 选择器变化
|
||||
const onPickerChange = (event: any) => {
|
||||
const value = event.detail.value
|
||||
pickerValue.value = value
|
||||
|
||||
// 省份变化
|
||||
if (value[0] !== pickerValue.value[0]) {
|
||||
const province = provinces.value[value[0]]
|
||||
selectedRegion.value.province = province
|
||||
loadCities(province.code)
|
||||
pickerValue.value = [value[0], 0, 0]
|
||||
}
|
||||
|
||||
// 城市变化
|
||||
if (value[1] !== pickerValue.value[1]) {
|
||||
const city = cities.value[value[1]]
|
||||
selectedRegion.value.city = city
|
||||
loadDistricts(city.code)
|
||||
pickerValue.value = [value[0], value[1], 0]
|
||||
}
|
||||
|
||||
// 区县变化
|
||||
if (value[2] !== pickerValue.value[2]) {
|
||||
const district = districts.value[value[2]]
|
||||
selectedRegion.value.district = district
|
||||
}
|
||||
}
|
||||
|
||||
// 确认地区选择
|
||||
const confirmRegion = () => {
|
||||
const province = provinces.value[pickerValue.value[0]]
|
||||
const city = cities.value[pickerValue.value[1]]
|
||||
const district = districts.value[pickerValue.value[2]]
|
||||
|
||||
if (province && city && district) {
|
||||
selectedRegion.value = { province, city, district }
|
||||
formData.value.province = province.name
|
||||
formData.value.city = city.name
|
||||
formData.value.district = district.name
|
||||
}
|
||||
|
||||
hideRegionPicker()
|
||||
}
|
||||
|
||||
// 切换默认地址
|
||||
const toggleDefault = (event: any) => {
|
||||
formData.value.is_default = event.detail.value
|
||||
}
|
||||
|
||||
// 从地址簿填充
|
||||
const fillFromAddressBook = (address: AddressType) => {
|
||||
formData.value = { ...address }
|
||||
selectedRegion.value = {
|
||||
province: { code: '', name: address.province },
|
||||
city: { code: '', name: address.city },
|
||||
district: { code: '', name: address.district }
|
||||
}
|
||||
}
|
||||
|
||||
// 保存地址
|
||||
const saveAddress = async () => {
|
||||
// 验证表单
|
||||
if (!formData.value.recipient_name.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入收货人姓名',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!formData.value.phone.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入手机号码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!/^1[3-9]\d{9}$/.test(formData.value.phone)) {
|
||||
uni.showToast({
|
||||
title: '手机号码格式错误',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!formData.value.province) {
|
||||
uni.showToast({
|
||||
title: '请选择所在地区',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!formData.value.detail.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入详细地址',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) {
|
||||
uni.showToast({
|
||||
title: '用户信息错误',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
formData.value.user_id = userId
|
||||
|
||||
try {
|
||||
if (formData.value.is_default) {
|
||||
// 取消其他默认地址
|
||||
await supa
|
||||
.from('user_addresses')
|
||||
.update({ is_default: false })
|
||||
.eq('user_id', userId)
|
||||
.eq('is_default', true)
|
||||
}
|
||||
|
||||
if (addressId.value) {
|
||||
// 更新地址
|
||||
const { error } = await supa
|
||||
.from('user_addresses')
|
||||
.update(formData.value)
|
||||
.eq('id', addressId.value)
|
||||
|
||||
if (error !== null) {
|
||||
throw error
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '地址更新成功',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
// 新增地址
|
||||
const { error } = await supa
|
||||
.from('user_addresses')
|
||||
.insert(formData.value)
|
||||
|
||||
if (error !== null) {
|
||||
throw error
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '地址添加成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
|
||||
} catch (err) {
|
||||
console.error('保存地址失败:', err)
|
||||
uni.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 删除地址
|
||||
const deleteAddress = () => {
|
||||
uni.showModal({
|
||||
title: '删除地址',
|
||||
content: '确定要删除这个地址吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const { error } = await supa
|
||||
.from('user_addresses')
|
||||
.delete()
|
||||
.eq('id', addressId.value)
|
||||
|
||||
if (error !== null) {
|
||||
throw error
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '地址删除成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
|
||||
} catch (err) {
|
||||
console.error('删除地址失败:', err)
|
||||
uni.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 返回
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.address-edit-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.edit-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;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
color: #ff4757;
|
||||
font-size: 14px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.edit-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background-color: #ffffff;
|
||||
margin-bottom: 10px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
width: 80px;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.item-input {
|
||||
flex: 1;
|
||||
height: 30px;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.item-value {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.item-arrow {
|
||||
color: #999999;
|
||||
font-size: 16px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.item-textarea {
|
||||
flex: 1;
|
||||
min-height: 60px;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.default-switch {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.address-book {
|
||||
background-color: #ffffff;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.book-item {
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.book-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.book-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.book-name {
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
font-weight: bold;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.book-phone {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.book-address {
|
||||
font-size: 13px;
|
||||
color: #666666;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.save-btn-container {
|
||||
background-color: #ffffff;
|
||||
padding: 15px;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background-color: #007aff;
|
||||
color: #ffffff;
|
||||
height: 50px;
|
||||
border-radius: 25px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.region-picker {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.picker-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.picker-content {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #ffffff;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
.picker-header {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
color: #999999;
|
||||
font-size: 14px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.picker-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
color: #007aff;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.picker-view {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.picker-item {
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
}
|
||||
</style>
|
||||
474
pages/mall/consumer/address.uvue
Normal file
474
pages/mall/consumer/address.uvue
Normal file
@@ -0,0 +1,474 @@
|
||||
<!-- 地址管理页面 -->
|
||||
<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="addNewAddress">
|
||||
<text class="btn-icon">+</text>
|
||||
<text class="btn-text">添加新地址</text>
|
||||
</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)
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
const eventChannel = uni.getEventChannel()
|
||||
if (eventChannel) {
|
||||
eventChannel.on('fromSelect', (data: any) => {
|
||||
fromSelect.value = data.fromSelect || false
|
||||
selectCallback.value = data.callback
|
||||
})
|
||||
}
|
||||
|
||||
loadAddresses()
|
||||
})
|
||||
|
||||
// 加载地址列表
|
||||
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 addNewAddress = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/address-edit'
|
||||
})
|
||||
}
|
||||
</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;
|
||||
}
|
||||
</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>
|
||||
529
pages/mall/consumer/cart.uvue
Normal file
529
pages/mall/consumer/cart.uvue
Normal file
@@ -0,0 +1,529 @@
|
||||
<!-- pages/mall/consumer/cart.uvue -->
|
||||
<template>
|
||||
<view class="cart-page">
|
||||
<!-- 顶部标题 -->
|
||||
<view class="cart-header">
|
||||
<text class="header-title">购物车</text>
|
||||
</view>
|
||||
|
||||
<!-- 购物车内容 -->
|
||||
<scroll-view scroll-y class="cart-content">
|
||||
<!-- 空购物车 -->
|
||||
<view v-if="!loading && cartItems.length === 0" class="empty-cart">
|
||||
<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="cart-list">
|
||||
<view
|
||||
v-for="item in cartItems"
|
||||
:key="item.id"
|
||||
class="cart-item"
|
||||
>
|
||||
<view class="item-select" @click="toggleSelect(item.id)">
|
||||
<text v-if="item.selected" class="selected-icon">✓</text>
|
||||
<text v-else class="unselected-icon"></text>
|
||||
</view>
|
||||
|
||||
<image
|
||||
class="item-image"
|
||||
:src="item.image"
|
||||
mode="aspectFill"
|
||||
@click="navigateToProduct(item)"
|
||||
/>
|
||||
|
||||
<view class="item-info">
|
||||
<text class="item-name">{{ item.name }}</text>
|
||||
<text class="item-spec">{{ item.spec }}</text>
|
||||
|
||||
<view class="item-footer">
|
||||
<text class="item-price">¥{{ item.price }}</text>
|
||||
<view class="quantity-control">
|
||||
<text class="quantity-btn" @click="decreaseQuantity(item.id)">-</text>
|
||||
<text class="quantity-value">{{ item.quantity }}</text>
|
||||
<text class="quantity-btn" @click="increaseQuantity(item.id)">+</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 推荐商品 -->
|
||||
<view v-if="recommendProducts.length > 0" class="recommend-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">猜你喜欢</text>
|
||||
</view>
|
||||
<view class="recommend-list">
|
||||
<view
|
||||
v-for="product in recommendProducts"
|
||||
:key="product.id"
|
||||
class="recommend-item"
|
||||
@click="navigateToProduct(product)"
|
||||
>
|
||||
<image
|
||||
class="recommend-image"
|
||||
:src="product.image"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<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="cart-footer">
|
||||
<view class="footer-left">
|
||||
<view class="select-all" @click="toggleSelectAll">
|
||||
<text v-if="allSelected" class="selected-icon">✓</text>
|
||||
<text v-else class="unselected-icon"></text>
|
||||
<text class="select-all-text">全选</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="footer-right">
|
||||
<view class="total-info">
|
||||
<text class="total-text">合计:</text>
|
||||
<text class="total-price">¥{{ totalPrice }}</text>
|
||||
</view>
|
||||
<button class="checkout-btn" @click="goToCheckout">
|
||||
去结算({{ selectedCount }})
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
// 响应式数据
|
||||
const cartItems = ref<any[]>([])
|
||||
const recommendProducts = ref<any[]>([])
|
||||
const loading = ref<boolean>(false)
|
||||
|
||||
// Mock 购物车数据
|
||||
const mockCartItems = [
|
||||
{
|
||||
id: '1',
|
||||
name: '无线蓝牙耳机',
|
||||
price: 299,
|
||||
image: 'https://picsum.photos/100/100?random=1',
|
||||
spec: '白色',
|
||||
quantity: 1,
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '运动T恤',
|
||||
price: 79,
|
||||
image: 'https://picsum.photos/100/100?random=2',
|
||||
spec: '黑色 L',
|
||||
quantity: 2,
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '智能手环',
|
||||
price: 199,
|
||||
image: 'https://picsum.photos/100/100?random=3',
|
||||
spec: '黑色',
|
||||
quantity: 1,
|
||||
selected: false
|
||||
}
|
||||
]
|
||||
|
||||
const mockRecommendProducts = [
|
||||
{
|
||||
id: '101',
|
||||
name: '保温杯',
|
||||
price: 49,
|
||||
image: 'https://picsum.photos/100/100?random=4'
|
||||
},
|
||||
{
|
||||
id: '102',
|
||||
name: '电动牙刷',
|
||||
price: 89,
|
||||
image: 'https://picsum.photos/100/100?random=5'
|
||||
},
|
||||
{
|
||||
id: '103',
|
||||
name: '运动鞋',
|
||||
price: 199,
|
||||
image: 'https://picsum.photos/100/100?random=6'
|
||||
}
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const allSelected = computed(() => {
|
||||
return cartItems.value.length > 0 && cartItems.value.every(item => item.selected)
|
||||
})
|
||||
|
||||
const selectedCount = computed(() => {
|
||||
return cartItems.value.filter(item => item.selected).reduce((sum, item) => sum + item.quantity, 0)
|
||||
})
|
||||
|
||||
const totalPrice = computed(() => {
|
||||
return cartItems.value
|
||||
.filter(item => item.selected)
|
||||
.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
||||
.toFixed(2)
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadCartData()
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
const loadCartData = () => {
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
cartItems.value = [...mockCartItems]
|
||||
recommendProducts.value = [...mockRecommendProducts]
|
||||
loading.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// 商品操作
|
||||
const toggleSelect = (itemId: string) => {
|
||||
const index = cartItems.value.findIndex(item => item.id === itemId)
|
||||
if (index !== -1) {
|
||||
cartItems.value[index].selected = !cartItems.value[index].selected
|
||||
cartItems.value = [...cartItems.value] // 触发响应式更新
|
||||
}
|
||||
}
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
const newSelectedState = !allSelected.value
|
||||
cartItems.value = cartItems.value.map(item => ({
|
||||
...item,
|
||||
selected: newSelectedState
|
||||
}))
|
||||
}
|
||||
|
||||
const increaseQuantity = (itemId: string) => {
|
||||
const index = cartItems.value.findIndex(item => item.id === itemId)
|
||||
if (index !== -1) {
|
||||
cartItems.value[index].quantity++
|
||||
cartItems.value = [...cartItems.value]
|
||||
}
|
||||
}
|
||||
|
||||
const decreaseQuantity = (itemId: string) => {
|
||||
const index = cartItems.value.findIndex(item => item.id === itemId)
|
||||
if (index !== -1 && cartItems.value[index].quantity > 1) {
|
||||
cartItems.value[index].quantity--
|
||||
cartItems.value = [...cartItems.value]
|
||||
}
|
||||
}
|
||||
|
||||
// 导航函数
|
||||
const goShopping = () => {
|
||||
uni.switchTab({ url: '/pages/mall/consumer/index' })
|
||||
}
|
||||
|
||||
const navigateToProduct = (product: any) => {
|
||||
uni.navigateTo({ url: `/pages/mall/consumer/product-detail?id=${product.id}` })
|
||||
}
|
||||
|
||||
const goToCheckout = () => {
|
||||
if (selectedCount.value === 0) {
|
||||
uni.showToast({
|
||||
title: '请选择商品',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.navigateTo({ url: '/pages/mall/consumer/checkout' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.cart-page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 头部 */
|
||||
.cart-header {
|
||||
background-color: white;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 内容区 */
|
||||
.cart-content {
|
||||
flex: 1;
|
||||
padding-bottom: 60px; /* 为底部结算栏留出空间 */
|
||||
}
|
||||
|
||||
/* 空购物车 */
|
||||
.empty-cart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 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;
|
||||
}
|
||||
|
||||
/* 购物车商品列表 */
|
||||
.cart-list {
|
||||
background-color: white;
|
||||
margin: 10px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cart-item {
|
||||
display: flex;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-select {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.selected-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: #ff5000;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.unselected-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.item-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.item-spec {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.item-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-price {
|
||||
font-size: 18px;
|
||||
color: #ff5000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.quantity-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quantity-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.quantity-value {
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* 推荐商品 */
|
||||
.recommend-section {
|
||||
margin: 20px 10px;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.recommend-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.recommend-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.recommend-image {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.recommend-name {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
line-height: 1.4;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.recommend-price {
|
||||
font-size: 15px;
|
||||
color: #ff5000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 底部结算栏 */
|
||||
.cart-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 60px;
|
||||
background-color: white;
|
||||
border-top: 1px solid #eee;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.select-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.select-all-text {
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.total-info {
|
||||
margin-right: 15px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.total-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
font-size: 18px;
|
||||
color: #ff5000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.checkout-btn {
|
||||
background-color: #ff5000;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
padding: 8px 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
526
pages/mall/consumer/category - 副本.uvue
Normal file
526
pages/mall/consumer/category - 副本.uvue
Normal file
@@ -0,0 +1,526 @@
|
||||
<!-- 商品分类页面 -->
|
||||
<template>
|
||||
<view class="category-page">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-header">
|
||||
<view class="search-box" @click="goToSearch">
|
||||
<text class="search-icon">🔍</text>
|
||||
<text class="search-placeholder">搜索商品</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="category-container">
|
||||
<!-- 左侧分类导航 -->
|
||||
<scroll-view class="category-nav" scroll-y>
|
||||
<view v-for="category in categories"
|
||||
:key="category.id"
|
||||
:class="['nav-item', { active: activeCategoryId === category.id }]"
|
||||
@click="selectCategory(category)">
|
||||
<text class="nav-text">{{ category.name }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 右侧内容区域 -->
|
||||
<scroll-view class="category-content" scroll-y>
|
||||
<!-- 当前分类的banner -->
|
||||
<view v-if="currentCategory" class="category-banner">
|
||||
<image class="banner-image" :src="currentCategory.image_url || '/static/default-banner.png'" />
|
||||
</view>
|
||||
|
||||
<!-- 子分类 -->
|
||||
<view v-if="subCategories.length > 0" class="sub-category-section">
|
||||
<view class="section-title">{{ currentCategory?.name }}分类</view>
|
||||
<view class="sub-category-grid">
|
||||
<view v-for="subCategory in subCategories"
|
||||
:key="subCategory.id"
|
||||
class="sub-category-item"
|
||||
@click="navigateToSubCategory(subCategory)">
|
||||
<image class="sub-category-icon" :src="subCategory.icon_url || '/static/default-category.png'" />
|
||||
<text class="sub-category-name">{{ subCategory.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 品牌专区 -->
|
||||
<view v-if="brands.length > 0" class="brand-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">品牌推荐</text>
|
||||
<text class="more-btn" @click="viewAllBrands">更多 ›</text>
|
||||
</view>
|
||||
<scroll-view class="brand-scroll" scroll-x>
|
||||
<view class="brand-list">
|
||||
<view v-for="brand in brands"
|
||||
:key="brand.id"
|
||||
class="brand-item"
|
||||
@click="viewBrandProducts(brand)">
|
||||
<image class="brand-logo" :src="brand.logo_url || '/static/default-brand.png'" />
|
||||
<text class="brand-name">{{ brand.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 热销商品 -->
|
||||
<view v-if="hotProducts.length > 0" class="hot-products-section">
|
||||
<view class="section-title">热销商品</view>
|
||||
<view class="hot-products-grid">
|
||||
<view v-for="product in hotProducts"
|
||||
:key="product.id"
|
||||
class="product-item"
|
||||
@click="viewProductDetail(product)">
|
||||
<image class="product-image" :src="getProductFirstImage(product)" />
|
||||
<view class="product-info">
|
||||
<text class="product-name">{{ product.name }}</text>
|
||||
<view class="product-price-row">
|
||||
<text class="current-price">¥{{ product.price }}</text>
|
||||
<text v-if="product.original_price && product.original_price > product.price"
|
||||
class="original-price">¥{{ product.original_price }}</text>
|
||||
</view>
|
||||
<view class="sales-info">
|
||||
<text class="sales-text">已售{{ product.sales }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="isLoadingProducts" class="loading-more">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import type { ProductType } from '@/types/mall-types.uts'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type CategoryType = {
|
||||
id: string
|
||||
name: string
|
||||
parent_id: string | null
|
||||
icon_url: string | null
|
||||
image_url: string | null
|
||||
sort_order: number
|
||||
is_active: boolean
|
||||
}
|
||||
|
||||
type BrandType = {
|
||||
id: string
|
||||
name: string
|
||||
logo_url: string | null
|
||||
description: string | null
|
||||
}
|
||||
|
||||
const categories = ref<Array<CategoryType>>([])
|
||||
const activeCategoryId = ref<string>('')
|
||||
const currentCategory = ref<CategoryType | null>(null)
|
||||
const subCategories = ref<Array<CategoryType>>([])
|
||||
const brands = ref<Array<BrandType>>([])
|
||||
const hotProducts = ref<Array<ProductType>>([])
|
||||
const isLoadingProducts = ref<boolean>(false)
|
||||
|
||||
// 监听分类切换
|
||||
watch(activeCategoryId, (newId) => {
|
||||
if (newId) {
|
||||
loadCategoryData(newId)
|
||||
}
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadCategories()
|
||||
})
|
||||
|
||||
// 加载一级分类
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
const { data, error } = await supa
|
||||
.from('categories')
|
||||
.select('*')
|
||||
.eq('is_active', true)
|
||||
.is('parent_id', null)
|
||||
.order('sort_order', { ascending: true })
|
||||
|
||||
if (error !== null) {
|
||||
console.error('加载分类失败:', error)
|
||||
return
|
||||
}
|
||||
|
||||
categories.value = data ?? []
|
||||
|
||||
// 默认选中第一个分类
|
||||
if (categories.value.length > 0) {
|
||||
activeCategoryId.value = categories.value[0].id
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载分类异常:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载分类数据
|
||||
const loadCategoryData = async (categoryId: string) => {
|
||||
// 1. 获取当前分类信息
|
||||
try {
|
||||
const { data: categoryData, error: categoryError } = await supa
|
||||
.from('categories')
|
||||
.select('*')
|
||||
.eq('id', categoryId)
|
||||
.single()
|
||||
|
||||
if (categoryError !== null) {
|
||||
console.error('加载分类详情失败:', categoryError)
|
||||
return
|
||||
}
|
||||
|
||||
currentCategory.value = categoryData
|
||||
|
||||
// 2. 加载子分类
|
||||
const { data: subData, error: subError } = await supa
|
||||
.from('categories')
|
||||
.select('*')
|
||||
.eq('parent_id', categoryId)
|
||||
.eq('is_active', true)
|
||||
.order('sort_order', { ascending: true })
|
||||
|
||||
if (subError !== null) {
|
||||
console.error('加载子分类失败:', subError)
|
||||
} else {
|
||||
subCategories.value = subData ?? []
|
||||
}
|
||||
|
||||
// 3. 加载品牌
|
||||
const { data: brandData, error: brandError } = await supa
|
||||
.from('brands')
|
||||
.select('*')
|
||||
.eq('is_active', true)
|
||||
.order('sort_order', { ascending: true })
|
||||
.limit(8)
|
||||
|
||||
if (brandError !== null) {
|
||||
console.error('加载品牌失败:', brandError)
|
||||
} else {
|
||||
brands.value = brandData ?? []
|
||||
}
|
||||
|
||||
// 4. 加载热销商品
|
||||
loadHotProducts(categoryId)
|
||||
} catch (err) {
|
||||
console.error('加载分类数据异常:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载热销商品
|
||||
const loadHotProducts = async (categoryId: string) => {
|
||||
isLoadingProducts.value = true
|
||||
|
||||
try {
|
||||
const { data, error } = await supa
|
||||
.from('products')
|
||||
.select('*')
|
||||
.eq('status', 1)
|
||||
.eq('category_id', categoryId)
|
||||
.order('sales', { ascending: false })
|
||||
.limit(12)
|
||||
|
||||
if (error !== null) {
|
||||
console.error('加载热销商品失败:', error)
|
||||
return
|
||||
}
|
||||
|
||||
hotProducts.value = data ?? []
|
||||
} catch (err) {
|
||||
console.error('加载热销商品异常:', err)
|
||||
} finally {
|
||||
isLoadingProducts.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取商品第一张图片
|
||||
const getProductFirstImage = (product: ProductType): string => {
|
||||
return product.images?.[0] || '/static/default-product.png'
|
||||
}
|
||||
|
||||
// 选择分类
|
||||
const selectCategory = (category: CategoryType) => {
|
||||
activeCategoryId.value = category.id
|
||||
}
|
||||
|
||||
// 导航到子分类
|
||||
const navigateToSubCategory = (subCategory: CategoryType) => {
|
||||
// 可以跳转到子分类的商品列表页
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/product-list?categoryId=${subCategory.id}&title=${encodeURIComponent(subCategory.name)}`
|
||||
})
|
||||
}
|
||||
|
||||
// 查看品牌商品
|
||||
const viewBrandProducts = (brand: BrandType) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/product-list?brandId=${brand.id}&title=${encodeURIComponent(brand.name)}`
|
||||
})
|
||||
}
|
||||
|
||||
// 查看商品详情
|
||||
const viewProductDetail = (product: ProductType) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/product-detail?id=${product.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 查看所有品牌
|
||||
const viewAllBrands = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/brands'
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转到搜索页
|
||||
const goToSearch = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/search'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.category-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.search-header {
|
||||
background-color: #ffffff;
|
||||
padding: 10px 15px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 20px;
|
||||
padding: 10px 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
color: #999999;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
color: #999999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.category-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.category-nav {
|
||||
width: 100px;
|
||||
background-color: #f8f8f8;
|
||||
border-right: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 15px 10px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background-color: #ffffff;
|
||||
color: #007aff;
|
||||
border-left: 3px solid #007aff;
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.nav-item.active .nav-text {
|
||||
color: #007aff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.category-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.category-banner {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.banner-image {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.sub-category-section,
|
||||
.brand-section,
|
||||
.hot-products-section {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.more-btn {
|
||||
color: #007aff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sub-category-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -5px;
|
||||
}
|
||||
|
||||
.sub-category-item {
|
||||
width: 33.33%;
|
||||
padding: 0 5px;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sub-category-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 25px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sub-category-name {
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.brand-scroll {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.brand-list {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.brand-item {
|
||||
width: 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 12px;
|
||||
color: #333333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hot-products-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -5px;
|
||||
}
|
||||
|
||||
.product-item {
|
||||
width: 50%;
|
||||
padding: 0 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 13px;
|
||||
color: #333333;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 8px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.product-price-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.current-price {
|
||||
font-size: 14px;
|
||||
color: #ff4757;
|
||||
font-weight: bold;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.original-price {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.sales-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sales-text {
|
||||
font-size: 11px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.loading-more {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #999999;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
361
pages/mall/consumer/category.uvue
Normal file
361
pages/mall/consumer/category.uvue
Normal file
@@ -0,0 +1,361 @@
|
||||
<!-- pages/mall/consumer/category.uvue -->
|
||||
<template>
|
||||
<view class="category-page">
|
||||
<!-- 顶部搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-box" @click="navigateToSearch">
|
||||
<text class="search-icon">🔍</text>
|
||||
<text class="search-placeholder">搜索商品</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类内容区 -->
|
||||
<view class="category-content">
|
||||
<!-- 左侧一级分类 -->
|
||||
<scroll-view scroll-y class="primary-category">
|
||||
<view
|
||||
v-for="item in primaryCategories"
|
||||
:key="item.id"
|
||||
:class="['primary-item', { active: activePrimary === item.id }]"
|
||||
@click="selectPrimaryCategory(item.id)"
|
||||
>
|
||||
<text class="primary-name">{{ item.name }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 右侧二级分类和商品 -->
|
||||
<scroll-view scroll-y class="secondary-content">
|
||||
<!-- 二级分类 -->
|
||||
<view v-if="secondaryCategories.length > 0" class="secondary-category">
|
||||
<view
|
||||
v-for="sub in secondaryCategories"
|
||||
:key="sub.id"
|
||||
class="secondary-item"
|
||||
@click="selectSecondaryCategory(sub.id)"
|
||||
>
|
||||
<image
|
||||
class="sub-category-image"
|
||||
:src="sub.image"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<text class="sub-category-name">{{ sub.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<view v-if="productList.length > 0" class="product-list">
|
||||
<view
|
||||
v-for="product in productList"
|
||||
:key="product.id"
|
||||
class="product-item"
|
||||
@click="navigateToProduct(product)"
|
||||
>
|
||||
<view class="product-image-wrapper">
|
||||
<image
|
||||
class="product-image"
|
||||
:src="product.image"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
<text class="product-name">{{ product.name }}</text>
|
||||
<text class="product-price">¥{{ product.price }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="!loading && productList.length === 0 && secondaryCategories.length === 0" class="empty-state">
|
||||
<text class="empty-icon">📁</text>
|
||||
<text class="empty-text">暂无商品</text>
|
||||
<button class="reload-btn" @click="loadData">刷新</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
// 响应式数据
|
||||
const primaryCategories = ref<any[]>([])
|
||||
const secondaryCategories = ref<any[]>([])
|
||||
const productList = ref<any[]>([])
|
||||
const activePrimary = ref<string>('1')
|
||||
const loading = ref<boolean>(false)
|
||||
|
||||
// Mock 数据
|
||||
const mockCategories = {
|
||||
primary: [
|
||||
{ id: '1', name: '推荐' },
|
||||
{ id: '2', name: '女装' },
|
||||
{ id: '3', name: '男装' },
|
||||
{ id: '4', name: '鞋靴' },
|
||||
{ id: '5', name: '箱包' },
|
||||
{ id: '6', name: '美妆' },
|
||||
{ id: '7', name: '数码' },
|
||||
{ id: '8', name: '家电' },
|
||||
{ id: '9', name: '家居' },
|
||||
{ id: '10', name: '母婴' },
|
||||
{ id: '11', name: '食品' },
|
||||
{ id: '12', name: '运动' },
|
||||
{ id: '13', name: '汽车' },
|
||||
{ id: '14', name: '百货' }
|
||||
],
|
||||
|
||||
secondary: {
|
||||
'1': [
|
||||
{ id: '101', name: '热销推荐', image: 'https://picsum.photos/100/100?random=1' },
|
||||
{ id: '102', name: '新品上市', image: 'https://picsum.photos/100/100?random=2' },
|
||||
{ id: '103', name: '限时特惠', image: 'https://picsum.photos/100/100?random=3' },
|
||||
{ id: '104', name: '精选好物', image: 'https://picsum.photos/100/100?random=4' }
|
||||
],
|
||||
'2': [
|
||||
{ id: '201', name: '连衣裙', image: 'https://picsum.photos/100/100?random=5' },
|
||||
{ id: '202', name: 'T恤', image: 'https://picsum.photos/100/100?random=6' },
|
||||
{ id: '203', name: '裤子', image: 'https://picsum.photos/100/100?random=7' },
|
||||
{ id: '204', name: '外套', image: 'https://picsum.photos/100/100?random=8' },
|
||||
{ id: '205', name: '内衣', image: 'https://picsum.photos/100/100?random=9' }
|
||||
]
|
||||
},
|
||||
|
||||
products: {
|
||||
'101': [
|
||||
{ id: '1001', name: '热销商品1', price: 99.9, image: 'https://picsum.photos/150/150?random=10' },
|
||||
{ id: '1002', name: '热销商品2', price: 199, image: 'https://picsum.photos/150/150?random=11' },
|
||||
{ id: '1003', name: '热销商品3', price: 299, image: 'https://picsum.photos/150/150?random=12' }
|
||||
],
|
||||
'201': [
|
||||
{ id: '2001', name: '连衣裙1', price: 199, image: 'https://picsum.photos/150/150?random=13' },
|
||||
{ id: '2002', name: '连衣裙2', price: 299, image: 'https://picsum.photos/150/150?random=14' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
const loadData = () => {
|
||||
loading.value = true
|
||||
|
||||
// 使用模拟数据
|
||||
setTimeout(() => {
|
||||
primaryCategories.value = mockCategories.primary
|
||||
secondaryCategories.value = mockCategories.secondary[activePrimary.value] || []
|
||||
productList.value = mockCategories.products['101'] || []
|
||||
loading.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// 选择一级分类
|
||||
const selectPrimaryCategory = (categoryId: string) => {
|
||||
activePrimary.value = categoryId
|
||||
secondaryCategories.value = mockCategories.secondary[categoryId] || []
|
||||
productList.value = mockCategories.products[categoryId + '01'] || []
|
||||
}
|
||||
|
||||
// 选择二级分类
|
||||
const selectSecondaryCategory = (subCategoryId: string) => {
|
||||
productList.value = mockCategories.products[subCategoryId] || []
|
||||
}
|
||||
|
||||
// 导航函数
|
||||
const navigateToSearch = () => {
|
||||
uni.navigateTo({ url: '/pages/mall/consumer/search' })
|
||||
}
|
||||
|
||||
const navigateToProduct = (product: any) => {
|
||||
uni.navigateTo({ url: `/pages/mall/consumer/product-detail?id=${product.id}` })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.category-page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
.search-bar {
|
||||
padding: 10px 15px;
|
||||
background-color: white;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 20px;
|
||||
padding: 10px 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
color: #999;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 分类内容区 */
|
||||
.category-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 左侧一级分类 */
|
||||
.primary-category {
|
||||
width: 100px;
|
||||
background-color: white;
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
|
||||
.primary-item {
|
||||
padding: 15px 10px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.primary-item.active {
|
||||
background-color: #fff0e8;
|
||||
color: #ff5000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.primary-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background-color: #ff5000;
|
||||
}
|
||||
|
||||
.primary-name {
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 右侧内容区 */
|
||||
.secondary-content {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 二级分类 */
|
||||
.secondary-category {
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.secondary-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sub-category-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 30px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sub-category-name {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 商品列表 */
|
||||
.product-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.product-item {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.product-image-wrapper {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-size: 16px;
|
||||
color: #ff5000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 50px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 60px;
|
||||
color: #ccc;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.reload-btn {
|
||||
background-color: #ff5000;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
padding: 8px 25px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
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>
|
||||
739
pages/mall/consumer/checkout.uvue
Normal file
739
pages/mall/consumer/checkout.uvue
Normal file
@@ -0,0 +1,739 @@
|
||||
<!-- 结算页面 -->
|
||||
<template>
|
||||
<view class="checkout-page">
|
||||
<!-- 顶部栏 -->
|
||||
<view class="checkout-header">
|
||||
<text class="back-btn" @click="goBack">‹</text>
|
||||
<text class="header-title">订单结算</text>
|
||||
</view>
|
||||
|
||||
<scroll-view class="checkout-content" scroll-y>
|
||||
<!-- 收货地址 -->
|
||||
<view class="address-section" @click="selectAddress">
|
||||
<view v-if="selectedAddress" class="address-info">
|
||||
<view class="address-header">
|
||||
<text class="recipient">{{ selectedAddress.recipient_name }}</text>
|
||||
<text class="phone">{{ selectedAddress.phone }}</text>
|
||||
<view v-if="selectedAddress.is_default" class="default-tag">
|
||||
<text class="tag-text">默认</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="address-detail">{{ getFullAddress(selectedAddress) }}</text>
|
||||
</view>
|
||||
<view v-else class="no-address">
|
||||
<text class="no-address-text">请选择收货地址</text>
|
||||
<text class="no-address-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<view class="products-section">
|
||||
<view v-for="item in checkoutItems" :key="item.id" class="product-item">
|
||||
<image class="product-image" :src="item.product_image" />
|
||||
<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 class="product-bottom">
|
||||
<text class="product-price">¥{{ item.price }}</text>
|
||||
<text class="product-quantity">×{{ item.quantity }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 配送方式 -->
|
||||
<view class="delivery-section">
|
||||
<text class="section-title">配送方式</text>
|
||||
<view class="delivery-options">
|
||||
<view v-for="option in deliveryOptions"
|
||||
:key="option.id"
|
||||
:class="['delivery-option', { selected: selectedDelivery === option.id }]"
|
||||
@click="selectDelivery(option)">
|
||||
<text class="option-name">{{ option.name }}</text>
|
||||
<text class="option-price">¥{{ option.price }}</text>
|
||||
<text v-if="selectedDelivery === option.id" class="option-selected">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 优惠券 -->
|
||||
<view class="coupon-section" @click="selectCoupon">
|
||||
<text class="section-title">优惠券</text>
|
||||
<view class="coupon-info">
|
||||
<text v-if="selectedCoupon" class="coupon-selected">{{ selectedCoupon.template?.name || '优惠券' }}</text>
|
||||
<text v-else class="coupon-placeholder">选择优惠券</text>
|
||||
<text class="coupon-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 买家留言 -->
|
||||
<view class="remark-section">
|
||||
<text class="section-title">买家留言</text>
|
||||
<textarea class="remark-input"
|
||||
v-model="remark"
|
||||
placeholder="选填,请先和商家协商一致"
|
||||
maxlength="100" />
|
||||
</view>
|
||||
|
||||
<!-- 价格明细 -->
|
||||
<view class="price-section">
|
||||
<text class="section-title">价格明细</text>
|
||||
<view class="price-detail">
|
||||
<view class="price-row">
|
||||
<text class="price-label">商品总价</text>
|
||||
<text class="price-value">¥{{ totalAmount.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">¥{{ actualAmount.toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部结算栏 -->
|
||||
<view class="bottom-bar">
|
||||
<view class="price-summary">
|
||||
<text class="summary-label">合计:</text>
|
||||
<text class="summary-price">¥{{ actualAmount.toFixed(2) }}</text>
|
||||
</view>
|
||||
<button class="submit-btn" @click="submitOrder">提交订单</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type CheckoutItemType = {
|
||||
id: string
|
||||
product_id: string
|
||||
sku_id: string
|
||||
product_name: string
|
||||
product_image: string
|
||||
sku_specifications: any
|
||||
price: number
|
||||
quantity: number
|
||||
}
|
||||
|
||||
type DeliveryOptionType = {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
description: string
|
||||
}
|
||||
|
||||
type UserCouponType = {
|
||||
id: string
|
||||
template: {
|
||||
name: string
|
||||
discount_value: number
|
||||
min_order_amount: number
|
||||
} | null
|
||||
}
|
||||
|
||||
const checkoutItems = ref<Array<CheckoutItemType>>([])
|
||||
const selectedAddress = ref<any>(null)
|
||||
const deliveryOptions = ref<Array<DeliveryOptionType>>([
|
||||
{ id: 'standard', name: '快递配送', price: 8.00, description: '1-3天送达' },
|
||||
{ id: 'express', name: '加急配送', price: 15.00, description: '当天送达' }
|
||||
])
|
||||
const selectedDelivery = ref<string>('standard')
|
||||
const selectedCoupon = ref<UserCouponType | null>(null)
|
||||
const remark = ref<string>('')
|
||||
|
||||
// 计算属性
|
||||
const totalAmount = computed(() => {
|
||||
return checkoutItems.value.reduce((sum, item) =>
|
||||
sum + (item.price * item.quantity), 0)
|
||||
})
|
||||
|
||||
const deliveryFee = computed(() => {
|
||||
const option = deliveryOptions.value.find(opt => opt.id === selectedDelivery.value)
|
||||
return option?.price || 0
|
||||
})
|
||||
|
||||
const discountAmount = computed(() => {
|
||||
if (!selectedCoupon.value || !selectedCoupon.value.template) return 0
|
||||
|
||||
const coupon = selectedCoupon.value.template
|
||||
if (totalAmount.value < coupon.min_order_amount) return 0
|
||||
|
||||
// 简单处理:假设都是满减券
|
||||
return coupon.discount_value
|
||||
})
|
||||
|
||||
const actualAmount = computed(() => {
|
||||
let amount = totalAmount.value + deliveryFee.value - discountAmount.value
|
||||
return amount > 0 ? amount : 0
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadCheckoutData()
|
||||
})
|
||||
|
||||
// 加载结算数据
|
||||
const loadCheckoutData = () => {
|
||||
// 从上一页获取数据
|
||||
const eventChannel = uni.getEventChannel()
|
||||
if (eventChannel) {
|
||||
eventChannel.on('acceptData', (data: any) => {
|
||||
checkoutItems.value = data.selectedItems || []
|
||||
loadDefaultAddress()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 加载默认地址
|
||||
const loadDefaultAddress = async () => {
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) return
|
||||
|
||||
try {
|
||||
const { data, error } = await supa
|
||||
.from('user_addresses')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.eq('is_default', true)
|
||||
.single()
|
||||
|
||||
if (error !== null) {
|
||||
console.error('加载默认地址失败:', error)
|
||||
return
|
||||
}
|
||||
|
||||
selectedAddress.value = data
|
||||
} catch (err) {
|
||||
console.error('加载默认地址异常:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前用户ID
|
||||
const getCurrentUserId = (): string => {
|
||||
const userStore = uni.getStorageSync('userInfo')
|
||||
return userStore?.id || ''
|
||||
}
|
||||
|
||||
// 获取完整地址
|
||||
const getFullAddress = (address: any): string => {
|
||||
return `${address.province}${address.city}${address.district}${address.detail}`
|
||||
}
|
||||
|
||||
// 获取规格文本
|
||||
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 selectAddress = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/address',
|
||||
events: {
|
||||
addressSelected: (address: any) => {
|
||||
selectedAddress.value = address
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 选择配送方式
|
||||
const selectDelivery = (option: DeliveryOptionType) => {
|
||||
selectedDelivery.value = option.id
|
||||
}
|
||||
|
||||
// 选择优惠券
|
||||
const selectCoupon = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/coupons',
|
||||
success: (res) => {
|
||||
res.eventChannel.emit('setSelectMode', { selectMode: true })
|
||||
}
|
||||
})
|
||||
|
||||
// 监听优惠券选择
|
||||
uni.$on('couponSelected', (coupon: any) => {
|
||||
selectedCoupon.value = coupon
|
||||
uni.$off('couponSelected')
|
||||
})
|
||||
}
|
||||
|
||||
// 提交订单
|
||||
const submitOrder = async () => {
|
||||
if (!selectedAddress.value) {
|
||||
uni.showToast({
|
||||
title: '请选择收货地址',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) {
|
||||
uni.showToast({
|
||||
title: '用户信息错误',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 生成订单号
|
||||
const orderNo = generateOrderNo()
|
||||
|
||||
const orderData = {
|
||||
order_no: orderNo,
|
||||
user_id: userId,
|
||||
merchant_id: 'default', // 这里需要根据商品确定商家
|
||||
status: 1, // 待支付
|
||||
total_amount: totalAmount.value,
|
||||
discount_amount: discountAmount.value,
|
||||
delivery_fee: deliveryFee.value,
|
||||
actual_amount: actualAmount.value,
|
||||
payment_method: 0, // 待选择
|
||||
payment_status: 0,
|
||||
delivery_address: selectedAddress.value,
|
||||
remark: remark.value,
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建订单
|
||||
const { data: order, error: orderError } = await supa
|
||||
.from('orders')
|
||||
.insert(orderData)
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (orderError !== null) {
|
||||
throw orderError
|
||||
}
|
||||
|
||||
// 创建订单商品项
|
||||
const orderItems = checkoutItems.value.map(item => ({
|
||||
order_id: order.id,
|
||||
product_id: item.product_id,
|
||||
sku_id: item.sku_id,
|
||||
product_name: item.product_name,
|
||||
sku_specifications: item.sku_specifications,
|
||||
price: item.price,
|
||||
quantity: item.quantity,
|
||||
total_amount: item.price * item.quantity
|
||||
}))
|
||||
|
||||
const { error: itemsError } = await supa
|
||||
.from('order_items')
|
||||
.insert(orderItems)
|
||||
|
||||
if (itemsError !== null) {
|
||||
throw itemsError
|
||||
}
|
||||
|
||||
// 使用优惠券
|
||||
if (selectedCoupon.value) {
|
||||
const { error: couponError } = await supa
|
||||
.from('user_coupons')
|
||||
.update({
|
||||
status: 2, // 已使用
|
||||
used_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', selectedCoupon.value.id)
|
||||
|
||||
if (couponError !== null) {
|
||||
console.error('更新优惠券状态失败:', couponError)
|
||||
}
|
||||
}
|
||||
|
||||
// 清空购物车
|
||||
await clearShoppingCart()
|
||||
|
||||
// 跳转到支付页面
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/payment?orderId=${order.id}&amount=${actualAmount.value}`
|
||||
})
|
||||
|
||||
} catch (err) {
|
||||
console.error('创建订单失败:', err)
|
||||
uni.showToast({
|
||||
title: '订单创建失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 生成订单号
|
||||
const generateOrderNo = (): string => {
|
||||
const date = new Date()
|
||||
const year = date.getFullYear()
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
const random = Math.random().toString().slice(2, 8)
|
||||
return `ORD${year}${month}${day}${random}`
|
||||
}
|
||||
|
||||
// 清空购物车
|
||||
const clearShoppingCart = async () => {
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) return
|
||||
|
||||
const productIds = checkoutItems.value.map(item => item.product_id)
|
||||
|
||||
try {
|
||||
const { error } = await supa
|
||||
.from('shopping_cart')
|
||||
.delete()
|
||||
.eq('user_id', userId)
|
||||
.in('product_id', productIds)
|
||||
|
||||
if (error !== null) {
|
||||
console.error('清空购物车失败:', error)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('清空购物车异常:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 返回
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.checkout-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.checkout-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;
|
||||
}
|
||||
|
||||
.checkout-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.address-section {
|
||||
background-color: #ffffff;
|
||||
margin-bottom: 10px;
|
||||
padding: 20px 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.address-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.address-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.recipient {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.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.4;
|
||||
}
|
||||
|
||||
.no-address {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.no-address-text {
|
||||
font-size: 16px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.no-address-arrow {
|
||||
color: #999999;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.products-section {
|
||||
background-color: #ffffff;
|
||||
margin-bottom: 10px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.product-item {
|
||||
display: flex;
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.product-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.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: 5px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.product-spec {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.product-bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-size: 16px;
|
||||
color: #ff4757;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.product-quantity {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.delivery-section,
|
||||
.coupon-section,
|
||||
.remark-section,
|
||||
.price-section {
|
||||
background-color: #ffffff;
|
||||
margin-bottom: 10px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.delivery-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.delivery-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 15px;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.delivery-option.selected {
|
||||
border-color: #007aff;
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
.option-name {
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.option-price {
|
||||
font-size: 14px;
|
||||
color: #ff4757;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.option-selected {
|
||||
color: #007aff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.coupon-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 15px;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.coupon-selected {
|
||||
font-size: 14px;
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.coupon-placeholder {
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.coupon-arrow {
|
||||
color: #999999;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.remark-input {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
padding: 10px;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.price-detail {
|
||||
padding: 15px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
background-color: #ffffff;
|
||||
padding: 15px;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
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;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background-color: #007aff;
|
||||
color: #ffffff;
|
||||
padding: 0 40px;
|
||||
height: 45px;
|
||||
border-radius: 22.5px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
590
pages/mall/consumer/coupons.uvue
Normal file
590
pages/mall/consumer/coupons.uvue
Normal file
@@ -0,0 +1,590 @@
|
||||
<!-- 优惠券页面 -->
|
||||
<template>
|
||||
<view class="coupons-page">
|
||||
<!-- 顶部栏 -->
|
||||
<view class="coupons-header">
|
||||
<view class="header-tabs">
|
||||
<view :class="['header-tab', { active: activeTab === 'available' }]" @click="changeTab('available')">
|
||||
<text class="tab-text">可用券</text>
|
||||
</view>
|
||||
<view :class="['header-tab', { active: activeTab === 'unavailable' }]" @click="changeTab('unavailable')">
|
||||
<text class="tab-text">已失效</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 优惠券列表 -->
|
||||
<scroll-view class="coupons-list" scroll-y>
|
||||
<!-- 为空提示 -->
|
||||
<view v-if="coupons.length === 0" class="empty-coupons">
|
||||
<text class="empty-icon">🎫</text>
|
||||
<text class="empty-text">{{ getEmptyText() }}</text>
|
||||
<text class="empty-subtext">{{ getEmptySubtext() }}</text>
|
||||
<button v-if="activeTab === 'available'" class="get-coupons-btn" @click="goToCouponCenter">
|
||||
去领券中心
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 优惠券项 -->
|
||||
<view v-for="coupon in coupons" :key="coupon.id" class="coupon-item">
|
||||
<view class="coupon-left">
|
||||
<text class="coupon-value">{{ formatCouponValue(coupon) }}</text>
|
||||
<text class="coupon-condition">{{ formatCondition(coupon) }}</text>
|
||||
</view>
|
||||
<view class="coupon-right">
|
||||
<view class="coupon-info">
|
||||
<text class="coupon-name">{{ coupon.template?.name || coupon.name }}</text>
|
||||
<text class="coupon-desc">{{ coupon.template?.description || '' }}</text>
|
||||
<text class="coupon-time">{{ formatTimeRange(coupon) }}</text>
|
||||
</view>
|
||||
<view class="coupon-actions">
|
||||
<button v-if="coupon.status === 1 && coupon.is_valid"
|
||||
class="use-btn"
|
||||
@click="useCoupon(coupon)">
|
||||
立即使用
|
||||
</button>
|
||||
<button v-else-if="coupon.template_id && activeTab === 'available'"
|
||||
class="receive-btn"
|
||||
@click="receiveCoupon(coupon)">
|
||||
领取
|
||||
</button>
|
||||
<text v-else class="status-text">{{ getStatusText(coupon) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type CouponTemplateType = {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
coupon_type: number
|
||||
discount_value: number
|
||||
min_order_amount: number
|
||||
start_time: string
|
||||
end_time: string
|
||||
per_user_limit: number
|
||||
total_quantity: number
|
||||
used_quantity: number
|
||||
status: number
|
||||
}
|
||||
|
||||
type UserCouponType = {
|
||||
id: string
|
||||
user_id: string
|
||||
template_id: string
|
||||
coupon_code: string
|
||||
status: number
|
||||
is_valid: boolean
|
||||
used_at: string | null
|
||||
expire_at: string
|
||||
created_at: string
|
||||
template: CouponTemplateType | null
|
||||
}
|
||||
|
||||
const activeTab = ref<string>('available')
|
||||
const coupons = ref<Array<any>>([])
|
||||
const isLoading = ref<boolean>(false)
|
||||
|
||||
// 监听标签页变化
|
||||
watch(activeTab, () => {
|
||||
loadCoupons()
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadCoupons()
|
||||
})
|
||||
|
||||
// 加载优惠券
|
||||
const loadCoupons = async () => {
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) {
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/login'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
if (activeTab.value === 'available') {
|
||||
await loadAvailableCoupons(userId)
|
||||
} else {
|
||||
await loadUnavailableCoupons(userId)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载优惠券异常:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载可用优惠券
|
||||
const loadAvailableCoupons = async (userId: string) => {
|
||||
try {
|
||||
const now = new Date().toISOString()
|
||||
|
||||
// 加载用户已领取的优惠券
|
||||
const { data: userCoupons, error: userError } = await supa
|
||||
.from('user_coupons')
|
||||
.select(`
|
||||
*,
|
||||
template:coupon_templates(*)
|
||||
`)
|
||||
.eq('user_id', userId)
|
||||
.eq('status', 1)
|
||||
.gte('expire_at', now)
|
||||
.order('expire_at', { ascending: true })
|
||||
|
||||
if (userError !== null) {
|
||||
console.error('加载用户优惠券失败:', userError)
|
||||
return
|
||||
}
|
||||
|
||||
// 加载可领取的优惠券
|
||||
const { data: availableCoupons, error: availableError } = await supa
|
||||
.from('coupon_templates')
|
||||
.select('*')
|
||||
.eq('status', 1)
|
||||
.gte('end_time', now)
|
||||
.lte('start_time', now)
|
||||
.order('created_at', { ascending: false })
|
||||
|
||||
if (availableError !== null) {
|
||||
console.error('加载可领取优惠券失败:', availableError)
|
||||
return
|
||||
}
|
||||
|
||||
// 合并结果
|
||||
const allCoupons = [...(userCoupons || []), ...(availableCoupons || [])]
|
||||
coupons.value = allCoupons
|
||||
} catch (err) {
|
||||
console.error('加载可用优惠券异常:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载不可用优惠券
|
||||
const loadUnavailableCoupons = async (userId: string) => {
|
||||
try {
|
||||
const now = new Date().toISOString()
|
||||
|
||||
const { data, error } = await supa
|
||||
.from('user_coupons')
|
||||
.select(`
|
||||
*,
|
||||
template:coupon_templates(*)
|
||||
`)
|
||||
.eq('user_id', userId)
|
||||
.or('status.eq.2,expire_at.lt.' + now)
|
||||
.order('used_at', { ascending: false })
|
||||
.order('expire_at', { ascending: false })
|
||||
|
||||
if (error !== null) {
|
||||
console.error('加载失效优惠券失败:', error)
|
||||
return
|
||||
}
|
||||
|
||||
coupons.value = data ?? []
|
||||
} catch (err) {
|
||||
console.error('加载失效优惠券异常:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前用户ID
|
||||
const getCurrentUserId = (): string | null => {
|
||||
const userStore = uni.getStorageSync('userInfo')
|
||||
return userStore?.id || null
|
||||
}
|
||||
|
||||
// 获取空状态文本
|
||||
const getEmptyText = (): string => {
|
||||
return activeTab.value === 'available' ? '暂无可用优惠券' : '暂无失效优惠券'
|
||||
}
|
||||
|
||||
// 获取空状态副文本
|
||||
const getEmptySubtext = (): string => {
|
||||
return activeTab.value === 'available' ? '去领券中心看看' : '努力使用优惠券吧'
|
||||
}
|
||||
|
||||
// 格式化优惠券价值
|
||||
const formatCouponValue = (coupon: any): string => {
|
||||
if (coupon.template_id) {
|
||||
// 用户优惠券
|
||||
const template = coupon.template
|
||||
if (!template) return '未知'
|
||||
|
||||
if (template.coupon_type === 1) {
|
||||
return `¥${template.discount_value}`
|
||||
} else if (template.coupon_type === 2) {
|
||||
return `${template.discount_value}折`
|
||||
} else {
|
||||
return '未知'
|
||||
}
|
||||
} else {
|
||||
// 优惠券模板
|
||||
if (coupon.coupon_type === 1) {
|
||||
return `¥${coupon.discount_value}`
|
||||
} else if (coupon.coupon_type === 2) {
|
||||
return `${coupon.discount_value}折`
|
||||
} else {
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化使用条件
|
||||
const formatCondition = (coupon: any): string => {
|
||||
const minAmount = coupon.template?.min_order_amount || coupon.min_order_amount
|
||||
if (minAmount > 0) {
|
||||
return `满${minAmount}元可用`
|
||||
}
|
||||
return '无门槛'
|
||||
}
|
||||
|
||||
// 格式化时间范围
|
||||
const formatTimeRange = (coupon: any): string => {
|
||||
const startTime = coupon.template?.start_time || coupon.start_time
|
||||
const endTime = coupon.template?.end_time || coupon.expire_at
|
||||
|
||||
if (startTime && endTime) {
|
||||
const start = new Date(startTime)
|
||||
const end = new Date(endTime)
|
||||
|
||||
const startStr = `${start.getMonth() + 1}月${start.getDate()}日`
|
||||
const endStr = `${end.getMonth() + 1}月${end.getDate()}日`
|
||||
|
||||
return `${startStr}-${endStr}`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (coupon: any): string => {
|
||||
if (coupon.status === 2) {
|
||||
return '已使用'
|
||||
} else if (!coupon.is_valid) {
|
||||
return '已失效'
|
||||
} else if (new Date(coupon.expire_at) < new Date()) {
|
||||
return '已过期'
|
||||
}
|
||||
return '未知'
|
||||
}
|
||||
|
||||
// 切换标签页
|
||||
const changeTab = (tab: string) => {
|
||||
activeTab.value = tab
|
||||
}
|
||||
|
||||
// 使用优惠券
|
||||
const useCoupon = (coupon: any) => {
|
||||
// 如果是从订单页面跳转过来的,返回选择的优惠券
|
||||
const pages = getCurrentPages()
|
||||
const prevPage = pages[pages.length - 2]
|
||||
|
||||
if (prevPage && prevPage.route === 'pages/mall/consumer/checkout') {
|
||||
uni.$emit('couponSelected', coupon)
|
||||
uni.navigateBack()
|
||||
return
|
||||
}
|
||||
|
||||
// 否则跳转到商品列表页
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/index'
|
||||
})
|
||||
}
|
||||
|
||||
// 领取优惠券
|
||||
const receiveCoupon = async (coupon: any) => {
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) return
|
||||
|
||||
// 检查是否已领取
|
||||
const { data: existingCoupons, error: checkError } = await supa
|
||||
.from('user_coupons')
|
||||
.select('id')
|
||||
.eq('user_id', userId)
|
||||
.eq('template_id', coupon.id)
|
||||
|
||||
if (checkError !== null) {
|
||||
console.error('检查优惠券失败:', checkError)
|
||||
return
|
||||
}
|
||||
|
||||
if (existingCoupons && existingCoupons.length >= coupon.per_user_limit) {
|
||||
uni.showToast({
|
||||
title: '已达领取上限',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查库存
|
||||
if (coupon.used_quantity >= coupon.total_quantity) {
|
||||
uni.showToast({
|
||||
title: '优惠券已领完',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 生成优惠券码
|
||||
const couponCode = generateCouponCode()
|
||||
const expireAt = coupon.end_time
|
||||
|
||||
try {
|
||||
const { error: insertError } = await supa
|
||||
.from('user_coupons')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
template_id: coupon.id,
|
||||
coupon_code: couponCode,
|
||||
expire_at: expireAt,
|
||||
status: 1,
|
||||
is_valid: true
|
||||
})
|
||||
|
||||
if (insertError !== null) {
|
||||
console.error('领取优惠券失败:', insertError)
|
||||
uni.showToast({
|
||||
title: '领取失败',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 更新优惠券模板的已领取数量
|
||||
const { error: updateError } = await supa
|
||||
.from('coupon_templates')
|
||||
.update({ used_quantity: coupon.used_quantity + 1 })
|
||||
.eq('id', coupon.id)
|
||||
|
||||
if (updateError !== null) {
|
||||
console.error('更新优惠券数量失败:', updateError)
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '领取成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 重新加载数据
|
||||
loadCoupons()
|
||||
} catch (err) {
|
||||
console.error('领取优惠券异常:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成优惠券码
|
||||
const generateCouponCode = (): string => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
let result = ''
|
||||
for (let i = 0; i < 12; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 跳转到领券中心
|
||||
const goToCouponCenter = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/coupon-center'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.coupons-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.coupons-header {
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.header-tabs {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.header-tab {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-tab.active {
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.header-tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background-color: #007aff;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.header-tab.active .tab-text {
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.coupons-list {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.empty-coupons {
|
||||
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;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.get-coupons-btn {
|
||||
background-color: #007aff;
|
||||
color: #ffffff;
|
||||
padding: 10px 40px;
|
||||
border-radius: 25px;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.coupon-item {
|
||||
background-color: #ffffff;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.coupon-left {
|
||||
width: 100px;
|
||||
background: linear-gradient(135deg, #ff6b6b, #ffa726);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px 10px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.coupon-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.coupon-condition {
|
||||
font-size: 11px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.coupon-right {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.coupon-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.coupon-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 5px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.coupon-desc {
|
||||
font-size: 13px;
|
||||
color: #666666;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.coupon-time {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.coupon-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.use-btn,
|
||||
.receive-btn {
|
||||
padding: 8px 20px;
|
||||
border-radius: 15px;
|
||||
font-size: 12px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.use-btn {
|
||||
background-color: #007aff;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.receive-btn {
|
||||
background-color: #ff4757;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
694
pages/mall/consumer/favorites.uvue
Normal file
694
pages/mall/consumer/favorites.uvue
Normal file
@@ -0,0 +1,694 @@
|
||||
<!-- 收藏页面 -->
|
||||
<template>
|
||||
<view class="favorites-page">
|
||||
<!-- 顶部栏 -->
|
||||
<view class="favorites-header">
|
||||
<view class="header-title">
|
||||
<text class="title-text">我的收藏</text>
|
||||
</view>
|
||||
<view v-if="favorites.length > 0" class="edit-btn" @click="toggleEditMode">
|
||||
<text class="edit-text">{{ isEditMode ? '完成' : '编辑' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<view class="favorites-tabs">
|
||||
<view :class="['favorites-tab', { active: activeTab === 'product' }]" @click="changeTab('product')">
|
||||
<text class="tab-text">商品</text>
|
||||
</view>
|
||||
<view :class="['favorites-tab', { active: activeTab === 'shop' }]" @click="changeTab('shop')">
|
||||
<text class="tab-text">店铺</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收藏内容 -->
|
||||
<scroll-view class="favorites-content" scroll-y @scrolltolower="loadMore">
|
||||
<!-- 空状态 -->
|
||||
<view v-if="favorites.length === 0 && !isLoading" class="empty-favorites">
|
||||
<text class="empty-icon">❤️</text>
|
||||
<text class="empty-text">{{ getEmptyText() }}</text>
|
||||
<text class="empty-subtext">{{ getEmptySubtext() }}</text>
|
||||
<button class="go-shopping-btn" @click="goShopping">去逛逛</button>
|
||||
</view>
|
||||
|
||||
<!-- 商品收藏 -->
|
||||
<view v-if="activeTab === 'product'" class="product-favorites">
|
||||
<view v-for="item in favorites" :key="item.id" class="product-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="getProductImage(item)" />
|
||||
<view class="product-info">
|
||||
<text class="product-name">{{ item.product?.name || '商品已下架' }}</text>
|
||||
<text class="product-price">¥{{ item.product?.price || 0 }}</text>
|
||||
<view class="product-meta">
|
||||
<text class="meta-text">收藏时间: {{ formatTime(item.created_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 店铺收藏 -->
|
||||
<view v-if="activeTab === 'shop'" class="shop-favorites">
|
||||
<view v-for="item in favorites" :key="item.id" class="shop-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="viewShop(item)">
|
||||
<image class="shop-logo" :src="item.shop?.shop_logo || '/static/default-shop.png'" />
|
||||
<view class="shop-info">
|
||||
<text class="shop-name">{{ item.shop?.shop_name || '店铺已关闭' }}</text>
|
||||
<view class="shop-rating">
|
||||
<text class="rating-text">评分: {{ item.shop?.rating?.toFixed(1) || '0.0' }}</text>
|
||||
<text class="sales-text">销量: {{ item.shop?.total_sales || 0 }}</text>
|
||||
</view>
|
||||
<view class="shop-meta">
|
||||
<text class="meta-text">收藏时间: {{ formatTime(item.created_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="isLoading" class="loading-more">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
<view v-if="!hasMore && favorites.length > 0" class="no-more">
|
||||
<text class="no-more-text">没有更多了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 编辑操作栏 -->
|
||||
<view v-if="isEditMode && favorites.length > 0" class="edit-bar">
|
||||
<view class="select-all" @click="toggleSelectAll">
|
||||
<view :class="['all-select-icon', { selected: isAllSelected }]">
|
||||
<text v-if="isAllSelected" class="icon-text">✓</text>
|
||||
</view>
|
||||
<text class="select-all-text">全选</text>
|
||||
</view>
|
||||
<view class="delete-btn" @click="deleteSelected">
|
||||
<text class="delete-text">删除({{ selectedCount }})</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type FavoriteType = {
|
||||
id: string
|
||||
user_id: string
|
||||
product_id: string | null
|
||||
merchant_id: string | null
|
||||
type: string // 'product' | 'shop'
|
||||
created_at: string
|
||||
selected?: boolean
|
||||
product?: {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
images: string[]
|
||||
status: number
|
||||
}
|
||||
shop?: {
|
||||
id: string
|
||||
shop_name: string
|
||||
shop_logo: string
|
||||
rating: number
|
||||
total_sales: number
|
||||
shop_status: number
|
||||
}
|
||||
}
|
||||
|
||||
const activeTab = ref<string>('product')
|
||||
const favorites = ref<Array<FavoriteType>>([])
|
||||
const isEditMode = ref<boolean>(false)
|
||||
const isLoading = ref<boolean>(false)
|
||||
const currentPage = ref<number>(1)
|
||||
const pageSize = ref<number>(20)
|
||||
const hasMore = ref<boolean>(true)
|
||||
|
||||
// 计算属性
|
||||
const selectedCount = computed(() => {
|
||||
return favorites.value.filter(item => item.selected).length
|
||||
})
|
||||
|
||||
const isAllSelected = computed(() => {
|
||||
return favorites.value.length > 0 && favorites.value.every(item => item.selected)
|
||||
})
|
||||
|
||||
// 监听标签页变化
|
||||
watch(activeTab, () => {
|
||||
resetData()
|
||||
loadFavorites()
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadFavorites()
|
||||
})
|
||||
|
||||
// 重置数据
|
||||
const resetData = () => {
|
||||
favorites.value = []
|
||||
currentPage.value = 1
|
||||
hasMore.value = true
|
||||
isEditMode.value = false
|
||||
}
|
||||
|
||||
// 加载收藏数据
|
||||
const loadFavorites = 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('user_favorites')
|
||||
.select(`
|
||||
*,
|
||||
product:product_id(*),
|
||||
shop:merchant_id(*)
|
||||
`)
|
||||
.eq('user_id', userId)
|
||||
.eq('type', activeTab.value)
|
||||
.order('created_at', { ascending: false })
|
||||
|
||||
// 分页
|
||||
query = query.range((page - 1) * pageSize.value, page * pageSize.value - 1)
|
||||
|
||||
const { data, error } = await query
|
||||
|
||||
if (error !== null) {
|
||||
console.error('加载收藏失败:', error)
|
||||
return
|
||||
}
|
||||
|
||||
const newFavorites = (data || []).map((item: any) => ({
|
||||
...item,
|
||||
selected: false
|
||||
}))
|
||||
|
||||
if (loadMore) {
|
||||
favorites.value.push(...newFavorites)
|
||||
currentPage.value = page
|
||||
} else {
|
||||
favorites.value = newFavorites
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
hasMore.value = newFavorites.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 getProductImage = (item: FavoriteType): string => {
|
||||
if (!item.product?.images?.[0]) {
|
||||
return '/static/default-product.png'
|
||||
}
|
||||
return item.product.images[0]
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
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')
|
||||
return `${month}-${day}`
|
||||
}
|
||||
|
||||
// 获取空状态文本
|
||||
const getEmptyText = (): string => {
|
||||
return activeTab.value === 'product' ? '暂无商品收藏' : '暂无店铺收藏'
|
||||
}
|
||||
|
||||
// 获取空状态副文本
|
||||
const getEmptySubtext = (): string => {
|
||||
return activeTab.value === 'product' ? '快去收藏喜欢的商品吧' : '快去收藏喜欢的店铺吧'
|
||||
}
|
||||
|
||||
// 切换编辑模式
|
||||
const toggleEditMode = () => {
|
||||
isEditMode.value = !isEditMode.value
|
||||
// 重置选择状态
|
||||
favorites.value.forEach(item => {
|
||||
item.selected = false
|
||||
})
|
||||
}
|
||||
|
||||
// 切换标签页
|
||||
const changeTab = (tab: string) => {
|
||||
activeTab.value = tab
|
||||
}
|
||||
|
||||
// 切换选择状态
|
||||
const toggleSelect = (item: FavoriteType) => {
|
||||
item.selected = !item.selected
|
||||
favorites.value = [...favorites.value]
|
||||
}
|
||||
|
||||
// 全选/取消全选
|
||||
const toggleSelectAll = () => {
|
||||
const newSelectedState = !isAllSelected.value
|
||||
favorites.value.forEach(item => {
|
||||
item.selected = newSelectedState
|
||||
})
|
||||
favorites.value = [...favorites.value]
|
||||
}
|
||||
|
||||
// 删除选中项
|
||||
const deleteSelected = async () => {
|
||||
const selectedItems = favorites.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) {
|
||||
try {
|
||||
const ids = selectedItems.map(item => item.id)
|
||||
|
||||
const { error } = await supa
|
||||
.from('user_favorites')
|
||||
.delete()
|
||||
.in('id', ids)
|
||||
|
||||
if (error !== null) {
|
||||
throw error
|
||||
}
|
||||
|
||||
// 从列表中移除
|
||||
favorites.value = favorites.value.filter(item => !item.selected)
|
||||
|
||||
uni.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 如果删完了,退出编辑模式
|
||||
if (favorites.value.length === 0) {
|
||||
isEditMode.value = false
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('删除收藏失败:', err)
|
||||
uni.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 查看商品
|
||||
const viewProduct = (item: FavoriteType) => {
|
||||
if (isEditMode.value) return
|
||||
|
||||
if (!item.product_id || !item.product?.status) {
|
||||
uni.showToast({
|
||||
title: '商品已下架',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/product-detail?id=${item.product_id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 查看店铺
|
||||
const viewShop = (item: FavoriteType) => {
|
||||
if (isEditMode.value) return
|
||||
|
||||
if (!item.merchant_id || !item.shop?.shop_status) {
|
||||
uni.showToast({
|
||||
title: '店铺已关闭',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/shop-detail?id=${item.merchant_id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = () => {
|
||||
if (hasMore.value && !isLoading.value) {
|
||||
loadFavorites(true)
|
||||
}
|
||||
}
|
||||
|
||||
// 去逛逛
|
||||
const goShopping = () => {
|
||||
uni.switchTab({
|
||||
url: '/pages/mall/consumer/index'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.favorites-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.favorites-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;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.edit-text {
|
||||
color: #007aff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.favorites-tabs {
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.favorites-tab {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.favorites-tab.active {
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.favorites-tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background-color: #007aff;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 16px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.favorites-tab.active .tab-text {
|
||||
color: #007aff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.favorites-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.empty-favorites {
|
||||
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;
|
||||
}
|
||||
|
||||
.product-favorites,
|
||||
.shop-favorites {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.product-item,
|
||||
.shop-item {
|
||||
background-color: #ffffff;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.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;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
font-size: 16px;
|
||||
color: #ff4757;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.product-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.meta-text {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.shop-logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.shop-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.shop-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.shop-rating {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.rating-text,
|
||||
.sales-text {
|
||||
font-size: 13px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.shop-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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>
|
||||
674
pages/mall/consumer/footprint.uvue
Normal file
674
pages/mall/consumer/footprint.uvue
Normal file
@@ -0,0 +1,674 @@
|
||||
<!-- 足迹页面 -->
|
||||
<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="getProductImage(item)" />
|
||||
<view class="product-info">
|
||||
<text class="product-name">{{ item.product?.name || '商品已下架' }}</text>
|
||||
<view class="product-price-row">
|
||||
<text class="current-price">¥{{ item.product?.price || 0 }}</text>
|
||||
<text v-if="item.product?.original_price && item.product.original_price > item.product.price"
|
||||
class="original-price">¥{{ item.product.original_price }}</text>
|
||||
</view>
|
||||
<view class="product-meta">
|
||||
<text class="sales-text">已售{{ item.product?.sales || 0 }}</text>
|
||||
<text class="time-text">{{ formatTime(item.created_at) }}</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'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type FootprintType = {
|
||||
id: string
|
||||
user_id: string
|
||||
product_id: string
|
||||
created_at: string
|
||||
selected?: boolean
|
||||
product?: {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
original_price: number | null
|
||||
images: string[]
|
||||
sales: number
|
||||
status: number
|
||||
}
|
||||
}
|
||||
|
||||
const footprints = ref<Array<FootprintType>>([])
|
||||
const isEditMode = ref<boolean>(false)
|
||||
const isLoading = ref<boolean>(false)
|
||||
const currentPage = ref<number>(1)
|
||||
const pageSize = ref<number>(30)
|
||||
const hasMore = ref<boolean>(true)
|
||||
|
||||
// 计算属性
|
||||
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 = item.created_at.split('T')[0]
|
||||
if (!groups[date]) {
|
||||
groups[date] = []
|
||||
}
|
||||
groups[date].push(item)
|
||||
})
|
||||
|
||||
return groups
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadFootprints()
|
||||
})
|
||||
|
||||
// 加载足迹数据
|
||||
const loadFootprints = 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
|
||||
|
||||
const { data, error } = await supa
|
||||
.from('user_footprints')
|
||||
.select(`
|
||||
*,
|
||||
product:product_id(*)
|
||||
`)
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false })
|
||||
.range((page - 1) * pageSize.value, page * pageSize.value - 1)
|
||||
|
||||
if (error !== null) {
|
||||
console.error('加载足迹失败:', error)
|
||||
return
|
||||
}
|
||||
|
||||
const newFootprints = (data || []).map((item: any) => ({
|
||||
...item,
|
||||
selected: false
|
||||
}))
|
||||
|
||||
if (loadMore) {
|
||||
footprints.value.push(...newFootprints)
|
||||
currentPage.value = page
|
||||
} else {
|
||||
footprints.value = newFootprints
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
hasMore.value = newFootprints.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 getProductImage = (item: FootprintType): string => {
|
||||
if (!item.product?.images?.[0]) {
|
||||
return '/static/default-product.png'
|
||||
}
|
||||
return item.product.images[0]
|
||||
}
|
||||
|
||||
// 格式化日期分组
|
||||
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 = (timeStr: string): string => {
|
||||
const date = new Date(timeStr)
|
||||
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: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) return
|
||||
|
||||
const { error } = await supa
|
||||
.from('user_footprints')
|
||||
.delete()
|
||||
.eq('user_id', userId)
|
||||
|
||||
if (error !== null) {
|
||||
throw error
|
||||
}
|
||||
|
||||
footprints.value = []
|
||||
|
||||
uni.showToast({
|
||||
title: '已清空',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
} catch (err) {
|
||||
console.error('清空足迹失败:', err)
|
||||
uni.showToast({
|
||||
title: '清空失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 切换选择状态
|
||||
const toggleSelect = (item: FootprintType) => {
|
||||
item.selected = !item.selected
|
||||
footprints.value = [...footprints.value]
|
||||
}
|
||||
|
||||
// 切换分组全选
|
||||
const toggleGroupSelect = (date: string) => {
|
||||
const group = groupedFootprints.value[date]
|
||||
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 = (date: string): boolean => {
|
||||
const group = groupedFootprints.value[date]
|
||||
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 = async () => {
|
||||
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: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const ids = selectedItems.map(item => item.id)
|
||||
|
||||
const { error } = await supa
|
||||
.from('user_footprints')
|
||||
.delete()
|
||||
.in('id', ids)
|
||||
|
||||
if (error !== null) {
|
||||
throw error
|
||||
}
|
||||
|
||||
// 从列表中移除
|
||||
footprints.value = footprints.value.filter(item => !item.selected)
|
||||
|
||||
uni.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 如果删完了,退出编辑模式
|
||||
if (footprints.value.length === 0) {
|
||||
isEditMode.value = false
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('删除足迹失败:', err)
|
||||
uni.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 查看商品
|
||||
const viewProduct = (item: FootprintType) => {
|
||||
if (isEditMode.value) return
|
||||
|
||||
if (!item.product_id || !item.product?.status) {
|
||||
uni.showToast({
|
||||
title: '商品已下架',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/product-detail?id=${item.product_id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = () => {
|
||||
if (hasMore.value && !isLoading.value) {
|
||||
loadFootprints(true)
|
||||
}
|
||||
}
|
||||
|
||||
// 去逛逛
|
||||
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>
|
||||
1455
pages/mall/consumer/index - 副本 (2).uvue
Normal file
1455
pages/mall/consumer/index - 副本 (2).uvue
Normal file
File diff suppressed because it is too large
Load Diff
1550
pages/mall/consumer/index - 副本 (3).uvue
Normal file
1550
pages/mall/consumer/index - 副本 (3).uvue
Normal file
File diff suppressed because it is too large
Load Diff
1239
pages/mall/consumer/index - 副本.uvue
Normal file
1239
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
1550
pages/mall/consumer/indexback.uvue
Normal file
1550
pages/mall/consumer/indexback.uvue
Normal file
File diff suppressed because it is too large
Load Diff
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>
|
||||
1011
pages/mall/consumer/orders - 副本.uvue
Normal file
1011
pages/mall/consumer/orders - 副本.uvue
Normal file
File diff suppressed because it is too large
Load Diff
908
pages/mall/consumer/orders.uvue
Normal file
908
pages/mall/consumer/orders.uvue
Normal file
@@ -0,0 +1,908 @@
|
||||
<!-- 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'
|
||||
|
||||
// 响应式数据
|
||||
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)
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadOrders()
|
||||
})
|
||||
|
||||
// 加载订单数据
|
||||
const loadOrders = () => {
|
||||
loading.value = true
|
||||
|
||||
// 模拟API请求延迟
|
||||
setTimeout(() => {
|
||||
orders.value = [...mockOrders]
|
||||
loading.value = false
|
||||
}, 800)
|
||||
}
|
||||
|
||||
// 切换标签
|
||||
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
|
||||
|
||||
loadingMore.value = true
|
||||
|
||||
// 模拟加载更多数据
|
||||
setTimeout(() => {
|
||||
const newOrders = [...mockOrders].map((order, index) => ({
|
||||
...order,
|
||||
id: `${order.id}_${page.value}${index}`,
|
||||
order_no: `${order.order_no}_${page.value}${index}`
|
||||
}))
|
||||
|
||||
orders.value = [...orders.value, ...newOrders]
|
||||
loadingMore.value = false
|
||||
page.value++
|
||||
hasMore.value = orders.value.length < 20
|
||||
}, 1200)
|
||||
}
|
||||
|
||||
// 订单操作函数
|
||||
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: white;
|
||||
border-bottom: 1px solid #eee;
|
||||
position: sticky;
|
||||
top: 50px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tab-scroll {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.tab-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0 15px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex-shrink: 0;
|
||||
padding: 0 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.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>
|
||||
755
pages/mall/consumer/payment.uvue
Normal file
755
pages/mall/consumer/payment.uvue
Normal file
@@ -0,0 +1,755 @@
|
||||
<!-- 支付页面 -->
|
||||
<template>
|
||||
<view class="payment-page">
|
||||
<!-- 顶部栏 -->
|
||||
<view class="payment-header">
|
||||
<text class="back-btn" @click="goBack">‹</text>
|
||||
<text class="header-title">收银台</text>
|
||||
</view>
|
||||
|
||||
<view class="payment-content">
|
||||
<!-- 支付金额 -->
|
||||
<view class="amount-section">
|
||||
<text class="amount-label">支付金额</text>
|
||||
<text class="amount-value">¥{{ amount.toFixed(2) }}</text>
|
||||
<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 } from 'vue'
|
||||
import 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>('')
|
||||
|
||||
// 生命周期
|
||||
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)
|
||||
}
|
||||
|
||||
loadPaymentMethods()
|
||||
loadUserBalance()
|
||||
})
|
||||
|
||||
// 加载订单信息
|
||||
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
|
||||
}
|
||||
} 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
|
||||
} 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 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))
|
||||
|
||||
// 更新订单状态
|
||||
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
|
||||
})
|
||||
|
||||
// 跳转到支付成功页面
|
||||
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 goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</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;
|
||||
}
|
||||
|
||||
.amount-section {
|
||||
background-color: #ffffff;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.amount-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
display: block;
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: #ff4757;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.order-no {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.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>
|
||||
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>
|
||||
772
pages/mall/consumer/review.uvue
Normal file
772
pages/mall/consumer/review.uvue
Normal file
@@ -0,0 +1,772 @@
|
||||
<!-- 评价页面 -->
|
||||
<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)
|
||||
}
|
||||
|
||||
// 显示成功提示
|
||||
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>
|
||||
13
pages/mall/consumer/search.uvue
Normal file
13
pages/mall/consumer/search.uvue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
698
pages/mall/consumer/settings.uvue
Normal file
698
pages/mall/consumer/settings.uvue
Normal file
@@ -0,0 +1,698 @@
|
||||
<!-- 设置页面 -->
|
||||
<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>
|
||||
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>
|
||||
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,73 @@ 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[]
|
||||
}
|
||||
|
||||
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
Reference in New Issue
Block a user