fix:修复bug

This commit is contained in:
2026-02-02 14:08:17 +08:00
parent 6a1dff4b94
commit 9f66ee9658
33 changed files with 9381 additions and 153 deletions
+1 -1
View File
@@ -82,7 +82,7 @@
<!-- 底部操作区 -->
<view class="footer">
<button class="rent-button" :class="{ 'return-button': hasActiveOrder }"
@click="handleRent('wx-score-pay')">
@click="handleRent('wx-pay')">
<text>{{ hasActiveOrder ? $t('order.returnDevice') : $t('device.rentDepositFree') }}</text>
</button>
<view class="wechat-credit">
File diff suppressed because it is too large Load Diff
+710
View File
@@ -0,0 +1,710 @@
<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">
再次定制
</view>
</template>
<!-- 其他状态显示联系客服和再次定制 -->
<template v-else>
<view class="action-btn secondary" @click="onContactService">
联系客服
</view>
<view class="action-btn primary" @click="onReorder">
再次定制
</view>
</template>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } 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 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 = '待付款';
} 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 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,
// 收货信息
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,
// 兼容旧字段
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 || ''
};
// 根据订单状态更新页面标题
updatePageTitle();
}
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) => {
uni.navigateTo({
url: `/pages/device/goods?productId=${order.productId}`
});
};
</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: 26rpx;
height: 40rpx;
&: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>
+799
View File
@@ -0,0 +1,799 @@
<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 {
confirmPaymentAndRent
} from '../../config/api/device.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.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'
});
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: `/pages/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>
+120 -40
View File
@@ -24,7 +24,7 @@
:enableMarkers="true" :bannerImages="bannerImages"
:hideMapOverlays="showGuidePopup || showNoticePopup || showActivityPopup" @relocate="handleRelocate"
@scan="handleScan" @showList="showLocationList" @markerTap="selectPosition"
@mapCenterChange="onMapCenterChange" @bannerClick="handleBannerClick" />
@mapCenterChange="onMapCenterChange" @bannerClick="handleBannerClick" @guide="openGuidePopup" />
<!-- 地图加载状态 -->
<view v-if="isLoading || !userLocation" class="map-loading-placeholder">
@@ -44,11 +44,11 @@
<text class="action-label">{{ $t('home.nearbyDevices') }}</text>
</view> -->
<view class="action-btn secondary small btn-nearby" @click="openPopup">
<view class="action-btn secondary small btn-nearby" @click="goToBuy">
<view class="icon-wrap">
<image src="/static/use_help.png" class="action-icon" mode="aspectFit"></image>
<image src="/static/shop_icon.png" class="action-icon" mode="scaleToFill"></image>
</view>
<text class="action-label">{{ $t('home.useGuide') }}</text>
<text class="action-label">{{ $t('home.buyDevice') }}</text>
</view>
<view class="action-btn primary btn-scan" @click="handleScan">
@@ -206,6 +206,9 @@
getCurrentAnnouncement,
getCurrentAdvertisement
} from '../../config/api/system.js'
import {
getProductList
} from '../../config/api/product.js'
// 导入地图工具函数
import {
getUserLocation,
@@ -524,10 +527,20 @@
onMounted(() => {
initNavBarHeight()
init()
// #ifdef H5
// 监听 H5 扫码结果
uni.$on('h5ScanSuccess', (data) => {
console.log('接收到 H5 扫码结果:', data);
processScanResult(data);
});
// #endif
})
onUnmounted(() => {
// 清理工作在子组件中处理
// #ifdef H5
uni.$off('h5ScanSuccess');
// #endif
})
// 方法
@@ -564,10 +577,10 @@
} catch (error) {
console.error('初始化失败:', error)
uni.showToast({
title: t('home.getLocationFailed'),
icon: 'none'
})
// uni.showToast({
// title: t('home.getLocationFailed'),
// icon: 'none'
// })
} finally {
isLoading.value = false
}
@@ -760,10 +773,10 @@
try {
isRelocating.value = true
uni.showLoading({
title: t('home.relocating'),
mask: true
})
// uni.showLoading({
// title: t('home.relocating'),
// mask: true
// })
// 重新获取用户真实位置
const loc = await getUserLocation()
@@ -804,20 +817,20 @@
uni.hideLoading()
uni.showToast({
title: t('home.locateSuccess'),
icon: 'success',
duration: 1500
})
// uni.showToast({
// title: t('home.locateSuccess'),
// icon: 'success',
// duration: 1500
// })
} catch (e) {
console.error('定位失败:', e)
uni.hideLoading()
uni.showToast({
title: e.errMsg || t('home.locateFailed'),
icon: 'none',
duration: 2000
})
// uni.showToast({
// title: e.errMsg || t('home.locateFailed'),
// icon: 'none',
// duration: 2000
// })
} finally {
// 1秒后解除防抖锁定
setTimeout(() => {
@@ -866,6 +879,53 @@
})
}
const goToBuy = async () => {
try {
uni.showLoading({
title: t('common.loading'),
mask: true
})
// 查询商品列表
const res = await getProductList({
pageNum: 1,
pageSize: 10
})
uni.hideLoading()
console.log('商品列表查询结果:', res)
// 根据实际返回格式:data.rows 是商品列表数组
if (res && res.code === 200 && res.data && res.data.rows && res.data.rows.length > 0) {
// 获取第一个商品的ID
const firstProduct = res.data.rows[0]
const productId = firstProduct.id
console.log('获取到商品ID:', productId)
// 跳转到商品详情页面,传递商品ID
uni.navigateTo({
url: `/pages/device/goods?productId=${productId}`
})
} else {
console.warn('没有查询到商品数据')
// 如果没有商品数据,仍然跳转到商品页面(显示空状态)
uni.navigateTo({
url: '/pages/device/goods'
})
}
} catch (error) {
console.error('查询商品列表失败:', error)
uni.hideLoading()
// 即使查询失败,也跳转到商品页面
uni.navigateTo({
url: '/pages/device/goods'
})
}
}
const selectPositionFromPopup = (position) => {
// 先关闭弹窗
hideLocationList()
@@ -905,6 +965,13 @@
}
const handleScan = async () => {
// #ifdef H5
uni.navigateTo({
url: '/pages/scan/index'
});
return;
// #endif
try {
const scanResult = await new Promise((resolve, reject) => {
uni.scanCode({
@@ -914,16 +981,23 @@
})
console.log(scanResult);
let deviceNo;
if (scanResult.scanType=='"QR_CODE"') {
processScanResult(scanResult);
} catch (error) {
console.error('扫码处理失败:', error)
}
}
const processScanResult = async (scanResult) => {
try {
let deviceNo;
if (scanResult.scanType == 'MANUAL') {
deviceNo = scanResult.result;
} else if (scanResult.scanType == '"QR_CODE"') {
deviceNo = getQueryString(scanResult.result, 'deviceNo')
} else {
deviceNo = getQueryString(scanResult.path, 'deviceNo')
deviceNo = getQueryString(scanResult.path || scanResult.result, 'deviceNo')
}
if (!deviceNo) {
uni.showToast({
title: t('home.invalidQRCode'),
@@ -975,10 +1049,10 @@
})
}
} else {
uni.showToast({
title: t('device.getDeviceInfoFailed'),
icon: 'none'
})
// uni.showToast({
// title: t('device.getDeviceInfoFailed'),
// icon: 'none'
// })
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
@@ -991,11 +1065,7 @@
}
}
} catch (error) {
console.error('扫码处理失败:', error)
// uni.showToast({
// title: '扫码失败',
// icon: 'none'
// })
console.error('处理扫码结果失败:', error)
}
}
@@ -1015,6 +1085,16 @@
// 使用指南弹窗控制
const openPopup = () => {
uni.navigateTo({
url:'/pages/device/goods'
})
// try {
// showGuidePopup.value = true
// guidePopup.value && typeof guidePopup.value.open === 'function' && guidePopup.value.open()
// } catch (e) {}
}
const openGuidePopup = () => {
try {
showGuidePopup.value = true
guidePopup.value && typeof guidePopup.value.open === 'function' && guidePopup.value.open()
@@ -1494,8 +1574,8 @@
// box-shadow: 0 8rpx 24rpx rgba(62, 171, 100, 0.3);
.icon-wrap {
width: 48rpx;
height: 48rpx;
width: 52rpx;
height: 52rpx;
display: flex;
align-items: center;
justify-content: center;
@@ -1745,7 +1825,7 @@
border-radius: 20rpx;
}
/* 使用指南弹窗样式 */
/* 弹窗样式 */
.guide-popup {
width: 640rpx;
max-width: 86vw;
+13 -10
View File
@@ -3,7 +3,7 @@
<!-- 会员卡列表 -->
<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: 200rpx;'">
@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;">
<!-- 卡片头部标题和日期 -->
@@ -22,9 +22,9 @@
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: 26rpx; padding: 6rpx 20rpx;"
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: #D4A574;">{{ $t('myCard.toUse') }}</text>
<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)">
@@ -33,8 +33,7 @@
</view>
</view>
<!-- 使用情况和操作按钮 -->
<view style="position: absolute; bottom: -10rpx; left: 0; right: 0; padding: 20rpx;z-index:1;"
v-if="card.cardType==='COUNT'">
<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">
@@ -42,8 +41,8 @@
$t('myCard.times') }}</text>
</view>
<!-- 时长卡信息 -->
<view v-if="card.cardType === 'TIME'" class="card-usage-info">
<text class="usage-text">{{ $t('myCard.durationCard') }}</text>
<view v-else class="card-usage-info">
<text class="usage-text">每日限用次数{{card.dailyLimitCount}}</text>
</view>
<!-- 操作按钮 -->
@@ -52,7 +51,10 @@
<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>
<!-- <text class="arrow">{{ '>' }}</text> -->
</view>
<view v-else class="renew-btn">
<text class="renew-text">单次限时{{card.singleLimitMinutes}}分钟</text>
</view>
</view>
@@ -356,7 +358,8 @@
display: flex;
justify-content: space-between;
align-items: center;
/* padding-top: 24rpx ; */
padding: 20rpx;
padding-top: 50rpx;
position: absolute;
background: rgba(255, 244, 227, 1);
z-index: 1;
@@ -364,7 +367,7 @@
bottom: 0;
left: 0;
border-radius: 0 0 25rpx 25rpx;
padding: 20rpx;
/* text-align: 30rpx ; */
}
+8 -5
View File
@@ -40,7 +40,7 @@
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 === 'available'" @click="useCoupon(coupon)">
<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>
@@ -85,7 +85,7 @@ const couponList = ref([])
// 过滤后的优惠券
const filteredCoupons = computed(() => {
return couponList.value.filter(coupon => coupon.status === currentTab.value)
return couponList.value;
})
// 获取优惠券列表
@@ -114,6 +114,7 @@ const getCouponList = async () => {
} else if (item.couponEndTime) {
validity = `${item.couponEndTime.split(' ')[0]} 过期`
}
console.log(item.status);
return {
id: item.id,
@@ -123,7 +124,7 @@ const getCouponList = async () => {
discount: item.discountRate ? parseFloat(item.discountRate) * 10 : null,
condition: condition,
validity: validity,
status: currentTab.value,
status: item.status,
positionName: item.positionName
}
})
@@ -148,15 +149,17 @@ const switchTab = (tab) => {
// 获取优惠券样式类名
const getCouponClass = (status) => {
return status === 'available' ? '' : 'disabled'
return status === 'unused' ? '' : 'disabled'
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
'used': t('myCoupon.usedStatus'),
'expired': t('myCoupon.expiredStatus')
'expired': t('myCoupon.expiredStatus'),
'refunded':t('myCoupon.refundedStatus')
}
console.log("获取状态文本:"+statusMap[status]);
return statusMap[status] || ''
}
+106 -78
View File
@@ -89,7 +89,8 @@
<view class="rent-label">{{ $t('order.usedPromotion') }}</view>
<view class="rent-value promotion-value">
<image src="/static/promotion-icon.png" class="promotion-icon" mode="aspectFit"></image>
{{ orderInfo.discountTypeName }}
{{ orderInfo.discountTypeName }}<text
v-if="orderInfo.discountAmount">{{'-'+orderInfo.discountAmount||''}}</text>
</view>
</view>
<view class="rent-item" v-if="isOrderCompleted() && orderInfo.endTime">
@@ -108,7 +109,8 @@
</view>
<view class="">
<view class="" style="font-size: 24rpx;text-align: center;">
{{ $t('order.returnProblemTip') }}<text @click="contactService" style="color:#07c160 ;">{{ $t('user.customerService') }}</text>{{ $t('order.contactStaff') }}
{{ $t('order.returnProblemTip') }}<text @click="contactService"
style="color:#07c160 ;">{{ $t('user.customerService') }}</text>{{ $t('order.contactStaff') }}
</view>
</view>
@@ -161,7 +163,8 @@
<!-- 已完成状态 -->
<template v-if="isOrderCompleted()">
<view class="bottom-icon-btn" @click="handleWithdraw" v-if="!orderInfo.isWithdrawn && orderInfo.refundAmount > 0">
<view class="bottom-icon-btn" @click="handleWithdraw"
v-if="!orderInfo.isWithdrawn && orderInfo.refundAmount > 0">
<image src="/static/suggess.png" class="icon" mode="aspectFit"></image>
<text>{{ $t('order.feeAppeal') }}</text>
</view>
@@ -187,14 +190,8 @@
</view>
<!-- 转为自用确认弹窗 -->
<uv-popup
ref="convertToOwnPopup"
mode="center"
round="20"
:overlay="true"
:closeOnClickOverlay="false"
:safeAreaInsetBottom="false"
>
<uv-popup ref="convertToOwnPopup" mode="center" round="20" :overlay="true" :closeOnClickOverlay="false"
:safeAreaInsetBottom="false">
<view class="convert-to-own-popup">
<view class="popup-header">
<text class="popup-title">{{ $t('order.convertToOwnWithMaxFeeTitle') }}</text>
@@ -216,8 +213,19 @@
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, getCurrentInstance } from 'vue'
import { onLoad, onShow, onHide, onUnload } from '@dcloudio/uni-app'
import {
ref,
computed,
onMounted,
onUnmounted,
getCurrentInstance
} from 'vue'
import {
onLoad,
onShow,
onHide,
onUnload
} from '@dcloudio/uni-app'
import {
queryById,
cancelOrder,
@@ -238,9 +246,13 @@
import {
URL
} from "@/config/url.js"
import { useI18n } from '@/utils/i18n.js'
import {
useI18n
} from '@/utils/i18n.js'
const { t } = useI18n()
const {
t
} = useI18n()
const instance = getCurrentInstance()
const $orderMonitor = instance?.proxy?.$orderMonitor || null
@@ -274,7 +286,9 @@
canUseCoupon: false,
userMemberCardId: '',
userPurchaseId: '',
discountTypeName: ''
discountTypeName: '',
originalFee:'',
discountAmount:''
})
const timer = ref(null)
const statusCheckTimer = ref(null)
@@ -289,12 +303,12 @@
const countdownTimer = ref(null)
const feeRuleText = ref('')
const convertToOwnPopup = ref(null)
// 计算属性:是否显示优惠券/会员卡可用提示
const canUsePromotionTag = computed(() => {
return orderInfo.value.canUseMember === true || orderInfo.value.canUseCoupon === true
})
// 计算属性:是否使用了优惠
const hasUsedPromotion = computed(() => {
return !!(orderInfo.value.userMemberCardId || orderInfo.value.userPurchaseId)
@@ -308,11 +322,11 @@
}
return '-'
}
// 判断订单是否已完成
const isOrderCompleted = () => {
return orderInfo.value.orderStatus === 'used_done' ||
orderInfo.value.orderStatus === 'used_down'
return orderInfo.value.orderStatus === 'used_done' ||
orderInfo.value.orderStatus === 'used_down'
}
// 获取订单状态文字
@@ -362,13 +376,13 @@
// 获取计费规则显示
const getPricingRuleDisplay = () => {
if (!orderInfo.value.unitPrice || !orderInfo.value.orderType) return '-'
const orderTypeMap = {
'hours': t('time.hours'),
'minutes': t('time.minutes'),
'halfhours': t('time.halfHours')
}
const orderTypeText = orderTypeMap[orderInfo.value.orderType] || orderInfo.value.orderType
return `${orderInfo.value.unitPrice}${t('unit.yuan')}/${orderTypeText}`
}
@@ -407,39 +421,40 @@
// 获取使用时长显示信息
const getUsedTimeDisplay = () => {
let usedTime = orderInfo.value.usedTime
// 如果 usedTime 为空,通过开始时间和结束时间计算
if (usedTime === '0分钟' && orderInfo.value.startTime && orderInfo.value.endTime) {
const startMs = parseStartTimeToMs(orderInfo.value.startTime)
const endMs = parseStartTimeToMs(orderInfo.value.endTime)
if (!isNaN(startMs) && !isNaN(endMs)) {
const diffMs = endMs - startMs
const totalMinutes = Math.floor(diffMs / (1000 * 60))
// 格式化为 "X小时X分钟" 或 "X分钟"
if (totalMinutes >= 60) {
const hours = Math.floor(totalMinutes / 60)
const minutes = totalMinutes % 60
usedTime = minutes > 0 ? `${hours}${t('time.hour')}${minutes}${t('time.minute')}` : `${hours}${t('time.hour')}`
usedTime = minutes > 0 ? `${hours}${t('time.hour')}${minutes}${t('time.minute')}` :
`${hours}${t('time.hour')}`
} else {
usedTime = `${totalMinutes}${t('time.minute')}`
}
}
}
// 如果还是没有值,使用默认值
if (!usedTime) {
usedTime = `0${t('time.minute')}`
}
// 解析时长字符串,例如 "1小时5分钟" 或 "5分钟"
const hourMatch = usedTime.match(new RegExp(`(\\d+)${t('time.hour')}`))
const minuteMatch = usedTime.match(new RegExp(`(\\d+)${t('time.minute')}`))
let displayNumber = ''
let displayUnit = ''
if (hourMatch && minuteMatch) {
// 有小时也有分钟,如 "1小时5分钟"
displayNumber = `${hourMatch[1]}`
@@ -457,7 +472,7 @@
displayNumber = '0'
displayUnit = t('time.minute')
}
return {
number: displayNumber,
unit: displayUnit
@@ -472,7 +487,8 @@
// 获取订单费用(不含单位)
const getOrderFee = () => {
const fee = orderInfo.value.currentFee || orderInfo.value.payAmount || '0'
const fee = orderInfo.value.originalFee || orderInfo.value.originalFee || '0'
// 移除可能的"元"字符
return String(fee).replace(/[元¥]/g, '')
}
@@ -526,7 +542,7 @@
countdownRemaining.value = 0
return
}
if (orderInfo.value.orderStatus !== 'in_used') {
showExpressAction.value = false
countdownRemaining.value = 0
@@ -559,7 +575,7 @@
countdownRemaining.value = 0
return
}
clearExpressCountdown()
recomputeExpressCountdownFromStartTime()
if (showExpressAction.value) return
@@ -590,13 +606,16 @@
const loadSystemConfig = async () => {
try {
// 优先使用订单数据中的 expressReturnStart(小时转秒)
if (orderInfo.value.expressReturnStart && typeof orderInfo.value.expressReturnStart === 'number' && orderInfo.value.expressReturnStart > 0) {
if (orderInfo.value.expressReturnStart && typeof orderInfo.value.expressReturnStart === 'number' &&
orderInfo.value.expressReturnStart > 0) {
expressThresholdSeconds.value = orderInfo.value.expressReturnStart * 3600
console.log('使用订单配置的快递归还阈值:', orderInfo.value.expressReturnStart, '小时 =>', expressThresholdSeconds.value, '秒')
console.log('使用订单配置的快递归还阈值:', orderInfo.value.expressReturnStart, '小时 =>', expressThresholdSeconds
.value, '秒')
} else {
// 如果订单数据中没有,则使用系统配置
const res = await getSystemConfig()
if (res && res.code === 200 && res.data && typeof res.data.expressReturnCountdownSeconds === 'number') {
if (res && res.code === 200 && res.data && typeof res.data.expressReturnCountdownSeconds ===
'number') {
const seconds = res.data.expressReturnCountdownSeconds
if (seconds > 0) {
expressThresholdSeconds.value = seconds
@@ -604,7 +623,7 @@
}
}
}
if (orderInfo.value.orderStatus === 'in_used' && orderInfo.value.startTime) {
recomputeExpressCountdownFromStartTime()
}
@@ -692,13 +711,13 @@
clearStatusCheckTimer()
return
}
if (orderInfo.value.orderStatus !== 'in_used') {
console.log('订单状态已变更,停止状态检查计时器')
clearStatusCheckTimer()
return
}
currentStatusChecks.value++
console.log(`执行归还状态检查 (${currentStatusChecks.value}/${maxStatusChecks.value})`)
checkReturnStatus()
@@ -730,9 +749,10 @@
// 更新订单信息
const updateOrderInfo = (orderData) => {
const oldStatus = orderInfo.value.orderStatus
orderInfo.value.usedTime = orderData.usedTime || '0分钟'
orderInfo.value.currentFee = orderData.currentFee || orderData.actualDeviceAmount || orderData.payAmount || '0.00'
orderInfo.value.currentFee = orderData.currentFee || orderData.actualDeviceAmount || orderData.payAmount ||
'0.00'
orderInfo.value.orderStatus = orderData.orderStatus || ''
orderInfo.value.payWay = orderData.payWay || ''
orderInfo.value.startTime = orderData.startTime || orderData.createTime || ''
@@ -747,47 +767,52 @@
orderInfo.value.withdrawStatus = orderData.withdrawStatus || 'waiting'
orderInfo.value.isWithdrawn = orderData.withdrawStatus === 'success'
orderInfo.value.positionName = orderData.positionName || orderData.positionLocation || ''
orderInfo.value.returnPosition = orderData.returnPosition || orderData.positionName || orderData.positionLocation || ''
orderInfo.value.returnPosition = orderData.returnPosition || orderData.positionName || orderData
.positionLocation || ''
orderInfo.value.freeRentTime = orderData.freeRentTime || ''
orderInfo.value.unitPrice = orderData.unitPrice || ''
orderInfo.value.orderType = orderData.orderType || ''
orderInfo.value.discountAmount = orderData.discountAmount||''
// 保存优惠券/会员卡相关信息
orderInfo.value.canUseMember = orderData.canUseMember === true
orderInfo.value.canUseCoupon = orderData.canUseCoupon === true
orderInfo.value.userMemberCardId = orderData.userMemberCardId || ''
orderInfo.value.userPurchaseId = orderData.userPurchaseId || ''
orderInfo.value.discountTypeName = orderData.discountTypeName || ''
orderInfo.value.originalFee = orderData.originalFee||''
// 保存快递归还开始时间(小时为单位)
orderInfo.value.expressReturnStart = orderData.expressReturnStart || null
// 保存是否支持快递归还
orderInfo.value.isSupportExpressReturn = orderData.isSupportExpressReturn || 'yes'
// 如果有有效的 expressReturnStart,立即更新倒计时阈值(小时转秒)
if (orderInfo.value.expressReturnStart && typeof orderInfo.value.expressReturnStart === 'number' && orderInfo.value.expressReturnStart > 0) {
if (orderInfo.value.expressReturnStart && typeof orderInfo.value.expressReturnStart === 'number' && orderInfo
.value.expressReturnStart > 0) {
expressThresholdSeconds.value = orderInfo.value.expressReturnStart * 3600
console.log('从订单数据更新快递归还阈值:', orderInfo.value.expressReturnStart, '小时 =>', expressThresholdSeconds.value, '秒')
console.log('从订单数据更新快递归还阈值:', orderInfo.value.expressReturnStart, '小时 =>', expressThresholdSeconds.value,
'秒')
}
if (orderData.deviceNo && !deviceId.value) {
deviceId.value = orderData.deviceNo
}
// 如果订单已完成,从监控中移除
if (isOrderCompleted()) {
removeFromOrderMonitor()
console.log('订单已完成,已从监控队列移除')
}
// 如果订单状态从 'in_used' 变为其他状态,清理所有定时器
if (oldStatus === 'in_used' && orderInfo.value.orderStatus !== 'in_used') {
console.log('订单状态已从使用中变为:', orderInfo.value.orderStatus, ',清理所有定时器')
clearTimer()
clearStatusCheckTimer()
clearExpressCountdown()
// 显示订单完成提示
if (orderInfo.value.orderStatus === 'used_done' || orderInfo.value.orderStatus === 'used_down') {
uni.showToast({
@@ -841,7 +866,8 @@
// 如果订单已完成,从监控中移除
if (isOrderCompleted()) {
removeFromOrderMonitor()
console.log('订单已完成,已从监控队列移除:', orderInfo.value.orderId, '当前状态:', orderInfo.value.orderStatus)
console.log('订单已完成,已从监控队列移除:', orderInfo.value.orderId, '当前状态:', orderInfo.value
.orderStatus)
} else {
// 订单未完成,添加到监控队列
uni.setStorageSync('activeOrderId', orderInfo.value.orderId)
@@ -853,7 +879,8 @@
$orderMonitor.addOrder({
orderId: orderInfo.value.orderId
}, 'detail')
console.log('订单已添加到监控队列:', orderInfo.value.orderId, '当前状态:', orderInfo.value.orderStatus)
console.log('订单已添加到监控队列:', orderInfo.value.orderId, '当前状态:', orderInfo.value
.orderStatus)
} else {
console.warn('$orderMonitor 未定义,无法添加订单到监控队列')
}
@@ -1035,7 +1062,7 @@
}
})
})
uni.showToast({
title: t('payment.subscriptionSuccess'),
icon: 'success',
@@ -1055,7 +1082,8 @@
// 处理"不想还了转为自用"(按最高费用)
const handleConvertToOwnedWithMaxFee = () => {
try {
convertToOwnPopup.value && typeof convertToOwnPopup.value.open === 'function' && convertToOwnPopup.value.open()
convertToOwnPopup.value && typeof convertToOwnPopup.value.open === 'function' && convertToOwnPopup.value
.open()
} catch (e) {
console.error('打开弹窗失败', e)
}
@@ -1064,7 +1092,8 @@
// 关闭转为自用弹窗
const closeConvertToOwnPopup = () => {
try {
convertToOwnPopup.value && typeof convertToOwnPopup.value.close === 'function' && convertToOwnPopup.value.close()
convertToOwnPopup.value && typeof convertToOwnPopup.value.close === 'function' && convertToOwnPopup.value
.close()
} catch (e) {
console.error('关闭弹窗失败', e)
}
@@ -1077,9 +1106,9 @@
uni.showLoading({
title: t('common.processing')
})
const result = await closeWithMaxFee(orderInfo.value.orderNo)
if (result.code === 200) {
uni.hideLoading()
uni.showToast({
@@ -1087,7 +1116,7 @@
icon: 'success',
duration: 2000
})
// 刷新订单详情
setTimeout(() => {
getOrderDetails()
@@ -1115,7 +1144,7 @@
})
isPageActive.value = true
// 从缓存读取通知内容(计费规则)
try {
const cachedNotice = uni.getStorageSync('noticeContent')
@@ -1196,7 +1225,7 @@
.header-left {
flex: 1;
}
.header-title-row {
display: flex;
align-items: center;
@@ -1218,7 +1247,7 @@
.header-right {
display: flex;
align-items: center;
height:100%;
height: 100%;
text-align: center;
}
@@ -1227,7 +1256,7 @@
flex-direction: column;
align-items: center;
justify-content: center;
// padding: 12rpx 20rpx;
// background: #E8F5E9;
// border-radius: 12rpx;
@@ -1251,7 +1280,7 @@
}
}
}
.promotion-tag {
padding: 6rpx 16rpx;
background: linear-gradient(135deg, #FFF4E6 0%, #FFE9CC 100%);
@@ -1366,7 +1395,7 @@
border-radius: 12rpx;
font-size: 26rpx;
z-index: 9;
&:active {
opacity: 0.85;
}
@@ -1408,14 +1437,14 @@
color: #333;
text-align: right;
max-width: 400rpx;
&.promotion-value {
display: flex;
align-items: center;
justify-content: flex-end;
color: #FF8C00;
font-weight: 500;
.promotion-icon {
width: 32rpx;
height: 32rpx;
@@ -1431,7 +1460,7 @@
align-items: baseline;
padding-top: 20rpx;
margin-top: 10rpx;
border-top: 1rpx solid #f0f0f0;
// border-top: 1rpx solid #f0f0f0;
.paid-label {
font-size: 28rpx;
@@ -1580,7 +1609,7 @@
padding: 0 0 40rpx;
box-sizing: border-box;
text-align: left;
.popup-text {
font-size: 28rpx;
color: #666;
@@ -1596,7 +1625,7 @@
border-top: 1rpx solid #f0f0f0;
margin-left: -40rpx;
margin-right: -40rpx;
.popup-btn {
flex: 1;
height: 88rpx;
@@ -1605,20 +1634,20 @@
justify-content: center;
font-size: 32rpx;
box-sizing: border-box;
&.cancel-btn {
color: #666;
border-right: 1rpx solid #f0f0f0;
&:active {
background-color: #f5f5f5;
}
}
&.confirm-btn {
color: #07c160;
font-weight: 600;
&:active {
background-color: #f0f9f4;
}
@@ -1626,5 +1655,4 @@
}
}
}
</style>
</style>
+442
View File
@@ -0,0 +1,442 @@
<template>
<view class="scan-page">
<!-- 扫码区域 -->
<view class="scan-window">
<video id="scanVideo" ref="videoRef" class="scan-video" autoplay playsinline muted></video>
<canvas id="scanCanvas" ref="canvasRef" class="hidden-canvas" style="display: none;"></canvas>
<!-- 扫描装饰 -->
<view class="scan-mask">
<view class="scan-frame">
<view class="scan-line"></view>
<view class="corner top-left"></view>
<view class="corner top-right"></view>
<view class="corner bottom-left"></view>
<view class="corner bottom-right"></view>
</view>
</view>
<view class="scan-tip">{{ tipText }}</view>
</view>
<!-- 底部操作栏 -->
<view class="bottom-actions">
<view class="action-item" @click="chooseImage">
<uv-icon name="photo" size="28" color="#fff"></uv-icon>
<text>相册</text>
</view>
<view class="action-item" @click="toggleInput">
<uv-icon name="edit-pen" size="28" color="#fff"></uv-icon>
<text>手动输入</text>
</view>
<view class="action-item" @click="goBack">
<uv-icon name="arrow-left" size="28" color="#fff"></uv-icon>
<text>返回</text>
</view>
</view>
<!-- 手动输入弹窗 -->
<uv-popup ref="inputPopup" mode="center" round="16" :closeOnClickOverlay="true">
<view class="input-dialog">
<view class="dialog-title">手动输入设备号</view>
<input
v-model="manualDeviceNo"
placeholder="请输入设备上的编号"
class="device-input"
type="text"
focus
/>
<view class="dialog-btns">
<button class="cancel-btn" @click="closeInput">取消</button>
<button class="confirm-btn" @click="confirmManualInput">确定</button>
</view>
</view>
</uv-popup>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { getQueryString } from '@/util/index.js';
const videoRef = ref(null);
const canvasRef = ref(null);
const inputPopup = ref(null);
const manualDeviceNo = ref('');
const tipText = ref('正在启动扫描...');
const scanning = ref(false);
let stream = null;
let animationId = null;
// 动态加载解码库
const loadJsQR = () => {
return new Promise((resolve, reject) => {
if (window.jsQR) {
resolve(window.jsQR);
return;
}
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js';
script.onload = () => resolve(window.jsQR);
script.onerror = (e) => {
console.error('jsQR 加载失败:', e);
reject(new Error('解码组件加载失败,请检查网络'));
};
document.head.appendChild(script);
});
};
const initScan = async () => {
try {
// 1. 检查环境
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw new Error('您的浏览器不支持摄像头访问,请使用微信扫描或手动输入');
}
// 2. 加载解码库
const jsQR = await loadJsQR();
// 3. 启动摄像头 - 尝试逐步降低约束
let constraints = {
video: {
facingMode: 'environment',
width: { ideal: 1280 },
height: { ideal: 720 }
}
};
try {
stream = await navigator.mediaDevices.getUserMedia(constraints);
} catch (e) {
console.warn('尝试理想约束失败,降级请求:', e);
// 降级:仅请求视频,不限制分辨率和模式
stream = await navigator.mediaDevices.getUserMedia({ video: true });
}
if (videoRef.value) {
const video = videoRef.value;
// 关键:在赋值 srcObject 之前先重置
video.pause();
video.srcObject = stream;
// 使用更稳健的事件监听
const startPlay = () => {
video.play().then(() => {
console.log('视频开始播放');
scanning.value = true;
tipText.value = '将二维码放入框内即可自动扫描';
tick(jsQR);
}).catch(e => {
console.error('视频播放 Promise 失败:', e);
// 如果是由于用户交互限制,提示用户点击
tipText.value = '请点击屏幕启动扫描';
});
};
if (video.readyState >= 2) {
startPlay();
} else {
video.onloadedmetadata = startPlay;
}
}
} catch (err) {
console.error('摄像头初始化失败:', err);
let errMsg = '摄像头开启失败';
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
errMsg = '请授予摄像头访问权限后重试';
} else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
errMsg = '未找到可用的摄像头';
} else if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
errMsg = '非加密连接(HTTPS)无法开启摄像头';
}
tipText.value = errMsg;
uni.showModal({
title: '提示',
content: errMsg,
showCancel: false,
success: () => {
// 如果失败且无法恢复,引导手动输入
toggleInput();
}
});
}
};
const tick = (jsQR) => {
if (!scanning.value) return;
const video = videoRef.value;
const canvas = canvasRef.value;
if (video && video.readyState === video.HAVE_ENOUGH_DATA && canvas) {
const ctx = canvas.getContext('2d');
canvas.height = video.videoHeight;
canvas.width = video.videoWidth;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: 'dontInvert',
});
if (code && code.data) {
console.log('扫码结果:', code.data);
handleSuccess(code.data);
return; // 停止循环
}
}
animationId = requestAnimationFrame(() => tick(jsQR));
};
const handleSuccess = (result) => {
stopScan();
// 通过全局事件通知首页
uni.$emit('h5ScanSuccess', {
result: result,
scanType: 'QR_CODE'
});
uni.navigateBack();
};
const stopScan = () => {
scanning.value = false;
if (stream) {
stream.getTracks().forEach(track => track.stop());
stream = null;
}
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
};
const chooseImage = () => {
uni.chooseImage({
count: 1,
sourceType: ['album'],
success: async (res) => {
uni.showLoading({ title: '正在识别...' });
try {
const jsQR = await loadJsQR();
const img = new Image();
img.src = res.tempFilePaths[0];
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height);
uni.hideLoading();
if (code && code.data) {
handleSuccess(code.data);
} else {
uni.showToast({ title: '未识别到二维码', icon: 'none' });
}
};
} catch (e) {
uni.hideLoading();
uni.showToast({ title: '识别出错', icon: 'none' });
}
}
});
};
const toggleInput = () => {
inputPopup.value.open();
};
const closeInput = () => {
inputPopup.value.close();
};
const confirmManualInput = () => {
if (!manualDeviceNo.value) return;
let deviceNo = manualDeviceNo.value.trim();
if (deviceNo.includes('deviceNo=')) {
deviceNo = getQueryString(deviceNo, 'deviceNo') || deviceNo;
}
closeInput();
stopScan();
uni.$emit('h5ScanSuccess', {
result: deviceNo,
scanType: 'MANUAL'
});
uni.navigateBack();
};
const goBack = () => {
uni.navigateBack();
};
onMounted(() => {
initScan();
});
onUnmounted(() => {
stopScan();
});
</script>
<style lang="scss" scoped>
.scan-page {
width: 100vw;
height: 100vh;
background: #000;
position: relative;
overflow: hidden;
}
.scan-window {
width: 100%;
height: 100%;
position: relative;
.scan-video {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.scan-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
.scan-frame {
width: 500rpx;
height: 500rpx;
position: relative;
box-shadow: 0 0 0 1000px rgba(0, 0, 0, 0.4);
.scan-line {
width: 100%;
height: 4rpx;
background: linear-gradient(to right, transparent, #3EAB64, transparent);
position: absolute;
top: 0;
animation: scanMove 3s linear infinite;
}
.corner {
position: absolute;
width: 40rpx;
height: 40rpx;
border: 6rpx solid #3EAB64;
}
.top-left { top: -2rpx; left: -2rpx; border-right: none; border-bottom: none; }
.top-right { top: -2rpx; right: -2rpx; border-left: none; border-bottom: none; }
.bottom-left { bottom: -2rpx; left: -2rpx; border-right: none; border-top: none; }
.bottom-right { bottom: -2rpx; right: -2rpx; border-left: none; border-top: none; }
}
}
@keyframes scanMove {
0% { top: 0; }
100% { top: 100%; }
}
.scan-tip {
position: absolute;
top: 15%;
left: 0;
right: 0;
text-align: center;
color: #fff;
font-size: 28rpx;
padding: 0 40rpx;
}
.bottom-actions {
position: absolute;
bottom: 80rpx;
left: 0;
right: 0;
display: flex;
justify-content: space-around;
padding: 0 60rpx;
.action-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
text {
color: #fff;
font-size: 24rpx;
}
}
}
.input-dialog {
width: 600rpx;
background: #fff;
padding: 40rpx;
border-radius: 24rpx;
.dialog-title {
font-size: 32rpx;
font-weight: 600;
text-align: center;
margin-bottom: 40rpx;
}
.device-input {
height: 88rpx;
background: #F8F9FA;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 28rpx;
border: 1rpx solid #eee;
margin-bottom: 40rpx;
}
.dialog-btns {
display: flex;
gap: 20rpx;
button {
flex: 1;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
border-radius: 40rpx;
border: none;
}
.cancel-btn {
background: #f5f5f5;
color: #666;
}
.confirm-btn {
background: #3EAB64;
color: #fff;
}
}
}
</style>
+4 -1
View File
@@ -122,8 +122,11 @@
url: `/pages/device/detail?deviceNo=${option.deviceNo}`
});
} else {
// uni.switchTab({
// url:'/pages/index/index'
// })
// 如果连deviceNo都没有,则返回首页
uni.switchTab({ url: '/pages/index/index' });
uni.reLaunch({ url: '/pages/index/index' });
}
}, 2000);
} finally {