wx-shop/pages/order/detail.vue
2025-11-25 22:28:34 +08:00

639 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="order-detail-page">
<view class="detail-content" v-if="orderInfo">
<view class="card">
<view class="section-title">
<text>订单概览</text>
<text class="status-tag" :class="statusClass">{{ statusText }}</text>
</view>
<view class="row">
<text class="label">订单编号</text>
<text class="value">{{ orderInfo.orderNo }}</text>
</view>
<view class="row">
<text class="label">创建时间</text>
<text class="value">{{ orderInfo.createdAt }}</text>
</view>
<view class="row amount-row">
<text class="label">应付金额</text>
<text class="amount">¥{{ orderInfo.payableAmount }}</text>
</view>
</view>
<view class="card warning-card" v-if="showPaymentWarning">
<view class="warning-title">
<text>付款提醒</text>
<text class="countdown" v-if="countdownText">{{ countdownText }}</text>
</view>
<text class="warning-text">
订单生成后需在15分钟内完成支付逾期将自动取消
</text>
</view>
<view class="card">
<view class="section-title">
<text>商品信息</text>
<text class="sub-info">{{ totalQuantity }}</text>
</view>
<view v-if="orderInfo.products.length" class="product-list">
<view class="product-item" v-for="item in orderInfo.products" :key="item.id">
<image class="product-img" :src="item.image" mode="aspectFill" />
<view class="product-info">
<view class="product-name-row">
<text class="product-name">{{ item.name }}</text>
<text class="product-tag" v-if="item.isHot">热销</text>
<text class="product-tag limited" v-if="item.isLimited">限量</text>
</view>
<text class="product-sku" v-if="item.skuName">{{ item.skuName }}</text>
<text class="product-desc" v-if="item.desc">{{ item.desc }}</text>
<view class="product-meta">
<text class="product-price">¥{{ item.price }}</text>
<text class="product-qty">x{{ item.quantity }}</text>
</view>
</view>
</view>
</view>
<view class="empty-tip" v-else>暂无商品信息</view>
</view>
<view class="card">
<view class="section-title">
<text>配送信息</text>
<text class="sub-info">{{ shippingStatusText }}</text>
</view>
<view class="row">
<text class="label">收件人</text>
<text class="value">{{ orderInfo.delivery.receiverName }}</text>
</view>
<view class="row">
<text class="label">联系电话</text>
<text class="value">{{ orderInfo.delivery.phone }}</text>
</view>
<view class="row">
<text class="label">收货地址</text>
<text class="value address">{{ orderInfo.delivery.fullAddress }}</text>
</view>
<view class="row">
<text class="label">物流公司</text>
<text class="value">{{ orderInfo.delivery.logisticsCompany }}</text>
</view>
<view class="row">
<text class="label">运单号</text>
<text class="value">{{ orderInfo.delivery.trackingNumber }}</text>
</view>
<view class="row">
<text class="label">预计送达</text>
<text class="value">{{ orderInfo.delivery.estimatedDelivery }}</text>
</view>
</view>
<view class="card">
<view class="section-title">
<text>金额明细</text>
</view>
<view class="row">
<text class="label">商品总额</text>
<text class="value">¥{{ orderInfo.originalAmount }}</text>
</view>
<view class="row">
<text class="label">优惠券抵扣</text>
<text class="value">-¥{{ orderInfo.couponAmount }}</text>
</view>
<view class="row" v-if="orderInfo.pointsUsed">
<text class="label">积分抵扣({{ orderInfo.pointsUsed }}积分)</text>
<text class="value">-¥{{ orderInfo.pointsAmount }}</text>
</view>
<view class="row total-row">
<text class="label">实付金额</text>
<text class="amount">¥{{ orderInfo.payableAmount }}</text>
</view>
</view>
</view>
<view class="action-bar" v-if="showPaymentWarning">
<button class="pay-btn" :disabled="isPaying" @tap="handleRepay">
{{ isPaying ? '正在唤起支付...' : '重新发起支付' }}
</button>
</view>
<view class="empty-block" v-else>
<text class="placeholder-text">正在加载订单详情...</text>
</view>
</view>
</template>
<script>
import request from '@/api/request.js';
const STATUS_META = {
1: { text: '待支付', class: 'status-pending' },
2: { text: '支付失败', class: 'status-failed' },
3: { text: '待发货', class: 'status-delivering' },
4: { text: '待收货', class: 'status-receiving' },
5: { text: '已完成', class: 'status-success' },
6: { text: '已取消', class: 'status-cancel' },
7: { text: '已退款', class: 'status-refund' },
default: { text: '未知状态', class: 'status-default' }
};
const SHIPPING_STATUS_META = {
0: '待发货',
1: '已发货',
2: '运输中',
3: '派送中',
4: '已签收'
};
export default {
data() {
return {
orderId: '',
orderInfo: null,
payCountdown: 0,
countdownTimer: null,
isPaying: false
};
},
computed: {
countdownText() {
if (this.payCountdown <= 0) return '';
const minutes = Math.floor(this.payCountdown / 60);
const seconds = this.payCountdown % 60;
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
},
statusClass() {
return this.orderStatusMeta.class;
},
statusText() {
return this.orderStatusMeta.text;
},
orderStatusMeta() {
if (!this.orderInfo) return STATUS_META.default;
return STATUS_META[this.orderInfo.status] || STATUS_META.default;
},
showPaymentWarning() {
return !!this.orderInfo && this.orderInfo.status === 1;
},
totalQuantity() {
if (!this.orderInfo) return 0;
return this.orderInfo.products.reduce((acc, item) => acc + Number(item.quantity || 0), 0);
},
shippingStatusText() {
if (!this.orderInfo) return '--';
const code = this.orderInfo.delivery.shippingStatus;
return SHIPPING_STATUS_META[code] || '物流信息更新中';
}
},
onLoad(options) {
this.orderId = options.id || '';
this.loadOrderDetail(this.orderId);
},
onUnload() {
this.clearCountdown();
},
methods: {
async loadOrderDetail(orderId) {
try {
const res = await request(`xcx/order/${orderId}`, 'get');
const parsed = this.formatOrderInfo(res || {});
this.orderInfo = parsed;
this.initCountdown(parsed.createdAt, parsed.status);
} catch (error) {
console.error('获取订单详情失败', error);
const mockData = this.formatOrderInfo({
order_id: orderId || 'PO202411230001',
status: 1,
created_at: '2024-11-23 10:00',
total_amount: 688,
coupon_amount: 50,
payable_amount: 638,
points_amount: 0,
points_used: 0,
product_list: [
{
product_id: 'P001',
product_name: '至臻香水 50ml',
sku_name: '清新花香调',
price: 488,
quantity: 1,
product_main_image_url: 'https://dummyimage.com/120x120',
product_description: '灵感源于花园的香氛体验'
}
],
delivery_info: {
receiver_name: '张三',
phone: '138****6666',
province: '上海市',
city: '上海市',
district: '浦东新区',
detail_address: '世纪大道100号',
logistics_company: '顺丰速运',
tracking_number: 'SF123456789',
estimated_delivery: '2024-11-24 18:00',
shipping_status: 0
}
});
this.orderInfo = mockData;
this.initCountdown(mockData.createdAt, mockData.status);
}
},
async handleRepay() {
if (!this.orderInfo || this.isPaying) return;
this.isPaying = true;
try {
const payload = {
order_id: this.orderInfo.orderNo,
pay_amount: this.orderInfo.payableAmount
};
// TODO: 根据实际支付接口调整请求方式/参数
const result = await request('xcx/order/repay/' + this.orderId, 'post');
// 发起微信支付
wx.requestPayment({
timeStamp: result.time_stamp,
nonceStr: result.nonce_str,
package: result.package,
signType: result.sign_type,
paySign: result.pay_sign,
success: () => {
// console.log('支付成功');
// // 跳转到订单详情或订单列表页面
// setTimeout(() => {
// // 根据实际路由调整
// uni.redirectTo({
// url: `/pages/order/detail?id=${result.order_id || result.id}`
// });
// }, 2000);
},
complete: () => {
console.log('支付xxx');
this.loadOrderDetail(this.orderId);
}
});
} catch (error) {
console.error('重新发起支付失败', error);
uni.showToast({ title: error.message || '发起支付失败', icon: 'none' });
} finally {
this.isPaying = false;
}
},
initCountdown(createdAt, statusCode = 1) {
if (statusCode !== 1) {
this.clearCountdown();
this.payCountdown = 0;
return;
}
const createdTime = new Date((createdAt || '').replace(/-/g, '/')).getTime();
if (Number.isNaN(createdTime)) {
this.payCountdown = 0;
return;
}
const deadline = createdTime + 15 * 60 * 1000;
const diff = Math.max(0, Math.floor((deadline - Date.now()) / 1000));
this.payCountdown = diff;
this.clearCountdown();
if (diff > 0) {
this.countdownTimer = setInterval(() => {
if (this.payCountdown <= 1) {
this.payCountdown = 0;
this.clearCountdown();
this.orderInfo.status = 6;
return;
}
this.payCountdown -= 1;
}, 1000);
}
},
formatOrderInfo(raw = {}) {
const statusCode = this.normalizeStatus(raw.status);
const products = this.formatProducts(raw.product_list);
const delivery = this.formatDeliveryInfo(raw.delivery_info);
return {
orderNo: raw.order_id || raw.orderNo || '--',
status: statusCode,
createdAt: raw.created_at || raw.createdAt || raw.createTime || '--',
originalAmount: this.formatAmount(raw.total_amount ?? raw.amount),
couponAmount: this.formatAmount(raw.coupon_amount),
payableAmount: this.formatAmount(raw.payable_amount ?? raw.payableAmount ?? raw.amount),
pointsAmount: this.formatAmount(raw.points_amount),
pointsUsed: Number(raw.points_used) || 0,
products,
delivery
};
},
formatProducts(list = []) {
if (!Array.isArray(list)) return [];
return list.map((item) => ({
id: item.product_id || item.sku_id || item.product_name,
name: item.product_name || '--',
skuName: item.sku_name || '',
desc: item.product_description || '',
price: this.formatAmount(item.price ?? item.original_price),
quantity: item.quantity || 0,
image: item.product_main_image_url || item.category_image_url || '',
isHot: Number(item.is_hot_selling) === 1,
isLimited: Number(item.is_limited) === 1
}));
},
formatDeliveryInfo(info = {}) {
const province = info.province || '';
const city = info.city || '';
const district = info.district || '';
const detail = info.detail_address || '';
const fullAddress = [province, city, district, detail].filter(Boolean).join('');
const status = Number(info.shipping_status);
return {
receiverName: info.receiver_name || '--',
phone: info.phone || '--',
fullAddress: fullAddress || '--',
logisticsCompany: info.logistics_company || '--',
trackingNumber: info.tracking_number || '--',
estimatedDelivery: info.estimated_delivery || '--',
shippingStatus: Number.isNaN(status) ? 0 : status,
shippedAt: info.shipped_at || '',
deliveredAt: info.delivered_at || ''
};
},
formatAmount(value) {
const num = Number(value);
return Number.isFinite(num) ? num.toFixed(2) : '0.00';
},
normalizeStatus(status) {
const code = Number(status);
if (Number.isNaN(code) || code <= 0) return 1;
return code;
},
clearCountdown() {
if (this.countdownTimer) {
clearInterval(this.countdownTimer);
this.countdownTimer = null;
}
}
}
};
</script>
<style scoped>
.order-detail-page {
min-height: 100vh;
background-color: #f5f5f5;
padding: 32rpx;
box-sizing: border-box;
}
.detail-content {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.card {
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx;
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.06);
}
.section-title {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 30rpx;
font-weight: 600;
margin-bottom: 24rpx;
}
.sub-info {
font-size: 24rpx;
color: #888;
font-weight: 400;
}
.status-tag {
padding: 6rpx 20rpx;
border-radius: 999rpx;
font-size: 24rpx;
}
.status-pending {
background-color: #fff5e5;
color: #f57c00;
}
.status-failed {
background-color: #ffe5e5;
color: #d32f2f;
}
.status-delivering,
.status-receiving {
background-color: #e8f4ff;
color: #1976d2;
}
.status-success {
background-color: #e5f9f0;
color: #22a46d;
}
.status-cancel {
background-color: #f0f0f0;
color: #777;
}
.status-refund {
background-color: #f0e8ff;
color: #7b1fa2;
}
.status-default {
background-color: #ececec;
color: #666;
}
.row {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.label {
width: 160rpx;
font-size: 26rpx;
color: #666;
}
.value {
flex: 1;
font-size: 28rpx;
color: #111;
text-align: right;
}
.amount-row {
margin-top: 12rpx;
}
.amount {
font-size: 40rpx;
font-weight: 600;
color: #d81e06;
}
.total-row .amount {
font-size: 44rpx;
}
.warning-card {
background: linear-gradient(135deg, #fff6e6, #ffe8d1);
}
.warning-title {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 28rpx;
font-weight: 600;
margin-bottom: 16rpx;
color: #b34700;
}
.countdown {
font-size: 26rpx;
color: #d81e06;
}
.warning-text {
font-size: 26rpx;
color: #8c4a00;
line-height: 1.6;
}
.address {
text-align: right;
word-break: break-all;
}
.product-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.product-item {
display: flex;
gap: 24rpx;
}
.product-img {
width: 140rpx;
height: 140rpx;
border-radius: 12rpx;
background-color: #f6f6f6;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.product-name-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12rpx;
}
.product-name {
font-size: 30rpx;
color: #111;
font-weight: 600;
}
.product-tag {
font-size: 20rpx;
color: #f57c00;
background-color: #fff4e0;
padding: 2rpx 12rpx;
border-radius: 999rpx;
}
.product-tag.limited {
color: #c2185b;
background-color: #ffe1ec;
}
.product-sku {
font-size: 24rpx;
color: #666;
}
.product-desc {
font-size: 24rpx;
color: #999;
}
.product-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8rpx;
}
.product-price {
font-size: 30rpx;
color: #d81e06;
font-weight: 600;
}
.product-qty {
font-size: 26rpx;
color: #555;
}
.empty-tip {
padding: 40rpx 0;
text-align: center;
color: #999;
font-size: 28rpx;
}
.empty-block {
min-height: 400rpx;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 28rpx;
}
.action-bar {
position: sticky;
bottom: 0;
left: 0;
right: 0;
padding: 24rpx 32rpx 48rpx;
background: linear-gradient(180deg, rgba(245, 245, 245, 0), #f5f5f5 40%, #f5f5f5);
display: flex;
justify-content: center;
}
.pay-btn {
width: 100%;
border: none;
border-radius: 999rpx;
background-image: linear-gradient(120deg, #ff8a00, #ff4d4f);
color: #fff;
font-size: 32rpx;
font-weight: 600;
box-shadow: 0 12rpx 20rpx rgba(255, 77, 79, 0.25);
}
.pay-btn:disabled {
opacity: 0.6;
}
</style>