Files
consumer-front/src/pages/order/detail.vue
T

470 lines
11 KiB
Vue
Raw Normal View History

<template>
<view class="detail-page">
<!-- 顶部导航 -->
<view class="nav-bar">
<view class="nav-back" @click="goBack">
<text class="back-arrow"></text>
</view>
<text class="nav-title">订单详情</text>
<view class="nav-placeholder"></view>
</view>
<!-- 内容区域 -->
<scroll-view class="detail-scroll" scroll-y>
<view v-if="loading" class="loading-state">
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="order" class="detail-body">
<!-- 状态卡片 -->
<view class="status-card">
<view class="status-icon-row">
<view class="status-dot" :class="statusDotClass"></view>
<text class="status-main">{{ orderStatusText(order.order_status) }}</text>
</view>
<text class="status-sub">{{ payStatusText(order.payment_status) }}</text>
</view>
<!-- 商品信息 -->
<view class="section-card">
<view class="section-title">商品信息</view>
<view
class="product-item"
v-for="(product, idx) in order.products"
:key="idx"
>
<image class="product-img" :src="product.image" mode="aspectFill" />
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<view class="product-bottom">
<text class="product-price">{{ getDecimal(product.unit_price) }}</text>
<text class="product-qty">x{{ product.qty }}</text>
</view>
</view>
</view>
</view>
<!-- 订单信息 -->
<view class="section-card">
<view class="section-title">订单信息</view>
<view class="info-row">
<text class="info-label">订单编号</text>
<text class="info-value" @click="copyText(order.order_id)">{{ order.order_id }}</text>
</view>
<view class="info-row">
<text class="info-label">下单时间</text>
<text class="info-value">{{ formatTime(order.createdAt) }}</text>
</view>
<view class="info-row">
<text class="info-label">设备编号</text>
<text class="info-value">{{ order.device_id }}</text>
</view>
<view class="info-row">
<text class="info-label">订单来源</text>
<text class="info-value">{{ orderSourceText(order.order_source) }}</text>
</view>
<view class="info-row">
<text class="info-label">商品数量</text>
<text class="info-value">{{ order.total_qty }} </text>
</view>
</view>
<!-- 金额信息 -->
<view class="section-card">
<view class="section-title">金额信息</view>
<view class="info-row">
<text class="info-label">商品总额</text>
<text class="info-value">{{ getDecimal(order.total_amount) }}</text>
</view>
<view class="info-row" v-if="getDecimal(order.discount_amount) !== '0'">
<text class="info-label">优惠金额</text>
<text class="info-value discount">-{{ getDecimal(order.discount_amount) }}</text>
</view>
<view class="info-row total">
<text class="info-label">实付金额</text>
<text class="info-value price">{{ getDecimal(order.paid_amount) }}</text>
</view>
</view>
<!-- 退款信息 -->
<view class="section-card" v-if="order.has_refund && order.refund_info">
<view class="section-title">退款信息</view>
<view class="info-row">
<text class="info-label">退款状态</text>
<text class="info-value refunded">已退款</text>
</view>
</view>
</view>
<view v-else class="empty-state">
<text class="empty-text">订单不存在</text>
</view>
<view class="bottom-spacer"></view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="bottom-bar" v-if="order">
<view
v-if="order.payment_status === 'PENDING'"
class="btn-action btn-pay"
@click="handlePay"
>
立即支付
</view>
<view
v-else-if="order.payment_status === 'PAID' && !order.has_refund"
class="btn-action btn-refund"
@click="handleRefund"
>
申请退款
</view>
</view>
</view>
</template>
<script>
import { get } from '@/utils/request.js';
import { checkLoginWithPrompt } from '@/utils/auth-guard.js';
export default {
data() {
return {
orderId: '',
order: null,
loading: false
};
},
computed: {
statusDotClass() {
if (!this.order) return '';
const s = this.order.order_status;
if (s === 'COMPLETED') return 'dot-success';
if (s === 'CANCELLED') return 'dot-grey';
return 'dot-blue';
}
},
onLoad(options) {
this.orderId = options.order_id || '';
2026-07-11 17:17:34 +08:00
// 优先从缓存读取(订单列表/交易完成页跳转时已存入)
const cached = uni.getStorageSync('__order_detail_cache');
if (cached) {
this.order = cached;
this.orderId = cached.order_id || this.orderId;
uni.removeStorageSync('__order_detail_cache');
} else if (this.orderId) {
this.fetchOrder();
}
},
methods: {
async fetchOrder() {
this.loading = true;
try {
const res = await get('/user/orders', {
page: 1,
limit: 1,
filter: JSON.stringify({ order_id: this.orderId })
});
const ret = res.data || {};
if (ret.code === 0 && Array.isArray(ret.data) && ret.data.length > 0) {
this.order = ret.data[0];
}
} catch (e) {
uni.showToast({ title: '获取订单失败', icon: 'none' });
} finally {
this.loading = false;
}
},
getDecimal(val) {
if (!val) return '0.00';
if (val.$numberDecimal) return val.$numberDecimal;
return String(val);
},
formatTime(iso) {
if (!iso) return '';
const d = new Date(iso);
const pad = n => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
},
orderStatusText(status) {
const map = { PROCESSING: '处理中', COMPLETED: '已完成', CANCELLED: '已取消' };
return map[status] || status;
},
payStatusText(status) {
const map = { PENDING: '待支付', PAID: '已支付', REFUNDED: '已退款', PARTIAL_REFUND: '部分退款' };
return map[status] || status;
},
orderSourceText(source) {
const map = { SCAN: '扫码开门', APP: 'APP下单' };
return map[source] || source;
},
copyText(text) {
uni.setClipboardData({
data: text,
success: () => uni.showToast({ title: '已复制', icon: 'success' })
});
},
handlePay() {
uni.showModal({
title: '立即支付',
content: `确认支付¥${this.getDecimal(this.order.total_amount)}`,
success: (res) => {
if (res.confirm) {
uni.showToast({ title: '支付功能开发中', icon: 'none' });
}
}
});
},
handleRefund() {
uni.showModal({
title: '申请退款',
content: '确认申请退款?',
success: (res) => {
if (res.confirm) {
uni.showToast({ title: '退款功能开发中', icon: 'none' });
}
}
});
},
goBack() {
uni.navigateBack();
}
}
};
</script>
<style scoped>
page {
height: 100%;
background-color: #f5f5f5;
}
.detail-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
/* 导航栏 */
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
height: 88rpx;
background: #fff;
padding-top: constant(safe-area-inset-top);
padding-top: env(safe-area-inset-top);
}
.nav-back {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-arrow {
font-size: 48rpx;
color: #333;
line-height: 1;
}
.nav-title {
font-size: 34rpx;
font-weight: bold;
color: #333;
}
.nav-placeholder {
width: 60rpx;
}
/* 内容区域 */
.detail-scroll {
flex: 1;
height: 0;
}
/* 状态卡片 */
.status-card {
background: linear-gradient(135deg, #2979ff, #1e60e0);
padding: 40rpx 32rpx;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.status-icon-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.status-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
}
.dot-blue { background: #fff; }
.dot-success { background: #52c41a; }
.dot-grey { background: #999; }
.status-main {
font-size: 36rpx;
font-weight: bold;
color: #fff;
}
.status-sub {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.75);
padding-left: 28rpx;
}
/* 通用卡片 */
.section-card {
background: #fff;
margin: 20rpx;
border-radius: 16rpx;
padding: 24rpx 28rpx;
}
.section-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #f0f0f0;
}
/* 商品项 */
.product-item {
display: flex;
align-items: center;
padding: 16rpx 0;
}
.product-item + .product-item {
border-top: 1rpx solid #f5f5f5;
}
.product-img {
width: 120rpx;
height: 120rpx;
border-radius: 10rpx;
background: #f0f0f0;
flex-shrink: 0;
}
.product-info {
flex: 1;
margin-left: 20rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 120rpx;
}
.product-name {
font-size: 28rpx;
color: #333;
font-weight: 500;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.product-bottom {
display: flex;
justify-content: space-between;
align-items: center;
}
.product-price {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.product-qty {
font-size: 26rpx;
color: #999;
}
/* 信息行 */
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
}
.info-row + .info-row {
border-top: 1rpx solid #f5f5f5;
}
.info-row.total {
border-top: 1rpx solid #f0f0f0;
margin-top: 8rpx;
padding-top: 20rpx;
}
.info-label {
font-size: 26rpx;
color: #999;
}
.info-value {
font-size: 26rpx;
color: #333;
}
.info-value.discount {
color: #ff6b6b;
}
.info-value.price {
font-size: 32rpx;
font-weight: bold;
color: #e4393c;
}
.info-value.refunded {
color: #ff6b6b;
}
/* 加载/空状态 */
.loading-state, .empty-state {
display: flex;
justify-content: center;
align-items: center;
padding: 120rpx 0;
}
.loading-text, .empty-text {
font-size: 28rpx;
color: #999;
}
/* 底部操作栏 */
.bottom-bar {
background: #fff;
padding: 16rpx 30rpx;
padding-bottom: calc(16rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.btn-action {
width: 100%;
height: 88rpx;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: bold;
}
.btn-pay {
background: linear-gradient(135deg, #2979ff, #1e60e0);
color: #fff;
}
.btn-refund {
background: #fff;
border: 2rpx solid #2979ff;
color: #2979ff;
}
/* 底部占位 */
.bottom-spacer {
height: 40rpx;
}
</style>