Files
----/前端源码/uni-app/components/screens/ProfileOrderDetailScreen.vue

471 lines
12 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-screen">
<view class="order-detail-bg"></view>
<view class="status-bar-placeholder"></view>
<view class="order-detail-header">
<view class="order-detail-back-btn" @click="emit('back')">
<text class="order-detail-back-icon"></text>
</view>
<text class="order-detail-title">订单详情</text>
<view class="order-detail-header-placeholder"></view>
</view>
<scroll-view scroll-y class="order-detail-content">
<view class="order-detail-inner">
<view class="detail-card detail-card-highlight">
<view class="detail-row">
<text class="detail-label">订单状态</text>
<text class="detail-status" :class="`status-${order.status}`">{{ getStatusText(order.status) }}</text>
</view>
<view class="detail-price-wrap">
<text class="detail-price-symbol">¥</text>
<text class="detail-price">{{ displayAmount }}</text>
</view>
<text class="detail-desc">{{ order.description || getBusinessTypeName(order.business_type) }}</text>
</view>
<view class="detail-card">
<view class="section-title">订单信息</view>
<view class="detail-item">
<text class="detail-item-label">订单号</text>
<text class="detail-item-value detail-item-mono">{{ order.out_trade_no || '-' }}</text>
</view>
<view class="detail-item">
<text class="detail-item-label">微信订单号</text>
<text class="detail-item-value detail-item-mono">{{ order.transaction_id || '-' }}</text>
</view>
<view class="detail-item">
<text class="detail-item-label">业务类型</text>
<text class="detail-item-value">{{ getBusinessTypeName(order.business_type) }}</text>
</view>
<view class="detail-item">
<text class="detail-item-label">业务ID</text>
<text class="detail-item-value">{{ order.business_id ?? '-' }}</text>
</view>
</view>
<view class="detail-card">
<view class="section-title">支付信息</view>
<view class="detail-item">
<text class="detail-item-label">应付金额</text>
<text class="detail-item-value">¥{{ order.total_amount ?? '-' }}</text>
</view>
<view class="detail-item">
<text class="detail-item-label">实付金额</text>
<text class="detail-item-value">¥{{ order.paid_amount ?? order.total_amount ?? '-' }}</text>
</view>
<view class="detail-item">
<text class="detail-item-label">支付时间</text>
<text class="detail-item-value">{{ order.paid_at || '待支付' }}</text>
</view>
</view>
</view>
</scroll-view>
<view class="order-action-bar" v-if="showActionBar">
<view v-if="isPending" class="order-action-btn order-action-cancel" @click="handleCancelOrder">
<text class="order-action-text">取消订单</text>
</view>
<view v-if="isPending" class="order-action-btn order-action-pay" @click="handlePayOrder">
<text class="order-action-text order-action-text-light">{{ actionLoading ? '处理中...' : '继续支付' }}</text>
</view>
<view v-if="isPending" class="order-action-btn order-action-plain" @click="refreshOrderStatus()">
<text class="order-action-text">刷新状态</text>
</view>
<template v-else-if="order.status === 'paid'">
<view class="order-action-btn order-action-pay" @click="handleOpenBusiness">
<text class="order-action-text order-action-text-light">查看对应业务</text>
</view>
<view class="order-action-btn order-action-plain" @click="emit('back')">
<text class="order-action-text">返回订单列表</text>
</view>
<view class="order-action-btn order-action-pay" @click="refreshOrderStatus()">
<text class="order-action-text order-action-text-light">刷新状态</text>
</view>
</template>
<view v-else class="order-action-btn order-action-plain" @click="emit('back')">
<text class="order-action-text">返回订单列表</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { computed, ref, watch, onMounted } from 'vue';
import type { QueryOrderResponse } from '@/api/types';
import { closeOrder, wxPay } from '@/utils/payment';
import { paymentApi } from '@/api/payment';
declare const uni: any;
const props = defineProps<{
data?: QueryOrderResponse | null;
}>();
const emit = defineEmits<{
back: [];
openBusiness: [order: QueryOrderResponse];
}>();
const buildFallbackOrder = (): QueryOrderResponse => ({
out_trade_no: '',
status: 'pending',
total_amount: 0,
business_type: '',
business_id: 0,
});
const localOrder = ref<QueryOrderResponse>(props.data || buildFallbackOrder());
watch(
() => props.data,
(next) => {
localOrder.value = next || buildFallbackOrder();
},
{ immediate: true }
);
const order = computed(() => localOrder.value);
const displayAmount = computed(() => order.value.paid_amount ?? order.value.total_amount ?? 0);
const isPending = computed(() => order.value.status === 'pending');
const showActionBar = computed(() => Boolean(order.value.out_trade_no));
const actionLoading = ref(false);
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
pending: '待支付',
paid: '已支付',
cancelled: '已关闭',
refunded: '已退款',
};
return statusMap[status] || status;
};
const getBusinessTypeName = (type: string) => {
const typeMap: Record<string, string> = {
naming_report: '命名报告',
partner_apply: '推广合伙人',
test_report: '测名报告',
fortune_report: '财运报告',
test: '测试商品',
};
return typeMap[type] || type || '-';
};
const refreshOrderStatus = async (silent = false) => {
if (!order.value.out_trade_no || actionLoading.value) return;
actionLoading.value = true;
try {
const latest = await paymentApi.queryOrder(order.value.out_trade_no);
if (latest) {
localOrder.value = latest;
if (!silent) {
uni.showToast({ title: `状态已更新:${getStatusText(latest.status)}`, icon: 'none' });
}
}
} catch (e: any) {
if (!silent) {
uni.showToast({ title: e?.msg || '刷新失败', icon: 'none' });
}
} finally {
actionLoading.value = false;
}
};
const handleCancelOrder = () => {
if (!order.value.out_trade_no || actionLoading.value) return;
uni.showModal({
title: '提示',
content: '确定要取消该订单吗?',
success: async (res: any) => {
if (!res.confirm) return;
actionLoading.value = true;
try {
const success = await closeOrder(order.value.out_trade_no);
if (success) {
uni.showToast({ title: '订单已取消', icon: 'success' });
await refreshOrderStatus(true);
}
} finally {
actionLoading.value = false;
}
},
});
};
const handlePayOrder = async () => {
if (actionLoading.value) return;
actionLoading.value = true;
try {
const result = await wxPay({
description: order.value.description || getBusinessTypeName(order.value.business_type),
total_amount: order.value.total_amount,
business_type: order.value.business_type,
business_id: order.value.business_id,
});
if (result.success) {
uni.showToast({ title: '支付成功', icon: 'success' });
await refreshOrderStatus(true);
if (localOrder.value.status === 'paid') {
handleOpenBusiness();
}
return;
}
uni.showToast({ title: result.msg || '支付失败', icon: 'none' });
} catch (e: any) {
uni.showToast({ title: e?.msg || '支付失败', icon: 'none' });
} finally {
actionLoading.value = false;
}
};
const handleOpenBusiness = () => {
emit('openBusiness', order.value);
};
onMounted(() => {
if (order.value.out_trade_no && order.value.status === 'pending') {
refreshOrderStatus(true);
}
});
</script>
<style scoped>
.order-detail-screen {
height: 100%;
display: flex;
flex-direction: column;
background-color: #f0efe9;
position: relative;
}
.status-bar-placeholder {
height: var(--status-bar-height, 0);
width: 100%;
flex-shrink: 0;
}
.order-detail-bg {
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.3;
background-image: url('https://www.transparenttextures.com/patterns/rice-paper.png');
}
.order-detail-header {
position: relative;
z-index: 10;
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
border-bottom: 1rpx solid #dcd3c9;
background-color: rgba(253, 251, 247, 0.8);
backdrop-filter: blur(10rpx);
}
.order-detail-back-btn {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: -16rpx;
}
.order-detail-back-icon {
font-size: 48rpx;
color: #5a5a5a;
font-weight: 300;
}
.order-detail-title {
font-size: 32rpx;
font-weight: 700;
color: #2c2c2c;
letter-spacing: 0.2em;
}
.order-detail-header-placeholder {
width: 64rpx;
}
.order-detail-content {
flex: 1;
height: 0;
position: relative;
z-index: 10;
}
.order-detail-inner {
padding: 32rpx;
padding-bottom: calc(180rpx + env(safe-area-inset-bottom, 0px));
display: flex;
flex-direction: column;
gap: 24rpx;
}
.detail-card {
background-color: #fffdf9;
border-radius: 16rpx;
border: 1rpx solid #e5e5e5;
padding: 24rpx;
}
.detail-card-highlight {
border-color: #e5d6c4;
background: linear-gradient(135deg, #fffdf9 0%, #f9f4ee 100%);
}
.section-title {
font-size: 28rpx;
font-weight: 700;
color: #2c2c2c;
margin-bottom: 16rpx;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.detail-label {
font-size: 24rpx;
color: #999;
}
.detail-status {
font-size: 24rpx;
padding: 4rpx 16rpx;
border-radius: 8rpx;
}
.status-paid {
background-color: #e8f5e9;
color: #4caf50;
}
.status-pending {
background-color: #fff3e0;
color: #ff9800;
}
.status-cancelled {
background-color: #f5f5f5;
color: #999;
}
.status-refunded {
background-color: #ffebee;
color: #f44336;
}
.detail-price-wrap {
margin-top: 16rpx;
display: flex;
align-items: baseline;
}
.detail-price-symbol {
font-size: 28rpx;
color: #8b2323;
font-weight: 700;
}
.detail-price {
font-size: 56rpx;
color: #8b2323;
font-weight: 700;
}
.detail-desc {
display: block;
margin-top: 10rpx;
font-size: 24rpx;
color: #666;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16rpx;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.detail-item:last-child {
border-bottom: none;
}
.detail-item-label {
font-size: 24rpx;
color: #999;
}
.detail-item-value {
flex: 1;
text-align: right;
font-size: 24rpx;
color: #333;
word-break: break-all;
}
.detail-item-mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
}
.order-action-bar {
position: relative;
z-index: 12;
display: flex;
flex-wrap: wrap;
gap: 16rpx;
padding: 20rpx 24rpx calc(20rpx + env(safe-area-inset-bottom, 0px));
border-top: 1rpx solid #e5e5e5;
background-color: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(8rpx);
}
.order-action-btn {
flex: 1 1 220rpx;
height: 80rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
border: 1rpx solid #e5e5e5;
}
.order-action-text {
font-size: 26rpx;
color: #666;
}
.order-action-text-light {
color: #fff;
}
.order-action-cancel,
.order-action-plain {
background-color: #fff;
}
.order-action-pay {
background-color: #8b2323;
border-color: #8b2323;
}
</style>