fix:修复bug

This commit is contained in:
2026-02-06 18:09:23 +08:00
parent f476cee76d
commit bb5a6dd100
51 changed files with 4491 additions and 2630 deletions
+479
View File
@@ -0,0 +1,479 @@
<template>
<view class="my-card-page">
<!-- 会员卡列表 -->
<view class="card-list" v-if="cardList.length > 0">
<view v-for="card in cardList" :key="card.id" style="position: relative;background-color: #f5f5f5;"
@click="viewCardDetail(card)" :style="card.cardType==='COUNT'?'height: 240rpx;':'height: 240rpx;'">
<view
style="height: 120rpx;background-color: #ffffff;z-index: 999;border-radius: 25rpx;padding: 32rpx;position: absolute;top: 0;left: 0;right: 0;">
<!-- 卡片头部标题和日期 -->
<view class="card-header">
<text class="card-name">{{ card.name }}</text>
<view class="card-date">
<text class="date-text" v-if="card.status !== 'expired'">{{ card.endDate }}{{
$t('myCard.expire') }}</text>
<text class="date-text expired" v-else>{{ $t('myCard.expiredOn') }}{{ card.endDate }}</text>
</view>
</view>
<!-- 地区信息 -->
<view class="card-region">
<text
class="region-text">{{ $t('myCard.onlyForRegionBefore') }}{{ card.positionName }}{{ $t('myCard.onlyForRegionAfter') }}</text>
<!-- 状态标签 / 去使用按钮 -->
<view v-if="card.status !== 'expired'" class="status-tag active"
style="display: flex; align-items: center; gap: 4rpx; background-color: #FFE2B8; border-radius: 20rpx; padding: 6rpx 20rpx;"
@click.stop="handleUseCard(card)">
<text class="status-text" style="color: #A16300;">{{ $t('myCard.toUse') }}</text>
<!-- <uv-icon name="scan" size="12" color="#D4A574"></uv-icon> -->
</view>
<view v-else class="status-tag" :class="getStatusClass(card.status)">
<text class="status-text">{{ getStatusText(card.status) }}</text>
</view>
</view>
</view>
<!-- 使用情况和操作按钮 -->
<view style="position: absolute; bottom: -20rpx; left: 0; right: 0; padding: 20rpx;z-index:1;">
<view class="card-footer">
<!-- 次卡信息 -->
<view v-if="card.cardType === 'COUNT'" class="card-usage-info">
<text class="usage-text">{{ $t('myCard.remainingTimes') }}{{ card.remainingCount }}{{
$t('myCard.times') }}</text>
</view>
<!-- 时长卡信息 -->
<view v-else class="card-usage-info">
<text class="usage-text">每日限用次数{{card.dailyLimitCount}}</text>
</view>
<!-- 操作按钮 -->
<view class="card-actions">
<!-- 续卡按钮仅次卡显示 -->
<view v-if="card.cardType === 'COUNT'" class="renew-btn" @click.stop="renewCard(card)">
<text class="renew-text">{{ $t('myCard.renew') }}</text>
<uv-icon name="arrow-right" size="14" color="#D4A574"></uv-icon>
</view>
<view v-else class="renew-btn">
<text class="renew-text">单次限时{{card.singleLimitMinutes}}分钟</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else>
<image class="empty-icon" src="/static/empty-card.png" mode="aspectFit"></image>
<text class="empty-text">{{ $t('myCard.noCards') }}</text>
<view class="buy-btn" @click="goToBuy">
<text class="buy-text">{{ $t('myCard.buyNow') }}</text>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
onMounted
} from 'vue'
import {
useI18n
} from '@/utils/i18n.js'
import {
getMemberCardsByStatus
} from '@/config/api/member.js'
import {
getQueryString
} from '@/util/index.js'
import {
getDeviceInfo
} from '@/config/api/device.js'
import {
getInUseOrder,
getUnpaidOrder
} from '@/config/api/order.js'
const {
t
} = useI18n()
// 会员卡列表
const cardList = ref([])
// 获取会员卡列表
const getCardList = async () => {
try {
const response = await getMemberCardsByStatus()
// 处理API返回的数据,转换为模板需要的格式
if (response.code === 200 && response.data) {
cardList.value = response.data.map(item => ({
id: item.id,
name: item.cardName,
cardType: item.cardType, // TIME 或 COUNT
// 次卡相关
totalCount: item.totalCount,
remainingCount: item.remainingCount,
singleLimitMinutesForCount: item.singleLimitMinutesForCount,
// 时长卡相关
cycleDays: item.cycleDays,
dailyLimitCount: item.dailyLimitCount,
singleLimitMinutes: item.singleLimitMinutes,
currentCycleStartTime: item.currentCycleStartTime,
currentCycleUsedCount: item.currentCycleUsedCount,
// 通用信息
status: item.status,
startDate: item.startTime?.split(' ')[0] || item.startTime,
endDate: item.endTime?.split(' ')[0] || item.endTime,
positionId: item.positionId,
positionName: item.positionName,
purchasePrice: item.purchasePrice,
remark: item.remark
}))
} else {
cardList.value = []
}
} catch (error) {
console.error('获取会员卡列表失败:', error)
uni.showToast({
title: t('myCard.getListFailed'),
icon: 'none'
})
}
}
// 计算进度条宽度
const getProgressWidth = (used, total) => {
if (!total || total === 0) return '0%'
const percentage = (used / total) * 100
return `${Math.min(percentage, 100)}%`
}
// 获取状态类名
const getStatusClass = (status) => {
const statusMap = {
'unused': 'active',
'expired': 'expired',
'used': 'used',
'active': 'active' // 兼容原始状态
}
return statusMap[status] || 'active' // 默认为active
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
'unused': t('myCard.active'), // unused表示未使用,即活跃状态
'expired': t('myCard.expired'),
'used': t('myCard.used'),
'active': t('myCard.active') // 兼容原始状态
}
return statusMap[status] || t('myCard.active') // 默认为active
}
// 查看卡详情
const viewCardDetail = (card) => {
// TODO: 跳转到卡详情页面
// uni.showToast({
// title: t('common.functionDeveloping'),
// icon: 'none'
// })
}
// 续卡
const renewCard = (card) => {
uni.navigateTo({
url: `/pages/purchase/index?positionId=${card.positionId}`
})
}
// 去使用会员卡
const handleUseCard = async (card) => {
try {
const scanResult = await new Promise((resolve, reject) => {
uni.scanCode({
success: resolve,
fail: reject
})
})
console.log('扫码结果:', scanResult);
let deviceNo;
// 兼容不同平台的扫码结果
if (scanResult.scanType === 'QR_CODE' || scanResult.scanType === 'qrCode') {
deviceNo = getQueryString(scanResult.result, 'deviceNo')
} else if (scanResult.path) {
deviceNo = getQueryString(scanResult.path, 'deviceNo')
} else {
deviceNo = scanResult.result
}
if (!deviceNo) {
uni.showToast({
title: t('home.invalidQRCode'),
icon: 'none'
})
return
}
uni.showLoading({
title: t('common.getting')
})
// 检查是否有使用中的订单
const inUseRes = await getInUseOrder()
if (inUseRes && inUseRes.code === 200 && inUseRes.data) {
uni.hideLoading()
const inUseOrder = inUseRes.data
uni.reLaunch({
url: `/pages/order/detail?orderId=${inUseOrder.orderId}&deviceId=${deviceNo || inUseOrder.deviceNo}`
})
return
}
// 检查是否有待支付订单
const orderRes = await getUnpaidOrder()
if (orderRes && orderRes.code === 200 && orderRes.data) {
uni.hideLoading()
const unpaidOrder = orderRes.data
uni.navigateTo({
url: `/pages/order/payment?orderId=${unpaidOrder.orderId}`
})
return
}
// 获取设备信息并跳转详情
const deviceInfoRes = await getDeviceInfo(deviceNo)
uni.hideLoading()
if (deviceInfoRes.code === 200 && deviceInfoRes.data && deviceInfoRes.data.device) {
const deviceInfo = deviceInfoRes.data.device
let url = `/pages/device/detail?deviceNo=${deviceNo}`
if (deviceInfo.feeConfig) {
url += `&feeConfig=${encodeURIComponent(deviceInfo.feeConfig)}`
}
uni.navigateTo({
url
})
} else {
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
}
} catch (error) {
console.error('扫码处理失败:', error)
if (error && error.errMsg !== 'scanCode:fail cancel') {
uni.showToast({
title: t('home.scanFailed'),
icon: 'none'
})
}
}
}
// 去购买
const goToBuy = () => {
uni.navigateTo({
url: '/pages/purchase/index'
})
}
onMounted(() => {
uni.setNavigationBarTitle({
title: t('user.myCards')
})
getCardList()
})
</script>
<style lang="scss" scoped>
.my-card-page {
min-height: 100vh;
background-color: #f5f5f5;
padding: 24rpx;
}
.card-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.card-item {
// background-color: #ffffff;
border-radius: 25rpx;
padding: 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
position: relative;
z-index: 5;
}
// 卡片头部
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16rpx;
}
.card-name {
font-size: 36rpx;
font-weight: 600;
color: #333333;
line-height: 50rpx;
}
.card-date {
.date-text {
font-size: 24rpx;
color: #999999;
line-height: 34rpx;
&.expired {
color: #999999;
}
}
}
// 地区信息
.card-region {
margin-bottom: 24rpx;
align-items: center;
display: flex;
justify-content: space-between;
gap: 16rpx;
.region-text {
font-size: 26rpx;
color: #666666;
line-height: 36rpx;
}
}
// 卡片底部
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
padding-top: 50rpx;
position: absolute;
background: rgba(255, 244, 227, 1);
z-index: 1;
right: 0;
bottom: 0;
left: 0;
border-radius: 0 0 25rpx 25rpx;
/* text-align: 30rpx ; */
}
.card-usage-info {
flex: 1;
.usage-text {
font-size: 26rpx;
color: #D4A574;
font-weight: 500;
line-height: 36rpx;
}
}
.card-actions {
display: flex;
align-items: center;
gap: 16rpx;
}
// 续卡按钮
.renew-btn {
display: flex;
align-items: center;
gap: 4rpx;
padding: 8rpx 16rpx;
// background-color: #FFF9F0;
border-radius: 8rpx;
.renew-text {
font-size: 24rpx;
color: #D4A574;
line-height: 34rpx;
}
.arrow {
font-size: 24rpx;
color: #D4A574;
}
}
// 状态标签
.status-tag {
padding: 8rpx 20rpx;
border-radius: 8rpx;
.status-text {
font-size: 24rpx;
line-height: 34rpx;
}
&.active {
// background-color: #FFF9F0;
.status-text {
color: #D4A574;
}
}
&.expired {
// background-color: #F5F5F5;
.status-text {
color: #999999;
}
}
&.used {
// background-color: #F5F5F5;
.status-text {
color: #999999;
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: 40rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-bottom: 40rpx;
}
.buy-btn {
padding: 20rpx 60rpx;
background-color: #B8741A;
border-radius: 48rpx;
.buy-text {
font-size: 28rpx;
color: #ffffff;
font-weight: 500;
}
}
}
</style>
+542
View File
@@ -0,0 +1,542 @@
<template>
<view class="my-coupon-page">
<!-- Tab 切换 -->
<!-- <view class="tab-container">
<view class="tab-item" :class="{ active: currentTab === 'available' }" @click="switchTab('available')">
<text class="tab-text">{{ $t('myCoupon.available') }}</text>
</view>
<view class="tab-item" :class="{ active: currentTab === 'used' }" @click="switchTab('used')">
<text class="tab-text">{{ $t('myCoupon.used') }}</text>
</view>
<view class="tab-item" :class="{ active: currentTab === 'expired' }" @click="switchTab('expired')">
<text class="tab-text">{{ $t('myCoupon.expired') }}</text>
</view>
</view> -->
<!-- 优惠券列表 -->
<view class="coupon-list" v-if="filteredCoupons.length > 0">
<view v-for="coupon in filteredCoupons" :key="coupon.id" class="coupon-item-wrapper">
<view class="coupon-item" :class="getCouponClass(coupon.status)">
<!-- 虚线上下圆形缺口 -->
<view class="coupon-circle-top"></view>
<view class="coupon-circle-bottom"></view>
<view class="coupon-left">
<view class="coupon-value">
<text v-if="coupon.type === 'cash'" class="coupon-unit">¥</text>
<text class="coupon-amount">{{ coupon.type === 'discount' ? coupon.discount : coupon.value }}</text>
<text v-if="coupon.type === 'discount'" class="coupon-unit"></text>
</view>
<view style="display: flex;flex-direction: column;">
<text class="coupon-condition">{{ coupon.condition }}</text>
<text class="coupon-validity-left">{{ coupon.validity }}</text>
</view>
</view>
<view class="coupon-divider"></view>
<view class="coupon-right">
<!-- <text class="coupon-name">{{ coupon.name }}</text> -->
<!-- <text class="coupon-region" v-if="coupon.positionName"
style="font-size: 22rpx; color: #999; margin-top: 4rpx;">
{{ $t('myCoupon.onlyForRegionBefore') }}{{ coupon.positionName }}{{ $t('myCoupon.onlyForRegionAfter') }}
</text> -->
<view class="use-btn" v-if="coupon.status == 'unused'" @click="useCoupon(coupon)">
<text class="use-text">{{ $t('myCoupon.useNow') }}</text>
</view>
<text class="coupon-status" v-else>{{ getStatusText(coupon.status) }}</text>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else>
<image class="empty-icon" src="/static/empty-coupon.png" mode="aspectFit"></image>
<text class="empty-text">{{ getEmptyText() }}</text>
<view class="buy-btn" @click="goToBuy" v-if="currentTab === 'available'">
<text class="buy-text">{{ $t('myCoupon.buyNow') }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useI18n } from '@/utils/i18n.js'
import { getUserCoupons } from '@/config/api/coupon.js'
import { getQueryString } from '@/util/index.js'
import { getDeviceInfo } from '@/config/api/device.js'
import { getInUseOrder, getUnpaidOrder } from '@/config/api/order.js'
const { t } = useI18n()
// 当前选中的 Tab
const currentTab = ref('available')
// // Tab 与 API 状态的映射
// const tabToStatusMap = {
// available: 'unused',
// used: 'used',
// expired: 'expired'
// }
// 优惠券列表
const couponList = ref([])
// 过滤后的优惠券
const filteredCoupons = computed(() => {
return couponList.value;
})
// 获取优惠券列表
const getCouponList = async () => {
try {
// const apiStatus = tabToStatusMap[currentTab.value]
const res = await getUserCoupons('')
if (res.code === 200 && res.data) {
// 将后端数据转换为前端需要的格式
couponList.value = (res.data || []).map(item => {
// 判断优惠券类型:discount_coupon 折扣券,deduction_coupon 抵扣券
const isCashCoupon = item.couponType === 'deduction_coupon'
// 格式化使用条件
let condition = '无门槛'
if (item.usableCondition && item.usableCondition > 0) {
condition = `${item.usableCondition}可用`
}
// 格式化有效期
let validity = ''
if (currentTab.value === 'used') {
// 已使用显示使用时间
validity = item.couponStartTime ? `使用时间 ${item.couponStartTime.split(' ')[0]}` : ''
} else if (item.couponEndTime) {
validity = `${item.couponEndTime.split(' ')[0]} 过期`
}
console.log(item.status);
return {
id: item.id,
name: item.couponName || '优惠券',
type: isCashCoupon ? 'cash' : 'discount',
value: item.deductAmount ? parseFloat(item.deductAmount) : 0,
discount: item.discountRate ? parseFloat(item.discountRate) * 10 : null,
condition: condition,
validity: validity,
status: item.status,
positionName: item.positionName
}
})
} else {
couponList.value = []
}
} catch (error) {
console.error('获取优惠券列表失败:', error)
couponList.value = []
uni.showToast({
title: t('myCoupon.getListFailed'),
icon: 'none'
})
}
}
// 切换 Tab
const switchTab = (tab) => {
currentTab.value = tab
getCouponList()
}
// 获取优惠券样式类名
const getCouponClass = (status) => {
return status === 'unused' ? '' : 'disabled'
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
'used': t('myCoupon.usedStatus'),
'expired': t('myCoupon.expiredStatus'),
'refunded':t('myCoupon.refundedStatus')
}
console.log("获取状态文本:"+statusMap[status]);
return statusMap[status] || ''
}
// 获取空状态文本
const getEmptyText = () => {
const textMap = {
'available': t('myCoupon.noAvailableCoupons'),
'used': t('myCoupon.noUsedCoupons'),
'expired': t('myCoupon.noExpiredCoupons')
}
return textMap[currentTab.value] || ''
}
// 使用优惠券
const useCoupon = async (coupon) => {
try {
const scanResult = await new Promise((resolve, reject) => {
uni.scanCode({
success: resolve,
fail: reject
})
})
console.log('扫码结果:', scanResult);
let deviceNo;
// 兼容不同平台的扫码结果
if (scanResult.scanType === 'QR_CODE' || scanResult.scanType === 'qrCode') {
deviceNo = getQueryString(scanResult.result, 'deviceNo')
} else if (scanResult.path) {
deviceNo = getQueryString(scanResult.path, 'deviceNo')
} else {
deviceNo = scanResult.result
}
if (!deviceNo) {
uni.showToast({
title: t('home.invalidQRCode'),
icon: 'none'
})
return
}
uni.showLoading({
title: t('common.getting')
})
// 检查是否有使用中的订单
const inUseRes = await getInUseOrder()
if (inUseRes && inUseRes.code === 200 && inUseRes.data) {
uni.hideLoading()
const inUseOrder = inUseRes.data
uni.reLaunch({
url: `/pages/order/detail?orderId=${inUseOrder.orderId}&deviceId=${deviceNo || inUseOrder.deviceNo}`
})
return
}
// 检查是否有待支付订单
const orderRes = await getUnpaidOrder()
if (orderRes && orderRes.code === 200 && orderRes.data) {
uni.hideLoading()
const unpaidOrder = orderRes.data
uni.navigateTo({
url: `/pages/order/payment?orderId=${unpaidOrder.orderId}`
})
return
}
// 获取设备信息并跳转详情
const deviceInfoRes = await getDeviceInfo(deviceNo)
uni.hideLoading()
if (deviceInfoRes.code === 200 && deviceInfoRes.data && deviceInfoRes.data.device) {
const deviceInfo = deviceInfoRes.data.device
let url = `/pages/device/detail?deviceNo=${deviceNo}`
if (deviceInfo.feeConfig) {
url += `&feeConfig=${encodeURIComponent(deviceInfo.feeConfig)}`
}
uni.navigateTo({
url
})
} else {
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
}
} catch (error) {
console.error('扫码处理失败:', error)
if (error && error.errMsg !== 'scanCode:fail cancel') {
uni.showToast({
title: t('home.scanFailed'),
icon: 'none'
})
}
}
}
// 去购买
const goToBuy = () => {
uni.navigateTo({
url: '/pages/purchase/index?tab=coupon'
})
}
onMounted(() => {
uni.setNavigationBarTitle({
title: t('user.myCoupons')
})
getCouponList()
})
</script>
<style lang="scss" scoped>
// 优惠券样式变量封装
$coupon-theme-color: #A16300;
$coupon-divider-color: #B8741A;
$coupon-bg-faded: #f5f5f5;
$coupon-active-bg-start: #FFF4E6;
$coupon-active-bg-end: #FFE8CC;
$coupon-divider-left: 65%;
$coupon-circle-radius: 16rpx;
.my-coupon-page {
min-height: 100vh;
background-color: #f5f5f5;
}
/* Tab 切换 */
.tab-container {
position: sticky;
top: 0;
display: flex;
background-color: #ffffff;
z-index: 999;
padding: 20rpx 0;
}
.tab-item {
flex: 1;
text-align: center;
padding: 20rpx 0;
position: relative;
.tab-text {
font-size: 28rpx;
color: #666;
font-weight: 500;
}
&.active {
.tab-text {
color: #333;
font-weight: 600;
}
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 6rpx;
background-color: #FFA928;
border-radius: 3rpx;
}
}
}
.coupon-list {
padding: 20rpx;
display: flex;
flex-direction: column;
gap: 20rpx;
}
.coupon-item-wrapper {
position: relative;
}
.coupon-item {
background: #FFF4E3;
border-radius: 20rpx;
padding: 40rpx 30rpx;
display: flex;
align-items: stretch;
position: relative;
overflow: visible;
min-height: 180rpx;
box-sizing: border-box;
border: 2rpx solid transparent;
transition: all 0.3s;
&.selected {
border-color: $coupon-divider-color;
box-shadow: 0 4rpx 20rpx rgba(184, 116, 26, 0.2);
.coupon-circle-top,
.coupon-circle-bottom {
background-color: $coupon-active-bg-start;
border: 2rpx solid $coupon-divider-color;
}
}
&.disabled {
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
opacity: 0.6;
.coupon-value,
.coupon-condition,
.coupon-name {
color: #999;
}
.coupon-circle-top,
.coupon-circle-bottom {
background-color: #f5f5f5;
}
}
}
/* 虚线顶部圆形缺口 */
.coupon-circle-top {
position: absolute;
left: $coupon-divider-left+4%;
top: -$coupon-circle-radius;
transform: translateX(-50%);
width: $coupon-circle-radius * 2;
height: $coupon-circle-radius * 2;
border-radius: 50%;
background-color: $coupon-bg-faded;
z-index: 10;
}
/* 虚线底部圆形缺口 */
.coupon-circle-bottom {
position: absolute;
left: $coupon-divider-left+4%;
bottom: -$coupon-circle-radius;
transform: translateX(-50%);
width: $coupon-circle-radius * 2;
height: $coupon-circle-radius * 2;
border-radius: 50%;
background-color: $coupon-bg-faded;
z-index: 10;
}
.coupon-left {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
gap: 8rpx;
}
.coupon-value {
display: flex;
align-items: flex-end; // 单位在脚
color: $coupon-theme-color;
line-height: 1;
width:120rpx;
}
.coupon-amount {
font-size: 56rpx; // 值要大
font-weight: 700;
}
.coupon-unit {
font-size: 24rpx; // 单位小
font-weight: 500;
margin-bottom: 6rpx; // 微调单位垂直位置,使其更贴合“脚”
margin-left: 4rpx;
margin-right: 4rpx;
}
.coupon-condition {
font-size: 24rpx;
color: #000;
font-weight: 600;
}
.coupon-validity-left {
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
}
.coupon-divider {
width: 2rpx;
height: 100%;
position: absolute;
left: $coupon-divider-left;
transform: translateX(-50%);
top: 0;
bottom: 0;
align-self: stretch;
background: repeating-linear-gradient(to bottom,
$coupon-divider-color 0rpx,
$coupon-divider-color 8rpx,
transparent 8rpx,
transparent 16rpx);
margin: 0 30rpx;
}
.coupon-right {
// flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
align-items: center;
margin: auto 0;
width: 160rpx;
// align-content: center;
// transform: translateY(50%);
}
.coupon-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.coupon-validity {
font-size: 22rpx;
color: #999;
}
.use-btn {
// margin-top: 10rpx;
// padding: 12rpx 28rpx;
// background-color: #B8741A;
// border-radius: 40rpx;
.use-text {
font-size: 28rpx;
color: #A16300;
font-weight: 600;
}
}
.coupon-status {
margin-top: 10rpx;
font-size: 24rpx;
color: #999;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: 40rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-bottom: 40rpx;
}
.buy-btn {
padding: 20rpx 60rpx;
background-color: #B8741A;
border-radius: 48rpx;
.buy-text {
font-size: 28rpx;
color: #ffffff;
font-weight: 500;
}
}
}
</style>
+867
View File
@@ -0,0 +1,867 @@
<template>
<view class="my-page">
<view class="user-card" @click="navigateTo('/pages/userProfile/index')">
<view class="avatar-box">
<image class="avatar" v-if="userInfo.avatar" :src="userInfo.avatar" mode="aspectFill"></image>
<image v-else class="avatar" src="@/static/head.png" mode="aspectFill"></image>
</view>
<view class="user-text">
<view class="nickname">{{ userInfo.nickName || $t('user.clickToLogin') }}</view>
<view class="subtext">{{ userInfo.phone ? maskPhone(userInfo.phone) : $t('user.loginPrompt') }}</view>
</view>
<uv-icon type="right" size="16" color="#999"></uv-icon>
</view>
<!-- <view class="assets-card">
<view class="assets-left">
<view class="label">押金余额</view>
<view class="amount">¥{{ deposit }}</view>
</view>
<view class="assets-right" @click="handleWithdraw">
<text class="withdraw-btn">提现</text>
</view>
</view> -->
<view class="section">
<!-- 广告轮播 -->
<view class="banner-card" v-if="bannerImages.length > 0">
<swiper class="banner-swiper" :indicator-dots="bannerImages.length > 1"
:autoplay="bannerImages.length > 1" :circular="true" :interval="3000">
<swiper-item v-for="(image, index) in bannerImages" :key="index">
<image class="banner-image" :src="image" mode="aspectFill" @click="handleBannerClick(index)">
</image>
</swiper-item>
</swiper>
</view>
<!-- 默认图片(当没有广告时显示) -->
<view class="banner-card" v-else @click="navigateTo('/pages/join/index')">
<image class="banner-image" src="/static/userCenter_swiper.png" mode="aspectFill"></image>
</view>
<!-- <view class="section-title">常用服务</view> -->
<view class="list">
<view class="list-item" @click="handleQuickReturn">
<view class="left">
<image class="icon" src="/static/express_return.png" mode="aspectFit"></image>
<text class="title">{{ $t('user.quickReturn') }}<text
style="font-size: 18rpx;">{{ $t('user.quickReturnDesc') }}</text></text>
</view>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
<view class="list-item" @click="navigateTo('/pages/expressReturn/index')" v-if="showMenuItem">
<view class="left">
<image class="icon" src="/static/express.png" mode="aspectFit"></image>
<text class="title">{{ $t('user.expressReturn') }}</text>
</view>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
<view class="list-item" @click="navigateTo('/subPackages/order/index')">
<view class="left">
<image class="icon" src="/static/orderList.png" mode="aspectFit"></image>
<text class="title">{{ $t('user.myOrders') }}</text>
</view>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
<view class="list-item" @click="navigateTo('/subPackages/user/my/card')">
<view class="left">
<image class="icon" src="/static/my_member.png" mode="aspectFit"></image>
<text class="title">{{ $t('user.myCards') }}</text>
</view>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
<view class="list-item" @click="navigateTo('/subPackages/user/my/coupon')">
<view class="left">
<image class="icon" src="/static/my_coupon.png" mode="aspectFit"></image>
<text class="title">{{ $t('user.myCoupons') }}</text>
</view>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
<view class="list-item" @click="navigateTo('/subPackages/service/help/index')">
<view class="left">
<image class="icon" src="/static/customer-service.png" mode="aspectFit"></image>
<text class="title">{{ $t('user.customerService') }}</text>
</view>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
<view class="list-item" @click="navigateTo('/subPackages/service/feedback/index')">
<view class="left">
<image class="icon" src="/static/suggess.png" mode="aspectFit"></image>
<text class="title">{{ $t('user.feedback') }}</text>
</view>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
<!-- <view class="list-item" @click="navigateTo('/pages/legal/agreement')">
<view class="left">
<image class="icon" src="/static/business-licence.png" mode="aspectFit"></image>
<text class="title">{{ $t('user.businessLicense') }}</text>
</view>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view> -->
<view class="list-item" @click="navigateTo('/subPackages/other/join/index')">
<view class="left">
<image class="icon" src="/static/peopleInWork.png" mode="aspectFit"></image>
<text class="title">{{ $t('user.cooperation') }}</text>
</view>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
<view class="list-item" @click="navigateTo('/subPackages/user/setting/index')">
<view class="left">
<image class="icon" src="/static/setting.png" mode="aspectFit"></image>
<text class="title">{{ $t('user.settings') }}</text>
</view>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
</view>
</view>
<view class="footer-agreements">
<view class="link-box">
<text class="link" @click="navigateTo('/subPackages/other/legal/agreement')">{{ $t('user.userAgreement') }}</text>
<text class="sep"></text>
<text class="link" @click="navigateTo('/subPackages/other/legal/privacy')">{{ $t('user.privacyPolicy') }}</text>
</view>
<view class="version">{{ $t('user.version') }}{{ appVersion }}</view>
</view>
<!-- 保留授权弹窗,暂不启用 -->
<!--
<u-popup ref="authPopup" mode="center" border-radius="15" width="600rpx" @open="onPopupOpen" @close="onPopupClose">
<view class="auth-popup">
<view class="auth-title">授权登录</view>
<view class="auth-desc">获取您的微信头像、昵称等公开信息</view>
<view class="auth-buttons">
<button class="cancel-btn" @click="closeAuthPopup">取消</button>
<button class="confirm-btn" @click="getUserProfile">确定</button>
</view>
</view>
</u-popup>
-->
</view>
</template>
<script setup>
import {
ref,
reactive,
onMounted
} from 'vue';
import {
onShow
} from '@dcloudio/uni-app';
import {
wxLogin,
getUserInfo
} from '@/util/index.js';
import {
uploadUserAvatar
} from '@/config/api/user.js'
import {
getCurrentAdvertisement
} from '@/config/api/system.js'
import {
getInUseOrder
} from '@/config/api/order.js'
import {
useI18n
} from '@/utils/i18n.js'
// 设置页执行退出登录,此页不再直接调用
const {
t
} = useI18n()
// 响应式状态
const userInfo = ref({});
const deposit = ref('0.00');
const openId = ref('');
const authPopup = ref(null); // u-popup 的引用
const isPopupVisible = ref(false);
const appVersion = ref('1.0.0');
const showMenuItem = ref(false)
const bannerImages = ref([]) // 广告图片列表
const bannerImageList = ref([]) // 完整的广告配置列表(包含链接信息)
// 获取广告图片
const getBannerImages = async () => {
try {
// 调用接口获取广告内容
const res = await getCurrentAdvertisement({
appPlatform: 'wechat', // 微信平台
appType: 'user', // 用户端
pictureLocation:'userProfile_banner'
})
if (res && res.code === 200 && res.data) {
// 使用 imageList 字段(包含图片和链接信息)
const imageList = res.data.imageList || []
if (imageList.length > 0) {
bannerImageList.value = imageList
// 提取图片URL用于展示
bannerImages.value = imageList.map(item => item.imageUrl)
}
} else {
console.warn('获取个人中心广告失败:', res?.msg || '未知错误')
}
} catch (error) {
console.error('获取个人中心广告失败:', error)
}
}
// 处理广告点击
const handleBannerClick = (index) => {
if (!bannerImageList.value || !bannerImageList.value[index]) {
return
}
const config = bannerImageList.value[index]
// 根据链接类型进行跳转
if (config.linkType === 'miniapp' && config.appId) {
// 跳转到外部小程序
// #ifdef MP-WEIXIN
uni.navigateToMiniProgram({
appId: config.appId,
path: config.linkUrl || '',
success: () => {},
fail: (err) => {
console.error('跳转小程序失败:', err)
uni.showToast({
title: t('common.loadFailed'),
icon: 'none'
})
}
})
// #endif
// #ifndef MP-WEIXIN
uni.showToast({
title: t('auth.pleaseUseInWechat'),
icon: 'none'
})
// #endif
} else if (config.linkType === 'external' && config.linkUrl) {
// 跳转到外部链接(H5页面)
uni.navigateTo({
url: `subPackages/other/webview/index?url=${encodeURIComponent(config.linkUrl)}`
})
} else if (config.linkType === 'internal' && config.linkUrl) {
// 跳转到内部页面
uni.navigateTo({
url: config.linkUrl
})
}
}
// 页面加载时初始化
onMounted(() => {
uni.setNavigationBarTitle({
title: t('user.personalCenter')
})
getInfo();
initVersion();
getBannerImages(); // 加载广告
});
// 页面显示时刷新用户信息
onShow(() => {
getInfo();
getBannerImages(); // 刷新广告
});
// 获取用户信息
const getInfo = async () => {
try {
const res = await getUserInfo();
if (res.code == 401 || res.code == 40101) {
redirectToLogin()
return
} else if (res.code == 200) {
// 保存openId
if (res.data.openId) {
openId.value = res.data.openId;
uni.setStorageSync('openId', res.data.openId);
}
// 更新用户信息
userInfo.value = {
nickName: res.data.nickname,
phone: res.data.phone,
avatar: res.data.iconUrl,
isAdmin: res.data.isAdmin
};
uni.setStorageSync('userInfo', userInfo.value);
deposit.value = res.data.balanceAmount || '0.00';
}
} catch (error) {
uni.showToast({
title: t('user.getUserInfoFailed'),
icon: 'none'
});
}
};
// 初始化应用版本号(多端兼容,取可用信息)
const initVersion = () => {
// #ifdef MP-WEIXIN
try {
const info = wx.getAccountInfoSync && wx.getAccountInfoSync();
if (info && info.miniProgram && info.miniProgram.version) {
appVersion.value = info.miniProgram.version;
}
} catch (e) {}
// #endif
// #ifdef APP-PLUS
try {
if (typeof plus !== 'undefined' && plus.runtime && plus.runtime.version) {
appVersion.value = plus.runtime.version;
}
} catch (e) {}
// #endif
};
const redirectToLogin = () => {
try {
const pages = getCurrentPages()
const current = pages && pages.length ? pages[pages.length - 1] : null
const route = current && current.route ? ('/' + current.route) : '/pages/index/index'
const query = current && current.options ? Object.keys(current.options).map(k =>
`${k}=${encodeURIComponent(current.options[k])}`).join('&') : ''
const redirect = encodeURIComponent(query ? `${route}?${query}` : route)
uni.reLaunch({
url: `/pages/login/index?redirect=${redirect}`
})
} catch (e) {
uni.reLaunch({
url: '/pages/login/index'
})
}
}
// 导航到指定页面
const navigateTo = (url) => {
uni.navigateTo({
url
});
};
// 处理快速归还
const handleQuickReturn = async () => {
try {
uni.showLoading({
title: t('common.loading')
});
// 获取使用中的订单
const res = await getInUseOrder();
uni.hideLoading();
if (res && res.code === 200 && res.data) {
const inUseOrder = res.data;
// 跳转到统一订单详情页面
uni.navigateTo({
url: `/pages/order/detail?orderId=${inUseOrder.orderId}&deviceId=${inUseOrder.deviceNo}`
});
} else {
uni.showToast({
title: t('order.noOrder'),
icon: 'none'
});
}
} catch (error) {
uni.showToast({
title: t('order.getOrderFailed'),
icon: 'none'
});
}
};
// 处理提现按钮点击
const handleWithdraw = () => {
navigateTo('/pages/deposit/index');
};
// 处理用户资料点击
const handleUserProfileClick = () => {
const token = uni.getStorageSync('token')
if (!token) {
redirectToLogin()
return
}
// #ifdef MP-WEIXIN
getUserProfile()
// #endif
// #ifndef MP-WEIXIN
uni.showToast({
title: t('auth.pleaseUseInWechat'),
icon: 'none'
})
// #endif
};
// 小程序原生选择头像回调(需基础库>=2.21.2
const onChooseAvatar = async (e) => {
try {
const token = uni.getStorageSync('token')
if (!token) {
redirectToLogin()
return
}
const avatarLocalPath = e?.detail?.avatarUrl
if (!avatarLocalPath) {
uni.showToast({
title: t('user.noAvatar'),
icon: 'none'
})
return
}
uni.showLoading({
title: t('common.uploading'),
mask: true
})
const uploadRes = await uploadUserAvatar(avatarLocalPath)
const serverAvatar = uploadRes?.data?.url || uploadRes?.url || uploadRes?.data || ''
if (serverAvatar) {
userInfo.value = {
...userInfo.value,
avatar: serverAvatar
}
uni.setStorageSync('userInfo', userInfo.value)
}
uni.showToast({
title: t('user.avatarUpdated'),
icon: 'success'
})
await getInfo()
} catch (err) {
uni.showToast({
title: t('user.avatarUploadFailed'),
icon: 'none'
})
} finally {
uni.hideLoading()
}
}
// 打开授权弹窗
const openAuthPopup = () => {
if (authPopup.value) {
authPopup.value.open();
isPopupVisible.value = true;
}
};
// 弹窗打开事件处理
const onPopupOpen = () => {
isPopupVisible.value = true;
// 这里可以添加弹窗打开后的逻辑
};
// 弹窗关闭事件处理
const onPopupClose = () => {
isPopupVisible.value = false;
// 这里可以添加弹窗关闭后的逻辑
};
// 获取微信用户个人信息
const getUserProfile = () => {
// #ifdef MP-WEIXIN
uni.showLoading({
title: t('common.getting'),
mask: true
});
wx.getUserProfile({
desc: '用于完善会员资料',
success: (res) => {
console.log('获取用户信息成功:', res);
updateUserInfo(res.userInfo);
uploadAvatarAndRefresh(res.userInfo);
},
fail: (err) => {
console.error('获取用户信息失败:', err);
uni.showToast({
title: t('user.getUserInfoFailed'),
icon: 'none'
});
},
complete: () => {
uni.hideLoading();
closeAuthPopup();
}
});
// #endif
// #ifndef MP-WEIXIN
uni.showToast({
title: t('auth.pleaseUseInWechat'),
icon: 'none'
});
closeAuthPopup();
// #endif
};
// 更新用户信息
const updateUserInfo = async (wxUserInfo) => {
try {
// 更新本地用户信息
const updatedInfo = {
...userInfo.value,
nickName: wxUserInfo.nickName,
avatar: wxUserInfo.avatarUrl
};
userInfo.value = updatedInfo;
uni.setStorageSync('userInfo', updatedInfo);
// 这里可以添加调用后端API更新用户信息的代码
// const updateRes = await updateUserInfoApi({
// openId: openId.value,
// nickName: wxUserInfo.nickName,
// avatarUrl: wxUserInfo.avatarUrl,
// gender: wxUserInfo.gender
// });
uni.showToast({
title: t('user.updateSuccess'),
icon: 'success'
});
// 更新完成后重新获取用户信息
getInfo();
} catch (error) {
console.error('更新用户信息失败:', error);
uni.showToast({
title: t('user.updateFailed'),
icon: 'none'
});
}
};
// 下载并上传头像,更新用户信息
const uploadAvatarAndRefresh = async (wxUserInfo) => {
try {
const avatarUrl = wxUserInfo?.avatarUrl
if (!avatarUrl) {
uni.showToast({
title: t('user.noAvatarUrl'),
icon: 'none'
})
return
}
// 下载微信头像为本地临时文件
const tempFilePath = await new Promise((resolve, reject) => {
uni.downloadFile({
url: avatarUrl,
success: (res) => {
if (res.statusCode === 200 && res.tempFilePath) {
resolve(res.tempFilePath)
return
}
reject(new Error(t('user.avatarDownloadFailed')))
},
fail: reject
})
})
// 上传到后端
const uploadRes = await uploadUserAvatar(tempFilePath)
// 直接使用返回的头像地址(如果有),并刷新用户信息
const serverAvatar = uploadRes?.data?.url || uploadRes?.url || uploadRes?.data || ''
if (serverAvatar) {
userInfo.value = {
...userInfo.value,
avatar: serverAvatar
}
uni.setStorageSync('userInfo', userInfo.value)
}
uni.showToast({
title: t('user.avatarUpdated'),
icon: 'success'
})
await getInfo()
} catch (error) {
uni.showToast({
title: t('user.avatarUploadFailed'),
icon: 'none'
})
} finally {
uni.hideLoading()
}
}
// 关闭授权弹窗
const closeAuthPopup = () => {
if (authPopup.value) {
authPopup.value.close();
isPopupVisible.value = false;
}
};
// 关于我们
const handleAboutUs = () => {
uni.showToast({
title: t('help.functionDeveloping'),
icon: 'none'
});
};
// 隐私政策
const handlePrivacyPolicy = () => {
uni.showToast({
title: t('help.functionDeveloping'),
icon: 'none'
});
};
// 手机号掩码函数
function maskPhone(phone) {
if (!phone) return '';
// 只处理11位手机号
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
}
// 退出登录移动至设置页
</script>
<style lang="scss" scoped>
.my-page {
min-height: 100vh;
background-color: #ffffff;
position: -webkit-sticky;
position: sticky;
top: 0;
padding-bottom: env(safe-area-inset-bottom);
display: flex;
flex-direction: column;
}
.user-card {
display: flex;
align-items: center;
padding: 24rpx 30rpx;
// background-color: #D1FFE1;
background: linear-gradient(180deg, #D1FFE1 0%, #ffffff 100%);
// border-bottom: 1rpx solid #f0f0f0;
// margin: 0 20rpx;
}
.banner-card {
margin: 20rpx 30rpx 0 30rpx;
background-color: #ffffff;
border-radius: 50rpx;
overflow: hidden;
border: 1rpx solid #f0f0f0;
}
.banner-swiper {
width: 100%;
height: 260rpx;
}
.banner-image {
width: 100%;
height: 260rpx;
border-radius: 50rpx;
display: block;
}
.avatar-box {
margin-right: 20rpx;
position: relative;
}
.avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50rpx;
background-color: #f0f0f0;
}
/* 仅小程序端存在,此按钮覆盖在头像上捕获点击以触发选择头像 */
/* #ifdef MP-WEIXIN */
.avatar-choose-btn {
position: absolute;
left: 0;
top: 0;
width: 100rpx;
height: 100rpx;
border: none;
background: transparent;
padding: 0;
margin: 0;
opacity: 0;
/* 保持可点击但不可见 */
}
/* #endif */
.user-text {
flex: 1;
}
.nickname {
font-size: 32rpx;
color: #222222;
font-weight: 600;
}
.subtext {
margin-top: 6rpx;
font-size: 24rpx;
color: #999999;
}
.assets-card {
margin: 20rpx 0;
padding: 24rpx 30rpx;
background-color: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
border-top: 1rpx solid #f0f0f0;
border-bottom: 1rpx solid #f0f0f0;
}
.assets-left .label {
font-size: 26rpx;
color: #666666;
}
.assets-left .amount {
margin-top: 8rpx;
font-size: 40rpx;
color: #e2231a;
font-weight: 600;
}
.withdraw-btn {
display: inline-block;
padding: 14rpx 28rpx;
color: #e2231a;
border: 1rpx solid #e2231a;
border-radius: 6rpx;
font-size: 26rpx;
}
.section {
margin-top: 20rpx;
margin: 0 20rpx 20rpx 20rpx;
background-color: #ffffff;
flex: 1;
// border-top: 1rpx solid #f0f0f0;
// border-bottom: 1rpx solid #f0f0f0;
border-radius: 20rpx;
}
.section-title {
padding: 20rpx 30rpx;
font-size: 26rpx;
color: #999999;
}
.list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 30rpx;
border-top: 1rpx solid #f5f5f5;
}
.list-item:first-child {
border-top: none;
}
.left {
display: flex;
align-items: center;
}
.icon {
width: 40rpx;
height: 40rpx;
margin-right: 16rpx;
}
.title {
font-size: 30rpx;
color: #333333;
}
.footer-agreements {
margin: 40rpx 0 20rpx 0;
position: absolute;
bottom: 10rpx;
left: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #999999;
font-size: 22rpx;
.link-box {
display: flex;
align-items: center;
justify-content: center;
}
}
.footer-agreements .link {
color: #999999;
}
.footer-agreements .sep {
margin: 0 10rpx;
color: #c0c0c0;
}
.footer-agreements .version {
margin-top: 10rpx;
color: #c0c0c0;
font-size: 20rpx;
}
/* 保留弹窗样式(未启用) */
.auth-popup {
background-color: #ffffff;
width: 100%;
padding: 40rpx;
border-radius: 12rpx;
}
.auth-title {
font-size: 34rpx;
font-weight: 600;
color: #333333;
text-align: center;
margin-bottom: 20rpx;
}
.auth-desc {
font-size: 28rpx;
color: #666666;
text-align: center;
margin-bottom: 40rpx;
}
.auth-buttons {
display: flex;
justify-content: space-between;
}
.cancel-btn,
.confirm-btn {
width: 240rpx;
height: 80rpx;
line-height: 80rpx;
text-align: center;
border-radius: 40rpx;
font-size: 28rpx;
}
</style>