fix:修复bug
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,783 @@
|
||||
<template>
|
||||
<view class="order-detail-container">
|
||||
<!-- 状态标题 -->
|
||||
<!-- <view class="status-header">
|
||||
<view class="status-icon">
|
||||
<uv-icon name="checkbox-mark" size="40" color="#07c160"></uv-icon>
|
||||
</view>
|
||||
<text class="status-text">{{ statusText }}</text>
|
||||
</view> -->
|
||||
|
||||
<!-- 产品信息卡片 -->
|
||||
<view class="product-card">
|
||||
<image
|
||||
:src="orderDetail.pictureUrl || orderDetail.productImage || '/static/default-product.png'"
|
||||
mode="aspectFill"
|
||||
class="product-image"
|
||||
></image>
|
||||
<view class="product-info">
|
||||
<view class="product-name">{{ orderDetail.productName || orderDetail.deviceName || '风电者2026新款风扇、充电宝、暖手宝三合一' }}</view>
|
||||
<view class="product-style">款式:{{ orderDetail.optionName || orderDetail.style || '标准' }}</view>
|
||||
<view class="product-price">¥{{ orderDetail.price || orderDetail.totalAmount }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单信息 -->
|
||||
<view class="info-section">
|
||||
<view class="section-title">订单信息</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="label">订单号</text>
|
||||
<text class="value">{{ orderDetail.orderNo || '-' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item" v-if="orderDetail.outTradeNo">
|
||||
<text class="label">交易单号</text>
|
||||
<text class="value">{{ orderDetail.outTradeNo }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="label">订单状态</text>
|
||||
<text class="value" :style="{color: statusColor}">{{ statusText }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="label">创建时间</text>
|
||||
<text class="value">{{ orderDetail.createTime || '-' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="label">支付方式</text>
|
||||
<text class="value">{{ paymentMethodText }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="label">支付时间</text>
|
||||
<text class="value">{{ orderDetail.payTime || '未支付' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="label">收货人</text>
|
||||
<text class="value">{{ receiverInfo }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="label">收货地址</text>
|
||||
<text class="value address">{{ orderDetail.receiverAddress || '-' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item" v-if="orderDetail.expressageNo">
|
||||
<text class="label">快递单号</text>
|
||||
<text class="value">{{ orderDetail.expressageNo }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item" v-if="orderDetail.remark">
|
||||
<text class="label">备注</text>
|
||||
<text class="value address">{{ orderDetail.remark }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 费用信息 -->
|
||||
<view class="info-section">
|
||||
<view class="section-title">费用信息</view>
|
||||
|
||||
<view class="info-item" v-if="orderDetail.quantity">
|
||||
<text class="label">数量</text>
|
||||
<text class="value">x{{ orderDetail.quantity }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="label">单价</text>
|
||||
<text class="value">¥ {{ orderDetail.price || orderDetail.totalAmount }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item total">
|
||||
<text class="label">合计</text>
|
||||
<text class="value highlight">¥ {{ totalAmount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="bottom-actions">
|
||||
<!-- 待付款状态:显示取消订单和立即支付 -->
|
||||
<template v-if="orderDetail.status === 0 || orderDetail.status === '0'">
|
||||
<view class="action-btn secondary" @click="onCancelOrder">
|
||||
取消订单
|
||||
</view>
|
||||
<view class="action-btn primary" @click="onPayNow">
|
||||
立即支付
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 已完成/已取消状态:显示删除订单和再次定制 -->
|
||||
<template v-else-if="orderDetail.status === 3 || orderDetail.status === '3' || orderDetail.status === 4 || orderDetail.status === '4'">
|
||||
<view class="action-btn secondary" @click="onDeleteOrder">
|
||||
删除订单
|
||||
</view>
|
||||
<view class="action-btn primary" @click="onReorder(orderDetail.productId)">
|
||||
再次定制
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 其他状态:显示联系客服和再次定制 -->
|
||||
<template v-else>
|
||||
<view class="action-btn secondary" @click="onContactService">
|
||||
联系客服
|
||||
</view>
|
||||
<view class="action-btn primary" @click="onReorder(orderDetail.productId)">
|
||||
再次定制
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import {
|
||||
queryById,
|
||||
getProductOrderDetail,
|
||||
deleteProductOrder,
|
||||
cancelProductOrder,
|
||||
createWxPayment
|
||||
} from '../../config/api/order.js';
|
||||
// import { getSystemParamByKey } from '../../config/api/system.js';
|
||||
import { useI18n } from '@/utils/i18n.js'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const orderDetail = ref({});
|
||||
const orderId = ref('');
|
||||
const countdownText = ref('');
|
||||
let countdownTimer = null;
|
||||
|
||||
// 订单状态文本
|
||||
const statusText = computed(() => {
|
||||
const status = orderDetail.value.status;
|
||||
if (status === 0 || status === '0') {
|
||||
return '待付款';
|
||||
}
|
||||
if (status === 1 || status === '1') {
|
||||
return '待发货';
|
||||
}
|
||||
if (status === 2 || status === '2') {
|
||||
return '待收货';
|
||||
}
|
||||
if (status === 3 || status === '3') {
|
||||
return '已完成';
|
||||
}
|
||||
if (status === 4 || status === '4') {
|
||||
return '已取消';
|
||||
}
|
||||
if (status === 5 || status === '5') {
|
||||
return '退款中';
|
||||
}
|
||||
return '待付款';
|
||||
});
|
||||
|
||||
// 订单状态颜色
|
||||
const statusColor = computed(() => {
|
||||
const status = orderDetail.value.status;
|
||||
if (status === 0 || status === '0') {
|
||||
return '#ff976a'; // 待付款 - 橙色
|
||||
}
|
||||
if (status === 1 || status === '1') {
|
||||
return '#1989fa'; // 待发货 - 蓝色
|
||||
}
|
||||
if (status === 2 || status === '2') {
|
||||
return '#1989fa'; // 待收货 - 蓝色
|
||||
}
|
||||
if (status === 3 || status === '3') {
|
||||
return '#07c160'; // 已完成 - 绿色
|
||||
}
|
||||
if (status === 4 || status === '4') {
|
||||
return '#999'; // 已取消 - 灰色
|
||||
}
|
||||
if (status === 5 || status === '5') {
|
||||
return '#ff6b6b'; // 退款中 - 红色
|
||||
}
|
||||
return '#ff976a';
|
||||
});
|
||||
|
||||
// 支付方式文本
|
||||
const paymentMethodText = computed(() => {
|
||||
const payWay = orderDetail.value.payWay;
|
||||
if (payWay === 'wx_score_pay') return '微信支付';
|
||||
if (payWay === 'wx_global_pay') return '微信支付';
|
||||
if (payWay === 'wx_member_pay') return '微信支付';
|
||||
return '微信支付';
|
||||
});
|
||||
|
||||
// 收货人信息
|
||||
const receiverInfo = computed(() => {
|
||||
const name = orderDetail.value.receiverName || '-';
|
||||
const phone = orderDetail.value.receiverPhone || '-';
|
||||
return `${name} ${phone}`;
|
||||
});
|
||||
|
||||
// 总金额
|
||||
const totalAmount = computed(() => {
|
||||
return orderDetail.value.totalAmount || orderDetail.value.amount || orderDetail.value.payAmount || '99.0';
|
||||
});
|
||||
|
||||
// 页面加载
|
||||
onLoad(async (options) => {
|
||||
if (options && options.orderId) {
|
||||
orderId.value = options.orderId;
|
||||
await loadOrderDetail();
|
||||
}
|
||||
});
|
||||
|
||||
// 根据订单状态设置页面标题
|
||||
const updatePageTitle = () => {
|
||||
const status = orderDetail.value.status;
|
||||
let title = '订单详情';
|
||||
|
||||
if (status === 0 || status === '0') {
|
||||
title = '待付款';
|
||||
// 如果有倒计时文本,添加到标题中
|
||||
if (countdownText.value) {
|
||||
title = `${title} ${countdownText.value}`;
|
||||
}
|
||||
} else if (status === 1 || status === '1') {
|
||||
title = '待发货';
|
||||
} else if (status === 2 || status === '2') {
|
||||
title = '待收货';
|
||||
} else if (status === 3 || status === '3') {
|
||||
title = '已完成';
|
||||
} else if (status === 4 || status === '4') {
|
||||
title = '已取消';
|
||||
} else if (status === 5 || status === '5') {
|
||||
title = '退款中';
|
||||
}
|
||||
|
||||
uni.setNavigationBarTitle({
|
||||
title: title
|
||||
});
|
||||
};
|
||||
|
||||
// 启动倒计时
|
||||
const startCountdown = () => {
|
||||
// 清除之前的定时器
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer);
|
||||
countdownTimer = null;
|
||||
}
|
||||
|
||||
const status = orderDetail.value.status;
|
||||
const expireTime = orderDetail.value.expireTime;
|
||||
|
||||
// 只有待付款状态且有过期时间才显示倒计时
|
||||
if ((status === 0 || status === '0') && expireTime) {
|
||||
// 计算倒计时
|
||||
const updateCountdown = () => {
|
||||
const now = new Date().getTime();
|
||||
const expireTimestamp = new Date(expireTime).getTime();
|
||||
const diff = expireTimestamp - now;
|
||||
|
||||
if (diff > 0) {
|
||||
// 计算总分钟数和秒数
|
||||
const totalMinutes = Math.floor(diff / (1000 * 60));
|
||||
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
||||
|
||||
// 格式化为 MM:SS
|
||||
const minutesStr = String(totalMinutes).padStart(2, '0');
|
||||
const secondsStr = String(seconds).padStart(2, '0');
|
||||
|
||||
countdownText.value = `${minutesStr}:${secondsStr}`;
|
||||
updatePageTitle();
|
||||
} else {
|
||||
// 倒计时结束
|
||||
countdownText.value = '';
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer);
|
||||
countdownTimer = null;
|
||||
}
|
||||
// 重新加载订单详情(订单可能已自动取消)
|
||||
loadOrderDetail();
|
||||
}
|
||||
};
|
||||
|
||||
// 立即执行一次
|
||||
updateCountdown();
|
||||
// 每秒更新一次
|
||||
countdownTimer = setInterval(updateCountdown, 1000);
|
||||
} else {
|
||||
countdownText.value = '';
|
||||
updatePageTitle();
|
||||
}
|
||||
};
|
||||
|
||||
// 组件卸载时清除定时器
|
||||
onUnmounted(() => {
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer);
|
||||
countdownTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 加载订单详情
|
||||
const loadOrderDetail = async () => {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: '加载中...'
|
||||
});
|
||||
|
||||
const res = await getProductOrderDetail(orderId.value);
|
||||
|
||||
if (res.code === 200 && res.data) {
|
||||
const data = res.data;
|
||||
orderDetail.value = {
|
||||
// 基础信息
|
||||
id: data.id,
|
||||
orderNo: data.orderNo,
|
||||
outTradeNo: data.outTradeNo,
|
||||
userId: data.userId,
|
||||
|
||||
|
||||
// 状态信息
|
||||
status: data.status,
|
||||
payStatus: data.payStatus,
|
||||
|
||||
// 金额信息
|
||||
totalAmount: data.totalAmount,
|
||||
payAmount: data.payAmount,
|
||||
price: data.price,
|
||||
|
||||
// 时间信息
|
||||
createTime: data.createTime,
|
||||
updateTime: data.updateTime,
|
||||
payTime: data.payTime,
|
||||
expireTime: data.expireTime, // 订单自动取消时间
|
||||
|
||||
// 收货信息
|
||||
receiverName: data.receiverName,
|
||||
receiverPhone: data.receiverPhone,
|
||||
receiverAddress: data.receiverAddress,
|
||||
|
||||
// 物流信息
|
||||
expressageNo: data.expressageNo,
|
||||
|
||||
// 备注
|
||||
remark: data.remark,
|
||||
|
||||
// 商品信息
|
||||
productName: data.productName,
|
||||
optionName: data.optionName,
|
||||
pictureUrl: data.pictureUrl,
|
||||
color: data.color,
|
||||
quantity: data.quantity,
|
||||
productId:data.productId,
|
||||
|
||||
// 兼容旧字段
|
||||
orderId: data.id,
|
||||
deviceId: data.deviceNo || '',
|
||||
deviceName: data.deviceName || '',
|
||||
style: data.optionName || data.style || data.deviceStyle || '',
|
||||
payWay: data.payWay || 'wx_global_pay',
|
||||
phone: data.receiverPhone,
|
||||
address: data.receiverAddress,
|
||||
deposit: data.deposit || '0',
|
||||
package: data.package || '',
|
||||
amount: data.payAmount,
|
||||
productImage: data.pictureUrl || data.productImage || ''
|
||||
};
|
||||
|
||||
// 启动倒计时
|
||||
startCountdown();
|
||||
}
|
||||
|
||||
uni.hideLoading();
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error('加载订单详情失败:', error);
|
||||
uni.showToast({
|
||||
title: '加载失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 退款/售后
|
||||
const onRefund = () => {
|
||||
uni.showToast({
|
||||
title: '退款/售后功能开发中',
|
||||
icon: 'none'
|
||||
});
|
||||
};
|
||||
|
||||
// 取消订单
|
||||
const onCancelOrder = () => {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要取消这个订单吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: '取消中...',
|
||||
mask: true
|
||||
});
|
||||
|
||||
const result = await cancelProductOrder(orderDetail.value.id);
|
||||
|
||||
uni.hideLoading();
|
||||
|
||||
if (result && result.code === 200) {
|
||||
uni.showToast({
|
||||
title: '订单已取消',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 重新加载订单详情
|
||||
await loadOrderDetail();
|
||||
} else {
|
||||
throw new Error(result?.msg || '取消失败');
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error('取消订单失败:', error);
|
||||
uni.showToast({
|
||||
title: error.message || '取消失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 删除订单
|
||||
const onDeleteOrder = () => {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除这个订单吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: '删除中...',
|
||||
mask: true
|
||||
});
|
||||
|
||||
const result = await deleteProductOrder(orderDetail.value.id);
|
||||
|
||||
uni.hideLoading();
|
||||
|
||||
if (result && result.code === 200) {
|
||||
uni.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 返回订单列表
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
} else {
|
||||
throw new Error(result?.msg || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error('删除订单失败:', error);
|
||||
uni.showToast({
|
||||
title: error.message || '删除失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 立即支付
|
||||
const onPayNow = async () => {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: '正在创建支付...',
|
||||
mask: true
|
||||
});
|
||||
|
||||
const res = await createWxPayment(orderDetail.value.orderNo);
|
||||
|
||||
if (res && res.code === 200 && res.data) {
|
||||
uni.hideLoading();
|
||||
|
||||
const payParams = res.data;
|
||||
|
||||
// 调用微信支付
|
||||
uni.requestPayment({
|
||||
timeStamp: payParams.timeStamp,
|
||||
nonceStr: payParams.nonceStr,
|
||||
package: payParams.package,
|
||||
signType: payParams.signType,
|
||||
paySign: payParams.paySign,
|
||||
success: async (payRes) => {
|
||||
console.log('支付成功:', payRes);
|
||||
uni.showToast({
|
||||
title: '支付成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
|
||||
// 重新加载订单详情
|
||||
await loadOrderDetail();
|
||||
},
|
||||
fail: async (err) => {
|
||||
console.error('支付失败:', err);
|
||||
|
||||
// 判断是用户取消还是支付失败
|
||||
if (err.errMsg && err.errMsg.includes('cancel')) {
|
||||
// 用户取消支付,调用取消订单接口
|
||||
try {
|
||||
await cancelProductOrder(orderDetail.value.id);
|
||||
uni.showToast({
|
||||
title: '支付已取消',
|
||||
icon: 'none'
|
||||
});
|
||||
|
||||
// 重新加载订单详情
|
||||
await loadOrderDetail();
|
||||
} catch (cancelError) {
|
||||
console.error('取消订单失败:', cancelError);
|
||||
uni.showToast({
|
||||
title: '支付已取消',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 支付失败
|
||||
uni.showToast({
|
||||
title: '支付失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: res?.msg || '创建支付订单失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('支付异常:', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: error.message || '支付失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 联系客服
|
||||
const onContactService = async () => {
|
||||
|
||||
|
||||
const phoneNumber = uni.getStorageSync('customerPhone');
|
||||
|
||||
// 拨打客服电话
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: phoneNumber,
|
||||
success: () => {
|
||||
console.log('拨打电话成功');
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('拨打电话失败:', err);
|
||||
uni.showToast({
|
||||
title: '拨打电话失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
// 再次定制
|
||||
const onReorder = (order) => {
|
||||
if(order){
|
||||
uni.navigateTo({
|
||||
url: `/subPackages/business/device-goods?productId=${order}`
|
||||
});
|
||||
}
|
||||
// console.log(order);
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.order-detail-container {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
padding-bottom: 120rpx;
|
||||
|
||||
// 状态头部
|
||||
.status-header {
|
||||
background: #fff;
|
||||
padding: 60rpx 0 40rpx;
|
||||
text-align: center;
|
||||
|
||||
.status-icon {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 36rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
// 产品卡片
|
||||
.product-card {
|
||||
background: #fff;
|
||||
margin: 20rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
.product-image {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #f5f5f5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
flex: 1;
|
||||
margin-left: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.product-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.product-style {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-size: 32rpx;
|
||||
color: #07c160;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 信息区块
|
||||
.info-section {
|
||||
background: #fff;
|
||||
margin: 20rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 28rpx;
|
||||
height: 50rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
width: 140rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
|
||||
&.address {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
&.total {
|
||||
margin-top: 12rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx dashed #e5e5e5;
|
||||
|
||||
.label {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.value.highlight {
|
||||
color: #07c160;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 底部按钮
|
||||
.bottom-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
padding: 20rpx;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 20rpx;
|
||||
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
z-index: 100;
|
||||
|
||||
.action-btn {
|
||||
width: 180rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
|
||||
&.secondary {
|
||||
background: #fff;
|
||||
color: #07c160;
|
||||
border: 2rpx solid #07c160;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background: #07c160;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,796 @@
|
||||
<template>
|
||||
<view class="order-container">
|
||||
<!-- 状态切换 -->
|
||||
<view class="status-tabs">
|
||||
<view v-for="(tab, index) in orderStatusTabs" :key="index" class="tab-item"
|
||||
:class="{ active: currentTab === index }" @click="switchTab(index)">
|
||||
{{ tab.text }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<view class="order-list">
|
||||
<view class="empty-state" v-if="orderList.length === 0">
|
||||
<view class="empty-icon">
|
||||
<image src="/static/orderList.png" mode="aspectFill" class="empty-icon"></image>
|
||||
</view>
|
||||
<text class="empty-text">{{ $t('order.noOrderRecord') }}</text>
|
||||
</view>
|
||||
|
||||
<DeviceOrderItemCard
|
||||
v-for="(order, index) in orderList"
|
||||
:key="index"
|
||||
:order="order"
|
||||
@click="navigateToDeviceOrderDetail"
|
||||
@delete="handleDeleteOrder"
|
||||
@pay="handlePayment"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
reactive,
|
||||
onMounted,
|
||||
onUnmounted
|
||||
} from 'vue';
|
||||
import DeviceOrderItemCard from '../../components/DeviceOrderItemCard.vue';
|
||||
import {
|
||||
onLoad,
|
||||
onShow
|
||||
} from '@dcloudio/uni-app';
|
||||
import {
|
||||
getOrderList,
|
||||
getProductOrderList,
|
||||
queryById,
|
||||
getOrderByOrderNoScorePayStatus,
|
||||
cancelOrder,
|
||||
createWxPayment,
|
||||
deleteProductOrder,
|
||||
cancelProductOrder,
|
||||
} from '../../config/api/order.js';
|
||||
import{
|
||||
createProductOrder
|
||||
}from "@/config/api/product.js"
|
||||
import {
|
||||
updateUserBalance
|
||||
} from '../../config/api/user.js';
|
||||
import {
|
||||
URL
|
||||
} from '../../config/url.js';
|
||||
import { useI18n } from '@/utils/i18n.js'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 初始化状态
|
||||
const currentTab = ref(0);
|
||||
const orderList = ref([]);
|
||||
|
||||
// 订单状态映射
|
||||
const orderStatusMap = reactive({
|
||||
'0': {
|
||||
text: '待付款',
|
||||
class: 'status-waiting'
|
||||
},
|
||||
'1': {
|
||||
text: '待发货',
|
||||
class: 'status-shipping'
|
||||
},
|
||||
'2': {
|
||||
text: '待收货',
|
||||
class: 'status-receiving'
|
||||
},
|
||||
'3': {
|
||||
text: '已完成',
|
||||
class: 'status-finished'
|
||||
},
|
||||
'4': {
|
||||
text: '已取消',
|
||||
class: 'status-cancelled'
|
||||
},
|
||||
'5': {
|
||||
text: '退款中',
|
||||
class: 'status-refunding'
|
||||
}
|
||||
});
|
||||
|
||||
// 订单状态标签
|
||||
const orderStatusTabs = reactive([
|
||||
{
|
||||
text: '全部',
|
||||
status: []
|
||||
},
|
||||
{
|
||||
text: '待付款',
|
||||
status: [0]
|
||||
},
|
||||
{
|
||||
text: '待发货',
|
||||
status: [1]
|
||||
},
|
||||
{
|
||||
text: '待收货',
|
||||
status: [2]
|
||||
},
|
||||
{
|
||||
text: '已完成',
|
||||
status: [3]
|
||||
},
|
||||
{
|
||||
text: '已取消',
|
||||
status: [4]
|
||||
},
|
||||
// {
|
||||
// text: '退款中',
|
||||
// status: [5]
|
||||
// }
|
||||
]);
|
||||
|
||||
// 页面加载
|
||||
onLoad(async (options) => {
|
||||
// 如果有传入orderId参数,说明是从设备租借页面跳转过来的
|
||||
if (options && options.orderId) {
|
||||
try {
|
||||
// 获取特定订单信息
|
||||
const res = await queryById(options.orderId);
|
||||
if (res.code === 200 && res.data) {
|
||||
// 获取到的订单数据
|
||||
const orderData = res.data;
|
||||
|
||||
// 使用实际的startTime字段,如果没有则尝试使用createTime
|
||||
const orderStartTime = orderData.startTime || orderData.createTime || '';
|
||||
|
||||
// 格式化订单数据
|
||||
const formattedOrder = {
|
||||
orderNo: orderData.orderId,
|
||||
status: orderData.orderStatus,
|
||||
deviceId: orderData.deviceNo,
|
||||
payWay: orderData.payWay,
|
||||
startTime: orderStartTime,
|
||||
endTime: orderData.endTime || '',
|
||||
positionName: orderData.positionName || orderData.positionLocation || '',
|
||||
deviceName: orderData.deviceName || '',
|
||||
amount: orderData.payAmount || orderData.actualDeviceAmount || orderData.currentFee || orderData.residueAmount || '0.00'
|
||||
};
|
||||
|
||||
// 将订单添加到列表开头
|
||||
orderList.value = [formattedOrder, ...orderList.value];
|
||||
|
||||
// 根据订单状态切换到对应标签
|
||||
const tabIndex = orderStatusTabs.findIndex(tab =>
|
||||
tab.status.includes(orderData.orderStatus)
|
||||
);
|
||||
|
||||
if (tabIndex !== -1) {
|
||||
switchTab(tabIndex);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取订单详情失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取订单列表
|
||||
await loadOrderList();
|
||||
});
|
||||
|
||||
// 页面显示时刷新订单列表
|
||||
onShow(async () => {
|
||||
// 根据当前选中的标签刷新对应状态的订单
|
||||
const statusArray = orderStatusTabs[currentTab.value].status;
|
||||
const statusValue = statusArray.length === 0 ? undefined : statusArray[0];
|
||||
await loadOrderList(statusValue);
|
||||
});
|
||||
|
||||
// 切换标签
|
||||
const switchTab = async (index) => {
|
||||
currentTab.value = index;
|
||||
// 根据状态获取订单列表
|
||||
const statusArray = orderStatusTabs[index].status;
|
||||
// 如果是全部,传undefined;否则传第一个数字状态值
|
||||
const statusValue = statusArray.length === 0 ? undefined : statusArray[0];
|
||||
await loadOrderList(statusValue);
|
||||
};
|
||||
|
||||
// 加载订单列表
|
||||
const loadOrderList = async (statusList) => {
|
||||
try {
|
||||
let params = {};
|
||||
if(statusList !== undefined){
|
||||
params = {
|
||||
status: statusList
|
||||
}
|
||||
}
|
||||
const res = await getProductOrderList(params);
|
||||
|
||||
// 根据实际接口返回结构处理数据
|
||||
if (res.code === 200 && res.data) {
|
||||
// 支持两种数据结构:res.data.rows 或 res.data.records
|
||||
const dataList = res.data.rows || res.data.records || [];
|
||||
|
||||
// 处理订单列表数据
|
||||
orderList.value = dataList.map(item => {
|
||||
return {
|
||||
// 基础信息
|
||||
id: item.id,
|
||||
orderNo: item.orderNo,
|
||||
orderId: item.id,
|
||||
userId: item.userId,
|
||||
|
||||
// 状态信息
|
||||
status: item.status,
|
||||
orderStatus: item.status,
|
||||
payStatus: item.payStatus,
|
||||
|
||||
// 金额信息
|
||||
totalAmount: item.totalAmount,
|
||||
payAmount: item.payAmount,
|
||||
price: item.price,
|
||||
|
||||
// 时间信息
|
||||
createTime: item.createTime,
|
||||
payTime: item.payTime,
|
||||
|
||||
// 收货信息
|
||||
receiverName: item.receiverName,
|
||||
receiverPhone: item.receiverPhone,
|
||||
receiverAddress: item.receiverAddress,
|
||||
|
||||
// 物流信息
|
||||
expressageNo: item.expressageNo,
|
||||
|
||||
// 备注
|
||||
remark: item.remark,
|
||||
|
||||
// 商品信息
|
||||
productName: item.productName,
|
||||
optionName: item.optionName,
|
||||
pictureUrl: item.pictureUrl,
|
||||
color: item.color,
|
||||
quantity: item.quantity,
|
||||
|
||||
// 兼容旧字段
|
||||
deviceId: item.deviceNo || '',
|
||||
deviceName: item.productName || item.deviceName || '',
|
||||
productImage: item.pictureUrl || '',
|
||||
style: item.optionName || item.style || item.deviceStyle || '',
|
||||
payWay: item.payWay || '',
|
||||
startTime: item.createTime || item.startTime || '',
|
||||
endTime: item.endTime || '',
|
||||
positionName: item.positionName || item.positionLocation || '',
|
||||
amount: item.payAmount || item.totalAmount || '0.00'
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取订单列表失败:', error);
|
||||
uni.showToast({
|
||||
title: t('order.getOrderListFailed'),
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理订单完成事件
|
||||
const handleOrderCompleted = (orderData) => {
|
||||
console.log('订单列表页收到订单完成事件:', orderData)
|
||||
// 刷新订单列表,根据当前选中的标签刷新对应状态的订单
|
||||
const statusArray = orderStatusTabs[currentTab.value].status
|
||||
const statusValue = statusArray.length === 0 ? undefined : statusArray[0]
|
||||
loadOrderList(statusValue)
|
||||
}
|
||||
|
||||
// 设置页面标题并监听订单完成事件
|
||||
onMounted(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: t('order.myDeviceOrders')
|
||||
})
|
||||
|
||||
// 监听订单完成事件
|
||||
uni.$on('orderCompleted', handleOrderCompleted)
|
||||
})
|
||||
|
||||
// 页面卸载时移除事件监听
|
||||
onUnmounted(() => {
|
||||
uni.$off('orderCompleted', handleOrderCompleted)
|
||||
})
|
||||
|
||||
// 同步订单状态
|
||||
const getOrderStatus = async (order) => {
|
||||
try {
|
||||
const res = await getOrderByOrderNoScorePayStatus(order.orderNo);
|
||||
if (res.code === 200) {
|
||||
uni.showToast({
|
||||
title: t('order.syncSuccess'),
|
||||
icon: 'success'
|
||||
});
|
||||
const statusArray = orderStatusTabs[currentTab.value].status;
|
||||
const statusValue = statusArray.length === 0 ? undefined : statusArray[0];
|
||||
await loadOrderList(statusValue);
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: t('order.syncFailed'),
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 跳转到订单详情页面(统一入口)
|
||||
const navigateToOrderDetail = (order) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/order/detail?orderId=${order.orderId || order.orderNo}&deviceId=${order.deviceId}`
|
||||
});
|
||||
};
|
||||
|
||||
// 组件事件:归还设备(实际跳转到订单详情页)
|
||||
const onReturnDevice = (order) => {
|
||||
navigateToOrderDetail(order);
|
||||
};
|
||||
|
||||
// 跳转到订单详情页
|
||||
const navigateToDetails = (order) => {
|
||||
navigateToOrderDetail(order);
|
||||
};
|
||||
|
||||
// 立即支付
|
||||
const handlePayment = async (order) => {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: '正在创建支付...',
|
||||
mask: true
|
||||
});
|
||||
console.log(order);
|
||||
|
||||
// 调用后端创建微信支付订单接口(使用订单号)
|
||||
// const res = await createWxPayment(order.orderNo);
|
||||
const res = await createProductOrder({orderNo:order.orderNo});
|
||||
|
||||
if (res && res.code === 200 && res.data) {
|
||||
uni.hideLoading();
|
||||
|
||||
const payParams = res.data;
|
||||
|
||||
// 调用微信支付
|
||||
uni.requestPayment({
|
||||
timeStamp: payParams.timeStamp,
|
||||
nonceStr: payParams.nonceStr,
|
||||
package: payParams.package,
|
||||
signType: payParams.signType,
|
||||
paySign: payParams.paySign,
|
||||
success: async (payRes) => {
|
||||
console.log('支付成功:', payRes);
|
||||
uni.showToast({
|
||||
title: '支付成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
|
||||
// 刷新订单列表
|
||||
const statusArray = orderStatusTabs[currentTab.value].status;
|
||||
const statusValue = statusArray.length === 0 ? undefined : statusArray[0];
|
||||
await loadOrderList(statusValue);
|
||||
},
|
||||
fail: async (err) => {
|
||||
console.error('支付失败:', err);
|
||||
|
||||
// 判断是用户取消还是支付失败
|
||||
if (err.errMsg && err.errMsg.includes('cancel')) {
|
||||
// 用户取消支付,调用取消订单接口
|
||||
try {
|
||||
// await cancelProductOrder(order.id || order.orderId);
|
||||
// uni.showToast({
|
||||
// title: '支付已取消',
|
||||
// icon: 'none'
|
||||
// });
|
||||
|
||||
// 刷新订单列表
|
||||
const statusArray = orderStatusTabs[currentTab.value].status;
|
||||
const statusValue = statusArray.length === 0 ? undefined : statusArray[0];
|
||||
await loadOrderList(statusValue);
|
||||
} catch (cancelError) {
|
||||
console.error('取消订单失败:', cancelError);
|
||||
uni.showToast({
|
||||
title: '支付已取消',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 支付失败
|
||||
uni.showToast({
|
||||
title: '支付失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: res?.msg || '创建支付订单失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('支付异常:', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: error.message || '支付失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 取消订单
|
||||
const handleCancelOrder = async (order) => {
|
||||
try {
|
||||
uni.showModal({
|
||||
title: t('order.confirmCancel'),
|
||||
content: t('order.confirmCancelContent'),
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({
|
||||
title: t('common.processing')
|
||||
});
|
||||
|
||||
const result = await cancelOrder({
|
||||
orderId: order.orderNo
|
||||
});
|
||||
|
||||
if (result) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: t('order.cancelSuccess'),
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 刷新订单列表
|
||||
await loadOrderList();
|
||||
} else {
|
||||
throw new Error(result.msg || t('order.cancelFailed'));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: error.message || t('order.cancelFailed'),
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 跳转到设备订单详情页
|
||||
const navigateToDeviceOrderDetail = (order) => {
|
||||
uni.navigateTo({
|
||||
url: `/subPackages/business/device-orderDetail?orderId=${order.orderId || order.orderNo}`
|
||||
});
|
||||
};
|
||||
|
||||
// 处理删除订单
|
||||
const handleDeleteOrder = (order) => {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除这个订单吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: '删除中...',
|
||||
mask: true
|
||||
});
|
||||
|
||||
// 调用删除订单接口
|
||||
const result = await deleteProductOrder(order.id || order.orderId);
|
||||
|
||||
uni.hideLoading();
|
||||
|
||||
if (result && result.code === 200) {
|
||||
uni.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 刷新订单列表
|
||||
const statusArray = orderStatusTabs[currentTab.value].status;
|
||||
const statusValue = statusArray.length === 0 ? undefined : statusArray[0];
|
||||
await loadOrderList(statusValue);
|
||||
} else {
|
||||
throw new Error(result?.msg || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error('删除订单失败:', error);
|
||||
uni.showToast({
|
||||
title: error.message || '删除失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.order-container {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
padding-bottom: 30rpx;
|
||||
|
||||
// 状态标签栏
|
||||
.status-tabs {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
padding: 0 20rpx;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
height: 90rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
color: #07c160;
|
||||
font-weight: 500;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40rpx;
|
||||
height: 4rpx;
|
||||
background: #07c160;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 订单列表
|
||||
.order-list {
|
||||
padding: 20rpx;
|
||||
|
||||
// 订单项
|
||||
.order-item {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
// 订单头部
|
||||
.order-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
.order-id {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.order-status {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
|
||||
&.status-waiting {
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
&.status-shipping {
|
||||
color: #1989fa;
|
||||
}
|
||||
|
||||
&.status-receiving {
|
||||
color: #1989fa;
|
||||
}
|
||||
|
||||
&.status-finished {
|
||||
color: #07c160;
|
||||
}
|
||||
|
||||
&.status-cancelled {
|
||||
color: #9E9E9E;
|
||||
}
|
||||
|
||||
&.status-refunding {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
&.status-express-return {
|
||||
color: #FF9800;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 订单内容
|
||||
.order-body {
|
||||
padding: 24rpx;
|
||||
|
||||
.device-info {
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
|
||||
.device-left {
|
||||
flex: 1;
|
||||
margin-right: 20rpx;
|
||||
|
||||
.device-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.device-id {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.device-right {
|
||||
|
||||
// 支付分标识
|
||||
.payment-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
white-space: nowrap;
|
||||
|
||||
&.wx-score {
|
||||
background: rgba(7, 193, 96, 0.08);
|
||||
|
||||
.badge-icon {
|
||||
width: 32rpx;
|
||||
height: 26rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.badge-text {
|
||||
font-size: 22rpx;
|
||||
color: #07c160;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.divider {
|
||||
margin: 0 6rpx;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.member {
|
||||
background: rgba(25, 118, 210, 0.08);
|
||||
|
||||
.badge-text {
|
||||
font-size: 22rpx;
|
||||
color: #1976D2;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
&.deposit {
|
||||
background: #f5f5f5;
|
||||
|
||||
.badge-text {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.order-times {
|
||||
.time-row {
|
||||
display: flex;
|
||||
font-size: 26rpx;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
.time-label {
|
||||
color: #999;
|
||||
width: 140rpx;
|
||||
}
|
||||
|
||||
.time-value {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 订单底部
|
||||
.order-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
background: #fafafa;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
|
||||
.price {
|
||||
font-size: 34rpx;
|
||||
font-weight: 500;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
|
||||
.action-item {
|
||||
font-size: 26rpx;
|
||||
padding: 10rpx 30rpx;
|
||||
border-radius: 30rpx;
|
||||
margin-left: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 10rpx;
|
||||
|
||||
&.primary {
|
||||
background: #07c160;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
border: 1rpx solid #e0e0e0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.empty-state {
|
||||
padding: 100rpx 0;
|
||||
text-align: center;
|
||||
|
||||
.empty-icon {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
margin: 0 auto 30rpx;
|
||||
background: #f5f5f5;
|
||||
// border-radius: 50%;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,361 @@
|
||||
<template>
|
||||
<view class="position-detail-page">
|
||||
<!-- 顶部设备柜图示 -->
|
||||
<view class="device-illustration">
|
||||
<image v-if="positionInfo.deviceImg" :src="positionInfo.deviceImg" class="device-img" mode="aspectFit"></image>
|
||||
<image v-else src="/static/device-info.png" class="device-img" mode="aspectFit"></image>
|
||||
</view>
|
||||
|
||||
<!-- 场地信息卡片 -->
|
||||
<view class="info-card">
|
||||
<!-- 场地名称 -->
|
||||
<view class="position-name">{{ positionInfo.name || $t('common.loading') }}</view>
|
||||
|
||||
<!-- 地址信息 -->
|
||||
<view class="info-item" v-if="positionInfo.location">
|
||||
<image src="/static/device-location.png" class="item-icon" mode="aspectFit"></image>
|
||||
<text class="item-text">{{ positionInfo.location }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 营业时间 -->
|
||||
<view class="info-item" v-if="positionInfo.workTime && positionInfo.workTime !== '0'">
|
||||
<image src="/static/device-time.png" class="item-icon" mode="aspectFit"></image>
|
||||
<text class="item-text">{{ $t('location.businessHours') }}{{ positionInfo.workTime }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 计费信息 -->
|
||||
<view class="info-item">
|
||||
<image src="/static/device-price.png" class="item-icon" mode="aspectFit"></image>
|
||||
<text class="item-text">{{ $t('device.pricing') }}:{{ pricingText }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 可用数量信息 -->
|
||||
<view class="info-item" v-if="positionInfo.availablePowerBankCount !== undefined && positionInfo.availablePowerBankCount !== null">
|
||||
<image src="/static/device-info.png" class="item-icon" mode="aspectFit"></image>
|
||||
<text class="item-text">可租借风扇:{{ positionInfo.availablePowerBankCount }} 台</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item" v-if="positionInfo.availableEmptyGridCount !== undefined && positionInfo.availableEmptyGridCount !== null">
|
||||
<image src="/static/device-info.png" class="item-icon" mode="aspectFit"></image>
|
||||
<text class="item-text">可归还空位:{{ positionInfo.availableEmptyGridCount }} 个</text>
|
||||
</view>
|
||||
|
||||
<!-- 按钮组 -->
|
||||
<view class="button-group">
|
||||
<view style="display: flex;flex-direction: row;gap: 10rpx;">
|
||||
<view class="status-btn" v-if="isRentable">{{ $t('location.rent') }}</view>
|
||||
<view class="status-btn" v-if="isReturnable">{{ $t('location.return') }}</view>
|
||||
</view>
|
||||
|
||||
<view class="nav-btn" @click.stop="navigateToPosition">{{ $t('location.navigateHere') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="footer-actions">
|
||||
<button class="action-btn btn-outline" @click="reportError">{{ $t('device.reportError') }}</button>
|
||||
<button class="action-btn btn-primary" @click="scanCode">{{ $t('device.scanToUse') }}</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
onMounted
|
||||
} from 'vue'
|
||||
import {
|
||||
onLoad
|
||||
} from '@dcloudio/uni-app'
|
||||
import {
|
||||
getNearbyDevices,
|
||||
transformDeviceData
|
||||
} from '@/config/api/device.js'
|
||||
import { useI18n } from '@/utils/i18n.js'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const positionInfo = ref({})
|
||||
const positionId = ref('')
|
||||
|
||||
const isRentable = computed(() => {
|
||||
if (typeof positionInfo.value?.canRent !== 'undefined') {
|
||||
return !!positionInfo.value.canRent
|
||||
}
|
||||
return String(positionInfo.value?.status || '').toLowerCase() === 'online'
|
||||
})
|
||||
|
||||
const isReturnable = computed(() => {
|
||||
if (typeof positionInfo.value?.canReturn !== 'undefined') {
|
||||
return !!positionInfo.value.canReturn
|
||||
}
|
||||
return String(positionInfo.value?.status || '').toLowerCase() === 'online'
|
||||
})
|
||||
|
||||
const pricingText = computed(() => {
|
||||
// 使用设备的 remark 字段作为计费信息
|
||||
if (positionInfo.value?.remark) {
|
||||
return positionInfo.value.remark
|
||||
}
|
||||
// 如果 remark 为空,显示默认提示
|
||||
return '暂无计费信息'
|
||||
})
|
||||
|
||||
onLoad(async (options) => {
|
||||
if (options.positionId) {
|
||||
positionId.value = options.positionId
|
||||
await loadPositionDetail()
|
||||
}
|
||||
})
|
||||
|
||||
const loadPositionDetail = async () => {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: t('common.loading')
|
||||
})
|
||||
|
||||
// 获取用户位置用于查询附近设备
|
||||
let userLocation = null
|
||||
try {
|
||||
userLocation = uni.getStorageSync('userLocation')
|
||||
} catch (e) {
|
||||
console.warn('获取用户位置失败:', e)
|
||||
}
|
||||
|
||||
if (!userLocation || !userLocation.latitude || !userLocation.longitude) {
|
||||
// 如果没有用户位置,使用默认位置
|
||||
userLocation = { latitude: 39.916527, longitude: 116.397128 }
|
||||
}
|
||||
|
||||
const res = await getNearbyDevices({
|
||||
userLatitude: userLocation.latitude,
|
||||
userLongitude: userLocation.longitude,
|
||||
queryType: 'rent', // 查询可租借设备
|
||||
radiusKm: 50, // 扩大查询范围以确保能找到目标设备
|
||||
pageNum: 1,
|
||||
pageSize: 100
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
const devices = res.data?.records || []
|
||||
// 将设备数据转换为统一格式
|
||||
const positions = devices.map(transformDeviceData)
|
||||
|
||||
const position = positions.find(p => p.positionId === positionId.value)
|
||||
if (position) {
|
||||
positionInfo.value = position
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: t('location.notExist'),
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载设备详情失败:', e)
|
||||
uni.showToast({
|
||||
title: t('common.loadFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
const navigateToPosition = () => {
|
||||
if (!positionInfo.value.latitude || !positionInfo.value.longitude) {
|
||||
uni.showToast({
|
||||
title: t('location.coordinateError'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const latitude = parseFloat(positionInfo.value.latitude)
|
||||
const longitude = parseFloat(positionInfo.value.longitude)
|
||||
|
||||
// 验证坐标有效性
|
||||
if (isNaN(latitude) || isNaN(longitude) ||
|
||||
latitude < -90 || latitude > 90 ||
|
||||
longitude < -180 || longitude > 180 ||
|
||||
(latitude === 0 && longitude === 0)) {
|
||||
uni.showToast({
|
||||
title: t('location.coordinateError'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.openLocation({
|
||||
latitude,
|
||||
longitude,
|
||||
name: positionInfo.value.name,
|
||||
address: positionInfo.value.location
|
||||
})
|
||||
}
|
||||
|
||||
const reportError = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/feedback/index'
|
||||
})
|
||||
}
|
||||
|
||||
const scanCode = () => {
|
||||
uni.scanCode({
|
||||
scanType: ['qrCode', 'barCode'],
|
||||
success: (res) => {
|
||||
console.log('扫码结果:', res)
|
||||
// 处理扫码结果,跳转到设备详情页
|
||||
if (res.result) {
|
||||
// 假设二维码内容是设备号
|
||||
uni.navigateTo({
|
||||
url: `/pages/device/detail?deviceNo=${res.result}`
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('扫码失败:', err)
|
||||
uni.showToast({
|
||||
title: this.$t('home.scanFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.position-detail-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #D1FFE1 0%, #F5F9F7 100%);
|
||||
padding-bottom: 200rpx;
|
||||
}
|
||||
|
||||
.device-illustration {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx 0 30rpx;
|
||||
|
||||
.device-img {
|
||||
width: 480rpx;
|
||||
height: 480rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: #ffffff;
|
||||
margin: 0 32rpx;
|
||||
border-radius: 32rpx;
|
||||
padding: 44rpx 36rpx 36rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
.position-name {
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
margin-bottom: 32rpx;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.item-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-right: 12rpx;
|
||||
margin-top: 2rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
margin-top: 36rpx;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.status-btn {
|
||||
padding: 12rpx 28rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
border: 2rpx solid #3EAB64;
|
||||
color: #3EAB64;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
padding: 12rpx 28rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
border: 2rpx solid #3EAB64;
|
||||
color: #ffffff;
|
||||
background: #3EAB64;
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #ffffff;
|
||||
padding: 24rpx 32rpx;
|
||||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||
box-shadow: 0 -2rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 96rpx;
|
||||
border-radius: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
|
||||
&.btn-outline {
|
||||
border: 2rpx solid #3EAB64;
|
||||
color: #3EAB64;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
&.btn-primary {
|
||||
background: #3EAB64;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.85;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,644 @@
|
||||
<template>
|
||||
<view class="order-container">
|
||||
<!-- 状态切换 -->
|
||||
<view class="status-tabs">
|
||||
<view v-for="(tab, index) in orderStatusTabs" :key="index" class="tab-item"
|
||||
:class="{ active: currentTab === index }" @click="switchTab(index)">
|
||||
{{ tab.text }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<view class="order-list">
|
||||
<view class="empty-state" v-if="orderList.length === 0">
|
||||
<view class="empty-icon">
|
||||
<image src="/static/orderList.png" mode="aspectFill" class="empty-icon"></image>
|
||||
</view>
|
||||
<text class="empty-text">{{ $t('order.noOrderRecord') }}</text>
|
||||
</view>
|
||||
|
||||
<OrderItemCard
|
||||
v-for="(order, index) in orderList"
|
||||
:key="index"
|
||||
:order="order"
|
||||
:orderStatusMap="orderStatusMap"
|
||||
@pay="handlePayment"
|
||||
@cancel="handleCancelOrder"
|
||||
@return-device="onReturnDevice"
|
||||
@details="navigateToDetails"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
reactive,
|
||||
onMounted,
|
||||
onUnmounted
|
||||
} from 'vue';
|
||||
import OrderItemCard from '../../components/OrderItemCard.vue';
|
||||
import {
|
||||
onLoad
|
||||
} from '@dcloudio/uni-app';
|
||||
import {
|
||||
getOrderList,
|
||||
queryById,
|
||||
getOrderByOrderNoScorePayStatus,
|
||||
cancelOrder,
|
||||
createWxPayment
|
||||
} from '../../config/api/order.js';
|
||||
import {
|
||||
updateUserBalance
|
||||
} from '../../config/api/user.js';
|
||||
import {
|
||||
URL
|
||||
} from '../../config/url.js';
|
||||
import { useI18n } from '@/utils/i18n.js'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 初始化状态
|
||||
const currentTab = ref(0);
|
||||
const orderList = ref([]);
|
||||
|
||||
// 订单状态映射
|
||||
const orderStatusMap = reactive({
|
||||
'0': {
|
||||
get text() { return t('order.waitingForPayment') },
|
||||
class: 'status-waiting'
|
||||
},
|
||||
'1': {
|
||||
get text() { return t('order.inUse') },
|
||||
class: 'status-using'
|
||||
},
|
||||
'2': {
|
||||
get text() { return t('order.finished') },
|
||||
class: 'status-finished'
|
||||
},
|
||||
'3': {
|
||||
get text() { return t('order.cancelled') },
|
||||
class: 'status-cancelled'
|
||||
},
|
||||
'waiting_for_payment': {
|
||||
get text() { return t('order.waitingForPayment') },
|
||||
class: 'status-waiting'
|
||||
},
|
||||
'in_used': {
|
||||
get text() { return t('order.inUse') },
|
||||
class: 'status-using'
|
||||
},
|
||||
'used_done': {
|
||||
get text() { return t('order.finished') },
|
||||
class: 'status-finished'
|
||||
},
|
||||
'order_cancelled': {
|
||||
get text() { return t('order.cancelled') },
|
||||
class: 'status-cancelled'
|
||||
},
|
||||
'express_return': {
|
||||
get text() { return t('express.title') },
|
||||
class: 'status-express-return'
|
||||
}
|
||||
});
|
||||
|
||||
// 订单状态标签
|
||||
const orderStatusTabs = reactive([{
|
||||
get text() { return t('common.all') },
|
||||
status: []
|
||||
},
|
||||
{
|
||||
get text() { return t('order.waitingForPayment') },
|
||||
status: ['waiting_for_payment']
|
||||
},
|
||||
{
|
||||
get text() { return t('order.inUse') },
|
||||
status: ['in_used']
|
||||
},
|
||||
{
|
||||
get text() { return t('order.finished') },
|
||||
status: ['used_done']
|
||||
},
|
||||
{
|
||||
get text() { return t('order.cancelled') },
|
||||
status: ['order_cancelled']
|
||||
}
|
||||
]);
|
||||
|
||||
// 页面加载
|
||||
onLoad(async (options) => {
|
||||
// 如果有传入orderId参数,说明是从设备租借页面跳转过来的
|
||||
if (options && options.orderId) {
|
||||
try {
|
||||
// 获取特定订单信息
|
||||
const res = await queryById(options.orderId);
|
||||
if (res.code === 200 && res.data) {
|
||||
// 获取到的订单数据
|
||||
const orderData = res.data;
|
||||
|
||||
// 使用实际的startTime字段,如果没有则尝试使用createTime
|
||||
const orderStartTime = orderData.startTime || orderData.createTime || '';
|
||||
|
||||
// 格式化订单数据
|
||||
const formattedOrder = {
|
||||
orderNo: orderData.orderId,
|
||||
status: orderData.orderStatus,
|
||||
deviceId: orderData.deviceNo,
|
||||
payWay: orderData.payWay,
|
||||
startTime: orderStartTime,
|
||||
endTime: orderData.endTime || '',
|
||||
positionName: orderData.positionName || orderData.positionLocation || '',
|
||||
deviceName: orderData.deviceName || '',
|
||||
amount: orderData.payAmount || orderData.actualDeviceAmount || orderData.currentFee || orderData.residueAmount || '0.00'
|
||||
};
|
||||
|
||||
// 将订单添加到列表开头
|
||||
orderList.value = [formattedOrder, ...orderList.value];
|
||||
|
||||
// 根据订单状态切换到对应标签
|
||||
const tabIndex = orderStatusTabs.findIndex(tab =>
|
||||
tab.status.includes(orderData.orderStatus)
|
||||
);
|
||||
|
||||
if (tabIndex !== -1) {
|
||||
switchTab(tabIndex);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取订单详情失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取订单列表
|
||||
await loadOrderList();
|
||||
});
|
||||
|
||||
// 切换标签
|
||||
const switchTab = async (index) => {
|
||||
currentTab.value = index;
|
||||
// 根据状态获取订单列表
|
||||
const statusList = orderStatusTabs[index].status[0];
|
||||
await loadOrderList(statusList);
|
||||
};
|
||||
|
||||
// 加载订单列表
|
||||
const loadOrderList = async (statusList) => {
|
||||
try {
|
||||
if(statusList!=undefined){
|
||||
statusList = {
|
||||
orderStatus:statusList
|
||||
}
|
||||
}
|
||||
const res = await getOrderList(statusList);
|
||||
if (res.code === 200 && res.data && res.data.records) {
|
||||
// 处理订单列表数据
|
||||
orderList.value = res.data.records.map(item => {
|
||||
// 使用实际的startTime字段,如果没有则尝试使用createTime
|
||||
const orderStartTime = item.startTime || item.createTime || '';
|
||||
|
||||
return {
|
||||
orderNo: item.orderNo,
|
||||
orderId: item.orderId,
|
||||
orderStatus: item.orderStatus,
|
||||
deviceId: item.deviceNo,
|
||||
payWay: item.payWay,
|
||||
startTime: orderStartTime,
|
||||
endTime: item.endTime || '',
|
||||
positionName: item.positionName || item.positionLocation || '',
|
||||
deviceName: item.deviceName || '',
|
||||
amount: item.payAmount || item.actualDeviceAmount || item.currentFee || item.residueAmount || '0.00'
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取订单列表失败:', error);
|
||||
uni.showToast({
|
||||
title: t('order.getOrderListFailed'),
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理订单完成事件
|
||||
const handleOrderCompleted = (orderData) => {
|
||||
console.log('订单列表页收到订单完成事件:', orderData)
|
||||
// 刷新订单列表,根据当前选中的标签刷新对应状态的订单
|
||||
const statusList = orderStatusTabs[currentTab.value].status[0]
|
||||
loadOrderList(statusList)
|
||||
}
|
||||
|
||||
// 设置页面标题并监听订单完成事件
|
||||
onMounted(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: t('order.myOrders')
|
||||
})
|
||||
|
||||
// 监听订单完成事件
|
||||
uni.$on('orderCompleted', handleOrderCompleted)
|
||||
})
|
||||
|
||||
// 页面卸载时移除事件监听
|
||||
onUnmounted(() => {
|
||||
uni.$off('orderCompleted', handleOrderCompleted)
|
||||
})
|
||||
|
||||
// 同步订单状态
|
||||
const getOrderStatus = async (order) => {
|
||||
try {
|
||||
const res = await getOrderByOrderNoScorePayStatus(order.orderNo);
|
||||
if (res.code === 200) {
|
||||
uni.showToast({
|
||||
title: t('order.syncSuccess'),
|
||||
icon: 'success'
|
||||
});
|
||||
await loadOrderList(orderStatusTabs[currentTab.value].status);
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: t('order.syncFailed'),
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 跳转到订单详情页面(统一入口)
|
||||
const navigateToOrderDetail = (order) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/order/detail?orderId=${order.orderId || order.orderNo}&deviceId=${order.deviceId}`
|
||||
});
|
||||
};
|
||||
|
||||
// 组件事件:归还设备(实际跳转到订单详情页)
|
||||
const onReturnDevice = (order) => {
|
||||
navigateToOrderDetail(order);
|
||||
};
|
||||
|
||||
// 跳转到订单详情页
|
||||
const navigateToDetails = (order) => {
|
||||
navigateToOrderDetail(order);
|
||||
};
|
||||
|
||||
// 立即支付
|
||||
const handlePayment = async (order) => {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: t('common.processing')
|
||||
});
|
||||
|
||||
// 调用后端创建微信支付订单接口
|
||||
const res = await createWxPayment(order.orderNo);
|
||||
|
||||
if (res && res.code === 200) {
|
||||
const payParams = res.data;
|
||||
|
||||
// 调用微信支付
|
||||
await uni.requestPayment({
|
||||
...payParams,
|
||||
success: async () => {
|
||||
uni.showToast({
|
||||
title: t('payment.paymentSuccess'),
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 更新用户余额
|
||||
try {
|
||||
await updateUserBalance(order.orderId || order.orderNo);
|
||||
} catch (error) {
|
||||
console.warn('更新用户余额失败:', error);
|
||||
}
|
||||
|
||||
// 刷新订单列表
|
||||
await loadOrderList(orderStatusTabs[currentTab.value].status);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('支付失败:', err);
|
||||
throw new Error(t('payment.paymentFailedRetry'));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new Error(res?.msg || '创建支付订单失败');
|
||||
}
|
||||
|
||||
uni.hideLoading();
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: error.message || t('payment.paymentFailed'),
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 取消订单
|
||||
const handleCancelOrder = async (order) => {
|
||||
try {
|
||||
uni.showModal({
|
||||
title: t('order.confirmCancel'),
|
||||
content: t('order.confirmCancelContent'),
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({
|
||||
title: t('common.processing')
|
||||
});
|
||||
|
||||
const result = await cancelOrder({
|
||||
orderId: order.orderNo
|
||||
});
|
||||
|
||||
if (result) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: t('order.cancelSuccess'),
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 刷新订单列表
|
||||
await loadOrderList();
|
||||
} else {
|
||||
throw new Error(result.msg || t('order.cancelFailed'));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: error.message || t('order.cancelFailed'),
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.order-container {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
padding-bottom: 30rpx;
|
||||
|
||||
// 状态标签栏
|
||||
.status-tabs {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
padding: 0 20rpx;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
height: 90rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
color: #07c160;
|
||||
font-weight: 500;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40rpx;
|
||||
height: 4rpx;
|
||||
background: #07c160;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 订单列表
|
||||
.order-list {
|
||||
padding: 20rpx;
|
||||
|
||||
// 订单项
|
||||
.order-item {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
// 订单头部
|
||||
.order-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
.order-id {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.order-status {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
|
||||
&.status-waiting {
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
&.status-using {
|
||||
color: #07c160;
|
||||
}
|
||||
|
||||
&.status-finished {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
&.status-cancelled {
|
||||
color: #9E9E9E;
|
||||
}
|
||||
|
||||
&.status-express-return {
|
||||
color: #FF9800;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 订单内容
|
||||
.order-body {
|
||||
padding: 24rpx;
|
||||
|
||||
.device-info {
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
|
||||
.device-left {
|
||||
flex: 1;
|
||||
margin-right: 20rpx;
|
||||
|
||||
.device-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.device-id {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.device-right {
|
||||
|
||||
// 支付分标识
|
||||
.payment-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
white-space: nowrap;
|
||||
|
||||
&.wx-score {
|
||||
background: rgba(7, 193, 96, 0.08);
|
||||
|
||||
.badge-icon {
|
||||
width: 32rpx;
|
||||
height: 26rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.badge-text {
|
||||
font-size: 22rpx;
|
||||
color: #07c160;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.divider {
|
||||
margin: 0 6rpx;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.member {
|
||||
background: rgba(25, 118, 210, 0.08);
|
||||
|
||||
.badge-text {
|
||||
font-size: 22rpx;
|
||||
color: #1976D2;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
&.deposit {
|
||||
background: #f5f5f5;
|
||||
|
||||
.badge-text {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.order-times {
|
||||
.time-row {
|
||||
display: flex;
|
||||
font-size: 26rpx;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
.time-label {
|
||||
color: #999;
|
||||
width: 140rpx;
|
||||
}
|
||||
|
||||
.time-value {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 订单底部
|
||||
.order-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
background: #fafafa;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
|
||||
.price {
|
||||
font-size: 34rpx;
|
||||
font-weight: 500;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
|
||||
.action-item {
|
||||
font-size: 26rpx;
|
||||
padding: 10rpx 30rpx;
|
||||
border-radius: 30rpx;
|
||||
margin-left: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 10rpx;
|
||||
|
||||
&.primary {
|
||||
background: #07c160;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
border: 1rpx solid #e0e0e0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.empty-state {
|
||||
padding: 100rpx 0;
|
||||
text-align: center;
|
||||
|
||||
.empty-icon {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
margin: 0 auto 30rpx;
|
||||
background: #f5f5f5;
|
||||
// border-radius: 50%;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,708 @@
|
||||
<template>
|
||||
<view class="payment-container">
|
||||
<!-- 地点信息卡片 -->
|
||||
<view class="location-card">
|
||||
<view class="location-header">
|
||||
<!-- <view class="location-icon">📍</view> -->
|
||||
<image src="@/static/device_location.png" mode="aspectFit" class="location-icon"></image>
|
||||
<text class="location-name">{{ locationName }}</text>
|
||||
<view class="status-badge">{{ $t('device.available') }}</view>
|
||||
</view>
|
||||
<view class="device-info">
|
||||
<text class="device-label">{{ $t('order.deviceNo') }}:</text>
|
||||
<text class="device-value">{{ orderInfo.deviceNo || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单信息和费用信息 -->
|
||||
<view class="order-card">
|
||||
<view class="card-header">
|
||||
<view class="card-title-bar"></view>
|
||||
<view class="card-title">{{ $t('payment.orderInfo') }}</view>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('order.orderNo') }}</text>
|
||||
<text class="value">{{ orderInfo.orderNo || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('order.deviceNo') }}</text>
|
||||
<text class="value">{{ orderInfo.deviceNo || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('payment.createTime') }}</text>
|
||||
<text class="value">{{ orderInfo.createTime || '-' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 费用信息部分 -->
|
||||
<view class="card-header" style="margin-top: 32rpx;">
|
||||
<view class="card-title-bar"></view>
|
||||
<view class="card-title">{{ $t('payment.feeInfo') }}</view>
|
||||
</view>
|
||||
<view class="price-item">
|
||||
<text class="label">{{ $t('payment.deposit') }}</text>
|
||||
<text class="value">¥ {{ orderInfo.deposit || '90' }}</text>
|
||||
</view>
|
||||
<!-- <view class="price-item">
|
||||
<text class="label">{{ $t('payment.package') }}</text>
|
||||
<text class="value">{{ packageText }}</text>
|
||||
</view> -->
|
||||
<view class="price-item total">
|
||||
<text class="label">{{ $t('payment.total') }}</text>
|
||||
<view class="total-value">
|
||||
<text class="currency">¥</text>
|
||||
<text class="amount">{{ totalAmount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-bar">
|
||||
<view class="pay-btn" @click="handlePayment">
|
||||
<text class="currency-small">¥</text>
|
||||
<text class="amount-large">{{ totalAmount }}</text>
|
||||
<text class="pay-text">{{ $t('payment.payNow') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
reactive,
|
||||
onMounted
|
||||
} from 'vue'
|
||||
import {
|
||||
onLoad
|
||||
} from '@dcloudio/uni-app'
|
||||
import {
|
||||
queryById,
|
||||
createAntomPayment,
|
||||
getAntomPaymentMethods,
|
||||
getAntomPaymentStatus
|
||||
} from '@/config/api/order.js'
|
||||
import {
|
||||
getDeviceInfo
|
||||
} from '@/config/api/device.js'
|
||||
import {
|
||||
updateUserBalance
|
||||
} from '@/config/api/user.js'
|
||||
import {
|
||||
useI18n
|
||||
} from '@/utils/i18n.js'
|
||||
|
||||
const {
|
||||
t
|
||||
} = useI18n()
|
||||
|
||||
const orderId = ref(null)
|
||||
const deviceNo = ref(null)
|
||||
const orderInfo = ref({})
|
||||
const deviceInfo = ref(null)
|
||||
const passedTotalAmount = ref(null)
|
||||
const passedDepositAmount = ref(null)
|
||||
|
||||
// 倒计时相关
|
||||
const countdown = ref(15 * 60) // 15分钟 = 900秒
|
||||
let countdownTimer = null
|
||||
|
||||
// 支付方式相关
|
||||
const paymentMethods = ref([])
|
||||
const selectedPaymentMethod = ref('ALIPAY') // 默认选择支付宝
|
||||
|
||||
// 地点名称(可以从设备信息中获取,这里先用默认值)
|
||||
const locationName = ref('澎创办公室')
|
||||
|
||||
// 套餐文本(可以从设备信息中获取,这里先用默认值)
|
||||
const packageText = computed(() => {
|
||||
// 这里可以根据实际的设备信息动态生成
|
||||
return '2元/小时'
|
||||
})
|
||||
|
||||
const orderStatus = reactive({
|
||||
get text() {
|
||||
return t('payment.waitingForPayment')
|
||||
},
|
||||
get desc() {
|
||||
return t('payment.pleasePayIn15Min')
|
||||
},
|
||||
class: 'waiting'
|
||||
})
|
||||
|
||||
const totalAmount = computed(() => {
|
||||
if (passedTotalAmount.value !== null) {
|
||||
return parseFloat(passedTotalAmount.value).toFixed(2);
|
||||
}
|
||||
const deposit = parseFloat(orderInfo.value.deposit || passedDepositAmount.value || 99)
|
||||
return deposit.toFixed(2)
|
||||
})
|
||||
|
||||
// 加载订单信息
|
||||
const loadOrderInfo = async () => {
|
||||
// 检查 orderId 是否存在,如果不存在则尝试从缓存获取
|
||||
if (!orderId.value) {
|
||||
// #ifdef H5
|
||||
const cachedOrderId = uni.getStorageSync('pendingPaymentNo');
|
||||
if (cachedOrderId) {
|
||||
orderId.value = cachedOrderId;
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
// 如果两种方式都获取不到 orderId,提示并跳转
|
||||
if (!orderId.value) {
|
||||
uni.showToast({
|
||||
title: t('order.orderNotExist'),
|
||||
icon: 'none'
|
||||
});
|
||||
setTimeout(() => {
|
||||
uni.switchTab({
|
||||
url: '/pages/index/index'
|
||||
});
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: t('common.loading')
|
||||
})
|
||||
|
||||
const res = await queryById(orderId.value)
|
||||
if (res.code === 200 && res.data) {
|
||||
const orderData = res.data
|
||||
|
||||
// 处理创建时间
|
||||
let formattedTime;
|
||||
try {
|
||||
if (orderData.createTime) {
|
||||
formattedTime = formatTime(new Date(orderData.createTime));
|
||||
} else {
|
||||
formattedTime = formatTime(new Date());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('时间格式化错误:', e);
|
||||
formattedTime = formatTime(new Date());
|
||||
}
|
||||
|
||||
orderInfo.value = {
|
||||
orderNo: orderData.orderNo || orderData.orderId,
|
||||
deviceNo: orderData.deviceNo,
|
||||
createTime: formattedTime,
|
||||
deposit: passedDepositAmount.value || orderData.depositAmount || '99.00',
|
||||
orderStatus:orderData.orderStatus
|
||||
}
|
||||
|
||||
deviceNo.value = orderData.deviceNo;
|
||||
await loadDeviceInfo();
|
||||
await loadPaymentMethods();
|
||||
// #ifdef H5
|
||||
if(orderInfo.value.orderStatus=='waiting_for_payment'){
|
||||
startPaymentStatusPolling();
|
||||
}
|
||||
// #endif
|
||||
} else {
|
||||
throw new Error(t('order.getOrderFailed'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取订单信息失败:', error)
|
||||
uni.showToast({
|
||||
title: error.message || t('order.getOrderFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载设备信息
|
||||
const loadDeviceInfo = async () => {
|
||||
if (!deviceNo.value) return;
|
||||
|
||||
try {
|
||||
const res = await getDeviceInfo(deviceNo.value);
|
||||
if (res.code === 200 && res.data) {
|
||||
deviceInfo.value = res.data.device;
|
||||
|
||||
if (deviceInfo.value && deviceInfo.value.depositAmount) {
|
||||
orderInfo.value.deposit = deviceInfo.value.depositAmount;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取设备信息失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取操作系统类型
|
||||
const getOsType = () => {
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
console.log(uni.getSystemInfoSync());
|
||||
|
||||
const platform = systemInfo.platform;
|
||||
console.log('当前系统类型:', uni.getSystemInfoSync().platform);
|
||||
|
||||
// 根据平台返回对应的 osType
|
||||
if (platform === 'android') {
|
||||
return 'ANDROID';
|
||||
} else if (platform === 'ios') {
|
||||
return 'IOS';
|
||||
} else {
|
||||
// 默认返回 ANDROID
|
||||
return 'ANDROID';
|
||||
}
|
||||
}
|
||||
|
||||
// 加载支付方式列表
|
||||
const loadPaymentMethods = async () => {
|
||||
if (!orderInfo.value.orderNo) return;
|
||||
|
||||
try {
|
||||
const osType = getOsType();
|
||||
console.log('当前系统类型:', osType);
|
||||
|
||||
const res = await getAntomPaymentMethods(orderInfo.value.orderNo, osType);
|
||||
if (res.code === 200 && res.data && res.data.paymentOptions) {
|
||||
paymentMethods.value = res.data.paymentOptions;
|
||||
console.log('支付方式列表:', paymentMethods.value);
|
||||
// 如果有支付方式,默认选择第一个
|
||||
if (paymentMethods.value.length > 0) {
|
||||
selectedPaymentMethod.value = paymentMethods.value[0].paymentMethodType;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取支付方式失败:', error);
|
||||
// 如果获取失败,使用默认支付方式
|
||||
paymentMethods.value = [{
|
||||
paymentMethodType: 'ALIPAY',
|
||||
paymentMethodName: '支付宝'
|
||||
},
|
||||
{
|
||||
paymentMethodType: 'WECHATPAY',
|
||||
paymentMethodName: '微信支付'
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 选择支付方式
|
||||
const selectPaymentMethod = (methodType) => {
|
||||
selectedPaymentMethod.value = methodType;
|
||||
}
|
||||
|
||||
// 获取支付方式图标
|
||||
const getPaymentIcon = (methodType) => {
|
||||
const iconMap = {
|
||||
'ALIPAY': 'alipay',
|
||||
'WECHATPAY': 'wechat',
|
||||
'WECHAT': 'wechat'
|
||||
};
|
||||
return iconMap[methodType] || 'default';
|
||||
}
|
||||
|
||||
// 处理支付
|
||||
const handlePayment = async () => {
|
||||
if (!selectedPaymentMethod.value) {
|
||||
uni.showToast({
|
||||
title: '请选择支付方式',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: t('common.processing')
|
||||
})
|
||||
|
||||
const osType = getOsType();
|
||||
const res = await createAntomPayment(orderInfo.value.orderNo, selectedPaymentMethod.value, osType)
|
||||
|
||||
if (res && res.code === 200 && res.data) {
|
||||
const paymentUrl = res.data.h5Url;
|
||||
|
||||
if (!paymentUrl) {
|
||||
throw new Error('未获取到支付链接');
|
||||
}
|
||||
|
||||
uni.hideLoading();
|
||||
// #ifdef H5
|
||||
uni.setStorageSync('pendingPaymentNo', orderId.value);
|
||||
// 跳转到支付页面
|
||||
// uni.navigateTo({
|
||||
// url: `/pages/webview/index?url=${encodeURIComponent(paymentUrl)}&title=支付`
|
||||
// });
|
||||
window.open(paymentUrl);
|
||||
// #endif
|
||||
|
||||
// 开始轮询支付状态
|
||||
startPaymentStatusPolling();
|
||||
|
||||
} else {
|
||||
throw new Error(res?.msg || t('payment.createPayOrderFailed'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('支付失败:', error)
|
||||
uni.showToast({
|
||||
title: error.message || t('payment.paymentFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// 轮询支付状态
|
||||
let pollingTimer = null;
|
||||
const startPaymentStatusPolling = () => {
|
||||
// 清除之前的定时器
|
||||
if (pollingTimer) {
|
||||
clearInterval(pollingTimer);
|
||||
}
|
||||
|
||||
let pollCount = 0;
|
||||
const maxPollCount = 60; // 最多轮询60次(5分钟)
|
||||
|
||||
pollingTimer = setInterval(async () => {
|
||||
pollCount++;
|
||||
|
||||
if (pollCount > maxPollCount) {
|
||||
clearInterval(pollingTimer);
|
||||
uni.showToast({
|
||||
title: '支付超时,请重新支付',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const osType = getOsType();
|
||||
const res = await getAntomPaymentStatus(orderInfo.value.orderNo, osType);
|
||||
if (res && res.code === 200 && res.data) {
|
||||
const paymentStatus = res.data.paymentStatus;
|
||||
|
||||
if (paymentStatus === 'SUCCESS') {
|
||||
clearInterval(pollingTimer);
|
||||
|
||||
// uni.showToast({
|
||||
// title: t('payment.paymentSuccess'),
|
||||
// icon: 'success'
|
||||
// });
|
||||
|
||||
try {
|
||||
await updateUserBalance(orderId.value);
|
||||
} catch (error) {
|
||||
console.warn('更新用户余额失败:', error);
|
||||
}
|
||||
console.log(orderInfo);
|
||||
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: `/pages/order/success?orderId=${orderId.value}&deviceId=${orderInfo.value.deviceNo}`
|
||||
});
|
||||
}, 1500);
|
||||
} else if (paymentStatus === 'FAIL' || paymentStatus === 'CANCELLED') {
|
||||
clearInterval(pollingTimer);
|
||||
uni.showToast({
|
||||
title: '支付失败,请重新支付',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询支付状态失败:', error);
|
||||
}
|
||||
}, 5000); // 每5秒查询一次
|
||||
}
|
||||
|
||||
// 更新导航栏倒计时
|
||||
const updateNavBarCountdown = () => {
|
||||
const minutes = Math.floor(countdown.value / 60).toString().padStart(2, '0')
|
||||
const seconds = (countdown.value % 60).toString().padStart(2, '0')
|
||||
uni.setNavigationBarTitle({
|
||||
title: `待支付 ${minutes}:${seconds}`
|
||||
})
|
||||
}
|
||||
|
||||
// 开始倒计时
|
||||
const startCountdown = () => {
|
||||
// 清除之前的定时器
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer)
|
||||
}
|
||||
|
||||
// 立即更新一次
|
||||
updateNavBarCountdown()
|
||||
|
||||
// 每秒更新
|
||||
countdownTimer = setInterval(() => {
|
||||
countdown.value--
|
||||
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(countdownTimer)
|
||||
uni.showToast({
|
||||
title: t('order.paymentFailedRetry'),
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.switchTab({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}, 1500)
|
||||
return
|
||||
}
|
||||
|
||||
updateNavBarCountdown()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 页面卸载时清除定时器
|
||||
onMounted(() => {
|
||||
return () => {
|
||||
if (pollingTimer) {
|
||||
clearInterval(pollingTimer);
|
||||
}
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
const hour = date.getHours().toString().padStart(2, '0')
|
||||
const minute = date.getMinutes().toString().padStart(2, '0')
|
||||
|
||||
return `${year}-${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
// 启动倒计时
|
||||
startCountdown()
|
||||
|
||||
// 优先从 options 中获取 orderId
|
||||
if (options && options.orderId) {
|
||||
orderId.value = options.orderId
|
||||
|
||||
if (options.totalAmount) {
|
||||
passedTotalAmount.value = options.totalAmount;
|
||||
}
|
||||
|
||||
if (options.depositAmount) {
|
||||
passedDepositAmount.value = options.depositAmount;
|
||||
}
|
||||
|
||||
if (options.feeConfig) {
|
||||
try {
|
||||
const feeConfigStr = decodeURIComponent(options.feeConfig)
|
||||
deviceInfo.value = {
|
||||
feeConfig: feeConfigStr
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析URL中的feeConfig失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
loadOrderInfo()
|
||||
}
|
||||
// #ifdef H5
|
||||
// 如果 options 中没有 orderId,尝试从缓存中获取(H5 环境)
|
||||
else {
|
||||
const cachedOrderId = uni.getStorageSync('pendingPaymentNo');
|
||||
console.log("订单编号:"+cachedOrderId);
|
||||
|
||||
if (cachedOrderId) {
|
||||
orderId.value = cachedOrderId;
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
loadOrderInfo()
|
||||
// 调用 loadOrderInfo,内部会再次检查 orderId 是否存在
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.payment-container {
|
||||
min-height: 100vh;
|
||||
background: #F5F5F5;
|
||||
padding: 24rpx;
|
||||
padding-bottom: 200rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
.location-card {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.location-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.location-icon {
|
||||
font-size: 40rpx;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.location-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
background: #D4F4DD;
|
||||
color: #52C41A;
|
||||
font-size: 24rpx;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.device-info {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
|
||||
.device-label {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.device-value {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.order-card {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
.card-title-bar {
|
||||
width: 8rpx;
|
||||
height: 32rpx;
|
||||
background: #52C41A;
|
||||
border-radius: 4rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.info-item,
|
||||
.price-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.price-item.total {
|
||||
padding-top: 32rpx;
|
||||
justify-content: flex-end !important;
|
||||
// border-top: 1rpx solid #F0F0F0;
|
||||
margin-top: 16rpx;
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.total-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.currency {
|
||||
font-size: 22rpx;
|
||||
color: #52C41A;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
|
||||
.amount {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #52C41A;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #fff;
|
||||
padding: 24rpx;
|
||||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||
z-index: 100;
|
||||
|
||||
.pay-btn {
|
||||
width: 100%;
|
||||
padding: 20rpx 0;
|
||||
// height: 96rpx;
|
||||
background: linear-gradient(135deg, #52C41A 0%, #73D13D 100%);
|
||||
border-radius: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
box-shadow: 0 8rpx 24rpx rgba(82, 196, 26, 0.3);
|
||||
|
||||
.currency-small {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
margin-right: 4rpx;
|
||||
// text-align: ;
|
||||
}
|
||||
|
||||
.amount-large {
|
||||
font-size: 52rpx;
|
||||
font-weight: 700;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.pay-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.9;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,441 @@
|
||||
<template>
|
||||
<view class="success-container">
|
||||
<!-- 支付成功状态 -->
|
||||
<view class="status-card">
|
||||
<view class="status-icon success"></view>
|
||||
<view class="status-text">{{ $t('success.returnSuccess') }}</view>
|
||||
<view class="status-desc">{{ $t('success.returnSuccessDesc') }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单信息 -->
|
||||
<view class="order-card">
|
||||
<view class="card-title">{{ $t('success.orderInfo') }}</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('order.orderNo') }}</text>
|
||||
<text class="value">{{ orderInfo.orderNo || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('order.deviceNo') }}</text>
|
||||
<text class="value">{{ orderInfo.deviceNo || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('success.usedTime') }}</text>
|
||||
<text class="value">{{ orderInfo.usedTime || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('success.packageTime') }}</text>
|
||||
<text class="value">{{ orderInfo.packageTime || '1' + $t('time.hour') }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('success.extraTime') }}</text>
|
||||
<text class="value">{{ orderInfo.extraTime || '0' + $t('time.minute') }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('success.returnTime') }}</text>
|
||||
<text class="value">{{ orderInfo.endTime || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 费用信息 -->
|
||||
<view class="refund-card">
|
||||
<view class="card-title">{{ $t('payment.feeInfo') }}</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('success.packageFee') }}</text>
|
||||
<text class="value">¥{{ orderInfo.packagePrice || '0.00' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('success.extraFee') }}</text>
|
||||
<text class="value">¥{{ orderInfo.extraFee || '0.00' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('success.totalFee') }}</text>
|
||||
<text class="value">¥{{ orderInfo.currentFee || '0.00' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('success.depositAmount') }}</text>
|
||||
<text class="value">¥{{ orderInfo.deposit || '99.00' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('success.refundAmount') }}</text>
|
||||
<text class="value highlight">¥{{ orderInfo.refundAmount || '99.00' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('success.refundStatus') }}</text>
|
||||
<text class="value" :class="orderInfo.withdrawStatus || 'waiting'">{{ getWithdrawStatusText() }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 退款说明卡片 -->
|
||||
<view class="notice-card">
|
||||
<view class="card-title">{{ $t('success.refundNotice') }}</view>
|
||||
<view class="notice-content">
|
||||
<view>1. {{ $t('success.refundNotice1') }}</view>
|
||||
<view>2. {{ $t('success.refundNotice2') }}</view>
|
||||
<view>3. {{ $t('success.refundNotice3') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="button-group">
|
||||
<button class="primary-btn" @click="handleWithdraw" v-if="!orderInfo.isWithdrawn && orderInfo.refundAmount > 0">{{ $t('success.applyRefund') }}</button>
|
||||
<button class="primary-btn" @click="goToHome">{{ $t('success.backToHome') }}</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { queryById } from '@/config/api/order.js'
|
||||
import { withdrawDeposit } from '@/config/api/user.js'
|
||||
import {
|
||||
URL
|
||||
}from"@/config/url.js"
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
orderId: '',
|
||||
orderInfo: {
|
||||
orderNo: '',
|
||||
deviceNo: '',
|
||||
usedTime: '',
|
||||
currentFee: '0.00',
|
||||
deposit: '99.00',
|
||||
refundAmount: '99.00',
|
||||
endTime: '',
|
||||
withdrawStatus: 'waiting',
|
||||
isWithdrawn: false
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
// 设置页面标题
|
||||
uni.setNavigationBarTitle({
|
||||
title: this.$t('success.returnSuccess')
|
||||
})
|
||||
|
||||
if (options && options.orderId) {
|
||||
this.orderId = options.orderId;
|
||||
this.loadOrderInfo();
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: this.$t('order.orderIdRequired'),
|
||||
icon: 'none'
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.goToHome();
|
||||
}, 1500);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 获取退款状态文本
|
||||
getWithdrawStatusText() {
|
||||
const statusMap = {
|
||||
'waiting': this.$t('success.refundWaiting'),
|
||||
'processing': this.$t('success.refundProcessing'),
|
||||
'success': this.$t('success.refundSuccess'),
|
||||
'failed': this.$t('success.refundFailed')
|
||||
};
|
||||
return statusMap[this.orderInfo.withdrawStatus] || this.$t('success.refundWaiting');
|
||||
},
|
||||
|
||||
// 加载订单信息
|
||||
async loadOrderInfo() {
|
||||
try {
|
||||
uni.showLoading({ title: this.$t('common.loading') });
|
||||
|
||||
const result = await queryById(this.orderId);
|
||||
if (result.code === 200 && result.data) {
|
||||
const orderData = result.data;
|
||||
|
||||
// 从remark字段中解析使用信息
|
||||
let packageMinutes = 60;
|
||||
let extraMinutes = 0;
|
||||
let usedMinutes = 0;
|
||||
let packagePrice = '0.00';
|
||||
let extraFee = '0.00';
|
||||
|
||||
if (orderData.remark) {
|
||||
try {
|
||||
// 解析remark字段
|
||||
const remarkInfo = orderData.remark;
|
||||
|
||||
// 尝试提取使用时长
|
||||
const usedTimeMatch = remarkInfo.match(/使用时长:(\d+)分钟/);
|
||||
if (usedTimeMatch && usedTimeMatch[1]) {
|
||||
usedMinutes = parseInt(usedTimeMatch[1]);
|
||||
}
|
||||
|
||||
// 尝试提取套餐时长
|
||||
const packageTimeMatch = remarkInfo.match(/套餐时长:(\d+)分钟/);
|
||||
if (packageTimeMatch && packageTimeMatch[1]) {
|
||||
packageMinutes = parseInt(packageTimeMatch[1]);
|
||||
}
|
||||
|
||||
// 尝试提取超出时长
|
||||
const extraTimeMatch = remarkInfo.match(/超出时长:(\d+)分钟/);
|
||||
if (extraTimeMatch && extraTimeMatch[1]) {
|
||||
extraMinutes = parseInt(extraTimeMatch[1]);
|
||||
}
|
||||
|
||||
// 尝试提取套餐费用
|
||||
const packagePriceMatch = remarkInfo.match(/套餐费用:([\d.]+)元/);
|
||||
if (packagePriceMatch && packagePriceMatch[1]) {
|
||||
packagePrice = packagePriceMatch[1];
|
||||
}
|
||||
|
||||
// 尝试提取超时费用
|
||||
const extraFeeMatch = remarkInfo.match(/超时费用:([\d.]+)元/);
|
||||
if (extraFeeMatch && extraFeeMatch[1]) {
|
||||
extraFee = extraFeeMatch[1];
|
||||
}
|
||||
|
||||
console.log('从remark解析到的信息:', {
|
||||
usedMinutes,
|
||||
packageMinutes,
|
||||
extraMinutes,
|
||||
packagePrice,
|
||||
extraFee
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('解析remark字段失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
this.orderInfo = {
|
||||
orderNo: orderData.orderNo || '',
|
||||
deviceNo: orderData.deviceNo || '',
|
||||
usedTime: usedMinutes + '分钟',
|
||||
packageTime: packageMinutes + '分钟',
|
||||
extraTime: extraMinutes + '分钟',
|
||||
packagePrice: packagePrice,
|
||||
extraFee: extraFee,
|
||||
currentFee: orderData.actualDeviceAmount || '0.00',
|
||||
deposit: orderData.depositAmount || '99.00',
|
||||
refundAmount: orderData.residueAmount || '99.00',
|
||||
endTime: orderData.endTime || '',
|
||||
withdrawStatus: orderData.withdrawStatus || 'waiting',
|
||||
isWithdrawn: orderData.withdrawStatus === 'success'
|
||||
};
|
||||
} else {
|
||||
throw new Error(result.msg || '获取订单信息失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载订单信息错误:', error);
|
||||
uni.showToast({
|
||||
title: error.message || this.$t('order.getOrderFailed'),
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
// 申请退款
|
||||
async handleWithdraw() {
|
||||
try {
|
||||
uni.showLoading({ title: this.$t('common.processing') });
|
||||
|
||||
const res = await withdrawDeposit(this.orderInfo.orderNo)
|
||||
|
||||
if (res && res.code === 200) {
|
||||
uni.showToast({
|
||||
title: this.$t('order.refundSuccess'),
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 更新状态
|
||||
this.orderInfo.withdrawStatus = 'processing';
|
||||
this.orderInfo.isWithdrawn = true;
|
||||
|
||||
// 刷新订单信息
|
||||
setTimeout(() => {
|
||||
this.loadOrderInfo();
|
||||
}, 1500);
|
||||
} else {
|
||||
throw new Error(res?.msg || this.$t('order.refundFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('退款申请错误:', error);
|
||||
uni.showToast({
|
||||
title: error.message || this.$t('order.refundFailed'),
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
// 返回首页
|
||||
goToHome() {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.success-container {
|
||||
padding: 20px;
|
||||
background-color: #f8f8f8;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
|
||||
.status-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 0 auto 16px;
|
||||
|
||||
&.success {
|
||||
background-color: #07c160;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 30px;
|
||||
height: 20px;
|
||||
border: 3px solid #fff;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
transform-origin: center;
|
||||
transform: translate(-50%, -70%) rotate(-45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #07c160;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.status-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.order-card, .refund-card {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
|
||||
&.highlight {
|
||||
color: #ff6b00;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: #07c160;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-top: 40rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20rpx;
|
||||
|
||||
.primary-btn, .secondary-btn {
|
||||
width: 50%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
background: #07c160;
|
||||
color: #fff;
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notice-card {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.notice-content {
|
||||
text-align: left;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.waiting {
|
||||
color: #ffaa00;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,399 @@
|
||||
<template>
|
||||
<view class="success-container">
|
||||
<!-- 支付成功状态和订单信息 -->
|
||||
<view class="status-order-card">
|
||||
<!-- 支付成功状态 -->
|
||||
<view class="status-section">
|
||||
<view class="status-icon success"></view>
|
||||
<view class="status-text">{{ $t('success.paymentSuccess') }}</view>
|
||||
<view class="status-desc">{{ $t('success.paymentSuccessDesc') }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<view class="section-divider"></view>
|
||||
|
||||
<!-- 订单信息 -->
|
||||
<view class="order-section">
|
||||
<view class="card-title">{{ $t('success.orderInfo') }}</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('order.orderNo') }}</text>
|
||||
<text class="value">{{ orderInfo.orderNo || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('order.deviceNo') }}</text>
|
||||
<text class="value">{{ orderInfo.deviceNo || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('success.paymentAmount') }}</text>
|
||||
<text class="value">¥{{ orderInfo.amount || '0.00' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('success.paymentTime') }}</text>
|
||||
<text class="value">{{ orderInfo.payTime || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 设备状态 -->
|
||||
<view class="device-status">
|
||||
<view class="status-message">{{ deviceMessage }}</view>
|
||||
<view class="loading-animation" v-if="isLoading">
|
||||
<view class="loading-circle"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="button-group">
|
||||
<view class="secondary-btn" @click="goToHome">{{ $t('success.backToHome') }}</view>
|
||||
<view class="primary-btn" @click="goToOrderList">{{ $t('success.viewOrder') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
getCurrentInstance
|
||||
} from 'vue'
|
||||
import {
|
||||
onLoad
|
||||
} from '@dcloudio/uni-app'
|
||||
import {
|
||||
queryById,
|
||||
getOrderByOrderNo
|
||||
} from '@/config/api/order.js'
|
||||
|
||||
// 获取当前实例以访问 $t 方法
|
||||
const {
|
||||
proxy
|
||||
} = getCurrentInstance()
|
||||
|
||||
// 响应式数据
|
||||
const orderId = ref('')
|
||||
const orderInfo = ref({})
|
||||
const isLoading = ref(true)
|
||||
const deviceMessage = ref('')
|
||||
const hasTriggeredDevice = ref(false)
|
||||
|
||||
// 页面加载
|
||||
onLoad((options) => {
|
||||
// 设置页面标题
|
||||
uni.setNavigationBarTitle({
|
||||
title: proxy.$t('success.paymentSuccess')
|
||||
})
|
||||
|
||||
deviceMessage.value = proxy.$t('success.preparingDevice')
|
||||
|
||||
// #ifdef H5
|
||||
if (uni.getStorageSync('pendingPaymentNo')) {
|
||||
orderId.value = options.orderId
|
||||
loadOrderInfo()
|
||||
|
||||
// 添加页面显示监听,防止页面切换后重复触发弹出
|
||||
uni.$once('orderSuccess:' + orderId.value, () => {
|
||||
console.log('已经触发过弹出逻辑,不再重复触发')
|
||||
hasTriggeredDevice.value = true
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: proxy.$t('order.orderNotExist'),
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
goToHome()
|
||||
}, 1500)
|
||||
}
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
if (options && options.orderId) {
|
||||
orderId.value = options.orderId
|
||||
loadOrderInfo()
|
||||
|
||||
// 添加页面显示监听,防止页面切换后重复触发弹出
|
||||
uni.$once('orderSuccess:' + orderId.value, () => {
|
||||
console.log('已经触发过弹出逻辑,不再重复触发')
|
||||
hasTriggeredDevice.value = true
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: proxy.$t('order.orderNotExist'),
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
goToHome()
|
||||
}, 1500)
|
||||
}
|
||||
// #endif
|
||||
})
|
||||
|
||||
// 加载订单信息
|
||||
const loadOrderInfo = async () => {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: proxy.$t('common.loading')
|
||||
})
|
||||
|
||||
const res = await queryById(orderId.value)
|
||||
if (res.code === 200 && res.data) {
|
||||
const orderData = res.data
|
||||
orderInfo.value = {
|
||||
orderNo: orderData.orderNo || orderData.orderId,
|
||||
deviceNo: orderData.deviceNo,
|
||||
amount: orderData.payAmount || orderData.amount,
|
||||
payTime: orderData.payTime || formatTime(new Date())
|
||||
}
|
||||
|
||||
// 检查订单状态
|
||||
if (orderData.orderStatus === 'IN_USED') {
|
||||
// 如果已经是使用中状态,可能说明开锁已经完成
|
||||
deviceMessage.value = '设备已弹出,请取走您的风扇'
|
||||
isLoading.value = false
|
||||
|
||||
// 如果是第一次加载页面且设备已弹出,记录状态,避免重复弹出
|
||||
if (!hasTriggeredDevice.value) {
|
||||
uni.$emit('orderSuccess:' + orderId.value)
|
||||
hasTriggeredDevice.value = true
|
||||
}
|
||||
} else {
|
||||
// 在此页面不再触发设备弹出操作,仅更新展示文案和加载状态
|
||||
deviceMessage.value = proxy.$t('success.paymentSuccessDesc')
|
||||
isLoading.value = false
|
||||
}
|
||||
} else {
|
||||
throw new Error('获取订单信息失败')
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: error.message || proxy.$t('order.getOrderFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
const hour = date.getHours().toString().padStart(2, '0')
|
||||
const minute = date.getMinutes().toString().padStart(2, '0')
|
||||
const second = date.getSeconds().toString().padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
|
||||
}
|
||||
|
||||
// 返回首页
|
||||
const goToHome = () => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
|
||||
// 查看订单列表
|
||||
const goToOrderList = () => {
|
||||
uni.redirectTo({
|
||||
url: '/pages/order/index'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.success-container {
|
||||
padding: 20px;
|
||||
padding-bottom: 180rpx;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.status-order-card {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
|
||||
.status-section {
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
|
||||
.status-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 0 auto 16px;
|
||||
background-color: #07c160;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 30px;
|
||||
height: 20px;
|
||||
border: 3px solid #fff;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
transform-origin: center;
|
||||
transform: translate(-50%, -70%) rotate(-45deg);
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #07c160;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.status-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
height: 1px;
|
||||
background-color: #f0f0f0;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.order-section {
|
||||
padding: 20px;
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #333;
|
||||
padding-left: 12px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background-color: #07c160;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.device-status {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
|
||||
.status-message {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.loading-animation {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
|
||||
.loading-circle {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid #f0f0f0;
|
||||
border-top-color: #07c160;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 20rpx 30rpx;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
|
||||
.primary-btn {
|
||||
background-color: #07c160;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 32rpx;
|
||||
padding: 0 32rpx;
|
||||
font-size: 24rpx;
|
||||
height: 64rpx;
|
||||
line-height: 64rpx;
|
||||
white-space: nowrap;
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
background-color: #fff;
|
||||
color: #07c160;
|
||||
border: 2rpx solid #07c160;
|
||||
border-radius: 32rpx;
|
||||
padding: 0 32rpx;
|
||||
font-size: 24rpx;
|
||||
height: 64rpx;
|
||||
line-height: 64rpx;
|
||||
white-space: nowrap;
|
||||
|
||||
&:active {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,323 @@
|
||||
<template>
|
||||
<view class="deposit-container">
|
||||
<!-- 押金金额卡片 -->
|
||||
<view class="deposit-card">
|
||||
<view class="title">{{ $t('deposit.depositBalance') }}</view>
|
||||
<view class="amount">¥{{ depositAmount }}</view>
|
||||
<button class="withdraw-btn" @click="handleWithdraw" :disabled="depositAmount <= 0">{{ $t('deposit.withdraw') }}</button>
|
||||
</view>
|
||||
|
||||
<!-- 提现说明 -->
|
||||
<view class="notice-card">
|
||||
<view class="notice-title">
|
||||
<view class="dot"></view>
|
||||
<text>{{ $t('deposit.withdrawNotice') }}</text>
|
||||
</view>
|
||||
<view class="notice-content">
|
||||
<view class="notice-item">1. {{ $t('deposit.withdrawNotice1') }}</view>
|
||||
<view class="notice-item">2. {{ $t('deposit.withdrawNotice2') }}</view>
|
||||
<view class="notice-item">3. {{ $t('deposit.withdrawNotice3') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 押金记录 -->
|
||||
<view class="record-card" v-if="records.length > 0">
|
||||
<view class="record-title">{{ $t('deposit.depositRecord') }}</view>
|
||||
<view class="record-list">
|
||||
<view class="record-item" v-for="(item, index) in records" :key="index">
|
||||
<view class="record-info">
|
||||
<text class="record-type">{{ item.typeText }}</text>
|
||||
<text class="record-time">{{ item.time }}</text>
|
||||
</view>
|
||||
<text class="record-amount" :class="item.type === 'refund' ? 'refund' : ''">
|
||||
{{ item.type === 'refund' ? '+' : '-' }}¥{{ item.amount }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { getUserInfo } from '@/util/index.js'
|
||||
import { withdrawDeposit } from '@/config/api/user.js'
|
||||
import { queryById } from '@/config/api/order.js'
|
||||
import { useI18n } from '@/utils/i18n.js'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const depositAmount = ref('0.00')
|
||||
const orderNo = ref('')
|
||||
const orderId = ref('')
|
||||
const records = ref([])
|
||||
|
||||
onMounted(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: t('deposit.title')
|
||||
})
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
loadUserInfo()
|
||||
})
|
||||
|
||||
const loadUserInfo = async () => {
|
||||
try {
|
||||
const res = await getUserInfo()
|
||||
if (res.code === 200) {
|
||||
depositAmount.value = res.data.balanceAmount || '0.00'
|
||||
orderNo.value = res.data.latestOrderNo || ''
|
||||
orderId.value = res.data.latestOrderId || ''
|
||||
|
||||
// 如果存在余额,获取押金记录
|
||||
if (parseFloat(depositAmount.value) > 0 && orderNo.value) {
|
||||
records.value = [
|
||||
{
|
||||
type: 'pay',
|
||||
typeText: t('deposit.payRecord'),
|
||||
time: formatDate(new Date()),
|
||||
amount: depositAmount.value
|
||||
}
|
||||
]
|
||||
} else {
|
||||
records.value = []
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: t('user.getUserInfoFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleWithdraw = async () => {
|
||||
if (parseFloat(depositAmount.value) <= 0) {
|
||||
uni.showToast({
|
||||
title: t('deposit.noBalance'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: t('deposit.confirmWithdraw'),
|
||||
content: t('deposit.withdrawDesc'),
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({
|
||||
title: t('deposit.withdrawing')
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await withdrawDeposit(orderNo.value)
|
||||
|
||||
if (result.code === 200) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: t('deposit.withdrawSubmitted'),
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 更新记录
|
||||
records.value.push({
|
||||
type: 'refund',
|
||||
typeText: t('deposit.refundRecord'),
|
||||
time: formatDate(new Date()),
|
||||
amount: depositAmount.value
|
||||
})
|
||||
// 更新余额为0
|
||||
depositAmount.value = '0.00'
|
||||
|
||||
// 重新加载用户信息
|
||||
setTimeout(() => {
|
||||
loadUserInfo()
|
||||
}, 1500)
|
||||
} else {
|
||||
throw new Error(result.msg || t('deposit.withdrawFailed'))
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
|
||||
// 更详细的错误处理
|
||||
let errorMessage = t('deposit.withdrawFailed');
|
||||
|
||||
if (error.message) {
|
||||
if (error.message.includes('尚未归还')) {
|
||||
errorMessage = t('deposit.orderNotReturned');
|
||||
} else if (error.message.includes('已退还')) {
|
||||
errorMessage = t('deposit.alreadyRefunded');
|
||||
} else if (error.message.includes('处理中')) {
|
||||
errorMessage = t('deposit.refundProcessing');
|
||||
} else if (error.message.includes('余额为0')) {
|
||||
errorMessage = t('deposit.noBalance');
|
||||
} else {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: t('deposit.withdrawFailed'),
|
||||
content: errorMessage,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const formatDate = (date) => {
|
||||
const year = date.getFullYear()
|
||||
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 `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.deposit-container {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
padding: 30rpx;
|
||||
|
||||
.deposit-card {
|
||||
background: linear-gradient(135deg, #1976D2, #64B5F6);
|
||||
border-radius: 20rpx;
|
||||
padding: 40rpx;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
box-shadow: 0 4rpx 20rpx rgba(25,118,210,0.2);
|
||||
|
||||
.title {
|
||||
font-size: 28rpx;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.amount {
|
||||
font-size: 72rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.withdraw-btn {
|
||||
background: #fff;
|
||||
color: #1976D2;
|
||||
width: 80%;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
margin: 0 auto;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
background: rgba(255,255,255,0.6);
|
||||
color: rgba(25,118,210,0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notice-card {
|
||||
margin-top: 30rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
|
||||
|
||||
.notice-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
background: #1976D2;
|
||||
border-radius: 50%;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
text {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-content {
|
||||
.notice-item {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
padding-left: 22rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.record-card {
|
||||
margin-top: 30rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
|
||||
|
||||
.record-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
border-left: 8rpx solid #1976D2;
|
||||
padding-left: 20rpx;
|
||||
}
|
||||
|
||||
.record-list {
|
||||
.record-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.record-info {
|
||||
.record-type {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 6rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.record-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.record-amount {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
|
||||
&.refund {
|
||||
color: #4CAF50;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<view class="join-page">
|
||||
<!-- 使用 web-view 嵌入外部网页 -->
|
||||
<web-view :src="webUrl" @message="handleMessage" @error="handleError"></web-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
onMounted
|
||||
} from 'vue'
|
||||
import { useI18n } from '@/utils/i18n.js'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 外部网页地址
|
||||
const webUrl = ref('https://joininvestment.gxfs123.com/')
|
||||
|
||||
// 处理来自 web-view 的消息(如果外部网页有通过 postMessage 发送消息)
|
||||
const handleMessage = (e) => {
|
||||
console.log('收到来自 web-view 的消息:', e)
|
||||
// 可以根据消息内容进行相应处理
|
||||
}
|
||||
|
||||
// 处理 web-view 加载错误
|
||||
const handleError = (e) => {
|
||||
console.error('web-view 加载错误:', e)
|
||||
uni.showToast({
|
||||
title: t('join.pageLoadFailed'),
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: t('join.title')
|
||||
})
|
||||
console.log('招商页面加载,外部网址:', webUrl.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.join-page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<view class="legal-page">
|
||||
<view class="header">
|
||||
<view class="title">User Agreement</view>
|
||||
<view class="subtitle">Applicable to this service (Last updated: {{ effectiveDate }})</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="content" scroll-y>
|
||||
<view class="h1">I. Introduction</view>
|
||||
<view class="p">Welcome to {{ brandName }} shared fan products and related services. This User Agreement (hereinafter referred to as "this Agreement") is entered into between you and {{ companyName }} ("we") regarding your use of {{ brandName }} mini program and shared fan rental services.</view>
|
||||
<view class="p">Before using {{ brandName }}, please carefully read and fully understand all contents of this Agreement, especially the terms highlighted in bold (including but not limited to liability limitations, dispute resolution, applicable law, protection of minors, etc.). By clicking "Login/Use" or actually using the service, you are deemed to have read and agreed to be bound by this Agreement.</view>
|
||||
|
||||
<view class="h1">II. Account and Login</view>
|
||||
<view class="p">2.1 You can log in and use this service through WeChat authorization. To complete deposit-free rental and order settlement, you agree that we conduct credit assessment and post-order settlement based on WeChat Payment Score.</view>
|
||||
<view class="p">2.2 You should ensure that the information provided is true, accurate, and complete, and update it in a timely manner. Any service restrictions, order abnormalities, or losses caused by untrue information or failure to update in time shall be borne by you.</view>
|
||||
<view class="p">2.3 You shall be responsible for all activities under the account, properly keep the device and account credentials, and shall not lend, rent, or otherwise provide them to others.</view>
|
||||
|
||||
<view class="h1">III. Rental and Usage Specifications</view>
|
||||
<view class="p">3.1 Rental process: Initiate rental in {{ brandName }} mini program → Pick up the fan at the device → Return it at the return point according to the instructions or through "Express Return" after use.</view>
|
||||
<view class="p">3.2 Usage specifications: Please use the device properly, avoid water ingress, dropping, unauthorized disassembly or modification; do not approach open flames and high-temperature environments; avoid outdoor use in rainy days; children should use under supervision.</view>
|
||||
<view class="p">3.3 Prohibited behaviors: Using the device for illegal or improper purposes; affecting the normal operation of the device or system in any way; circumventing billing or return processes through abnormal means.</view>
|
||||
|
||||
<view class="h1">IV. Billing and Settlement (Including WeChat Payment Score)</view>
|
||||
<view class="p"><text class="bold">4.1 Billing rules</text>: Subject to the real-time billing rules displayed in the mini program, which may include duration billing, capped prices, service fees, etc. Charges will be based on this after order generation.</view>
|
||||
<view class="p"><text class="bold">4.2 WeChat Payment Score deposit-free</text>: If you activate and pass the credit assessment, you can enjoy deposit-free rental; if the assessment fails, pre-authorization or deposit may be required. Please refer to the page prompts for details.</view>
|
||||
<view class="p">4.3 Settlement and deduction: After the order ends, we will complete the settlement based on actual usage and platform rules, and deduct through WeChat Payment Score/WeChat Pay.</view>
|
||||
<view class="p">4.4 Exceptions and disputes: If you have any objection to billing or settlement, please submit it through "My-Customer Service" within 48 hours after order completion; overdue submissions may affect processing results.</view>
|
||||
|
||||
<view class="h1">V. Device Return and Overdue Handling</view>
|
||||
<view class="p">5.1 Return method: Return at designated outlets according to mini program instructions, or send back through the "Express Return" function. Non-designated methods may lead to order abnormalities and additional fees.</view>
|
||||
<view class="p">5.2 Overdue handling: If not returned within the specified time, the system will continue billing or charge overdue fees according to rules. Long-term failure to return may result in compensation handling according to the agreement.</view>
|
||||
<view class="p">5.3 Acceptance and order completion: After return, the platform will conduct integrity inspection. The order can only be completed after inspection and fee settlement.</view>
|
||||
|
||||
<view class="h1">VI. Violation, Damage and Compensation</view>
|
||||
<view class="p">6.1 Device damage and loss: If device damage or loss is caused by improper use, intentional damage, or failure to keep in accordance with specifications, you need to bear compensation liability according to the platform's published standards or actual repair/depreciation costs.</view>
|
||||
<view class="p">6.2 Cleaning and parts: Additional costs caused by human contamination, missing accessories, etc., will be charged to you based on actual costs.</view>
|
||||
<view class="p">6.3 Risk control: In case of suspected malicious arrears, fraud, etc., the platform may take measures such as freezing services, pursuing compensation, and protecting rights according to law.</view>
|
||||
|
||||
<view class="h1">VII. User Behavior Standards</view>
|
||||
<view class="p">7.1 You promise to abide by laws, regulations and public order and good customs, not to publish or spread illegal or improper content, and not to interfere with or disrupt the normal operation of the platform and devices.</view>
|
||||
<view class="p">7.2 You shall not conduct reverse engineering, scraping, or unauthorized automated access to this mini program.</view>
|
||||
|
||||
<view class="h1">VIII. Intellectual Property</view>
|
||||
<view class="p">8.1 {{ companyName }} and its affiliates enjoy corresponding intellectual property rights or legal authorization for trademarks, logos, interfaces, texts, images, codes, etc. in this mini program and services.</view>
|
||||
<view class="p">8.2 Without written permission, no one may use, copy, disseminate or adapt the above content in any way.</view>
|
||||
|
||||
<view class="h1">IX. Disclaimer and Limitation of Liability</view>
|
||||
<view class="p"><text class="bold">9.1 Due to force majeure, network failures, third-party service stability and other reasons leading to service interruption or restriction, {{ companyName }} shall not be liable within the scope permitted by law, but will try to restore services.</text></view>
|
||||
<view class="p">9.2 You should be responsible for your own use. Any losses caused by your violation of this Agreement or improper storage and use of equipment shall be borne by you or compensated to relevant parties.</view>
|
||||
|
||||
<view class="h1">X. Privacy and Personal Information Protection</view>
|
||||
<view class="p">10.1 We strictly handle your personal information in accordance with the Privacy Policy, including WeChat login information, mobile phone number (obtained after your authorization), device and order information, location and outlet information, etc.</view>
|
||||
<view class="p">10.2 For details, please refer to the Privacy Policy in this mini program.</view>
|
||||
|
||||
<view class="h1">XI. Service Changes and Termination</view>
|
||||
<view class="p">11.1 We may make changes or terminate service content, functions or rules based on business adjustments, legal compliance or user experience optimization. Important changes will be notified through mini program announcements or in-app messages.</view>
|
||||
<view class="p">11.2 If you do not agree with the changes, you can stop using and apply for cancellation of relevant accounts/information (subject to laws, regulations and account settlement restrictions).</view>
|
||||
|
||||
<view class="h1">XII. Protection of Minors</view>
|
||||
<view class="p">12.1 Guardians of minors should guide them to correctly understand and abide by this Agreement. Minors should use the service under supervision and avoid using equipment in dangerous environments.</view>
|
||||
|
||||
<view class="h1">XIII. Notices and Contact</view>
|
||||
<view class="p">13.1 Contact method: Please contact us through "Support" in the mini program, and we will handle your questions or disputes as soon as possible.</view>
|
||||
|
||||
<view class="h1">XIV. Applicable Law and Dispute Resolution</view>
|
||||
<view class="p">14.1 The conclusion, effectiveness, performance, interpretation and dispute resolution of this Agreement shall be governed by the laws of the People's Republic of China (excluding conflict of laws rules).</view>
|
||||
<view class="p">14.2 Disputes arising from this Agreement shall be resolved through friendly negotiation first; if negotiation fails, they shall be submitted to the people's court with jurisdiction in {{ disputeVenue }} for litigation settlement.</view>
|
||||
|
||||
<view class="h1">XV. Supplementary Provisions</view>
|
||||
<view class="p">15.1 This Agreement shall take effect from {{ effectiveDate }} and remain valid for a long term, unless we publish another version update.</view>
|
||||
<view class="p">15.2 If any provision of the Agreement is deemed invalid or unenforceable, it shall not affect the validity and enforcement of other provisions.</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="footer">If you have any questions about this Agreement, please contact our customer service.</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
onMounted(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: 'User Agreement'
|
||||
})
|
||||
})
|
||||
|
||||
const brandName = 'WindPower'
|
||||
const companyName = 'Shenzhen Lemu Zhiyun Technology Co., Ltd.'
|
||||
const effectiveDate = '2025-10-13'
|
||||
const disputeVenue = 'the location of the platform'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.legal-page {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
padding: 24rpx 24rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
margin-bottom: 16rpx;
|
||||
.title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
.h1 { font-size: 30rpx; font-weight: 600; color: #222; margin: 18rpx 0 12rpx; }
|
||||
.p { font-size: 26rpx; color: #444; line-height: 1.8; margin-bottom: 10rpx; }
|
||||
.bold { font-weight: 600; color: #222; }
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<view class="legal-page">
|
||||
<view class="header">
|
||||
<view class="title">用户协议</view>
|
||||
<view class="subtitle">适用于本服务(最后更新:{{ effectiveDate }})</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="content" scroll-y>
|
||||
<view class="h1">一、导言</view>
|
||||
<view class="p">欢迎您使用{{ brandName }}共享风扇产品与相关服务。本《用户协议》(下称"本协议")由您与{{ companyName }}("我们")就您使用{{ brandName }}小程序及租借共享风扇服务所订立。</view>
|
||||
<view class="p">在使用{{ brandName }}前,请您务必仔细阅读并充分理解本协议全部内容,尤其是以加粗方式提示的条款(包括但不限于责任限制、争议解决、适用法律、未成年人保护等)。您点击"登录/使用"或实际使用服务即视为您已阅读并同意受本协议约束。</view>
|
||||
|
||||
<view class="h1">二、账号与登录</view>
|
||||
<view class="p">2.1 您可通过微信授权登录使用本服务。为完成免押租借与订单结算,您同意我们基于微信支付分进行信用评估及订单后结等必要处理。</view>
|
||||
<view class="p">2.2 您应保证提供信息真实、准确、完整,并及时更新。因您提供的信息不真实或未及时更新导致的服务受限、订单异常或损失,由您自行承担。</view>
|
||||
<view class="p">2.3 您应对账户下的全部行为负责,妥善保管设备与账户凭证,不得转借、出租或以其他方式提供给他人使用。</view>
|
||||
|
||||
<view class="h1">三、租借与使用规范</view>
|
||||
<view class="p">3.1 租借流程:在{{ brandName }}小程序中发起租借 → 在设备端取用风扇 → 使用完毕后按指引在归还点归还或通过"快递归还"。</view>
|
||||
<view class="p">3.2 使用规范:请合理使用设备,避免进水、摔落、私自拆卸或改装;请勿靠近明火与高温环境;室外雨天请避免使用;儿童应在监护下使用。</view>
|
||||
<view class="p">3.3 禁止行为:将设备用于违法或不当用途;以任何方式影响设备或系统的正常运行;通过非正常手段规避计费或归还流程。</view>
|
||||
|
||||
<view class="h1">四、计费与结算(含微信支付分)</view>
|
||||
<view class="p"><text class="bold">4.1 计费规则</text>:以小程序展示的实时计费规则为准,可能包含时长计费、封顶价、服务费等。订单生成后将据此计费。</view>
|
||||
<view class="p"><text class="bold">4.2 微信支付分免押</text>:若您开通并通过信用评估,可享受免押租借;如评估未通过,可能需预授权或押金。具体以页面提示为准。</view>
|
||||
<view class="p">4.3 结算与扣款:订单结束后,我们将基于实际使用情况与平台规则完成结算并通过微信支付分/微信支付进行扣款。</view>
|
||||
<view class="p">4.4 异常与争议:如对计费或结算有异议,请在订单完成后48小时内通过"我的-客服"提交;逾期可能影响处理结果。</view>
|
||||
|
||||
<view class="h1">五、设备归还与逾期处理</view>
|
||||
<view class="p">5.1 归还方式:按照小程序指引在指定网点归还,或通过"快递归还"功能寄回。非指定方式可能导致订单异常与额外费用。</view>
|
||||
<view class="p">5.2 逾期处理:未在规定时间内归还的,系统将持续计费或按规则收取逾期费用。长时间未归还的,可能依约进行赔偿处理。</view>
|
||||
<view class="p">5.3 验收与结单:归还后平台将进行完好性验收,验收完成且费用结清后,订单方可完结。</view>
|
||||
|
||||
<view class="h1">六、违规、损坏与赔偿</view>
|
||||
<view class="p">6.1 设备损坏丢失:若因不当使用、故意破坏或未按规范保管导致设备损坏、丢失,您需按平台公示标准或实际维修/折损成本承担赔偿责任。</view>
|
||||
<view class="p">6.2 清洁与部件:因人为污损、缺失附件等造成的额外成本,将据实向您收取。</view>
|
||||
<view class="p">6.3 风险控制:如出现涉嫌恶意拖欠、欺诈等,平台可采取冻结服务、追偿、依法维权等措施。</view>
|
||||
|
||||
<view class="h1">七、用户行为规范</view>
|
||||
<view class="p">7.1 您承诺遵守法律法规与公序良俗,不发表、不传播违法违规或不当内容,不干扰或破坏平台与设备的正常运行。</view>
|
||||
<view class="p">7.2 您不得对本小程序进行反向工程、抓取或未经授权的自动化访问。</view>
|
||||
|
||||
<view class="h1">八、知识产权</view>
|
||||
<view class="p">8.1 {{ companyName }}及关联方对本小程序与服务中的商标、标识、界面、文字、图片、代码等享有相应知识产权或合法授权。</view>
|
||||
<view class="p">8.2 未经书面许可,任何人不得以任何方式使用、复制、传播或改作上述内容。</view>
|
||||
|
||||
<view class="h1">九、免责声明与责任限制</view>
|
||||
<view class="p"><text class="bold">9.1 由于不可抗力、网络故障、第三方服务稳定性等原因导致的服务中断或受限,{{ companyName }}在法律允许范围内不承担责任,但将尽力恢复服务。</text></view>
|
||||
<view class="p">9.2 您应对自身使用行为负责。因您违反本协议或不当保管使用设备造成的损失,由您自行承担或向相关方赔偿。</view>
|
||||
|
||||
<view class="h1">十、隐私与个人信息保护</view>
|
||||
<view class="p">10.1 我们严格按照《隐私政策》处理您的个人信息,包括微信登录信息、手机号(经您授权后获取)、设备与订单信息、位置与网点信息等。</view>
|
||||
<view class="p">10.2 详情请查阅本小程序内的《隐私政策》。</view>
|
||||
|
||||
<view class="h1">十一、服务变更与终止</view>
|
||||
<view class="p">11.1 我们可能基于业务调整、法律合规或用户体验优化,对服务内容、功能或规则进行变更或终止。重要变更将通过小程序公告或站内消息提示。</view>
|
||||
<view class="p">11.2 如您不同意变更,可停止使用并申请注销相关账户/信息(受法律法规与账务结算限制)。</view>
|
||||
|
||||
<view class="h1">十二、未成年人保护</view>
|
||||
<view class="p">12.1 未成年人的监护人应指导其正确理解并遵守本协议。未成年人使用服务应在监护下进行,避免在危险环境中使用设备。</view>
|
||||
|
||||
<view class="h1">十三、通知与联系</view>
|
||||
<view class="p">13.1 联系方式:请通过小程序"我的-客服"与我们联系,我们将尽快处理您的问题或争议。</view>
|
||||
|
||||
<view class="h1">十四、法律适用与争议解决</view>
|
||||
<view class="p">14.1 本协议的订立、生效、履行、解释与争议解决,适用中华人民共和国法律(不含冲突规范)。</view>
|
||||
<view class="p">14.2 因本协议产生的争议,优先友好协商;协商不成的,提交{{ disputeVenue }}有管辖权的人民法院诉讼解决。</view>
|
||||
|
||||
<view class="h1">十五、附则</view>
|
||||
<view class="p">15.1 本协议自{{ effectiveDate }}起生效并长期有效,除非我们另行发布版本更新。</view>
|
||||
<view class="p">15.2 协议条款如被认定无效或不可执行,不影响其他条款的效力与执行。</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="footer">如您对本协议有任何疑问,请联系我们的客服。</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
onMounted(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: '用户协议'
|
||||
})
|
||||
})
|
||||
|
||||
const brandName = '风电者'
|
||||
const companyName = '深圳乐慕智云科技有限公司'
|
||||
const effectiveDate = '2025-10-13'
|
||||
const disputeVenue = '平台所在地'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.legal-page {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
padding: 24rpx 24rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
margin-bottom: 16rpx;
|
||||
.title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
.h1 { font-size: 30rpx; font-weight: 600; color: #222; margin: 18rpx 0 12rpx; }
|
||||
.p { font-size: 26rpx; color: #444; line-height: 1.8; margin-bottom: 10rpx; }
|
||||
.bold { font-weight: 600; color: #222; }
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<!-- 加载状态 -->
|
||||
<view v-if="loading" class="loading-page">
|
||||
<view class="loading-content">
|
||||
<view class="loading-spinner"></view>
|
||||
<text>{{ $t('common.loading') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<view v-else-if="error" class="error-page">
|
||||
<view class="error-content">
|
||||
<text class="error-icon">⚠️</text>
|
||||
<text class="error-message">{{ errorMessage }}</text>
|
||||
<button class="retry-btn" @click="loadAgreement">{{ $t('common.retry') }}</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 内容显示 -->
|
||||
<view v-else class="legal-page">
|
||||
<view class="header">
|
||||
<text class="title">{{ agreementData.title || $t('legal.agreement') }}</text>
|
||||
<text v-if="agreementData.remark" class="subtitle">{{ agreementData.remark }}</text>
|
||||
</view>
|
||||
|
||||
<scroll-view class="content" scroll-y>
|
||||
<rich-text :nodes="agreementData.content"></rich-text>
|
||||
</scroll-view>
|
||||
|
||||
<view class="footer">
|
||||
<text>{{ $t('legal.footerNotice') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useI18n } from '@/utils/i18n.js'
|
||||
import { getCurrentAgreement } from '@/config/api/system.js'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(true)
|
||||
const error = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const agreementData = ref({
|
||||
title: '',
|
||||
content: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 加载协议内容
|
||||
const loadAgreement = async () => {
|
||||
loading.value = true
|
||||
error.value = false
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
console.log('加载用户协议')
|
||||
|
||||
// 调用接口获取协议内容
|
||||
const res = await getCurrentAgreement({
|
||||
agreementCode: 'USER_AGREEMENT',
|
||||
appPlatform: 'wechat',
|
||||
appType: 'user'
|
||||
})
|
||||
|
||||
console.log('用户协议响应:', res)
|
||||
|
||||
if (res && res.code === 200 && res.data) {
|
||||
agreementData.value = {
|
||||
title: res.data.title || t('legal.agreement'),
|
||||
content: res.data.content || '',
|
||||
remark: res.data.remark || ''
|
||||
}
|
||||
|
||||
// 设置页面标题
|
||||
uni.setNavigationBarTitle({
|
||||
title: agreementData.value.title
|
||||
})
|
||||
} else {
|
||||
throw new Error(res?.msg || t('common.loadFailed'))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载用户协议失败:', err)
|
||||
error.value = true
|
||||
errorMessage.value = err.message || t('common.loadFailed')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadAgreement()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.loading-page {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border: 4rpx solid #f0f0f0;
|
||||
border-top: 4rpx solid #07c160;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error-page {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
|
||||
.error-icon {
|
||||
font-size: 80rpx;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
margin-top: 20rpx;
|
||||
padding: 20rpx 60rpx;
|
||||
background: #07c160;
|
||||
color: #fff;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.legal-page {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
padding: 24rpx 24rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<view class="legal-page">
|
||||
<view class="header">
|
||||
<view class="title">Privacy Policy</view>
|
||||
<view class="subtitle">Applicable to this service (Last updated: {{ effectiveDate }})</view>
|
||||
</view>
|
||||
|
||||
<view class="card notice">
|
||||
<view class="p">We are well aware of the importance of personal information to you and will do our best to protect your personal information security. Please carefully read and understand this Privacy Policy before using {{ brandName }} services.</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="content" scroll-y>
|
||||
<view class="h1">I. Scope of Application</view>
|
||||
<view class="p">
|
||||
This policy is formulated and published by {{ companyName }} and applies to {{ brandName }} mini program and its shared fan rental services. "We" in this text specifically refers to {{ companyName }}, and we will process your personal information in accordance with the principles of legality, legitimacy, and necessity.
|
||||
</view>
|
||||
|
||||
<view class="h1">II. Information We Collect</view>
|
||||
<view class="p">2.1 Account information: WeChat login identifier (such as openId/unionId), nickname and avatar (with your authorization), mobile phone number (obtained through WeChat after your authorization).</view>
|
||||
<view class="p">2.2 Order and device information: rental records, usage duration, fees, return points, device status, abnormal records, etc.</view>
|
||||
<view class="p">2.3 Location and outlet information: used to find nearby outlets and navigation after your authorization, and will not be obtained without authorization.</view>
|
||||
<view class="p">2.4 Log information: To ensure service security and stability, we may record operation logs, network requests, and error information.</view>
|
||||
|
||||
<view class="h1">III. Purpose of Information Use</view>
|
||||
<view class="p">3.1 Provide core functions: identity verification, deposit-free rental (WeChat Payment Score assessment), order billing and settlement, customer service and after-sales.</view>
|
||||
<view class="p">3.2 Security risk control: prevent fraud, violations and risk control; ensure system and device security.</view>
|
||||
<view class="p">3.3 Product optimization: statistics and analysis to improve experience (conducted after de-identification/anonymization).</view>
|
||||
|
||||
<view class="h1">IV. WeChat Payment Score and Payment</view>
|
||||
<view class="p">4.1 To implement deposit-free rental, we will conduct necessary data interaction with WeChat Payment Score (such as credit assessment results and order settlement). Related data processing follows the rules of WeChat Pay and WeChat Payment Score.</view>
|
||||
<view class="p">4.2 If you fail the assessment, pre-authorization or deposit processing may be required, subject to page prompts.</view>
|
||||
|
||||
<view class="h1">V. Sharing, Transfer and Public Disclosure</view>
|
||||
<view class="p">5.1 We will not sell your personal information to third parties.</view>
|
||||
<view class="p">5.2 When realizing necessary functions, we may share necessary information with partners (such as payment and logistics service providers), and require them to protect your information according to standards not lower than this policy.</view>
|
||||
<view class="p">5.3 In case of transfer due to merger, division, reorganization or bankruptcy liquidation, we will require the new holder to continue to be bound by this policy, otherwise we will obtain your consent again.</view>
|
||||
<view class="p">5.4 Only in circumstances such as laws and regulations or regulatory requirements, litigation dispute handling, protection of personal and property safety, etc., may disclosure be made in accordance with the law.</view>
|
||||
|
||||
<view class="h1">VI. Information Storage and Security</view>
|
||||
<view class="p">6.1 Storage location: Your personal information is in principle stored within the territory of the People's Republic of China. If cross-border transmission is required, we will comply with laws and regulations and obtain your consent.</view>
|
||||
<view class="p">6.2 Storage period: the shortest period necessary to achieve the purpose. After expiration, it will be deleted or anonymized, except as otherwise provided by laws and regulations.</view>
|
||||
<view class="p">6.3 Security measures: We adopt measures such as encrypted transmission, access control, minimized authorization, monitoring and auditing to protect the security of your information.</view>
|
||||
|
||||
<view class="h1">VII. Your Rights</view>
|
||||
<view class="p">7.1 Access and correction: You can access or correct some information through "My-Personal Information/Support".</view>
|
||||
<view class="p">7.2 Deletion and withdrawal of consent: When meeting legal requirements and necessary conditions such as account settlement and dispute handling, you can apply for deletion or withdrawal of authorization; some functions may not be available after withdrawal.</view>
|
||||
<view class="p">7.3 Account cancellation: After meeting the conditions and completing fee settlement, device return, and dispute handling, you can apply for account cancellation.</view>
|
||||
|
||||
<view class="h1">VIII. Protection of Minors</view>
|
||||
<view class="p">8.1 Minors should use the service under supervision. We will not knowingly collect unnecessary personal information of minors.</view>
|
||||
|
||||
<view class="h1">IX. Policy Updates and Notifications</view>
|
||||
<view class="p">9.1 We may update this policy due to function iteration and changes in laws and regulations. Important changes will be notified through mini program announcements or in-app messages. Continued use after update is deemed as your consent.</view>
|
||||
|
||||
<view class="h1">X. Contact Us</view>
|
||||
<view class="p">10.1 You can contact us through "My-Customer Service" to exercise the aforementioned rights or raise questions about this policy.</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="footer">If you have any questions about this policy, please contact our customer service.</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
onMounted(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: 'Privacy Policy'
|
||||
})
|
||||
})
|
||||
|
||||
const brandName = 'WindPower'
|
||||
const companyName = 'Shenzhen Lemu Zhiyun Technology Co., Ltd.'
|
||||
const effectiveDate = '2025-10-13'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.legal-page {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
padding: 24rpx 24rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.notice {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.h1 {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin: 18rpx 0 12rpx;
|
||||
}
|
||||
|
||||
.p {
|
||||
font-size: 26rpx;
|
||||
color: #444;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<view class="legal-page">
|
||||
<view class="header">
|
||||
<view class="title">隐私政策</view>
|
||||
<view class="subtitle">适用于本服务(最后更新:{{ effectiveDate }})</view>
|
||||
</view>
|
||||
|
||||
<view class="card notice">
|
||||
<view class="p">我们深知个人信息对您的重要性,并会尽全力保护您的个人信息安全。请您在使用{{ brandName }}服务前,仔细阅读并理解本《隐私政策》。</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="content" scroll-y>
|
||||
<view class="h1">一、适用范围</view>
|
||||
<view class="p">
|
||||
本政策由{{ companyName }}制定并发布,适用于{{ brandName }}小程序及其提供的共享风扇租借服务。"我们"在本文特指{{ companyName }},并将按照合法、正当、必要的原则处理您的个人信息。
|
||||
</view>
|
||||
|
||||
<view class="h1">二、我们收集的信息</view>
|
||||
<view class="p">2.1 账号信息:微信登录标识(如openId/unionId)、昵称头像(经您授权)、手机号(经您授权后通过微信获取)。</view>
|
||||
<view class="p">2.2 订单与设备信息:租借记录、使用时长、费用、归还点位、设备状态、异常记录等。</view>
|
||||
<view class="p">2.3 位置与网点信息:在您授权后用于查找附近网点与导航,不会在未授权情况下获取。</view>
|
||||
<view class="p">2.4 日志信息:为保障服务安全与稳定,我们可能记录操作日志、网络请求与错误信息。</view>
|
||||
|
||||
<view class="h1">三、信息使用目的</view>
|
||||
<view class="p">3.1 提供核心功能:身份验证、免押租借(微信支付分评估)、订单计费结算、客服与售后。</view>
|
||||
<view class="p">3.2 安全风控:防范欺诈、违规与风险控制;保障系统与设备安全。</view>
|
||||
<view class="p">3.3 产品优化:统计与分析以改进体验(在去标识化/匿名化后进行)。</view>
|
||||
|
||||
<view class="h1">四、微信支付分与支付</view>
|
||||
<view class="p">4.1 为实现免押租借,我们将与微信支付分进行必要的数据交互(如信用评估结果、订单结算)。相关数据处理遵循微信支付与微信支付分的规则。</view>
|
||||
<view class="p">4.2 如您未通过评估,可能需进行预授权或押金处理,以页面提示为准。</view>
|
||||
|
||||
<view class="h1">五、共享、转移与公开披露</view>
|
||||
<view class="p">5.1 我们不会向第三方出售您的个人信息。</view>
|
||||
<view class="p">5.2 在实现必要功能时,我们可能与合作方共享必要信息(如支付与物流服务商),并要求其按不低于本政策的标准保护您的信息。</view>
|
||||
<view class="p">5.3 因合并、分立、重组或破产清算导致的转移,我们将要求新持有方继续受本政策约束,否则将重新征得您的同意。</view>
|
||||
<view class="p">5.4 仅在法律法规或监管要求、诉讼争议处理、保护人身财产安全等情形下,可能依法进行披露。</view>
|
||||
|
||||
<view class="h1">六、信息存储与安全</view>
|
||||
<view class="p">6.1 存储地点:您的个人信息原则上存储于中华人民共和国境内。如需跨境传输,将遵循法律法规并征得您的同意。</view>
|
||||
<view class="p">6.2 存储期限:为实现目的所必需的最短期限,超期将删除或匿名化处理,法律法规另有规定的除外。</view>
|
||||
<view class="p">6.3 安全措施:我们采取加密传输、访问控制、最小化授权、监控审计等措施保护您的信息安全。</view>
|
||||
|
||||
<view class="h1">七、您的权利</view>
|
||||
<view class="p">7.1 访问与更正:您可通过"我的-个人信息/客服"访问或更正部分信息。</view>
|
||||
<view class="p">7.2 删除与撤回同意:在符合法律与账务结算、纠纷处理等必要条件时,您可申请删除或撤回授权;撤回后部分功能可能无法提供。</view>
|
||||
<view class="p">7.3 账号注销:在符合条件并完成费用结清、设备归还、争议处理后,您可申请注销账号。</view>
|
||||
|
||||
<view class="h1">八、未成年人保护</view>
|
||||
<view class="p">8.1 未成年人使用服务应在监护下进行。我们不会在明知的情况下收集未成年人不必要的个人信息。</view>
|
||||
|
||||
<view class="h1">九、政策更新与通知</view>
|
||||
<view class="p">9.1 我们可能因功能迭代、法律监管变化而更新本政策。重要变更将通过小程序公告或站内消息提示,更新后继续使用即视为您同意。</view>
|
||||
|
||||
<view class="h1">十、联系我们</view>
|
||||
<view class="p">10.1 您可通过"我的-客服"与我们联系以行使前述权利或就本政策提出疑问。</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="footer">如您对本政策有任何疑问,请联系我们的客服。</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
onMounted(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: '隐私政策'
|
||||
})
|
||||
})
|
||||
|
||||
const brandName = '风电者'
|
||||
const companyName = '深圳乐慕智云科技有限公司'
|
||||
const effectiveDate = '2025-10-13'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.legal-page {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
padding: 24rpx 24rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.notice {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.h1 {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin: 18rpx 0 12rpx;
|
||||
}
|
||||
|
||||
.p {
|
||||
font-size: 26rpx;
|
||||
color: #444;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<!-- 加载状态 -->
|
||||
<view v-if="loading" class="loading-page">
|
||||
<view class="loading-content">
|
||||
<view class="loading-spinner"></view>
|
||||
<text>{{ $t('common.loading') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<view v-else-if="error" class="error-page">
|
||||
<view class="error-content">
|
||||
<text class="error-icon">⚠️</text>
|
||||
<text class="error-message">{{ errorMessage }}</text>
|
||||
<button class="retry-btn" @click="loadAgreement">{{ $t('common.retry') }}</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 内容显示 -->
|
||||
<view v-else class="legal-page">
|
||||
<view class="header">
|
||||
<text class="title">{{ agreementData.title || $t('legal.privacy') }}</text>
|
||||
<text v-if="agreementData.remark" class="subtitle">{{ agreementData.remark }}</text>
|
||||
</view>
|
||||
|
||||
<scroll-view class="content" scroll-y>
|
||||
<rich-text :nodes="agreementData.content"></rich-text>
|
||||
</scroll-view>
|
||||
|
||||
<view class="footer">
|
||||
<text>{{ $t('legal.footerNoticePolicy') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useI18n } from '@/utils/i18n.js'
|
||||
import { getCurrentAgreement } from '@/config/api/system.js'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(true)
|
||||
const error = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const agreementData = ref({
|
||||
title: '',
|
||||
content: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 加载协议内容
|
||||
const loadAgreement = async () => {
|
||||
loading.value = true
|
||||
error.value = false
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
console.log('加载隐私政策')
|
||||
|
||||
// 调用接口获取协议内容
|
||||
const res = await getCurrentAgreement({
|
||||
agreementCode: 'PRIVACY_POLICY',
|
||||
appPlatform: 'wechat',
|
||||
appType: 'user'
|
||||
})
|
||||
|
||||
console.log('隐私政策响应:', res)
|
||||
|
||||
if (res && res.code === 200 && res.data) {
|
||||
agreementData.value = {
|
||||
title: res.data.title || t('legal.privacy'),
|
||||
content: res.data.content || '',
|
||||
remark: res.data.remark || ''
|
||||
}
|
||||
|
||||
// 设置页面标题
|
||||
uni.setNavigationBarTitle({
|
||||
title: agreementData.value.title
|
||||
})
|
||||
} else {
|
||||
throw new Error(res?.msg || t('common.loadFailed'))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载隐私政策失败:', err)
|
||||
error.value = true
|
||||
errorMessage.value = err.message || t('common.loadFailed')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadAgreement()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.loading-page {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border: 4rpx solid #f0f0f0;
|
||||
border-top: 4rpx solid #07c160;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error-page {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
|
||||
.error-icon {
|
||||
font-size: 80rpx;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
margin-top: 20rpx;
|
||||
padding: 20rpx 60rpx;
|
||||
background: #07c160;
|
||||
color: #fff;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.legal-page {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
padding: 24rpx 24rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<view class="terms-page">
|
||||
<view class="content">
|
||||
<view class="title">{{ $t('legal.termsAndConditions') }}</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title">{{ $t('legal.applicableLaw') }}</view>
|
||||
<view class="section-content">
|
||||
<text>{{ $t('legal.applicableLawContent') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title">{{ $t('legal.paymentMethods') }}</view>
|
||||
<view class="section-content">
|
||||
<text>{{ $t('legal.paymentMethodsContent') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title">{{ $t('legal.refundPolicy') }}</view>
|
||||
<view class="section-content">
|
||||
<text>{{ $t('legal.refundPolicyContent') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title">{{ $t('legal.serviceTerms') }}</view>
|
||||
<view class="section-content">
|
||||
<text>{{ $t('legal.serviceTermsContent') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title">{{ $t('legal.liabilityLimitation') }}</view>
|
||||
<view class="section-content">
|
||||
<text>{{ $t('legal.liabilityLimitationContent') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title">{{ $t('legal.disputeResolution') }}</view>
|
||||
<view class="section-content">
|
||||
<text>{{ $t('legal.disputeResolutionContent') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="footer">
|
||||
<text class="update-time">{{ $t('legal.lastUpdate') }}: 2025-02-05</text>
|
||||
<text class="notice">{{ $t('legal.footerNotice') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useI18n } from '@/utils/i18n.js'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
onMounted(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: t('legal.termsAndConditions')
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.terms-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f6f6f6;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: #ffffff;
|
||||
padding: 40rpx 30rpx;
|
||||
margin: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 20rpx;
|
||||
padding-left: 20rpx;
|
||||
border-left: 6rpx solid #07c160;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
line-height: 1.8;
|
||||
text-align: justify;
|
||||
padding: 20rpx;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 60rpx;
|
||||
padding-top: 30rpx;
|
||||
border-top: 1rpx solid #e5e5e5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.update-time {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.notice {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #666666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
wxLogin,
|
||||
} from '@/util/index'
|
||||
|
||||
import {
|
||||
getMyIndexInfo
|
||||
} from "@/config/api/user.js";
|
||||
import {
|
||||
queryHasOrder,
|
||||
checkOrdersByStatus
|
||||
} from "@/config/api/order.js";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
async onLoad(option) {
|
||||
console.log('bagCheck onLoad option:', option);
|
||||
|
||||
// 设置页面标题
|
||||
uni.setNavigationBarTitle({
|
||||
title: this.$t('device.checking')
|
||||
})
|
||||
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: this.$t('common.processing'),
|
||||
mask: true
|
||||
});
|
||||
|
||||
// 检查是否传入设备编号
|
||||
if (!option || !option.deviceNo) {
|
||||
throw new Error(this.$t('device.deviceNoNotRecognized'));
|
||||
}
|
||||
|
||||
const deviceNo = option.deviceNo;
|
||||
|
||||
// 检查用户是否有进行中(in_used)或待支付(waiting_for_payment)的订单
|
||||
const statusesToCheck = ['in_used', 'waiting_for_payment'];
|
||||
const res = await checkOrdersByStatus(deviceNo, statusesToCheck);
|
||||
|
||||
if (res.code === 200 && res.data && res.data.length > 0) {
|
||||
// 找到相关订单,取最新的一条
|
||||
const latestOrder = res.data[0]; // 假设返回结果按时间倒序
|
||||
|
||||
if (latestOrder.orderStatus === 'in_used') {
|
||||
// 如果是使用中的订单,跳转到归还页面
|
||||
console.log('检测到使用中订单,跳转归还页:', latestOrder.orderId);
|
||||
uni.redirectTo({
|
||||
url: `/pages/device/return?orderId=${latestOrder.orderId}`
|
||||
});
|
||||
} else if (latestOrder.orderStatus === 'waiting_for_payment') {
|
||||
// 如果是待支付订单,跳转到支付页面,并传递必要信息
|
||||
console.log('检测到待支付订单,跳转支付页:', latestOrder.orderId);
|
||||
|
||||
// 获取套餐时间(分钟)
|
||||
const packageTimeMinutes = latestOrder.packageTime || 60;
|
||||
|
||||
// 套餐小时数
|
||||
const packageTimeHours = (packageTimeMinutes / 60).toFixed(1);
|
||||
|
||||
// 套餐价格
|
||||
const packagePrice = latestOrder.packagePrice || '0.00';
|
||||
|
||||
// 计算每小时费率
|
||||
const hourlyRate = (parseFloat(packagePrice) / (packageTimeMinutes / 60)).toFixed(2);
|
||||
|
||||
// 押金金额
|
||||
const depositAmount = latestOrder.depositAmount || '99.00';
|
||||
|
||||
// 计算总金额(套餐+押金)
|
||||
const totalAmount = (parseFloat(depositAmount) + parseFloat(packagePrice)).toFixed(2);
|
||||
|
||||
uni.redirectTo({
|
||||
url: `/pages/order/payment?orderId=${latestOrder.orderId}&packageTimeHours=${packageTimeHours}&packagePrice=${packagePrice}&hourlyRate=${hourlyRate}&totalAmount=${totalAmount}&depositAmount=${depositAmount}`
|
||||
});
|
||||
} else {
|
||||
// 其他状态(理论上不应该到这里,除非statusesToCheck配置错误),默认到详情页
|
||||
console.log('检测到其他状态订单,跳转详情页:', latestOrder.orderId);
|
||||
uni.redirectTo({
|
||||
url: `/pages/device/detail?deviceNo=${deviceNo}`
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 没有找到相关状态的订单,跳转到设备详情页进行租借
|
||||
console.log('未检测到相关订单,跳转详情页');
|
||||
uni.redirectTo({
|
||||
url: `/pages/device/detail?deviceNo=${deviceNo}`
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// 只处理真正的错误,不是"没有订单"的情况
|
||||
if (error.message && (
|
||||
error.message.includes('未识别到设备编号') ||
|
||||
error.message.includes('网络请求失败') ||
|
||||
error.message.includes('服务器错误')
|
||||
) ) {
|
||||
console.error('扫码检查订单失败:', error);
|
||||
uni.showToast({
|
||||
title: error.message || this.$t('device.processFailed'),
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
} else {
|
||||
// 对于其他错误,包括"没有找到订单",直接跳转到详情页,不显示错误消息
|
||||
console.log('没有找到符合条件的订单或发生其他错误,直接跳转详情页');
|
||||
}
|
||||
|
||||
// 无论什么情况,都跳转到一个可用页面
|
||||
setTimeout(() => {
|
||||
if (option && option.deviceNo) {
|
||||
uni.redirectTo({
|
||||
url: `/pages/device/detail?deviceNo=${option.deviceNo}`
|
||||
});
|
||||
} else {
|
||||
// uni.switchTab({
|
||||
// url:'/pages/index/index'
|
||||
// })
|
||||
// 如果连deviceNo都没有,则返回首页
|
||||
uni.reLaunch({ url: '/pages/index/index' });
|
||||
}
|
||||
}, 2000);
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<view class="webview-container">
|
||||
<!-- web-view 组件用于显示外部网页 -->
|
||||
<web-view :src="webUrl" @message="handleMessage" @load="handleLoad" @error="handleError"></web-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useI18n } from '@/utils/i18n.js'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 网页链接
|
||||
const webUrl = ref('')
|
||||
|
||||
// 页面加载时获取传入的 URL 参数
|
||||
onLoad((options) => {
|
||||
if (options.url) {
|
||||
// 解码 URL
|
||||
webUrl.value = decodeURIComponent(options.url)
|
||||
console.log('加载外部链接:', webUrl.value)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: t('common.invalidUrl') || '无效的链接',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
// 2秒后返回上一页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
|
||||
// 网页加载完成
|
||||
const handleLoad = (e) => {
|
||||
console.log('网页加载完成:', e)
|
||||
}
|
||||
|
||||
// 网页加载错误
|
||||
const handleError = (e) => {
|
||||
console.error('网页加载错误:', e)
|
||||
uni.showToast({
|
||||
title: t('common.loadFailed') || '加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
// 接收网页消息(web-view 向小程序发送消息)
|
||||
const handleMessage = (e) => {
|
||||
console.log('收到网页消息:', e)
|
||||
// 可以根据需要处理网页发来的消息
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
// 支持分享
|
||||
onShareAppMessage() {
|
||||
const $t = this.$t || ((key) => key)
|
||||
return {
|
||||
title: $t('share.title') || '分享',
|
||||
path: '/pages/index/index'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.webview-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,378 @@
|
||||
<template>
|
||||
<view class="express-form-container">
|
||||
<!-- 订单摘要卡片 -->
|
||||
<view class="order-summary-card" v-if="orderInfo.orderNo">
|
||||
<view class="summary-row">
|
||||
<view class="label">{{ $t('order.orderNo') }}</view>
|
||||
<view class="value">{{ orderInfo.orderNo }}</view>
|
||||
</view>
|
||||
<view class="summary-row" v-if="orderInfo.deviceNo">
|
||||
<view class="label">{{ $t('order.deviceNo') }}</view>
|
||||
<view class="value">{{ orderInfo.deviceNo }}</view>
|
||||
</view>
|
||||
<view class="summary-row" v-if="orderInfo.startTime">
|
||||
<view class="label">{{ $t('express.openTime') }}</view>
|
||||
<view class="value">{{ orderInfo.startTime }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view class="form-section">
|
||||
<view class="form-title">{{ $t('express.fillExpressInfo') }}</view>
|
||||
|
||||
<view class="input-wrapper">
|
||||
<view class="input-label">{{ $t('express.contactPhone') }}</view>
|
||||
<input class="input-field" type="number" v-model="phone" maxlength="20" />
|
||||
</view>
|
||||
|
||||
<view class="input-wrapper">
|
||||
<view class="input-label">{{ $t('express.expressNo') }}</view>
|
||||
<input class="input-field" type="text" v-model="trackingNumber"
|
||||
:placeholder="isFillMode ? $t('express.fillTrackingPlaceholder') : $t('express.trackingPlaceholder')" maxlength="40" />
|
||||
</view>
|
||||
|
||||
<view class="tips" v-if="tipsText">{{ tipsText }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-btn" :class="{ disabled: submitting }" @click="!submitting && handleSubmit()">
|
||||
{{ isFillMode ? $t('express.confirmFill') : $t('express.submitInfo') }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
reactive,
|
||||
getCurrentInstance,
|
||||
onMounted
|
||||
} from 'vue'
|
||||
import {
|
||||
onLoad
|
||||
} from '@dcloudio/uni-app'
|
||||
import {
|
||||
queryById
|
||||
} from '@/config/api/order.js'
|
||||
import {
|
||||
applyExpressReturn,
|
||||
getExpressReturnByOrder,
|
||||
getExpressReturnDetail,
|
||||
fillExpressTrackingNumber
|
||||
} from '@/config/api/expressReturn.js'
|
||||
import { useI18n } from '@/utils/i18n.js'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
onMounted(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: t('express.fillExpress')
|
||||
})
|
||||
})
|
||||
|
||||
const orderId = ref('')
|
||||
const recordId = ref('')
|
||||
const isFillMode = ref(false)
|
||||
const orderInfo = reactive({
|
||||
orderNo: '',
|
||||
deviceNo: '',
|
||||
startTime: ''
|
||||
})
|
||||
|
||||
const phone = ref('')
|
||||
const trackingNumber = ref('')
|
||||
const tipsText = ref('')
|
||||
const submitting = ref(false)
|
||||
|
||||
onLoad(async (options) => {
|
||||
orderId.value = options?.orderId || ''
|
||||
recordId.value = options?.id || ''
|
||||
isFillMode.value = !!recordId.value
|
||||
|
||||
if (isFillMode.value) {
|
||||
await loadRecordAndOrderByRecord()
|
||||
return
|
||||
}
|
||||
|
||||
if (!orderId.value) {
|
||||
uni.showToast({
|
||||
title: t('express.orderNoMissing'),
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1200)
|
||||
return
|
||||
}
|
||||
await loadOrder()
|
||||
await checkExistingExpressReturn()
|
||||
})
|
||||
|
||||
const loadOrder = async () => {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: t('common.loading')
|
||||
})
|
||||
const res = await queryById(orderId.value)
|
||||
if (res?.code === 200 && res.data) {
|
||||
orderInfo.orderNo = res.data.orderNo || res.data.orderId || ''
|
||||
orderInfo.deviceNo = res.data.deviceNo || ''
|
||||
orderInfo.startTime = res.data.startTime || res.data.createTime || ''
|
||||
// 默认联系电话可回填订单上的手机号(若有)
|
||||
if (res.data.phone && !phone.value) phone.value = res.data.phone
|
||||
} else {
|
||||
throw new Error(res?.msg || t('order.getOrderFailed'))
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({
|
||||
title: e.message || t('express.loadFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
const loadRecordAndOrderByRecord = async () => {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: t('common.loading')
|
||||
})
|
||||
const res = await getExpressReturnDetail(recordId.value)
|
||||
if (res?.code === 200 && res.data) {
|
||||
if (res.data.orderId) {
|
||||
orderId.value = res.data.orderId
|
||||
await loadOrder()
|
||||
}
|
||||
if (res.data.userPhone && !phone.value) phone.value = res.data.userPhone
|
||||
} else {
|
||||
throw new Error(res?.msg || t('express.getRecordFailed'))
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({
|
||||
title: e.message || t('express.loadFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
const checkExistingExpressReturn = async () => {
|
||||
try {
|
||||
const res = await getExpressReturnByOrder(orderId.value)
|
||||
if (res && res.code === 200 && res.data) {
|
||||
const rec = res.data
|
||||
if (rec.status === 0) {
|
||||
recordId.value = rec.id
|
||||
uni.showModal({
|
||||
title: t('common.tips'),
|
||||
content: t('express.existingReturnNotice'),
|
||||
confirmText: t('express.goToFill'),
|
||||
cancelText: t('common.cancel'),
|
||||
success: (r) => {
|
||||
if (r.confirm) {
|
||||
uni.redirectTo({
|
||||
url: `/pages/expressReturn/addExpressReturn?id=${rec.id}`
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: t('express.alreadyHasRecord'),
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: `/pages/expressReturn/detail?id=${rec.id}`
|
||||
})
|
||||
}, 800)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 无记录则忽略
|
||||
}
|
||||
}
|
||||
|
||||
const validate = () => {
|
||||
// 简单手机检查(兼容座机/国际号,放宽到至少 5 位数字)
|
||||
const digits = (phone.value || '').replace(/\D/g, '')
|
||||
if (!digits || digits.length < 5) {
|
||||
uni.showToast({
|
||||
title: t('express.pleaseEnterValidPhone'),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
if (isFillMode.value && !trackingNumber.value) {
|
||||
uni.showToast({
|
||||
title: t('express.pleaseEnterTrackingNo'),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!validate() || submitting.value) return
|
||||
submitting.value = true
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: isFillMode.value ? t('common.filling') : t('common.submitting')
|
||||
})
|
||||
let res
|
||||
if (isFillMode.value) {
|
||||
res = await fillExpressTrackingNumber({
|
||||
id: Number(recordId.value),
|
||||
logisticsTrackingNumber: trackingNumber.value
|
||||
})
|
||||
} else {
|
||||
res = await applyExpressReturn({
|
||||
orderId: orderId.value,
|
||||
logisticsTrackingNumber: trackingNumber.value,
|
||||
remark: ''
|
||||
})
|
||||
}
|
||||
if (res && res.code === 200) {
|
||||
uni.showToast({
|
||||
title: isFillMode.value ? t('express.fillSuccess') : t('express.submitSuccess'),
|
||||
icon: 'success'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 800)
|
||||
} else {
|
||||
throw new Error(res?.msg || (isFillMode.value ? t('express.fillFailed') : t('express.submitFailed')))
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({
|
||||
title: e.message || (isFillMode.value ? t('express.fillFailed') : t('express.submitFailed')),
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
submitting.value = false
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.express-form-container {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 40rpx 32rpx 180rpx;
|
||||
}
|
||||
|
||||
/* 订单摘要卡片 - 淡绿色背景 */
|
||||
.order-summary-card {
|
||||
background: linear-gradient(135deg, #d4f4dd 0%, #e8f8ed 100%);
|
||||
border-radius: 24rpx;
|
||||
padding: 40rpx 32rpx;
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.summary-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 表单区域 */
|
||||
.form-section {
|
||||
background: transparent;
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.form-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
.input-label {
|
||||
position: absolute;
|
||||
top: -14rpx;
|
||||
left: 32rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
background: #ffffff;
|
||||
padding: 0 12rpx;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
height: 100rpx;
|
||||
background: #ffffff;
|
||||
border: 2rpx solid #333333;
|
||||
border-radius: 48rpx;
|
||||
padding: 0 32rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
line-height: 100rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input-field::placeholder {
|
||||
color: #cccccc;
|
||||
}
|
||||
}
|
||||
|
||||
.tips {
|
||||
margin-top: 16rpx;
|
||||
padding: 0 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
/* 提交按钮 */
|
||||
.submit-btn {
|
||||
position: fixed;
|
||||
left: 32rpx;
|
||||
right: 32rpx;
|
||||
bottom: 60rpx;
|
||||
bottom: calc(60rpx + env(safe-area-inset-bottom));
|
||||
height: 96rpx;
|
||||
background: linear-gradient(135deg, #7cd89f 0%, #6bc98c 100%);
|
||||
border-radius: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 8rpx 24rpx rgba(124, 216, 159, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 4rpx 16rpx rgba(124, 216, 159, 0.2);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,404 @@
|
||||
<template>
|
||||
<view class="express-detail-container">
|
||||
<!-- 状态卡片 -->
|
||||
<view class="status-card">
|
||||
<view class="status-header">
|
||||
<view class="status-icon" :class="getStatusClass(detailData.status)">
|
||||
<text class="icon-text">{{ getStatusIcon(detailData.status) }}</text>
|
||||
</view>
|
||||
<view class="status-info">
|
||||
<text class="status-title">{{ getStatusText(detailData.status) }}</text>
|
||||
<text class="status-desc">{{ getStatusDesc(detailData.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快递信息卡片 -->
|
||||
<view class="info-card">
|
||||
<view class="card-title">
|
||||
<text class="title-text">{{ $t('express.expressInfo') }}</text>
|
||||
</view>
|
||||
<view class="info-list">
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('express.expressCompany') }}</text>
|
||||
<text class="value">{{ detailData.expressCompany }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('express.trackingNo') }}</text>
|
||||
<text class="value tracking-number">{{ detailData.trackingNumber }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('express.packageType') }}</text>
|
||||
<text class="value">{{ detailData.packageType }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('express.packageWeight') }}</text>
|
||||
<text class="value">{{ detailData.weight }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 归还信息卡片 -->
|
||||
<view class="info-card">
|
||||
<view class="card-title">
|
||||
<text class="title-text">{{ $t('express.returnInfo') }}</text>
|
||||
</view>
|
||||
<view class="info-list">
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('express.returnAddress') }}</text>
|
||||
<text class="value address">{{ detailData.returnAddress }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('express.returnTime') }}</text>
|
||||
<text class="value">{{ detailData.returnTime }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('express.processTime') }}</text>
|
||||
<text class="value">{{ detailData.processTime || '--' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('express.completeTime') }}</text>
|
||||
<text class="value">{{ detailData.completeTime || '--' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 备注信息卡片 -->
|
||||
<view class="info-card" v-if="detailData.remark">
|
||||
<view class="card-title">
|
||||
<text class="title-text">{{ $t('express.remarkInfo') }}</text>
|
||||
</view>
|
||||
<view class="remark-content">
|
||||
<text class="remark-text">{{ detailData.remark }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-buttons">
|
||||
<button class="action-btn primary" @click="handleCopyTracking">
|
||||
<text class="btn-text">{{ $t('express.copyTrackingNo') }}</text>
|
||||
</button>
|
||||
<button class="action-btn secondary" @click="handleContactService">
|
||||
<text class="btn-text">{{ $t('user.customerService') }}</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getExpressReturnDetail } from '@/config/api/expressReturn.js'
|
||||
import { getCustomerPhone } from '@/util/index.js'
|
||||
import { useI18n } from '@/utils/i18n.js'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 详情数据
|
||||
const detailData = ref({
|
||||
id: '',
|
||||
expressCompany: '-',
|
||||
trackingNumber: '-',
|
||||
returnAddress: '-',
|
||||
returnTime: '-',
|
||||
processTime: '-',
|
||||
completeTime: '-',
|
||||
packageType: '-',
|
||||
weight: '-',
|
||||
status: 'pending',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 获取状态样式类
|
||||
const getStatusClass = (status) => {
|
||||
const statusMap = {
|
||||
'completed': 'status-completed',
|
||||
'processing': 'status-processing',
|
||||
'pending': 'status-pending'
|
||||
}
|
||||
return statusMap[status] || 'status-pending'
|
||||
}
|
||||
|
||||
// 获取状态图标
|
||||
const getStatusIcon = (status) => {
|
||||
const iconMap = {
|
||||
'completed': '✓',
|
||||
'processing': '⏳',
|
||||
'pending': '⏸'
|
||||
}
|
||||
return iconMap[status] || '⏸'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
const textMap = {
|
||||
'completed': t('express.returnCompleted'),
|
||||
'processing': t('express.processing'),
|
||||
'pending': t('express.pending')
|
||||
}
|
||||
return textMap[status] || t('express.pending')
|
||||
}
|
||||
|
||||
// 获取状态描述
|
||||
const getStatusDesc = (status) => {
|
||||
const descMap = {
|
||||
'completed': t('express.returnCompletedDesc'),
|
||||
'processing': t('express.processingDesc'),
|
||||
'pending': t('express.pendingDesc')
|
||||
}
|
||||
return descMap[status] || t('express.pendingDesc')
|
||||
}
|
||||
|
||||
// 复制运单号
|
||||
const handleCopyTracking = () => {
|
||||
uni.setClipboardData({
|
||||
data: detailData.value.trackingNumber,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: t('express.trackingNoCopied'),
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 联系客服
|
||||
const handleContactService = () => {
|
||||
const customerPhone = getCustomerPhone()
|
||||
uni.showModal({
|
||||
title: t('user.customerService'),
|
||||
content: `${t('help.phone')}:${customerPhone}\n${t('help.workingHours')}:${t('express.workingHours')}`,
|
||||
confirmText: t('express.call'),
|
||||
cancelText: t('common.cancel'),
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: customerPhone
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 页面加载时获取详情数据
|
||||
onMounted(async () => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: t('express.returnDetail')
|
||||
})
|
||||
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const options = currentPage.options || {}
|
||||
if (!options.id) return
|
||||
try {
|
||||
uni.showLoading({ title: t('common.loading') })
|
||||
const res = await getExpressReturnDetail(options.id)
|
||||
if (res && res.code === 200 && res.data) {
|
||||
const r = res.data
|
||||
detailData.value = {
|
||||
id: r.id,
|
||||
expressCompany: r.expressCompany || r.company || '-',
|
||||
trackingNumber: r.logisticsTrackingNumber || r.trackingNumber || '-',
|
||||
returnAddress: r.returnAddress || r.address || '-',
|
||||
returnTime: r.createTime || r.returnTime || '-',
|
||||
processTime: r.processTime || '-',
|
||||
completeTime: r.completeTime || '-',
|
||||
packageType: r.packageType || '-',
|
||||
weight: r.weight || '-',
|
||||
status: mapStatus(r.status),
|
||||
remark: r.remark || ''
|
||||
}
|
||||
} else {
|
||||
throw new Error(res?.msg || t('express.getDetailFailed'))
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || t('express.loadFailed'), icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
})
|
||||
|
||||
// 状态映射
|
||||
const mapStatus = (status) => {
|
||||
if (status === 5) return 'completed'
|
||||
if (status === 3 || status === 1) return 'processing'
|
||||
if (status === 4 || status === 2 || status === 0) return 'pending'
|
||||
return 'pending'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.express-detail-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f8f9fa;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 40rpx 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.status-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
margin-right: 24rpx;
|
||||
|
||||
&.status-completed {
|
||||
background-color: #e8f5e8;
|
||||
color: #27ae60;
|
||||
}
|
||||
|
||||
&.status-processing {
|
||||
background-color: #fff3cd;
|
||||
color: #f39c12;
|
||||
}
|
||||
|
||||
&.status-pending {
|
||||
background-color: #f8f9fa;
|
||||
color: #6c757d;
|
||||
}
|
||||
}
|
||||
|
||||
.status-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-title {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.status-desc {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin-bottom: 24rpx;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid #f1f3f4;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.info-list {
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #f8f9fa;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #7f8c8d;
|
||||
min-width: 160rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 28rpx;
|
||||
color: #2c3e50;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
|
||||
&.tracking-number {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.address {
|
||||
text-align: right;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.remark-content {
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.remark-text {
|
||||
font-size: 28rpx;
|
||||
color: #34495e;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 40rpx;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.primary {
|
||||
background-color: #1976D2;
|
||||
color: #ffffff;
|
||||
|
||||
&:active {
|
||||
background-color: #1565C0;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background-color: #ffffff;
|
||||
color: #1976D2;
|
||||
border: 2rpx solid #1976D2;
|
||||
|
||||
&:active {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,325 @@
|
||||
<template>
|
||||
<view class="express-return-container">
|
||||
<!-- 收件信息卡片 -->
|
||||
<view class="recipient-info-card">
|
||||
<view class="info-header">{{ $t('express.recipientInfo') }}</view>
|
||||
<view class="info-content">
|
||||
<text class="recipient-name">{{ $t('express.recipientName') }}</text>
|
||||
<text class="recipient-address">{{ $t('express.recipientAddress') }}</text>
|
||||
</view>
|
||||
<view class="copy-all-btn" @click="copyAllInfo">
|
||||
<text class="btn-text">{{ $t('express.copyAllInfo') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 列表容器 -->
|
||||
<view class="list-container">
|
||||
<view class="return-item" v-for="(item, index) in returnList" :key="index" @click="handleItemClick(item)">
|
||||
<!-- 左侧图标区域 -->
|
||||
<view class="item-left">
|
||||
<view class="avatar-placeholder"></view>
|
||||
</view>
|
||||
|
||||
<!-- 中间内容区域 -->
|
||||
<view class="item-content">
|
||||
<view class="content-header">
|
||||
<text class="status-label">{{ getStatusText(item.status) }}</text>
|
||||
</view>
|
||||
<view class="content-body">
|
||||
<text class="info-text">{{ $t('order.orderNo') }}:{{ item.orderId || '' }}</text>
|
||||
<text class="info-text">{{ $t('express.expressNo') }}:{{ item.trackingNumber || $t('express.toFill') }}</text>
|
||||
<text class="info-text">{{ $t('express.userPhone') }}:{{ item.userPhone || '' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧状态标签 -->
|
||||
<view class="item-right">
|
||||
<view class="status-badge" :class="getStatusClass(item.status)">
|
||||
{{ getStatusBadge(item.status) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="returnList.length === 0">
|
||||
<view class="empty-icon">📦</view>
|
||||
<text class="empty-text">{{ $t('express.noReturnRecord') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getExpressReturnList } from '@/config/api/expressReturn.js'
|
||||
import { useI18n } from '@/utils/i18n.js'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const returnList = ref([])
|
||||
const loading = ref(false)
|
||||
const query = ref({ pageNum: 1, pageSize: 20 })
|
||||
|
||||
onMounted(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: t('express.returnRecord')
|
||||
})
|
||||
loadList()
|
||||
})
|
||||
|
||||
// 收件信息
|
||||
const recipientName = '风电者 18163601305'
|
||||
const recipientAddress = '湖南省长沙市岳麓区麓谷街道新长海尖科技园A2栋623'
|
||||
|
||||
const loadList = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await getExpressReturnList(query.value)
|
||||
if (res && res.code === 200) {
|
||||
// 将后端字段映射到前端展示字段
|
||||
const rows = (res.data && (res.data.rows || res.data)) || []
|
||||
returnList.value = rows.map(r => ({
|
||||
id: r.id,
|
||||
expressCompany: r.expressCompany || r.company || t('express.toFill'),
|
||||
trackingNumber: r.logisticsTrackingNumber || r.trackingNumber || t('express.toFill'),
|
||||
returnAddress: r.returnAddress || r.address || t('express.toFill'),
|
||||
returnTime: r.expressFillTime || r.createTime || r.returnTime || t('express.toFill'),
|
||||
packageType: r.packageType || t('express.toFill'),
|
||||
weight: r.weight || t('express.toFill'),
|
||||
status: mapStatus(r.status),
|
||||
rawStatus: r.status,
|
||||
userPhone: r.userPhone,
|
||||
orderId: r.orderId,
|
||||
remark: r.remark
|
||||
}))
|
||||
} else {
|
||||
throw new Error(res?.msg || t('express.getListFailed'))
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || t('express.loadFailed'), icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 状态映射
|
||||
const mapStatus = (status) => {
|
||||
// 文档:0-未填写 1-已填写 2-已取消 3-审批通过 4-审批拒绝 5-订单完成
|
||||
if (status === 5) return 'completed'
|
||||
if (status === 3 || status === 1) return 'processing'
|
||||
if (status === 4 || status === 2 || status === 0) return 'pending'
|
||||
return 'pending'
|
||||
}
|
||||
|
||||
const getStatusClass = (status) => ({
|
||||
'completed': 'status-completed',
|
||||
'processing': 'status-processing',
|
||||
'pending': 'status-pending'
|
||||
}[status] || 'status-pending')
|
||||
|
||||
const getStatusText = (status) => ({
|
||||
'completed': t('express.billingPaused'),
|
||||
'processing': t('express.billingPaused'),
|
||||
'pending': t('express.billingPaused')
|
||||
}[status] || t('express.billingPaused'))
|
||||
|
||||
const getStatusBadge = (status) => ({
|
||||
'completed': t('express.completed'),
|
||||
'processing': t('express.processing'),
|
||||
'pending': t('express.pending')
|
||||
}[status] || t('express.pending'))
|
||||
|
||||
// 一键复制全部信息
|
||||
const copyAllInfo = () => {
|
||||
const allInfo = `${t('express.recipient')}:${recipientName}\n${t('express.recipientAddressLabel')}:${recipientAddress}`
|
||||
uni.setClipboardData({
|
||||
data: allInfo,
|
||||
success: () => {
|
||||
uni.showToast({ title: t('express.copySuccess'), icon: 'success' })
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({ title: t('express.copyFailed'), icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 点击列表项
|
||||
const handleItemClick = (item) => {
|
||||
// 未填写(status=0 -> mapped 'pending')时跳转到补填页,其它跳详情
|
||||
if (item && item.rawStatus === 0) {
|
||||
uni.navigateTo({ url: `/pages/expressReturn/addExpressReturn?id=${item.id}` })
|
||||
} else {
|
||||
uni.navigateTo({ url: `/pages/expressReturn/detail?id=${item.id}` })
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.express-return-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
// 收件信息卡片
|
||||
.recipient-info-card {
|
||||
background: linear-gradient(135deg, #D1FFE1 0%, #B8F5D0 100%);
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 28rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.info-header {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.recipient-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.recipient-address {
|
||||
font-size: 26rpx;
|
||||
color: #333333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.copy-all-btn {
|
||||
padding: 16rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 28rpx;
|
||||
color: #27ae60;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// 列表容器
|
||||
.list-container {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.return-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 28rpx 24rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||
position: relative;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
}
|
||||
|
||||
// 左侧头像区域
|
||||
.item-left {
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
// 中间内容区域
|
||||
.item-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.content-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: 26rpx;
|
||||
color: #666666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
// 右侧状态标签
|
||||
.item-right {
|
||||
position: absolute;
|
||||
top: 28rpx;
|
||||
right: 24rpx;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
|
||||
&.status-completed {
|
||||
background-color: #e8f5e8;
|
||||
color: #27ae60;
|
||||
}
|
||||
|
||||
&.status-processing {
|
||||
background-color: #fff3cd;
|
||||
color: #f39c12;
|
||||
}
|
||||
|
||||
&.status-pending {
|
||||
background-color: #f0f0f0;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 40rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,645 @@
|
||||
<template>
|
||||
<view class="feedback-detail-container">
|
||||
<!-- 投诉详情卡片 -->
|
||||
<view class="detail-card">
|
||||
<!-- 第一行:问题类型和状态 -->
|
||||
<view class="card-header">
|
||||
<view class="type-badge" :class="getTypeClass(detail.type)">
|
||||
{{ getTypeText(detail.type) }}
|
||||
</view>
|
||||
<view class="status-badge" :class="getStatusClass(detail.status)">
|
||||
{{ getStatusText(detail.status) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 标题 -->
|
||||
<view class="title-section">
|
||||
<text class="title-text">{{ detail.title || '-' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 问题描述 -->
|
||||
<view class="content-section">
|
||||
<view class="content-text">{{ detail.content || '-' }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 联系电话 -->
|
||||
<view class="contact-section" v-if="detail.phone">
|
||||
<text class="contact-label">{{ $t('feedback.contactPhone') }}</text>
|
||||
<text class="contact-value">{{ detail.phone }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 上传图片 -->
|
||||
<view class="image-section" v-if="getImageList(detail).length > 0">
|
||||
<view class="image-list">
|
||||
<image v-for="(img, index) in getImageList(detail)" :key="index" :src="img" mode="aspectFill" class="detail-image"
|
||||
@click="previewImage(img, index)"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交时间 -->
|
||||
<view class="time-section">
|
||||
<text class="time-text">{{ formatTime(detail.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 回复列表(合并平台和用户回复,按时间排序) -->
|
||||
<view class="reply-section" v-if="allReplies.length > 0">
|
||||
<view class="section-divider">
|
||||
<view class="divider-line"></view>
|
||||
<text class="divider-text">{{ $t('feedback.replyHistory') }}</text>
|
||||
<view class="divider-line"></view>
|
||||
</view>
|
||||
<view class="reply-list">
|
||||
<view class="reply-item" v-for="(reply, index) in allReplies" :key="reply.id || index"
|
||||
:class="{ 'reply-platform': reply.isPlatform, 'reply-user': !reply.isPlatform }">
|
||||
<view class="reply-avatar" :class="{ 'avatar-platform': reply.isPlatform, 'avatar-user': !reply.isPlatform }">
|
||||
<text class="avatar-text">{{ reply.isPlatform ? '平台' : '我' }}</text>
|
||||
</view>
|
||||
<view class="reply-content-wrapper">
|
||||
<view class="reply-name-time">
|
||||
<text class="reply-name">{{ reply.senderName || (reply.isPlatform ? $t('feedback.platform') : $t('feedback.me')) }}</text>
|
||||
<text class="reply-time">{{ formatTime(reply.createTime) }}</text>
|
||||
</view>
|
||||
<view class="reply-content">{{ reply.content || '-' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部输入框 -->
|
||||
<view class="bottom-input-bar" v-if="canSendMessage">
|
||||
<view class="input-wrapper">
|
||||
<textarea class="reply-input" v-model="replyContent" :placeholder="$t('feedback.replyPlaceholder')"
|
||||
maxlength="500" :auto-height="true"></textarea>
|
||||
</view>
|
||||
<view class="submit-btn" @click="submitReply" :class="{ disabled: !replyContent.trim() }">
|
||||
{{ $t('feedback.submitReply') }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
onMounted,
|
||||
computed
|
||||
} from 'vue';
|
||||
import {
|
||||
onLoad
|
||||
} from '@dcloudio/uni-app';
|
||||
import {
|
||||
getFeedbackDetail,
|
||||
getFeedbackMessages,
|
||||
sendFeedbackMessage
|
||||
} from '@/config/api/feedback.js';
|
||||
import {
|
||||
useI18n
|
||||
} from '@/utils/i18n.js'
|
||||
|
||||
const {
|
||||
t
|
||||
} = useI18n()
|
||||
|
||||
// 设置页面标题
|
||||
onMounted(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: t('feedback.detail')
|
||||
})
|
||||
})
|
||||
|
||||
// 初始化状态
|
||||
const detail = ref({});
|
||||
const allReplies = ref([]);
|
||||
const replyContent = ref('');
|
||||
const feedbackId = ref('');
|
||||
|
||||
const canSendMessage = computed(() => {
|
||||
const status = detail.value?.status
|
||||
return ['pending', 'in_progress'].includes(status)
|
||||
})
|
||||
|
||||
// 页面加载
|
||||
onLoad(async (options) => {
|
||||
if (options.id) {
|
||||
feedbackId.value = options.id;
|
||||
await loadDetail();
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: t('feedback.idRequired'),
|
||||
icon: 'none'
|
||||
});
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
}
|
||||
});
|
||||
|
||||
const normalizeMessages = (messages = []) => {
|
||||
return (messages || [])
|
||||
.map(message => ({
|
||||
...message,
|
||||
isPlatform: message.senderType === 'staff' || message.senderType === 'platform' || message.isPlatform === true
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
const timeA = new Date(a.createTime || 0).getTime();
|
||||
const timeB = new Date(b.createTime || 0).getTime();
|
||||
return timeA - timeB;
|
||||
});
|
||||
}
|
||||
|
||||
const loadMessages = async (initialMessages) => {
|
||||
try {
|
||||
if (Array.isArray(initialMessages)) {
|
||||
allReplies.value = normalizeMessages(initialMessages);
|
||||
return;
|
||||
}
|
||||
if (!feedbackId.value) return;
|
||||
const res = await getFeedbackMessages(feedbackId.value);
|
||||
if (res.code === 200) {
|
||||
allReplies.value = normalizeMessages(res.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取对话消息失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载详情
|
||||
const loadDetail = async (options = {}) => {
|
||||
const shouldShowLoading = options.showLoading !== false;
|
||||
try {
|
||||
if (shouldShowLoading) {
|
||||
uni.showLoading({
|
||||
title: t('common.loading')
|
||||
});
|
||||
}
|
||||
|
||||
const res = await getFeedbackDetail(feedbackId.value);
|
||||
if (res.code === 200 && res.data) {
|
||||
detail.value = res.data;
|
||||
await loadMessages(res.data.messages);
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.msg || t('feedback.getDetailFailed'),
|
||||
icon: 'none'
|
||||
});
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取投诉详情失败:', error);
|
||||
uni.showToast({
|
||||
title: t('feedback.getDetailFailed'),
|
||||
icon: 'none'
|
||||
});
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
} finally {
|
||||
if (shouldShowLoading) {
|
||||
uni.hideLoading();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 提交回复
|
||||
const submitReply = async () => {
|
||||
if (!replyContent.value.trim()) {
|
||||
uni.showToast({
|
||||
title: t('feedback.pleaseEnterReply'),
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: t('common.submitting')
|
||||
});
|
||||
|
||||
const res = await sendFeedbackMessage(feedbackId.value, {
|
||||
content: replyContent.value.trim()
|
||||
});
|
||||
|
||||
if (res.code === 200) {
|
||||
uni.showToast({
|
||||
title: t('feedback.replySuccess'),
|
||||
icon: 'success'
|
||||
});
|
||||
replyContent.value = '';
|
||||
// 重新加载详情和对话
|
||||
await loadDetail({
|
||||
showLoading: false
|
||||
});
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.msg || t('feedback.replyFailed'),
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交回复失败:', error);
|
||||
uni.showToast({
|
||||
title: t('feedback.replyFailed'),
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
'pending': t('feedback.pending'),
|
||||
'in_progress': t('feedback.processing'),
|
||||
'resolved': t('feedback.completed')
|
||||
};
|
||||
return statusMap[status] || t('feedback.pending');
|
||||
};
|
||||
|
||||
// 获取状态样式类
|
||||
const getStatusClass = (status) => {
|
||||
const classMap = {
|
||||
'pending': 'status-pending',
|
||||
'in_progress': 'status-processing',
|
||||
'resolved': 'status-completed'
|
||||
};
|
||||
return classMap[status] || 'status-pending';
|
||||
};
|
||||
|
||||
// 获取类型文本
|
||||
const getTypeText = (type) => {
|
||||
const typeMap = {
|
||||
'complain': t('feedback.complain'),
|
||||
'suggestion': t('feedback.suggestion')
|
||||
};
|
||||
return typeMap[type] || type || '-';
|
||||
};
|
||||
|
||||
// 获取类型样式类
|
||||
const getTypeClass = (type) => {
|
||||
const classMap = {
|
||||
'complain': 'type-complain',
|
||||
'suggestion': 'type-suggestion'
|
||||
};
|
||||
return classMap[type] || 'type-default';
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr) => {
|
||||
if (!timeStr) return '-';
|
||||
try {
|
||||
const date = new Date(timeStr);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hour = String(date.getHours()).padStart(2, '0');
|
||||
const minute = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hour}:${minute}`;
|
||||
} catch (e) {
|
||||
return timeStr;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取图片列表(支持字符串或数组)
|
||||
const getImageList = (item) => {
|
||||
if (!item) return [];
|
||||
const pictureSource = item.pictureUrls ?? item.picturePath;
|
||||
if (!pictureSource) return [];
|
||||
if (Array.isArray(pictureSource)) {
|
||||
return pictureSource.filter(img => !!img);
|
||||
}
|
||||
if (typeof pictureSource === 'string') {
|
||||
if (pictureSource.includes(',')) {
|
||||
return pictureSource.split(',').map(img => img.trim()).filter(img => img);
|
||||
}
|
||||
return pictureSource.trim() ? [pictureSource.trim()] : [];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// 预览图片
|
||||
const previewImage = (url, index) => {
|
||||
const imageList = getImageList(detail.value);
|
||||
uni.previewImage({
|
||||
urls: imageList,
|
||||
current: index !== undefined ? index : 0
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.feedback-detail-container {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20rpx;
|
||||
padding-bottom: 200rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
// 详情卡片
|
||||
.detail-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
// 第一行:问题类型和状态
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
.type-badge {
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 6rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
|
||||
&.type-complain {
|
||||
background: #fff3e0;
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
&.type-suggestion {
|
||||
background: #e8f5e9;
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
&.type-default {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 6rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
|
||||
&.status-processing {
|
||||
background: #fff3e0;
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
&.status-completed {
|
||||
background: #e8f5e9;
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
&.status-pending {
|
||||
background: #f5f5f5;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 标题
|
||||
.title-section {
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.title-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
}
|
||||
|
||||
// 问题描述
|
||||
.content-section {
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.content-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.8;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
// 联系方式
|
||||
.contact-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 16rpx;
|
||||
background: #fafafa;
|
||||
border-radius: 12rpx;
|
||||
|
||||
.contact-label {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.contact-value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 图片区域
|
||||
.image-section {
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.image-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
|
||||
.detail-image {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提交时间
|
||||
.time-section {
|
||||
padding-top: 16rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
|
||||
.time-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 回复区域
|
||||
.reply-section {
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.section-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 30rpx 0 20rpx 0;
|
||||
|
||||
.divider-line {
|
||||
flex: 1;
|
||||
height: 1rpx;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.divider-text {
|
||||
padding: 0 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-list {
|
||||
.reply-item {
|
||||
display: flex;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
&.reply-platform {
|
||||
.reply-content-wrapper {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
&.reply-user {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.reply-content-wrapper {
|
||||
background: #07c160;
|
||||
color: #fff;
|
||||
margin-right: 16rpx;
|
||||
margin-left: 0;
|
||||
|
||||
.reply-name-time {
|
||||
.reply-name,
|
||||
.reply-time {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.reply-content {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reply-avatar {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.avatar-platform {
|
||||
background: #ff9800;
|
||||
}
|
||||
|
||||
&.avatar-user {
|
||||
background: #07c160;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
font-size: 22rpx;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-content-wrapper {
|
||||
flex: 1;
|
||||
background: #f5f5f5;
|
||||
border-radius: 12rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
margin-left: 16rpx;
|
||||
max-width: calc(100% - 72rpx);
|
||||
|
||||
.reply-name-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
.reply-name {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.reply-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-content {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 底部输入栏
|
||||
.bottom-input-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 20rpx 30rpx;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 20rpx;
|
||||
|
||||
.input-wrapper {
|
||||
flex: 1;
|
||||
background: #f5f5f5;
|
||||
border-radius: 20rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
max-height: 200rpx;
|
||||
|
||||
.reply-input {
|
||||
width: 100%;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
min-height: 40rpx;
|
||||
max-height: 200rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
padding: 16rpx 40rpx;
|
||||
background: #07c160;
|
||||
color: #fff;
|
||||
border-radius: 20rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
|
||||
&.disabled {
|
||||
background: #ccc;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&:active:not(.disabled) {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,457 @@
|
||||
<template>
|
||||
<view class="feedback-container">
|
||||
<!-- 投诉记录入口 -->
|
||||
<view class="record-entry" @click="navigateToRecord">
|
||||
<view class="entry-left">
|
||||
<image class="entry-icon" src="/static/complaint.png" mode="aspectFit"></image>
|
||||
<text class="entry-text">{{ $t('feedback.recordList') }}</text>
|
||||
</view>
|
||||
<view class="entry-right">
|
||||
<text class="entry-desc">{{ $t('feedback.viewRecords') }}</text>
|
||||
<uv-icon name="arrow-right" size="16" color="#999"></uv-icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- <form> -->
|
||||
<!-- 问题类型选择 -->
|
||||
<view class="type-section">
|
||||
<view class="section-title">{{ $t('feedback.issueType') }}</view>
|
||||
<view class="type-grid">
|
||||
<view v-for="(type, index) in types" :key="index" class="type-item"
|
||||
:class="{ active: selectedType === index }" @click="selectType(index)">
|
||||
{{ $t(type) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 问题描述 -->
|
||||
<view class="description-section">
|
||||
<view class="section-title">{{ $t('feedback.issueDescription') }}</view>
|
||||
<textarea class="description-input" v-model="description" :placeholder="$t('feedback.placeholder')"
|
||||
maxlength="500" name="description" />
|
||||
<view class="word-count">{{ description.length }}/500</view>
|
||||
</view>
|
||||
|
||||
<!-- 图片上传 -->
|
||||
<view class="upload-section">
|
||||
<view class="section-title">{{ $t('feedback.imageUpload') }}</view>
|
||||
<view class="upload-grid">
|
||||
<view class="upload-item" v-for="(img, index) in images" :key="index">
|
||||
<image :src="img" mode="aspectFill" />
|
||||
<view class="delete-btn" @click="deleteImage(index)">×</view>
|
||||
</view>
|
||||
<view class="upload-btn" @click="chooseImage" v-if="images.length < 3">
|
||||
<text class="plus">+</text>
|
||||
<text class="tip">{{ $t('feedback.uploadImage') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 联系方式 -->
|
||||
<view class="contact-section">
|
||||
<view class="section-title">{{ $t('feedback.contactInfo') }}</view>
|
||||
<input class="contact-input" v-model="contact" :placeholder="$t('feedback.contactPlaceholder')"
|
||||
type="number" maxlength="11" name="contact" />
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-section">
|
||||
<view class="submit-btn" @click="submitFeedback">{{ $t('feedback.submit') }}</view>
|
||||
</view>
|
||||
<!-- </form> -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
onMounted
|
||||
} from 'vue'
|
||||
import {
|
||||
onLoad
|
||||
} from "@dcloudio/uni-app"
|
||||
import {
|
||||
addUserFeedback
|
||||
} from '@/config/api/feedback'
|
||||
import {
|
||||
uploadOssResource
|
||||
} from '@/config/api/user'
|
||||
import {
|
||||
useI18n
|
||||
} from '@/utils/i18n.js'
|
||||
const {
|
||||
t
|
||||
} = useI18n()
|
||||
|
||||
// 跳转到投诉记录列表
|
||||
const navigateToRecord = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/feedback/list'
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: t('feedback.title')
|
||||
})
|
||||
})
|
||||
|
||||
onLoad((options) => {
|
||||
if (uni.getStorageSync("userInfo").phone) {
|
||||
contact.value = uni.getStorageSync('userInfo').phone;
|
||||
}
|
||||
if(options.selectedType) {
|
||||
selectedType.value = parseInt(options.selectedType);
|
||||
}
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const types = ref(['feedback.deviceFault', 'feedback.chargingIssue', 'feedback.usageSuggestion', 'feedback.other'])
|
||||
const selectedType = ref(-1)
|
||||
const paramsType = ref('')
|
||||
const description = ref('')
|
||||
const images = ref([])
|
||||
const contact = ref('')
|
||||
|
||||
// 方法
|
||||
const selectType = (index) => {
|
||||
selectedType.value = index
|
||||
}
|
||||
|
||||
|
||||
const chooseImage = () => {
|
||||
uni.chooseImage({
|
||||
count: 3 - images.value.length,
|
||||
success: (res) => {
|
||||
// 直接保存本地路径,不上传
|
||||
const toUpload = res.tempFilePaths || []
|
||||
images.value.push(...toUpload)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const deleteImage = (index) => {
|
||||
images.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const submitFeedback = async () => {
|
||||
if (selectedType.value === -1) {
|
||||
uni.showToast({
|
||||
title: t('feedback.pleaseSelectType'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!description.value.trim()) {
|
||||
uni.showToast({
|
||||
title: t('feedback.pleaseDescribe'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!contact.value) {
|
||||
uni.showToast({
|
||||
title: t('feedback.pleaseContact'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (types.value[selectedType.value] === 'feedback.deviceFault' || types.value[selectedType.value] ===
|
||||
'feedback.chargingIssue') {
|
||||
paramsType.value = 'complain'
|
||||
} else {
|
||||
paramsType.value = 'suggestion'
|
||||
}
|
||||
|
||||
try {
|
||||
// 显示上传进度
|
||||
uni.showLoading({
|
||||
title: t('feedback.uploading') || '上传中...',
|
||||
mask: true
|
||||
})
|
||||
|
||||
// 先逐步上传所有文件到OSS
|
||||
const files = []
|
||||
if (images.value.length > 0) {
|
||||
for (let i = 0; i < images.value.length; i++) {
|
||||
const filePath = images.value[i]
|
||||
try {
|
||||
const remoteUrl = await uploadOssResource(filePath)
|
||||
files.push(remoteUrl)
|
||||
} catch (err) {
|
||||
console.error(`文件 ${i + 1} 上传失败:`, err)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: t('feedback.imageUploadFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建反馈数据
|
||||
const feedbackData = {
|
||||
type: paramsType.value,
|
||||
content: description.value,
|
||||
phone: contact.value,
|
||||
files: files
|
||||
}
|
||||
|
||||
// 调用API提交反馈(使用 hideLoading 避免重复显示loading)
|
||||
const res = await addUserFeedback(feedbackData)
|
||||
uni.hideLoading()
|
||||
|
||||
// 处理响应
|
||||
if (res && (res.code === 200 || res === true || res?.success === true)) {
|
||||
uni.showToast({
|
||||
title: t('feedback.submitSuccess'),
|
||||
icon: 'success'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: (res && (res.msg || res.message)) || t('feedback.submitFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('feedback submit failed:', err)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: t('error.networkError') || '网络错误,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.feedback-container {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
padding: 30rpx;
|
||||
|
||||
// 投诉记录入口
|
||||
.record-entry {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
.entry-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
|
||||
.entry-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.entry-text {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.entry-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
|
||||
.entry-desc {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.type-section {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
.type-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -10rpx;
|
||||
|
||||
.type-item {
|
||||
width: calc(50% - 20rpx);
|
||||
margin: 10rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
background: #E8F5EE;
|
||||
color: #07c160;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.description-section {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
.description-input {
|
||||
width: 100%;
|
||||
height: 240rpx;
|
||||
background: #f8f8f8;
|
||||
border-radius: 10rpx;
|
||||
padding: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.word-count {
|
||||
text-align: right;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
.upload-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.upload-item {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
margin-right: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
position: relative;
|
||||
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
position: absolute;
|
||||
right: -10rpx;
|
||||
top: -10rpx;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 10rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
|
||||
.plus {
|
||||
font-size: 60rpx;
|
||||
line-height: 1;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.tip {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contact-section {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
.contact-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
background: #f8f8f8;
|
||||
border-radius: 10rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.submit-section {
|
||||
padding: 20rpx 0 40rpx 0;
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: #07c160;
|
||||
color: #fff;
|
||||
border-radius: 44rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.3);
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,493 @@
|
||||
<template>
|
||||
<view class="feedback-list-container">
|
||||
<!-- 状态切换 -->
|
||||
<view class="status-tabs">
|
||||
<view v-for="(tab, index) in statusTabs" :key="index" class="tab-item"
|
||||
:class="{ active: currentTab === index }" @click="switchTab(index)">
|
||||
{{ tab.text }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 投诉列表 -->
|
||||
<scroll-view class="feedback-list" scroll-y @scrolltolower="loadMore" :refresher-enabled="true"
|
||||
:refresher-triggered="refreshing" @refresherrefresh="onRefresh">
|
||||
<view class="empty-state" v-if="feedbackList.length === 0 && !loading">
|
||||
<view class="empty-icon">
|
||||
<image src="/static/suggess.png" mode="aspectFill" class="empty-icon"></image>
|
||||
</view>
|
||||
<text class="empty-text">{{ $t('feedback.noRecord') }}</text>
|
||||
</view>
|
||||
|
||||
<view class="feedback-item" v-for="(item, index) in feedbackList" :key="index"
|
||||
@click="navigateToDetail(item)">
|
||||
<view class="item-header">
|
||||
<view class="header-left">
|
||||
<view class="status-chip" :class="getStatusClass(item.status)">
|
||||
{{ getStatusText(item.status) }}
|
||||
</view>
|
||||
<view class="type-text">{{ getTypeText(item.type) }}</view>
|
||||
</view>
|
||||
<view class="header-right">
|
||||
<text class="time-text">{{ formatTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="item-content">
|
||||
<view class="content-text">{{ item.content || '-' }}</view>
|
||||
<view class="content-images" v-if="getImageList(item).length > 0">
|
||||
<image v-for="(img, imgIndex) in getImageList(item)" :key="imgIndex" :src="img" mode="aspectFill" class="content-image"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="item-footer">
|
||||
<view class="footer-left">
|
||||
<text class="phone-text">{{ $t('feedback.contactPhone') }}:{{ item.phone || '-' }}</text>
|
||||
</view>
|
||||
<view class="footer-right">
|
||||
<uv-icon name="arrow-right" size="16" color="#999"></uv-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view class="load-more" v-if="hasMore && feedbackList.length > 0">
|
||||
<text class="load-more-text">{{ $t('common.loading') }}</text>
|
||||
</view>
|
||||
<view class="no-more" v-if="!hasMore && feedbackList.length > 0">
|
||||
<text class="no-more-text">{{ $t('common.noMore') }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
reactive,
|
||||
onMounted
|
||||
} from 'vue';
|
||||
import {
|
||||
onLoad,
|
||||
onShow
|
||||
} from '@dcloudio/uni-app';
|
||||
import {
|
||||
getFeedbackList
|
||||
} from '@/config/api/feedback.js';
|
||||
import {
|
||||
useI18n
|
||||
} from '@/utils/i18n.js'
|
||||
|
||||
const {
|
||||
t
|
||||
} = useI18n()
|
||||
|
||||
// 设置页面标题
|
||||
onMounted(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: t('feedback.recordList')
|
||||
})
|
||||
})
|
||||
|
||||
// 初始化状态
|
||||
const currentTab = ref(0);
|
||||
const feedbackList = ref([]);
|
||||
const loading = ref(false);
|
||||
const refreshing = ref(false);
|
||||
const hasMore = ref(true);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
|
||||
// 状态标签
|
||||
const statusTabs = reactive([{
|
||||
get text() {
|
||||
return t('common.all')
|
||||
},
|
||||
status: ''
|
||||
},
|
||||
{
|
||||
get text() {
|
||||
return t('feedback.pending')
|
||||
},
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
get text() {
|
||||
return t('feedback.processing')
|
||||
},
|
||||
status: 'in_progress'
|
||||
},
|
||||
{
|
||||
get text() {
|
||||
return t('feedback.completed')
|
||||
},
|
||||
status: 'resolved'
|
||||
}
|
||||
]);
|
||||
|
||||
// 页面加载
|
||||
onLoad(async () => {
|
||||
await loadFeedbackList();
|
||||
});
|
||||
|
||||
// 页面显示时刷新
|
||||
onShow(async () => {
|
||||
// 可以在这里刷新列表
|
||||
});
|
||||
|
||||
// 切换标签
|
||||
const switchTab = async (index) => {
|
||||
currentTab.value = index;
|
||||
currentPage.value = 1;
|
||||
feedbackList.value = [];
|
||||
hasMore.value = true;
|
||||
await loadFeedbackList();
|
||||
};
|
||||
|
||||
// 加载投诉列表
|
||||
const loadFeedbackList = async (isLoadMore = false) => {
|
||||
if (loading.value) return;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
const status = statusTabs[currentTab.value].status;
|
||||
const params = {
|
||||
pageNum: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
};
|
||||
if (status) {
|
||||
params.status = status;
|
||||
}
|
||||
|
||||
const res = await getFeedbackList(params);
|
||||
if (res.code === 200 && res.data) {
|
||||
const records = res.data.records || res.data.list || [];
|
||||
if (isLoadMore) {
|
||||
feedbackList.value = [...feedbackList.value, ...records];
|
||||
} else {
|
||||
feedbackList.value = records;
|
||||
}
|
||||
|
||||
// 判断是否还有更多数据
|
||||
const total = res.data.total || 0;
|
||||
hasMore.value = feedbackList.value.length < total;
|
||||
|
||||
if (hasMore.value) {
|
||||
currentPage.value += 1;
|
||||
}
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.msg || t('feedback.getListFailed'),
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取投诉列表失败:', error);
|
||||
uni.showToast({
|
||||
title: t('feedback.getListFailed'),
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
refreshing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载更多
|
||||
const loadMore = () => {
|
||||
if (hasMore.value && !loading.value) {
|
||||
loadFeedbackList(true);
|
||||
}
|
||||
};
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = () => {
|
||||
refreshing.value = true;
|
||||
currentPage.value = 1;
|
||||
feedbackList.value = [];
|
||||
hasMore.value = true;
|
||||
loadFeedbackList();
|
||||
};
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
'pending': t('feedback.pending'),
|
||||
'in_progress': t('feedback.processing'),
|
||||
'resolved': t('feedback.completed')
|
||||
};
|
||||
return statusMap[status] || t('feedback.pending');
|
||||
};
|
||||
|
||||
// 获取状态样式类
|
||||
const getStatusClass = (status) => {
|
||||
const classMap = {
|
||||
'pending': 'chip-pending',
|
||||
'in_progress': 'chip-processing',
|
||||
'resolved': 'chip-completed'
|
||||
};
|
||||
return classMap[status] || 'chip-pending';
|
||||
};
|
||||
|
||||
// 获取类型文本
|
||||
const getTypeText = (type) => {
|
||||
const typeMap = {
|
||||
'complain': t('feedback.complain'),
|
||||
'suggestion': t('feedback.suggestion')
|
||||
};
|
||||
return typeMap[type] || type || '-';
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr) => {
|
||||
if (!timeStr) return '-';
|
||||
try {
|
||||
const date = new Date(timeStr);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hour = String(date.getHours()).padStart(2, '0');
|
||||
const minute = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hour}:${minute}`;
|
||||
} catch (e) {
|
||||
return timeStr;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取图片列表(支持字符串或数组)
|
||||
const getImageList = (item) => {
|
||||
if (!item.picturePath) return [];
|
||||
if (Array.isArray(item.picturePath)) return item.picturePath;
|
||||
if (typeof item.picturePath === 'string') {
|
||||
// 如果是逗号分隔的字符串,拆分为数组
|
||||
if (item.picturePath.includes(',')) {
|
||||
return item.picturePath.split(',').filter(img => img.trim());
|
||||
}
|
||||
return [item.picturePath];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// 跳转到详情页
|
||||
const navigateToDetail = (item) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/feedback/detail?id=${item.id || item.feedbackId}`
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.feedback-list-container {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
padding-bottom: 30rpx;
|
||||
|
||||
// 状态标签栏
|
||||
.status-tabs {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
padding: 0 20rpx;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
height: 90rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
color: #07c160;
|
||||
font-weight: 500;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40rpx;
|
||||
height: 4rpx;
|
||||
background: #07c160;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 投诉列表
|
||||
.feedback-list {
|
||||
height: calc(100vh - 90rpx);
|
||||
padding: 20rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
// 投诉项
|
||||
.feedback-item {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
// 头部
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 16rpx;
|
||||
|
||||
.status-chip {
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
margin-right: 16rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.chip-processing {
|
||||
background: rgba(255, 152, 0, 0.12);
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
&.chip-completed {
|
||||
background: rgba(76, 175, 80, 0.12);
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
&.chip-pending {
|
||||
background: rgba(158, 158, 158, 0.12);
|
||||
color: #9E9E9E;
|
||||
}
|
||||
}
|
||||
|
||||
.type-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
flex-shrink: 0;
|
||||
|
||||
.time-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 内容
|
||||
.item-content {
|
||||
padding: 24rpx;
|
||||
|
||||
.content-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 16rpx;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-images {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
|
||||
.content-image {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 底部
|
||||
.item-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
background: #fafafa;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
|
||||
.footer-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 16rpx;
|
||||
|
||||
.phone-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.empty-state {
|
||||
padding: 100rpx 0;
|
||||
text-align: center;
|
||||
|
||||
.empty-icon {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
margin: 0 auto 30rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
.load-more,
|
||||
.no-more {
|
||||
padding: 30rpx 0;
|
||||
text-align: center;
|
||||
|
||||
.load-more-text,
|
||||
.no-more-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<view class="help-container">
|
||||
<!-- 常见问题 -->
|
||||
<view class="faq-section">
|
||||
<uv-collapse :border="false">
|
||||
<uv-collapse-item
|
||||
v-for="(item, index) in faqList"
|
||||
:key="index"
|
||||
:title="$t(item.question)"
|
||||
:name="index"
|
||||
>
|
||||
<view class="answer-content">
|
||||
<text class="answer-text">{{ $t(item.answer) }}</text>
|
||||
</view>
|
||||
</uv-collapse-item>
|
||||
</uv-collapse>
|
||||
</view>
|
||||
|
||||
<!-- 联系客服 -->
|
||||
<view class="contact-card">
|
||||
<view class="contact-title">{{ $t('help.contactUs') }}</view>
|
||||
<view class="contact-content">
|
||||
<view class="contact-item">
|
||||
<text class="label">{{ $t('help.phone') }}</text>
|
||||
<text class="value" @click="makePhoneCall">{{ customerPhone }}</text>
|
||||
</view>
|
||||
<view class="contact-item">
|
||||
<text class="label">{{ $t('help.workingHours') }}</text>
|
||||
<text class="value">{{ $t('help.workingHoursValue') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { HELP_CONTENT } from '@/constants/help'
|
||||
import { getCustomerPhone } from '@/util/index.js'
|
||||
import { useI18n } from '@/utils/i18n.js'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const faqList = ref(HELP_CONTENT.FAQ_LIST)
|
||||
const customerPhone = ref(HELP_CONTENT.CONTACT.PHONE.VALUE)
|
||||
|
||||
onLoad(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: t('help.title')
|
||||
})
|
||||
customerPhone.value = getCustomerPhone()
|
||||
})
|
||||
|
||||
const makePhoneCall = () => {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: customerPhone.value
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.help-container {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
padding: 30rpx;
|
||||
|
||||
.faq-section {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
|
||||
overflow: hidden;
|
||||
|
||||
.answer-content {
|
||||
padding: 20rpx 30rpx 30rpx;
|
||||
background: #f9f9f9;
|
||||
|
||||
.answer-text {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contact-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
|
||||
|
||||
.contact-title {
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
display: inline-block;
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 20rpx;
|
||||
padding-bottom: 8rpx;
|
||||
width: fit-content;
|
||||
|
||||
&::after {
|
||||
z-index: -1;
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 8rpx;
|
||||
width: 88%;
|
||||
height: 16rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #07C160;
|
||||
}
|
||||
}
|
||||
|
||||
.contact-content {
|
||||
.contact-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,275 @@
|
||||
<template>
|
||||
<view class="login-container">
|
||||
<view class="logo">
|
||||
<image src="/static/logo.png" mode="aspectFit" />
|
||||
<text class="app-name">{{ $t('app.slogan') }}</text>
|
||||
</view>
|
||||
|
||||
<view class="title">{{ $t('auth.loginTitle') }}</view>
|
||||
<view class="subtitle">{{ $t('auth.loginDesc') }}</view>
|
||||
|
||||
<!-- 微信一键手机号快捷登录(推荐) -->
|
||||
<button v-if="!isAgreed" class="btn primary" @click="handleLoginClick">
|
||||
{{ $t('auth.getPhoneNumber') }}
|
||||
</button>
|
||||
<button v-else class="btn primary" open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber">
|
||||
{{ $t('auth.getPhoneNumber') }}
|
||||
</button>
|
||||
|
||||
<!-- 手机号验证码登录 -->
|
||||
<button class="btn outline" @click="goToPhoneLogin" v-if="isHTML5">
|
||||
{{ $t('auth.phoneLogin') }}
|
||||
</button>
|
||||
|
||||
<view class="agreement-box">
|
||||
<checkbox-group @change="onAgreementChange">
|
||||
<label class="agreement-label">
|
||||
<checkbox value="agreed" :checked="isAgreed" color="#07c160" class="agreement-checkbox" />
|
||||
<text class="agreement-text">
|
||||
{{ $t('auth.agreeToTerms') }}
|
||||
<text class="link" @tap.stop="go('/subPackages/other/legal/agreement')">{{ $t('user.userAgreement') }}</text>
|
||||
{{ $t('common.and') }}
|
||||
<text class="link" @tap.stop="go('/subPackages/other/legal/privacy')">{{ $t('user.privacyPolicy') }}</text>
|
||||
</text>
|
||||
</label>
|
||||
</checkbox-group>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { wxLogin, getUserPhoneNumber, getUserInfo } from '@/util/index.js'
|
||||
import { useI18n } from '@/utils/i18n.js'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 设置页面标题
|
||||
onMounted(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: t('auth.loginTitle')
|
||||
})
|
||||
})
|
||||
|
||||
const isHTML5 = ref(false) // 是否是HTML5模式
|
||||
|
||||
const redirect = ref('/pages/index/index')
|
||||
const isAgreed = ref(false) // 是否同意协议
|
||||
|
||||
// 勾选协议变化
|
||||
const onAgreementChange = (e) => {
|
||||
isAgreed.value = e.detail.value.includes('agreed')
|
||||
}
|
||||
|
||||
// 未勾选协议时点击登录按钮
|
||||
const handleLoginClick = async () => {
|
||||
try {
|
||||
await checkAgreement()
|
||||
// 协议已同意后,按钮会自动切换为带open-type的版本
|
||||
} catch (error) {
|
||||
// 用户取消了协议同意
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否同意协议
|
||||
const checkAgreement = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isAgreed.value) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
// 未勾选,弹窗提示
|
||||
uni.showModal({
|
||||
title: t('common.tips'),
|
||||
content: t('auth.pleaseAgreeToTerms'),
|
||||
confirmText: t('common.confirm'),
|
||||
cancelText: t('common.cancel'),
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 用户点击同意,自动勾选
|
||||
isAgreed.value = true
|
||||
resolve()
|
||||
} else {
|
||||
// 用户点击取消
|
||||
reject(new Error(t('auth.pleaseAgreeToTerms')))
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const navigateAfterLogin = async () => {
|
||||
try {
|
||||
// 可选:刷新一次用户信息
|
||||
await getUserInfo().catch(() => {})
|
||||
} catch (e) {}
|
||||
|
||||
// 读取跳转路径(支持 tabBar 页面)
|
||||
const target = '/pages/index/index'
|
||||
const tabPages = ['/pages/index/index', '/pages/my/index']
|
||||
if (tabPages.includes(target)) {
|
||||
uni.reLaunch({ url: target })
|
||||
return
|
||||
}
|
||||
uni.reLaunch({ url: target })
|
||||
}
|
||||
|
||||
const onWeChatLogin = async () => {
|
||||
try {
|
||||
// 先检查是否同意协议
|
||||
await checkAgreement()
|
||||
|
||||
await wxLogin()
|
||||
uni.showToast({ title: t('auth.loginSuccess'), icon: 'success' })
|
||||
await navigateAfterLogin()
|
||||
} catch (error) {
|
||||
if (error.message !== t('auth.pleaseAgreeToTerms')) {
|
||||
uni.showToast({ title: error.message || t('auth.loginFailed'), icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onGetPhoneNumber = async (e) => {
|
||||
if (!e || e.detail.errMsg !== 'getPhoneNumber:ok') {
|
||||
uni.showToast({ title: t('auth.phoneCancelled'), icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 先微信登录,获取 token
|
||||
await wxLogin()
|
||||
// 再用微信返回的临时 code 换取手机号
|
||||
await getUserPhoneNumber(e.detail.code)
|
||||
uni.showToast({ title: t('auth.loginSuccess'), icon: 'success' })
|
||||
await navigateAfterLogin()
|
||||
} catch (error) {
|
||||
uni.showToast({ title: error.message || t('auth.loginFailed'), icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((opts) => {
|
||||
if (opts && opts.redirect) {
|
||||
try {
|
||||
redirect.value = decodeURIComponent(opts.redirect)
|
||||
} catch (_) {}
|
||||
}
|
||||
// #ifdef H5
|
||||
isHTML5.value = true
|
||||
// #endif
|
||||
})
|
||||
|
||||
const go = (url) => {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
// 跳转到手机号登录页面
|
||||
const goToPhoneLogin = () => {
|
||||
uni.navigateTo({ url: '/subPackages/user/login/phone' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #C8F4D9 0%, #FFFFFF 100%);
|
||||
padding: 80rpx 40rpx 40rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 60rpx;
|
||||
|
||||
image {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 26rpx;
|
||||
color: #888;
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
border-radius: 48rpx;
|
||||
font-size: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background: #07c160;
|
||||
color: #fff;
|
||||
box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.3);
|
||||
}
|
||||
|
||||
.outline {
|
||||
background: #fff;
|
||||
color: #07c160;
|
||||
border: 2rpx solid #07c160;
|
||||
}
|
||||
|
||||
.agreement-box {
|
||||
margin-top: 32rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
bottom: 40rpx;
|
||||
position: absolute;
|
||||
|
||||
.agreement-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.agreement-checkbox {
|
||||
flex-shrink: 0;
|
||||
// margin-right: 12rpx;
|
||||
// margin-top: 2rpx;
|
||||
transform:scale(0.7);
|
||||
}
|
||||
|
||||
.agreement-text {
|
||||
flex: 1;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
word-break: break-all;
|
||||
|
||||
.link {
|
||||
color: #07c160;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
<template>
|
||||
<view class="login-container">
|
||||
<view class="header">
|
||||
<view class="title">Hello,</view>
|
||||
<view class="subtitle">{{ $t('app.welcome') }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 国家区号选择器 -->
|
||||
<view class="form-group">
|
||||
<view class="phone-input-wrapper">
|
||||
<view class="country-code" @click="showCountryPicker">
|
||||
<text>{{ countryCode }}</text>
|
||||
<text class="arrow">▼</text>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
<input
|
||||
class="phone-input"
|
||||
v-model="phone"
|
||||
type="number"
|
||||
maxlength="11"
|
||||
:placeholder="$t('auth.phonePlaceholder')"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 验证码输入 -->
|
||||
<view class="form-group">
|
||||
<view class="code-input-wrapper">
|
||||
<input
|
||||
class="code-input"
|
||||
v-model="verifyCode"
|
||||
type="number"
|
||||
maxlength="6"
|
||||
:placeholder="$t('auth.codePlaceholder')"
|
||||
/>
|
||||
<view class="code-btn" @click="handleSendCode" :class="{ disabled: countdown > 0 }">
|
||||
<text class="code-btn-text">{{ countdown > 0 ? `${countdown}s` : $t('auth.getCode') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 区域提示 -->
|
||||
<view class="region-notice">
|
||||
<text>{{ $t('auth.regionNotSupported') }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<view class="login-btn" @click="handleLogin">
|
||||
<text class="login-btn-text">{{ $t('auth.loginBtn') }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 协议勾选 -->
|
||||
<view class="agreement-box">
|
||||
<checkbox-group @change="onAgreementChange">
|
||||
<label class="agreement-label">
|
||||
<checkbox value="agreed" :checked="isAgreed" color="#07c160" class="agreement-checkbox" />
|
||||
<text class="agreement-text">
|
||||
{{ $t('auth.agreeToTerms') }}
|
||||
<text class="link" @tap.stop="go('/pages/legal/agreement')">{{ $t('user.userAgreement') }}</text>
|
||||
{{ $t('common.and') }}
|
||||
<text class="link" @tap.stop="go('/pages/legal/privacy')">{{ $t('user.privacyPolicy') }}</text>
|
||||
</text>
|
||||
</label>
|
||||
</checkbox-group>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { sendVerifyCode, loginWithCode } from '@/config/api/user.js'
|
||||
import { fetchAndCacheCustomerPhone } from '@/util/index.js'
|
||||
import { useI18n } from '@/utils/i18n.js'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 设置页面标题
|
||||
onMounted(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: t('auth.phoneLogin')
|
||||
})
|
||||
})
|
||||
|
||||
const redirect = ref('/pages/index/index')
|
||||
const isAgreed = ref(false) // 是否同意协议
|
||||
const phone = ref('') // 手机号
|
||||
const verifyCode = ref('') // 验证码
|
||||
const countryCode = ref('+86') // 国家区号
|
||||
const countdown = ref(0) // 验证码倒计时
|
||||
let timer = null // 计时器
|
||||
|
||||
// 勾选协议变化
|
||||
const onAgreementChange = (e) => {
|
||||
isAgreed.value = e.detail.value.includes('agreed')
|
||||
}
|
||||
|
||||
// 显示国家区号选择器(暂时仅支持+86)
|
||||
const showCountryPicker = () => {
|
||||
uni.showToast({
|
||||
title: t('auth.onlyMainlandSupported'),
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
// 验证手机号格式
|
||||
const validatePhone = () => {
|
||||
if (!phone.value) {
|
||||
uni.showToast({ title: t('auth.phoneRequired'), icon: 'none' })
|
||||
return false
|
||||
}
|
||||
const phoneReg = /^1[3-9]\d{9}$/
|
||||
if (!phoneReg.test(phone.value)) {
|
||||
uni.showToast({ title: t('auth.phoneInvalid'), icon: 'none' })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
const handleSendCode = async () => {
|
||||
if (countdown.value > 0) return
|
||||
|
||||
if (!validatePhone()) return
|
||||
|
||||
try {
|
||||
uni.showLoading({ title: t('common.sending') })
|
||||
await sendVerifyCode(phone.value)
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: t('auth.codeSent'), icon: 'success' })
|
||||
|
||||
// 启动60秒倒计时
|
||||
countdown.value = 60
|
||||
timer = setInterval(() => {
|
||||
countdown.value--
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: error.message || t('auth.sendCodeFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 登录
|
||||
const handleLogin = async () => {
|
||||
if (!validatePhone()) return
|
||||
|
||||
if (!verifyCode.value) {
|
||||
uni.showToast({ title: t('auth.codeRequired'), icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!isAgreed.value) {
|
||||
uni.showToast({ title: t('auth.pleaseAgreeToTerms'), icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
uni.showLoading({ title: t('common.loggingIn') })
|
||||
const res = await loginWithCode(phone.value, verifyCode.value)
|
||||
|
||||
// 保存token和client_id
|
||||
// 兼容多种返回格式:res.data.token, res.token, res.data.access_token
|
||||
const token = res.token || (res.data && (res.data.token || res.data.access_token))
|
||||
const clientId = res.client_id || (res.data && (res.data.client_id || res.data.clientId))
|
||||
|
||||
if (token) {
|
||||
uni.setStorageSync('token', token)
|
||||
if (clientId) {
|
||||
uni.setStorageSync('client_id', clientId)
|
||||
}
|
||||
|
||||
// 登录成功后获取并缓存客服电话
|
||||
fetchAndCacheCustomerPhone().catch(err => {
|
||||
console.error(t('auth.getServicePhoneFailed'), err)
|
||||
})
|
||||
} else {
|
||||
throw new Error(t('auth.noAuthToken'))
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: t('auth.loginSuccess'), icon: 'success' })
|
||||
|
||||
// 跳转到首页
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: redirect.value })
|
||||
}, 1500)
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: error.message || t('auth.loginFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载
|
||||
onLoad((opts) => {
|
||||
if (opts && opts.redirect) {
|
||||
try {
|
||||
redirect.value = decodeURIComponent(opts.redirect)
|
||||
} catch (_) {}
|
||||
}
|
||||
})
|
||||
|
||||
const go = (url) => {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
// 清理定时器
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #C8F4D9 0%, #FFFFFF 100%);
|
||||
padding: 0 48rpx;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
|
||||
.header {
|
||||
padding-top: 120rpx;
|
||||
margin-bottom: 80rpx;
|
||||
|
||||
.title {
|
||||
font-size: 64rpx;
|
||||
font-weight: 700;
|
||||
color: #000000;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 64rpx;
|
||||
font-weight: 700;
|
||||
color: #000000;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
.phone-input-wrapper {
|
||||
background: #FFFFFF;
|
||||
border-radius: 48rpx;
|
||||
height: 96rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 32rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
.country-code {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 32rpx;
|
||||
color: #333333;
|
||||
padding-right: 16rpx;
|
||||
|
||||
.arrow {
|
||||
margin-left: 8rpx;
|
||||
font-size: 20rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 2rpx;
|
||||
height: 40rpx;
|
||||
background: #E5E5E5;
|
||||
margin: 0 16rpx;
|
||||
}
|
||||
|
||||
.phone-input {
|
||||
flex: 1;
|
||||
font-size: 32rpx;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
|
||||
.code-input-wrapper {
|
||||
background: #FFFFFF;
|
||||
border-radius: 48rpx;
|
||||
height: 96rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 32rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
.code-input {
|
||||
flex: 1;
|
||||
font-size: 32rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.code-btn {
|
||||
padding-left: 24rpx;
|
||||
border-left: 2rpx solid #E5E5E5;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.code-btn-text {
|
||||
font-size: 28rpx;
|
||||
color: #07c160;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.region-notice {
|
||||
margin-bottom: 48rpx;
|
||||
padding: 0 8rpx;
|
||||
|
||||
text {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
background: #07c160;
|
||||
border-radius: 60rpx;
|
||||
height: 112rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.3);
|
||||
margin-bottom: 48rpx;
|
||||
|
||||
&:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.login-btn-text {
|
||||
font-size: 36rpx;
|
||||
color: #FFFFFF;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.agreement-box {
|
||||
position: absolute;
|
||||
left: 48rpx;
|
||||
right: 48rpx;
|
||||
bottom: 60rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.agreement-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.agreement-checkbox {
|
||||
flex-shrink: 0;
|
||||
transform: scale(0.75);
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
|
||||
.agreement-text {
|
||||
flex: 1;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
word-break: break-all;
|
||||
|
||||
.link {
|
||||
color: #07c160;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<view class="setting-page">
|
||||
<view class="group">
|
||||
<view class="item" @click="showLanguageSelector">
|
||||
<text class="label">{{ $t('settings.language') }}</text>
|
||||
<view class="right">
|
||||
<text class="value">{{ currentLanguageText }}</text>
|
||||
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="group">
|
||||
<view class="item" @click="navigateTo('/subPackages/other/legal/agreement')">
|
||||
<text class="label">{{ $t('user.userAgreement') }}</text>
|
||||
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
|
||||
</view>
|
||||
<view class="item" @click="navigateTo('/subPackages/other/legal/privacy')">
|
||||
<text class="label">{{ $t('user.privacyPolicy') }}</text>
|
||||
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
|
||||
</view>
|
||||
<view class="item" @click="navigateTo('/subPackages/other/legal/terms')">
|
||||
<text class="label">{{ $t('legal.termsAndConditions') }}</text>
|
||||
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="group">
|
||||
<view class="item" @click="handleLogout">
|
||||
<text class="label">{{ $t('user.logout') }}</text>
|
||||
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, getCurrentInstance } from 'vue'
|
||||
import { userLogout } from '@/config/api/user.js'
|
||||
import { useI18n } from '@/utils/i18n.js'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 获取全局 i18n 实例
|
||||
const instance = getCurrentInstance()
|
||||
const globalI18n = instance?.appContext?.config?.globalProperties?.$i18n
|
||||
|
||||
// 设置页面标题
|
||||
onMounted(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: t('settings.title')
|
||||
})
|
||||
})
|
||||
|
||||
// 当前语言
|
||||
const currentLanguage = ref(uni.getStorageSync('language') || 'zh-CN')
|
||||
|
||||
// 当前语言文本显示
|
||||
const currentLanguageText = computed(() => {
|
||||
if (currentLanguage.value === 'zh-CN') {
|
||||
return t('settings.chinese')
|
||||
} else if (currentLanguage.value === 'id-ID') {
|
||||
return t('settings.indonesian')
|
||||
} else {
|
||||
return t('settings.english')
|
||||
}
|
||||
})
|
||||
|
||||
const navigateTo = (url) => {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
// 显示语言选择器
|
||||
const showLanguageSelector = () => {
|
||||
const languages = [
|
||||
{ code: 'zh-CN', label: t('settings.chinese') },
|
||||
{ code: 'en-US', label: t('settings.english') },
|
||||
{ code: 'id-ID', label: t('settings.indonesian') }
|
||||
]
|
||||
|
||||
uni.showActionSheet({
|
||||
itemList: languages.map(lang => lang.label),
|
||||
success: (res) => {
|
||||
const selectedLang = languages[res.tapIndex].code
|
||||
if (selectedLang !== currentLanguage.value) {
|
||||
// 1. 保存到缓存
|
||||
uni.setStorageSync('language', selectedLang)
|
||||
|
||||
// 2. 立即更新 i18n 实例(重要!)
|
||||
if (globalI18n) {
|
||||
globalI18n.locale = selectedLang
|
||||
}
|
||||
|
||||
// 3. 更新当前语言状态
|
||||
currentLanguage.value = selectedLang
|
||||
|
||||
// 4. 提示用户
|
||||
uni.showToast({
|
||||
title: t('settings.languageSwitched'),
|
||||
icon: 'none',
|
||||
duration: 800
|
||||
})
|
||||
|
||||
// 5. 延迟后重新加载应用(确保 i18n 更新已生效)
|
||||
setTimeout(() => {
|
||||
// 使用 reLaunch 完全重启应用
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}, 800)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
uni.showModal({
|
||||
title: t('common.tips'),
|
||||
content: t('user.confirmLogout'),
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const response = await userLogout();
|
||||
if (response.code == 200) {
|
||||
uni.showToast({ title: t('user.logoutSuccess'), icon: 'none' })
|
||||
setTimeout(() => {
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('userInfo')
|
||||
uni.reLaunch({ url: '/subPackages/user/login/index' })
|
||||
}, 1200)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.setting-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f6f6f6;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.group {
|
||||
margin-top: 20rpx;
|
||||
background-color: #ffffff;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx 30rpx;
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.item:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 30rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 用户信息卡片 -->
|
||||
<view class="user-card">
|
||||
<view class="avatar">
|
||||
<image :src="userInfo.avatar || '/static/images/default-avatar.png'" mode="aspectFill"></image>
|
||||
</view>
|
||||
<view class="user-info">
|
||||
<text class="nickname">{{ userInfo.nickName || $t('user.notLoggedIn') }}</text>
|
||||
<text class="phone">{{ userInfo.phone || $t('user.phoneNotBound') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 余额卡片 -->
|
||||
<view class="balance-card">
|
||||
<view class="balance-title">{{ $t('userProfile.balance') }}</view>
|
||||
<view class="balance-amount">¥{{ userInfo.balanceAmount || '0.00' }}</view>
|
||||
<view class="balance-desc">{{ $t('user.balanceDesc') }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能菜单 -->
|
||||
<view class="menu-list">
|
||||
<view class="menu-item" @click="navigateTo('/pages/order/index')">
|
||||
<text class="menu-icon">📋</text>
|
||||
<text class="menu-text">{{ $t('user.myOrders') }}</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
<view class="menu-item" @click="navigateTo('/pages/feedback/index')">
|
||||
<text class="menu-icon">💬</text>
|
||||
<text class="menu-text">{{ $t('user.feedback') }}</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
<view class="menu-item" @click="navigateTo('/pages/help/index')">
|
||||
<text class="menu-icon">ℹ️</text>
|
||||
<text class="menu-text">{{ $t('help.title') }}</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 退出登录按钮 -->
|
||||
<view class="logout-btn" @click="handleLogout" v-if="isLogin">
|
||||
<text>{{ $t('user.logout') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getUserInfo } from '@/util/index.js'
|
||||
import { URL } from '@/config/url'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
userInfo: {},
|
||||
isLogin: false
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
// 设置页面标题
|
||||
uni.setNavigationBarTitle({
|
||||
title: this.$t('user.personalCenter')
|
||||
})
|
||||
},
|
||||
onShow() {
|
||||
this.loadUserInfo()
|
||||
},
|
||||
methods: {
|
||||
async loadUserInfo() {
|
||||
try {
|
||||
const res = await getUserInfo()
|
||||
if (res.code === 401 || res.code === 40101) {
|
||||
// 无提示跳转至登录
|
||||
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' })
|
||||
}
|
||||
} else if (res.code === 200) {
|
||||
this.userInfo = res.data
|
||||
this.isLogin = true
|
||||
} else {
|
||||
this.isLogin = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载用户信息失败:', error)
|
||||
this.isLogin = false
|
||||
}
|
||||
},
|
||||
navigateTo(url) {
|
||||
uni.navigateTo({ url })
|
||||
},
|
||||
handleLogout() {
|
||||
uni.showModal({
|
||||
title: this.$t('common.tips'),
|
||||
content: this.$t('user.confirmLogout'),
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('userInfo')
|
||||
this.isLogin = false
|
||||
uni.showToast({
|
||||
title: this.$t('user.logoutSuccess'),
|
||||
icon: 'success'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: '/pages/login/index'
|
||||
})
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 60rpx;
|
||||
overflow: hidden;
|
||||
margin-right: 30rpx;
|
||||
}
|
||||
|
||||
.avatar image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.phone {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.balance-card {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.balance-title {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.balance-desc {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.menu-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 36rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
margin-top: 40rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
text-align: center;
|
||||
color: #ff4d4f;
|
||||
font-size: 28rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,413 @@
|
||||
<template>
|
||||
<view class="profile-page">
|
||||
<view class="avatar-section">
|
||||
<view class="avatar-container">
|
||||
<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>
|
||||
<!-- 覆盖在头像上的微信选择头像授权按钮,仅小程序生效 -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<button class="avatar-choose-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar"></button>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
<view class="avatar-tip">{{ $t('userProfile.clickToChange') }}</view>
|
||||
</view>
|
||||
|
||||
<view class="form-section">
|
||||
<!-- 昵称编辑区域 -->
|
||||
<view class="form-item nickname-item" :class="{ editing: isEditingNickname }">
|
||||
<view class="label">{{ $t('userProfile.nickname') }}</view>
|
||||
<view class="value" v-if="!isEditingNickname" @click="startEditNickname">
|
||||
<text class="value-text">{{ userInfo.nickName || $t('userProfile.notSet') }}</text>
|
||||
<uv-icon name="edit-pen" size="16" color="#999999"></uv-icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 昵称编辑输入框(展开状态) -->
|
||||
<view class="nickname-edit-area" v-if="isEditingNickname">
|
||||
<input
|
||||
class="nickname-input"
|
||||
v-model="newNickname"
|
||||
:placeholder="$t('userProfile.enterNickname')"
|
||||
maxlength="20"
|
||||
:focus="true"
|
||||
/>
|
||||
<view class="edit-buttons">
|
||||
<button class="cancel-btn" @click="cancelEditNickname">{{ $t('common.cancel') }}</button>
|
||||
<button class="save-btn" @click="saveNickname">{{ $t('common.save') }}</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="label">{{ $t('userProfile.phone') }}</view>
|
||||
<view class="value">
|
||||
<text class="value-text">{{ userInfo.phone ? maskPhone(userInfo.phone) : $t('userProfile.notBound') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- <view class="form-item" v-if="userInfo.balanceAmount !== undefined">
|
||||
<view class="label">{{ $t('userProfile.balance') }}</view>
|
||||
<view class="value">
|
||||
<text class="value-text amount">¥{{ userInfo.balanceAmount || '0.00' }}</text>
|
||||
</view>
|
||||
</view> -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { getMyIndexInfo, uploadUserAvatar, updateUserInfo } from '@/config/api/user.js';
|
||||
import { useI18n } from '@/utils/i18n.js'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 响应式状态
|
||||
const userInfo = ref({
|
||||
nickName: '',
|
||||
phone: '',
|
||||
avatar: '',
|
||||
balanceAmount: '0.00'
|
||||
});
|
||||
|
||||
const newNickname = ref('');
|
||||
const isEditingNickname = ref(false);
|
||||
|
||||
// 页面加载时初始化
|
||||
onMounted(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: t('userProfile.title')
|
||||
})
|
||||
loadUserInfo();
|
||||
});
|
||||
|
||||
// 获取用户信息
|
||||
const loadUserInfo = async () => {
|
||||
try {
|
||||
const res = await getMyIndexInfo();
|
||||
console.log('User info response:', res);
|
||||
|
||||
if (res.code == 401 || res.code == 40101) {
|
||||
redirectToLogin();
|
||||
return;
|
||||
} else if (res.code == 200) {
|
||||
userInfo.value = {
|
||||
nickName: res.data.nickname,
|
||||
phone: res.data.phone,
|
||||
avatar: res.data.iconUrl,
|
||||
balanceAmount: res.data.balanceAmount || '0.00',
|
||||
isAdmin: res.data.isAdmin
|
||||
};
|
||||
uni.setStorageSync('userInfo', userInfo.value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
uni.showToast({
|
||||
title: t('user.getUserInfoFailed'),
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 跳转登录
|
||||
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 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('userProfile.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 loadUserInfo();
|
||||
} catch (err) {
|
||||
console.error('选择/上传头像失败:', err);
|
||||
uni.showToast({
|
||||
title: t('user.avatarUploadFailed'),
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
// 开始编辑昵称
|
||||
const startEditNickname = () => {
|
||||
newNickname.value = userInfo.value.nickName || '';
|
||||
isEditingNickname.value = true;
|
||||
};
|
||||
|
||||
// 取消编辑昵称
|
||||
const cancelEditNickname = () => {
|
||||
isEditingNickname.value = false;
|
||||
newNickname.value = '';
|
||||
};
|
||||
|
||||
// 保存昵称
|
||||
const saveNickname = async () => {
|
||||
if (!newNickname.value || !newNickname.value.trim()) {
|
||||
uni.showToast({
|
||||
title: t('userProfile.nicknameRequired'),
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: t('userProfile.saving'),
|
||||
mask: true
|
||||
});
|
||||
|
||||
// 先获取最新的用户信息,确保数据是最新的
|
||||
const latestUserInfo = await getMyIndexInfo();
|
||||
|
||||
if (latestUserInfo.code !== 200) {
|
||||
throw new Error('获取用户信息失败');
|
||||
}
|
||||
|
||||
// 使用最新的服务器数据,只修改昵称字段
|
||||
const updateData = {
|
||||
nickname: newNickname.value.trim(),
|
||||
phone: latestUserInfo.data.phone,
|
||||
iconUrl: latestUserInfo.data.iconUrl,
|
||||
// 保留其他可能的字段
|
||||
...latestUserInfo.data
|
||||
};
|
||||
|
||||
// 确保昵称使用新值
|
||||
updateData.nickname = newNickname.value.trim();
|
||||
|
||||
// 调用后端接口更新用户信息
|
||||
const res = await updateUserInfo(updateData);
|
||||
|
||||
if (res.code === 200) {
|
||||
// 更新成功后重新获取用户信息,确保数据同步
|
||||
await loadUserInfo();
|
||||
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: t('userProfile.nicknameUpdated'),
|
||||
icon: 'success'
|
||||
});
|
||||
isEditingNickname.value = false;
|
||||
} else {
|
||||
throw new Error(res.message || t('userProfile.updateFailed'));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('修改昵称失败:', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: error.message || t('userProfile.updateFailed'),
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 手机号掩码函数
|
||||
function maskPhone(phone) {
|
||||
if (!phone) return '';
|
||||
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.profile-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.avatar-section {
|
||||
background: linear-gradient(180deg, #D1FFE1 0%, #ffffff 100%);
|
||||
padding: 60rpx 0 40rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
position: relative;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 80rpx;
|
||||
background-color: #f0f0f0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 仅小程序端存在,此按钮覆盖在头像上捕获点击以触发选择头像 */
|
||||
/* #ifdef MP-WEIXIN */
|
||||
.avatar-choose-btn {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
border-radius: 80rpx;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
.avatar-tip {
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin: 20rpx 30rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx 30rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.form-item.nickname-item.editing {
|
||||
border-bottom: none;
|
||||
padding-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 30rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.value-text {
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.value-text.amount {
|
||||
color: #e2231a;
|
||||
font-weight: 600;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
/* 昵称编辑区域样式 */
|
||||
.nickname-edit-area {
|
||||
padding: 0 30rpx 30rpx 30rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
animation: slideDown 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.nickname-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
border: 1rpx solid #e0e0e0;
|
||||
border-radius: 10rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20rpx;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.edit-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.cancel-btn,
|
||||
.save-btn {
|
||||
padding: 0 40rpx;
|
||||
height: 64rpx;
|
||||
line-height: 64rpx;
|
||||
text-align: center;
|
||||
border-radius: 32rpx;
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
min-width: 120rpx;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: #f5f5f5;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background: linear-gradient(135deg, #42d392 0%, #28c76f 100%);
|
||||
color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user