Files
medical-mall/pages/main/profile.uvue

1854 lines
52 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 消费者端 - 个人中心 -->
<template>
<view class="consumer-profile">
<!-- 智能顶部导航栏 - 与消息页保持一致 -->
<view class="smart-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-container">
<!-- 基础用户信息:头像和昵称 -->
<view class="nav-user-basic" @click="editProfile">
<image
:src="userInfo.avatar_url != '' ? userInfo.avatar_url : '/static/images/default-product.png'"
class="nav-avatar"
/>
<text class="nav-user-name">{{ userInfo.nickname != '' ? userInfo.nickname : userInfo.phone }}</text>
</view>
<!-- 用户资产横向排列 (积分、余额、优惠券) -->
<view class="nav-user-stats" :style="{ marginRight: navBarRight + 'px' }">
<view class="nav-stat-item" @click="goToPoints">
<text class="nav-stat-label">积分</text>
<text class="nav-stat-value">{{ userStats.points }}</text>
</view>
<view class="nav-stat-item" @click="goToWallet">
<text class="nav-stat-label">余额</text>
<text class="nav-stat-value">¥{{ userStats.balance }}</text>
</view>
<view class="nav-stat-item" @click="goToCoupons">
<text class="nav-stat-label">券</text>
<text class="nav-stat-value">{{ serviceCounts.coupons }}</text>
</view>
</view>
<!-- 设置按钮 (最右侧) -->
<view class="nav-actions">
<view class="action-btn" @click="goToSettings">
<text class="action-icon">⚙️</text>
</view>
</view>
</view>
</view>
<scroll-view class="profile-scroll-content" :scroll-y="true">
<!-- 导航栏占位符 - 需要包含statusBarHeight + 导航栏高度44px -->
<view :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 我的服务 (移到订单上方) -->
<view class="my-services" style="margin-top: 10px;">
<view class="section-title">我的服务</view>
<view class="service-grid">
<view class="service-item" @click="goToCoupons">
<text class="service-icon">🎫</text>
<text class="service-text">优惠券</text>
<text v-if="serviceCounts.coupons > 0" class="service-badge">{{ serviceCounts.coupons }}</text>
</view>
<view class="service-item" @click="goToAddress">
<text class="service-icon">📍</text>
<text class="service-text">收货地址</text>
</view>
<view class="service-item" @click="goToFavorites">
<text class="service-icon">❤️</text>
<text class="service-text">我的收藏</text>
<text v-if="serviceCounts.favorites > 0" class="service-badge">{{ serviceCounts.favorites }}</text>
</view>
<view class="service-item" @click="goToFootprint">
<text class="service-icon">👣</text>
<text class="service-text">浏览足迹</text>
</view>
<view class="service-item" @click="goToRefund">
<text class="service-icon">🔄</text>
<text class="service-text">退款/售后</text>
</view>
<view class="service-item" @click="goToOrderReviews">
<text class="service-icon">📝</text>
<text class="service-text">评价</text>
</view>
<view class="service-item" @click="goToFollowedShops">
<text class="service-icon">⭐</text>
<text class="service-text">关注店铺</text>
</view>
<view class="service-item" @click="goToPoints">
<text class="service-icon">💰</text>
<text class="service-text">我的积分</text>
</view>
<view class="service-item" @click="goToBalance">
<text class="service-icon">💵</text>
<text class="service-text">我的余额</text>
</view>
<view class="service-item" @click="goToShare">
<text class="service-icon">🔗</text>
<text class="service-text">我的分享</text>
</view>
<view class="service-item" @click="goToMember">
<text class="service-icon">👑</text>
<text class="service-text">会员中心</text>
</view>
<view class="service-item" @click="goToSettings">
<text class="service-icon">⚙️</text>
<text class="service-text">设置</text>
</view>
</view>
</view>
<!-- 订单状态快捷入口 -->
<view class="order-shortcuts">
<view class="section-header-row">
<text class="section-title">我的订单</text>
<text class="view-all" @click="goToOrders(currentOrderTab)">查看更多 </text>
</view>
<view class="order-tabs">
<view class="order-tab" :class="{ active: currentOrderTab === 'all' }" @click="switchOrderTab('all')">
<text class="tab-icon">📋</text>
<text class="tab-text">全部</text>
<text v-if="orderCounts.total > 0" class="tab-badge">{{ orderCounts.total }}</text>
</view>
<view class="order-tab" :class="{ active: currentOrderTab === 'pending' }" @click="switchOrderTab('pending')">
<text class="tab-icon">💰</text>
<text class="tab-text">待支付</text>
<text v-if="orderCounts.pending > 0" class="tab-badge">{{ orderCounts.pending }}</text>
</view>
<view class="order-tab" :class="{ active: currentOrderTab === 'toship' }" @click="switchOrderTab('toship')">
<text class="tab-icon">🚚</text>
<text class="tab-text">待发货</text>
<text v-if="orderCounts.toship > 0" class="tab-badge">{{ orderCounts.toship }}</text>
</view>
<view class="order-tab" :class="{ active: currentOrderTab === 'shipped' }" @click="switchOrderTab('shipped')">
<text class="tab-icon">📦</text>
<text class="tab-text">待收货</text>
<text v-if="orderCounts.shipped > 0" class="tab-badge">{{ orderCounts.shipped }}</text>
</view>
</view>
</view>
<!-- 最近订单列表 (根据Tab切换显示) -->
<view class="recent-orders">
<view class="section-header">
<text class="section-title">{{ getOrderSectionTitle() }}</text>
</view>
<view v-if="filteredOrders.length === 0" class="empty-orders">
<text class="empty-text">暂无相关订单记录</text>
<button class="start-shopping" @click="goShopping">去逛逛</button>
</view>
<view v-for="order in filteredOrders" :key="order.id" class="order-item" @click="viewOrderDetail(order)">
<view class="order-item-header">
<view class="order-shop">
<text class="shop-icon">🏪</text>
<text class="shop-name">{{ getOrderShopName(order) }}</text>
<text class="shop-arrow"> </text>
</view>
<view class="status-row">
<text class="order-status-text" :class="getOrderStatusClass(order.status)">{{ getOrderStatusText(order.status) }}</text>
<text class="more-btn" @click.stop="showOrderMenu(order)">⋯</text>
</view>
</view>
<view class="order-item-content">
<image :src="getOrderMainImage(order)" class="order-item-image" mode="aspectFill" @click.stop="goToProductFromOrder(order)" />
<view class="order-item-info">
<view class="order-title-row">
<text class="order-item-title">{{ getOrderTitle(order) }}</text>
<text class="order-item-price">¥{{ order.actual_amount }}</text>
</view>
<view class="order-spec-row">
<text class="order-item-spec">{{ getOrderSpec(order) }}</text>
<text class="order-item-num">x{{ getOrderItemCount(order) }}</text>
</view>
<text class="order-item-time">{{ formatDateTime(order.created_at) }}</text>
</view>
</view>
<view class="order-item-actions">
<button v-if="order.status === 1" class="order-action-btn pay" @click.stop="payOrder(order)">立即支付</button>
<button v-if="order.status === 3" class="order-action-btn confirm" @click.stop="confirmReceive(order)">确认收货</button>
<button v-if="order.status === 4" class="order-action-btn review" @click.stop="reviewOrder(order)">评价</button>
<button v-if="order.status === 2 || order.status === 3" class="order-action-btn secondary" @click.stop="viewOrderDetail(order)">查看物流</button>
</view>
</view>
</view>
<!-- 我的服务 -->
<!-- <view class="my-services">
<view class="section-title">我的服务</view>
<view class="service-grid">
<view class="service-item" @click="goToCoupons">
<text class="service-icon">🎫</text>
<text class="service-text">优惠券</text>
<text v-if="serviceCounts.coupons > 0" class="service-badge">{{ serviceCounts.coupons }}</text>
</view>
<view class="service-item" @click="goToAddress">
<text class="service-icon">📍</text>
<text class="service-text">收货地址</text>
</view>
<view class="service-item" @click="goToFavorites">
<text class="service-icon">❤️</text>
<text class="service-text">我的收藏</text>
<text v-if="serviceCounts.favorites > 0" class="service-badge">{{ serviceCounts.favorites }}</text>
</view>
<view class="service-item" @click="goToFootprint">
<text class="service-icon">👣</text>
<text class="service-text">浏览足迹</text>
</view>
<view class="service-item" @click="goToRefund">
<text class="service-icon">🔄</text>
<text class="service-text">退款/售后</text>
</view>
<view class="service-item" @click="contactService">
<text class="service-icon">💬</text>
<text class="service-text">在线客服</text>
</view>
<view class="service-item" @click="goToMySubscriptions">
<text class="service-icon">🧩</text>
<text class="service-text">我的订阅</text>
</view>
<view class="service-item" @click="goToSubscriptions">
<text class="service-icon">🧩</text>
<text class="service-text">软件订阅</text>
</view>
</view>
</view> -->
<!-- 消费统计 -->
<view class="consumption-stats">
<view class="section-title">消费统计</view>
<view class="stats-period">
<text v-for="period in statsPeriods" :key="period.key"
class="period-tab"
:class="{ active: activeStatsPeriod === period.key }"
@click="switchStatsPeriod(period.key)">{{ period.label }}</text>
</view>
<view class="stats-content">
<view class="stat-card">
<text class="stat-value">¥{{ currentStats.total_amount }}</text>
<text class="stat-label">总消费</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ currentStats.order_count }}</text>
<text class="stat-label">订单数</text>
</view>
<view class="stat-card">
<text class="stat-value">¥{{ currentStats.avg_amount }}</text>
<text class="stat-label">平均消费</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ currentStats.save_amount }}</text>
<text class="stat-label">节省金额</text>
</view>
</view>
</view>
<!-- 账户安全 -->
<!-- <view class="account-security">
<view class="section-title">账户安全</view>
<view class="security-items">
<view class="security-item" @click="changePassword">
<text class="security-icon">🔒</text>
<text class="security-text">修改密码</text>
<text class="security-arrow">></text>
</view>
<view class="security-item" @click="bindPhone">
<text class="security-icon">📱</text>
<text class="security-text">手机绑定</text>
<view class="security-status">
<text class="status-text" :class="{ bound: userInfo.phone }">{{ userInfo.phone ? '已绑定' : '未绑定' }}</text>
<text class="security-arrow">></text>
</view>
</view>
<view class="security-item" @click="bindEmail">
<text class="security-icon">📧</text>
<text class="security-text">邮箱绑定</text>
<view class="security-status">
<text class="status-text" :class="{ bound: userInfo.email }">{{ userInfo.email ? '已绑定' : '未绑定' }}</text>
<text class="security-arrow">></text>
</view>
</view>
</view>
</view> -->
</scroll-view>
</view>
</template>
<script>
import { UserType } from '@/types/mall-types.uts'
import supabaseService from '@/utils/supabaseService.uts'
type UserStatsType = {
points: number
balance: number
level: number
}
type OrderCountsType = {
total: number
pending: number
toship: number
shipped: number
review: number
}
type ServiceCountsType = {
coupons: number
favorites: number
}
type ConsumptionStatsType = {
total_amount: number
order_count: number
avg_amount: number
save_amount: number
}
type StatsPeriodType = {
key: string
label: string
}
type OrderItemType = {
id: string
order_no: string
status: number
actual_amount: number
created_at: string
ml_order_items: any | null
ml_shops: any | null
items_count: number
}
export default {
data() {
return {
userInfo: {
id: '',
phone: '',
email: '',
nickname: '',
avatar_url: '',
gender: 0,
user_type: 0,
status: 0,
created_at: ''
} as UserType,
userStats: {
points: 0,
balance: 0,
level: 1
} as UserStatsType,
orderCounts: {
total: 0,
pending: 0,
toship: 0,
shipped: 0,
review: 0
} as OrderCountsType,
serviceCounts: {
coupons: 0,
favorites: 0
} as ServiceCountsType,
recentOrders: [] as Array<OrderItemType>,
statsPeriods: [
{ key: 'month', label: '本月' },
{ key: 'quarter', label: '本季度' },
{ key: 'year', label: '本年' },
{ key: 'all', label: '全部' }
] as Array<StatsPeriodType>,
activeStatsPeriod: 'month',
currentStats: {
total_amount: 0,
order_count: 0,
avg_amount: 0,
save_amount: 0
} as ConsumptionStatsType,
statusBarHeight: 0,
navBarRight: 0, // 导航栏右侧预留空间(小程序胶囊按钮)
currentOrderTab: 'all' as string,
allOrders: [] as Array<OrderItemType>
}
},
onLoad() {
this.initPage()
this.loadUserProfile()
this.loadOrders()
// 监听订单更新事件
uni.$on('orderUpdated', this.handleOrderUpdated)
},
onShow() {
this.refreshData()
},
onUnload() {
// 移除事件监听
uni.$off('orderUpdated', this.handleOrderUpdated)
},
computed: {
filteredOrders(): Array<OrderItemType> {
const result: Array<OrderItemType> = []
if (this.currentOrderTab === 'all') {
for (let i: number = 0; i < this.allOrders.length; i++) {
result.push(this.allOrders[i])
}
return result
}
let targetStatus: number = 0
if (this.currentOrderTab === 'pending') {
targetStatus = 1
} else if (this.currentOrderTab === 'toship') {
targetStatus = 2
} else if (this.currentOrderTab === 'shipped') {
targetStatus = 3
} else if (this.currentOrderTab === 'review') {
targetStatus = 4
} else {
return result
}
for (let i: number = 0; i < this.allOrders.length; i++) {
if (this.allOrders[i].status === targetStatus) {
result.push(this.allOrders[i])
}
}
return result
}
},
methods: {
async loadOrders() {
try {
const orders = await supabaseService.getOrders()
const mappedOrders: Array<OrderItemType> = []
for (let i: number = 0; i < orders.length; i++) {
const rawItem = orders[i]
const o = JSON.parse(JSON.stringify(rawItem)) as UTSJSONObject
let status = o.getNumber('status')
if (status == null) {
const orderStatus = o.getNumber('order_status')
status = orderStatus != null ? orderStatus : 0
}
let actualAmount = o.getNumber('actual_amount')
if (actualAmount == null) {
const totalAmount = o.getNumber('total_amount')
actualAmount = totalAmount != null ? totalAmount : 0
}
const mlOrderItems = o.get('ml_order_items')
let itemsCount = 0
if (mlOrderItems != null && Array.isArray(mlOrderItems)) {
itemsCount = (mlOrderItems as any[]).length
}
const orderItem: OrderItemType = {
id: o.getString('id') ?? '',
order_no: o.getString('order_no') ?? '',
status: status,
actual_amount: actualAmount,
created_at: o.getString('created_at') ?? '',
ml_order_items: mlOrderItems,
ml_shops: o.get('ml_shops'),
items_count: itemsCount
}
mappedOrders.push(orderItem)
}
for (let i: number = 0; i < mappedOrders.length; i++) {
for (let j: number = i + 1; j < mappedOrders.length; j++) {
const dateA = mappedOrders[i]['created_at'] as string
const dateB = mappedOrders[j]['created_at'] as string
const timeA = new Date(dateA != null ? dateA : '1970-01-01').getTime()
const timeB = new Date(dateB != null ? dateB : '1970-01-01').getTime()
if (timeA < timeB) {
const temp = mappedOrders[i]
mappedOrders[i] = mappedOrders[j]
mappedOrders[j] = temp
}
}
}
this.allOrders = mappedOrders
const recentList: Array<OrderItemType> = []
const limit = mappedOrders.length < 5 ? mappedOrders.length : 5
for (let i: number = 0; i < limit; i++) {
recentList.push(mappedOrders[i])
}
this.recentOrders = recentList
let total = 0
let pending = 0
let toship = 0
let shipped = 0
let review = 0
for (let i: number = 0; i < mappedOrders.length; i++) {
total++
const status = mappedOrders[i].status
if (status === 1) pending++
else if (status === 2) toship++
else if (status === 3) shipped++
else if (status === 4) review++
}
this.orderCounts = {
total: total,
pending: pending,
toship: toship,
shipped: shipped,
review: review
}
} catch (e) {
console.error('加载订单异常', e)
}
},
// 切换订单Tab
switchOrderTab(tab: string) {
this.currentOrderTab = tab
},
// 获取当前订单部分标题
getOrderSectionTitle(): string {
if (this.currentOrderTab === 'all') return '全部订单'
if (this.currentOrderTab === 'pending') return '待支付订单'
if (this.currentOrderTab === 'shipped') return '待收货订单'
if (this.currentOrderTab === 'review') return '待评价订单'
return '我的订单'
},
initPage() {
const systemInfo = uni.getSystemInfoSync()
this.statusBarHeight = systemInfo.statusBarHeight ?? 0
// 获取小程序胶囊按钮信息
// #ifdef MP-WEIXIN
try {
const menuButton = uni.getMenuButtonBoundingClientRect()
if (menuButton != null) {
this.navBarRight = (systemInfo.screenWidth - menuButton.left) + 10
}
} catch (e) {
console.log('获取胶囊按钮信息失败', e)
this.navBarRight = 90
}
// #endif
},
async loadUserProfile() {
try {
// 获取用户资料
const profile = await supabaseService.getUserProfile()
if (profile != null) {
// 映射字段
let uId = ''
let uPhone = ''
let uEmail = ''
let uNickname = ''
let uAvatar = ''
let uGender = 0
if (profile instanceof UTSJSONObject) {
uId = profile.getString('user_id') ?? ''
uPhone = profile.getString('phone') ?? ''
uEmail = profile.getString('email') ?? ''
uNickname = profile.getString('nickname') ?? ''
uAvatar = profile.getString('avatar_url') ?? ''
uGender = profile.getNumber('gender') ?? 0
} else {
// 必须使用 JSON.parse(JSON.stringify()) 转换为 UTSJSONObject
const profileObj = JSON.parse(JSON.stringify(profile)) as UTSJSONObject
uId = profileObj.getString('user_id') ?? ''
uPhone = profileObj.getString('phone') ?? ''
uEmail = profileObj.getString('email') ?? ''
uNickname = profileObj.getString('nickname') ?? ''
uAvatar = profileObj.getString('avatar_url') ?? ''
uGender = profileObj.getNumber('gender') ?? 0
}
if (uNickname === '' && uPhone !== '') {
uNickname = uPhone.substring(0, 3) + '****' + uPhone.substring(7)
}
this.userInfo = {
id: uId,
phone: uPhone,
email: uEmail,
nickname: uNickname != '' ? uNickname : '微信用户',
avatar_url: uAvatar != '' ? uAvatar : '/static/images/default-product.png',
gender: uGender,
user_type: 1,
status: 1,
created_at: new Date().toISOString()
} as UserType
} else {
// 如果获取失败未登录或无档案尝试获取当前登录ID
const userId = supabaseService.getCurrentUserId()
if (userId != null) {
this.userInfo.id = userId
this.userInfo.nickname = '用户' + userId.substring(0, 4)
} else {
this.userInfo.nickname = '未登录'
}
}
// 获取积分和余额顺序获取UTS不支持Promise.all数组解构
const balanceResult = await supabaseService.getUserBalance()
const points = await supabaseService.getUserPoints()
const balanceValue = balanceResult.getNumber('balance') ?? 0
this.userStats = {
points: points,
balance: balanceValue,
level: this.calculateLevel(points) // 根据积分计算等级
} as UserStatsType
} catch (e) {
console.error('加载用户信息失败', e)
// 保持默认或显示错误
}
},
calculateLevel(points: number): number {
if (points < 1000) return 0
if (points < 5000) return 1
if (points < 20000) return 2
if (points < 50000) return 3
return 4
},
loadConsumptionStats() {
if (this.activeStatsPeriod === 'month') {
this.currentStats = {
total_amount: 1280.50,
order_count: 8,
avg_amount: 160.06,
save_amount: 85.20
} as ConsumptionStatsType
} else if (this.activeStatsPeriod === 'quarter') {
this.currentStats = {
total_amount: 3680.80,
order_count: 18,
avg_amount: 204.49,
save_amount: 256.30
} as ConsumptionStatsType
} else if (this.activeStatsPeriod === 'year') {
this.currentStats = {
total_amount: 15680.90,
order_count: 56,
avg_amount: 280.02,
save_amount: 986.50
} as ConsumptionStatsType
} else {
this.currentStats = {
total_amount: 25680.50,
order_count: 89,
avg_amount: 288.55,
save_amount: 1580.20
} as ConsumptionStatsType
}
},
refreshData() {
// 刷新页面数据
this.loadUserProfile()
this.loadOrders()
this.updateCouponCount() // 更新优惠券数量
},
async updateCouponCount() {
// 从 Supabase 获取真实的优惠券数量
try {
const count = await supabaseService.getUserCouponCount()
this.serviceCounts.coupons = count
} catch (e) {
console.error('获取优惠券数量失败', e)
this.serviceCounts.coupons = 0
}
},
getUserLevel(): string {
const levels = ['新手', '铜牌会员', '银牌会员', '金牌会员', '钻石会员']
if (this.userStats.level >= 0 && this.userStats.level < levels.length) {
return levels[this.userStats.level]
}
return '新手'
},
getOrderStatusText(status: number): string {
if (status === 6) return '退款中'
if (status === 7) return '已退款'
const statusTexts = ['异常', '待支付', '待发货', '待收货', '已完成', '已取消']
if (status >= 0 && status < statusTexts.length) {
return statusTexts[status]
}
return '未知'
},
getOrderStatusClass(status: number): string {
if (status === 6) return 'refunding'
if (status === 7) return 'refunded'
const statusClasses = ['error', 'pending', 'processing', 'shipping', 'completed', 'cancelled']
if (status >= 0 && status < statusClasses.length) {
return statusClasses[status]
}
return 'error'
},
showOrderMenu(order: OrderItemType) {
const status = order.status
let actions: string[] = []
if (status === 1) {
actions = ['取消订单', '联系卖家']
} else if (status === 2) {
actions = ['提醒发货', '申请退款', '联系卖家']
} else if (status === 3) {
actions = ['查看物流', '确认收货', '申请退款', '联系卖家']
} else if (status === 4) {
actions = ['申请售后', '再次购买', '联系卖家']
} else if (status === 5) {
actions = ['删除订单', '再次购买', '联系卖家']
} else if (status === 6) {
actions = ['退款进度', '联系卖家']
} else if (status === 7) {
actions = ['再次购买', '联系卖家']
}
uni.showActionSheet({
itemList: actions,
success: (res) => {
const action = actions[res.tapIndex]
this.handleOrderAction(order, action)
}
})
},
handleOrderAction(order: OrderItemType, action: string) {
if (action === '取消订单') {
this.cancelOrderAction(order)
} else if (action === '联系卖家') {
this.contactSeller(order)
} else if (action === '提醒发货') {
this.remindShipping(order)
} else if (action === '申请退款' || action === '申请售后') {
this.applyRefund(order)
} else if (action === '查看物流') {
this.viewLogistics(order.id)
} else if (action === '确认收货') {
this.confirmReceive(order)
} else if (action === '再次购买') {
this.repurchase(order)
} else if (action === '删除订单') {
this.deleteOrder(order.id)
} else if (action === '退款进度') {
this.viewRefundProgress(order.id)
}
},
cancelOrderAction(order: OrderItemType) {
uni.showModal({
title: '确认取消',
content: '确定要取消此订单吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({ title: '取消中...' })
supabaseService.cancelOrder(order.id).then(() => {
uni.hideLoading()
uni.showToast({ title: '订单已取消', icon: 'success' })
this.loadOrders()
}).catch(() => {
uni.hideLoading()
uni.showToast({ title: '取消失败', icon: 'none' })
})
}
}
})
},
contactSeller(order: OrderItemType) {
const merchantId = order.ml_shops != null ? this.getMerchantIdFromOrder(order) : ''
if (merchantId !== '') {
uni.navigateTo({
url: `/pages/mall/consumer/chat?merchantId=${merchantId}`
})
} else {
uni.showToast({ title: '暂无卖家联系方式', icon: 'none' })
}
},
getMerchantIdFromOrder(order: OrderItemType): string {
const shopsRaw = order.ml_shops
if (shopsRaw != null) {
const shopStr = JSON.stringify(shopsRaw)
const shopParsed = JSON.parse(shopStr)
if (shopParsed != null) {
const shopObj = shopParsed as UTSJSONObject
return shopObj.getString('merchant_id') ?? ''
}
}
return ''
},
remindShipping(order: OrderItemType) {
uni.showLoading({ title: '提醒中...' })
const merchantId = order.ml_shops != null ? this.getMerchantIdFromOrder(order) : ''
if (merchantId !== '') {
const message = `你好,我的订单[${order.order_no}]还没有发货,请尽快安排,谢谢。`
supabaseService.sendChatMessage(message, merchantId).then(() => {
uni.hideLoading()
uni.showToast({ title: '已提醒卖家发货', icon: 'success' })
}).catch(() => {
uni.hideLoading()
uni.showToast({ title: '提醒失败', icon: 'none' })
})
} else {
uni.hideLoading()
uni.showToast({ title: '无法联系卖家', icon: 'none' })
}
},
applyRefund(order: OrderItemType) {
uni.navigateTo({
url: `/pages/mall/consumer/apply-refund?orderId=${order.id}`
})
},
viewLogistics(orderId: string) {
uni.navigateTo({
url: `/pages/mall/consumer/logistics?orderId=${orderId}`
})
},
repurchase(order: OrderItemType) {
uni.showLoading({ title: '处理中...' })
const itemsRaw = order.ml_order_items
if (itemsRaw == null || (itemsRaw as any[]).length === 0) {
uni.hideLoading()
uni.showToast({ title: '订单无商品', icon: 'none' })
return
}
const items = itemsRaw as any[]
let completed = 0
const total = items.length
let successCount = 0
for (let i = 0; i < items.length; i++) {
const itemStr = JSON.stringify(items[i])
const itemParsed = JSON.parse(itemStr)
if (itemParsed == null) {
completed++
if (completed === total) {
uni.hideLoading()
if (successCount > 0) {
uni.showToast({ title: `已添加${successCount}件商品`, icon: 'success' })
} else {
uni.showToast({ title: '添加失败', icon: 'none' })
}
}
continue
}
const itemObj = itemParsed as UTSJSONObject
const productId = itemObj.getString('product_id') ?? ''
const merchantId = order.ml_shops != null ? this.getMerchantIdFromOrder(order) : ''
if (productId !== '') {
supabaseService.addToCart(productId, 1, '', merchantId).then((success) => {
completed++
if (success) successCount++
if (completed === total) {
uni.hideLoading()
if (successCount > 0) {
uni.showToast({ title: `已添加${successCount}件商品`, icon: 'success' })
} else {
uni.showToast({ title: '添加失败', icon: 'none' })
}
}
}).catch(() => {
completed++
if (completed === total) {
uni.hideLoading()
if (successCount > 0) {
uni.showToast({ title: `已添加${successCount}件商品`, icon: 'success' })
} else {
uni.showToast({ title: '添加失败', icon: 'none' })
}
}
})
} else {
completed++
if (completed === total) {
uni.hideLoading()
if (successCount > 0) {
uni.showToast({ title: `已添加${successCount}件商品`, icon: 'success' })
} else {
uni.showToast({ title: '添加失败', icon: 'none' })
}
}
}
}
},
deleteOrder(orderId: string) {
uni.showModal({
title: '删除订单',
content: '确定要删除此订单吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({ title: '删除中...' })
supabaseService.deleteOrder(orderId).then(() => {
uni.hideLoading()
uni.showToast({ title: '订单已删除', icon: 'success' })
this.loadOrders()
}).catch(() => {
uni.hideLoading()
uni.showToast({ title: '删除失败', icon: 'none' })
})
}
}
})
},
viewRefundProgress(orderId: string) {
uni.navigateTo({
url: `/pages/mall/consumer/refund?orderId=${orderId}`
})
},
getOrderShopName(order: OrderItemType): string {
const shopsRaw = order.ml_shops
if (shopsRaw != null) {
const shopStr = JSON.stringify(shopsRaw)
const shopParsed = JSON.parse(shopStr)
if (shopParsed != null) {
const shopObj = shopParsed as UTSJSONObject
const name = shopObj.getString('shop_name')
if (name != null && name !== '') return name
}
}
return '自营店铺'
},
getOrderMainImage(order: OrderItemType): string {
const itemsRaw = order.ml_order_items
if (itemsRaw == null) return '/static/images/default-product.png'
const items = itemsRaw as any[]
if (items.length > 0) {
const firstItem = items[0]
const itemStr = JSON.stringify(firstItem)
const itemParsed = JSON.parse(itemStr)
if (itemParsed == null) return '/static/images/default-product.png'
const itemObj = itemParsed as UTSJSONObject
const imgUrl = itemObj.getString('image_url')
const prodImg = itemObj.getString('product_image')
const img = (imgUrl != null && imgUrl !== '') ? imgUrl : prodImg
if (img != null && img !== '') return img
}
return '/static/images/default-product.png'
},
getOrderTitle(order: OrderItemType): string {
const itemsRaw = order.ml_order_items
if (itemsRaw == null) return '精选商品'
const items = itemsRaw as any[]
if (items.length > 0) {
const firstItem = items[0]
const itemStr = JSON.stringify(firstItem)
const itemParsed = JSON.parse(itemStr)
if (itemParsed == null) return '精选商品'
const itemObj = itemParsed as UTSJSONObject
const pName = itemObj.getString('product_name')
const name = (pName != null && pName !== '') ? pName : '商品'
return name
}
return '精选商品'
},
getOrderSpec(order: OrderItemType): string {
const itemsRaw = order.ml_order_items
if (itemsRaw == null) return ''
const items = itemsRaw as any[]
if (items.length > 0) {
const firstItem = items[0]
const itemStr = JSON.stringify(firstItem)
const itemParsed = JSON.parse(itemStr)
if (itemParsed == null) return ''
const itemObj = itemParsed as UTSJSONObject
const specRaw = itemObj.get('specifications')
if (specRaw == null) return ''
if (typeof specRaw === 'string') {
const specStr = specRaw as string
if (specStr.startsWith('{')) {
try {
const specObj = JSON.parse(specStr) as UTSJSONObject
const parts: string[] = []
const color = specObj.get('Color')
if (color != null) parts.push('颜色: ' + color)
const size = specObj.get('Size')
if (size != null) parts.push('尺码: ' + size)
if (parts.length > 0) return parts.join(' ')
return specStr.replace(/[{}"]/g, '')
} catch (e) {
return specStr
}
}
return specStr
}
return JSON.stringify(specRaw).replace(/[{}"]/g, '')
}
return ''
},
getOrderItemCount(order: OrderItemType): number {
if (order.items_count != null && order.items_count > 0) {
return order.items_count
}
return 1
},
getOrderShopName(order: OrderItemType): string {
const shopsRaw = order.ml_shops
if (shopsRaw != null) {
const shopStr = JSON.stringify(shopsRaw)
const shopParsed = JSON.parse(shopStr)
if (shopParsed != null) {
const shopObj = shopParsed as UTSJSONObject
const name = shopObj.getString('shop_name')
if (name != null && name !== '') return name
}
}
return '自营店铺'
},
formatDateTime(timeStr: string): string {
if (timeStr == null || timeStr === '') return ''
const date = new Date(timeStr)
const y = date.getFullYear()
const m = (date.getMonth() + 1).toString().padStart(2, '0')
const d = date.getDate().toString().padStart(2, '0')
const h = date.getHours().toString().padStart(2, '0')
const i = date.getMinutes().toString().padStart(2, '0')
return `${y}-${m}-${d} ${h}:${i}`
},
formatTime(timeStr: string): string {
const date = new Date(timeStr)
const now = new Date()
const diff = now.getTime() - date.getTime()
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
if (days === 0) {
return '今天'
} else if (days === 1) {
return '昨天'
} else {
return `${days}天前`
}
},
switchStatsPeriod(period: string) {
this.activeStatsPeriod = period
this.loadConsumptionStats()
},
editProfile() {
uni.navigateTo({
url: '/pages/mall/consumer/edit-profile'
})
},
// 跳转设置
goToSettings() {
uni.navigateTo({
url: '/pages/mall/consumer/settings'
})
},
// 跳转钱包
goToWallet() {
uni.navigateTo({
url: '/pages/mall/consumer/wallet'
})
},
goToOrders(type: string) {
uni.navigateTo({
url: `/pages/mall/consumer/orders?type=${type}`
})
},
goShopping() {
uni.switchTab({
url: '/pages/main/index'
})
},
viewOrderDetail(order: OrderItemType) {
uni.navigateTo({
url: `/pages/mall/consumer/order-detail?orderId=${order.id}`
})
},
goToProductFromOrder(order: OrderItemType) {
const itemsRaw = order.ml_order_items
if (itemsRaw == null) return
const items = itemsRaw as any[]
if (items.length > 0) {
const firstItem = items[0]
const itemStr = JSON.stringify(firstItem)
const itemParsed = JSON.parse(itemStr)
if (itemParsed == null) return
const itemObj = itemParsed as UTSJSONObject
const productId = itemObj.getString('product_id')
if (productId != null && productId !== '') {
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?id=${productId}`
})
}
}
},
payOrder(order: OrderItemType) {
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${order.id}`
})
},
confirmReceive(order: OrderItemType) {
uni.showModal({
title: '确认收货',
content: '确认已收到商品吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({ title: '处理中...' })
supabaseService.confirmOrderReceived(order.id).then(() => {
uni.hideLoading()
uni.showToast({
title: '确认收货成功',
icon: 'success'
})
this.loadOrders()
}).catch(() => {
uni.hideLoading()
uni.showToast({
title: '操作失败',
icon: 'none'
})
})
}
}
})
},
reviewOrder(order: OrderItemType) {
uni.navigateTo({
url: `/pages/mall/consumer/review?orderId=${order.id}`
})
},
goToCoupons() {
uni.navigateTo({
url: '/pages/mall/consumer/coupons'
})
},
goToPoints() {
uni.navigateTo({
url: '/pages/mall/consumer/points/index'
})
},
goToAddress() {
// 暂时跳转到设置页的地址管理
uni.navigateTo({
url: '/pages/mall/consumer/address-list'
})
},
goToFavorites() {
uni.navigateTo({
url: '/pages/mall/consumer/favorites'
})
},
goToFootprint() {
uni.navigateTo({
url: '/pages/mall/consumer/footprint'
})
},
goToRefund() {
uni.navigateTo({
url: '/pages/mall/consumer/orders?type=refund'
})
},
contactService() {
uni.navigateTo({
url: '/pages/mall/service/chat'
})
},
goToOrderReviews() {
uni.navigateTo({
url: '/pages/mall/consumer/orders?type=review'
})
},
goToMySubscriptions() {
uni.navigateTo({
url: '/pages/mall/consumer/subscription/my-subscriptions'
})
},
goToFollowedShops() {
uni.navigateTo({
url: '/pages/mall/consumer/subscription/followed-shops'
})
},
goToPoints() {
uni.navigateTo({
url: '/pages/mall/consumer/points/index'
})
},
goToBalance() {
uni.navigateTo({
url: '/pages/mall/consumer/balance/index'
})
},
goToShare() {
uni.navigateTo({
url: '/pages/mall/consumer/share/index'
})
},
goToMember() {
uni.navigateTo({
url: '/pages/mall/consumer/member/index'
})
},
changePassword() {
uni.navigateTo({
url: '/pages/mall/consumer/change-password'
})
},
bindPhone() {
uni.navigateTo({
url: '/pages/mall/consumer/bind-phone'
})
},
bindEmail() {
uni.navigateTo({
url: '/pages/mall/consumer/bind-email'
})
},
handleOrderUpdated(data: any) {
console.log('收到订单更新事件:', data)
this.refreshData()
const dataObj = data as UTSJSONObject
const status = dataObj.getNumber('status')
if (status === 1) {
uni.showToast({
title: '订单已保存到待支付',
icon: 'success'
})
} else if (status === 2) {
uni.showToast({
title: '支付成功,订单待发货',
icon: 'success'
})
}
}
}
}
</script>
<style>
.consumer-profile {
background-color: #f5f5f5;
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
.profile-scroll-content {
flex: 1;
}
/* 智能顶部导航栏 */
.smart-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: #ff5000;
z-index: 1000;
box-shadow: 0 2px 12px rgba(255, 80, 0, 0.15);
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.nav-container {
padding: 0 16px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
max-width: 1400px;
margin: 0 auto;
height: 44px;
}
/* 导航栏用户信息区域 */
.nav-user-basic {
display: flex;
flex-direction: row;
align-items: center;
flex-shrink: 0;
margin-right: 8px;
}
.nav-user-stats {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end; /* 将积分、余额、券推向右侧,但在设置按钮之前 */
margin-right: 8px;
}
.nav-user-name {
font-size: 14px;
font-weight: bold;
color: white;
width: 70px; /* 进一步压缩名字宽度 */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nav-stat-item {
display: flex;
flex-direction: row;
align-items: center;
background: rgba(255, 255, 255, 0.15); /* 降低透明度更清爽 */
border-radius: 10px;
padding: 2px 6px;
margin-right: 4px; /* 减小间距 */
flex-shrink: 0;
}
.nav-stat-label {
font-size: 10px; /* 调小字体 */
color: rgba(255, 255, 255, 0.85);
margin-right: 2px;
}
.nav-stat-value {
font-size: 11px; /* 调小字体 */
font-weight: bold;
color: white;
}
.nav-avatar {
width: 32px; /* 缩小头像 */
height: 32px;
border-radius: 16px;
border: 1.5px solid rgba(255, 255, 255, 0.8);
margin-right: 6px;
flex-shrink: 0;
}
.nav-actions {
display: flex;
flex-direction: row;
align-items: center;
flex-shrink: 0;
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: rgba(255, 255, 255, 0.2);
border-radius: 16px;
/* cursor: pointer; REMOVED */
}
.action-icon {
font-size: 18px;
color: white;
}
/* 导航栏占位符 */
.navbar-placeholder {
width: 100%;
flex-shrink: 0;
}
.order-shortcuts, .recent-orders, .my-services, .consumption-stats, .account-security {
background-color: #fff;
margin: 15px 15px; /* 顶部恢复 margin */
border-radius: 12px; /* 统一圆角 */
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 16px;
}
.section-header-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.section-header-row .section-title {
margin-bottom: 0;
}
.view-all {
font-size: 14px;
color: #999;
}
.order-tabs {
display: flex;
flex-direction: row; /* 显式横向排列 */
justify-content: space-between;
width: 100%;
}
.order-tab {
flex: 1;
display: flex;
flex-direction: row; /* 关键:改为横向排列 */
align-items: center;
justify-content: center; /* 居中 */
position: relative;
padding: 8px 0;
}
.tab-icon {
font-size: 20px;
margin-right: 6px; /* 图标和文字间距 */
margin-bottom: 0; /* 移除底部间距 */
}
.tab-text {
font-size: 14px;
color: #333;
}
/* 选中状态的Tab */
.order-tab.active .tab-icon,
.order-tab.active .tab-text {
color: #ff5000;
font-weight: bold;
}
.order-tab.active {
background-color: #fff8f5;
border-radius: 8px;
}
.tab-badge {
position: absolute;
top: 0;
right: 10%; /* 调整位置 */
background-color: #ff5000;
color: #fff;
font-size: 10px;
padding: 1px 5px;
border-radius: 8px;
min-width: 14px;
text-align: center;
line-height: 1.2;
}
.empty-orders {
text-align: center;
padding: 80rpx 0;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-bottom: 30rpx;
}
.start-shopping {
background-color: #007aff;
color: #fff;
padding: 20rpx 40rpx;
border-radius: 25rpx;
font-size: 26rpx;
border: none;
}
.order-item {
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.order-item-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.status-row {
display: flex;
flex-direction: row;
align-items: center;
}
.more-btn {
font-size: 18px;
color: #999;
margin-left: 8px;
padding: 0 5px;
}
.order-shop {
display: flex;
flex-direction: row;
align-items: center;
}
.shop-icon {
font-size: 32rpx;
margin-right: 8rpx;
}
.shop-name {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.shop-arrow {
font-size: 28rpx;
color: #ccc;
margin-left: 4rpx;
}
.order-status-text {
font-size: 26rpx;
font-weight: bold;
}
.order-status-text.pending {
color: #ff5000;
}
.order-status-text.processing {
color: #ff9500;
}
.order-status-text.shipping {
color: #007aff;
}
.order-status-text.completed {
color: #34c759;
}
.order-status-text.refunding {
color: #ff9500;
}
.order-status-text.refunded {
color: #999;
}
.order-status-text.cancelled {
color: #999;
}
.order-item-content {
display: flex;
flex-direction: row;
margin-bottom: 24rpx;
}
.order-item-image {
width: 140rpx;
height: 140rpx;
border-radius: 12rpx;
margin-right: 20rpx;
background-color: #f8f8f8;
}
.order-item-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.order-title-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12rpx;
}
.order-item-title {
flex: 1;
font-size: 28rpx;
color: #333;
line-height: 1.4;
margin-right: 20rpx;
lines: 2;
text-overflow: ellipsis;
}
.order-item-price {
font-size: 32rpx;
color: #333;
font-weight: bold;
text-align: right;
}
.order-spec-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-top: 4rpx;
margin-bottom: 8rpx;
}
.order-item-spec {
flex: 1;
font-size: 24rpx;
color: #999;
margin-right: 12rpx;
lines: 1;
text-overflow: ellipsis;
}
.order-item-time {
font-size: 22rpx;
color: #999;
margin-top: auto;
}
.order-item-num {
font-size: 24rpx;
color: #999;
}
.order-item-actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.order-action-btn {
margin-left: 16rpx;
padding: 10rpx 28rpx;
border-radius: 30rpx;
font-size: 24rpx;
border: 1px solid #ddd;
background-color: #fff;
}
.order-action-btn.pay {
background-color: #ff5000;
color: #fff;
border-color: #ff5000;
}
.order-action-btn.confirm {
background-color: #ff5000;
color: #fff;
border-color: #ff5000;
}
.order-action-btn.review {
color: #ff5000;
border-color: #ff5000;
}
.order-action-btn.secondary {
color: #666;
border-color: #ddd;
}
.service-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap; /* 允许换行 */
/* gap: 16px 0; REMOVED */
justify-content: flex-start; /* 从左开始排列 */
}
.service-item {
width: 25%; /* 每行4个 */
display: flex;
flex-direction: column;
align-items: center;
position: relative;
box-sizing: border-box; /* 确保 padding 不影响宽度 */
margin-bottom: 16px; /* Replace gap row */
}
.service-icon {
font-size: 48rpx;
margin-bottom: 15rpx;
}
.service-text {
font-size: 24rpx;
color: #333;
}
.service-badge {
position: absolute;
top: -5rpx;
right: 10rpx;
background-color: #ff5000;
color: #fff;
font-size: 18rpx;
padding: 4rpx 6rpx;
border-radius: 8rpx;
min-width: 24rpx;
text-align: center;
}
.stats-period {
display: flex;
/* gap: 30rpx; REMOVED */
margin-bottom: 30rpx;
}
.period-tab {
font-size: 26rpx;
color: #666;
padding: 12rpx 24rpx;
border-radius: 20rpx;
margin-right: 30rpx; /* Replace gap */
background-color: #f0f0f0;
}
.period-tab.active {
background-color: #007aff;
color: #fff;
}
.stats-content {
display: flex;
/* gap: 20rpx; REMOVED */
}
.stat-card {
flex: 1;
text-align: center;
padding: 30rpx 0;
background-color: #f8f9fa;
border-radius: 10rpx;
margin-right: 20rpx; /* Replace gap */
}
.stat-card:last-child {
margin-right: 0;
}
.stat-value {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 22rpx;
color: #666;
}
.security-items {
margin-top: 25rpx;
}
.security-item {
display: flex;
align-items: center;
padding: 25rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.security-item:last-child {
border-bottom: none;
}
.security-icon {
font-size: 32rpx;
margin-right: 20rpx;
}
.security-text {
flex: 1;
font-size: 28rpx;
color: #333;
}
.security-status {
display: flex;
align-items: center;
}
.status-text {
font-size: 24rpx;
color: #999;
margin-right: 10rpx;
}
.status-text.bound {
color: #4caf50;
}
.security-arrow {
font-size: 24rpx;
color: #999;
}
</style>