shinnosuke
2026-08-12 f40c2939c6f784fc6721554794154a336d12bc3a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
<template>
    <view class="page" :class="isCompleted ? 'paid-page' : 'pending-page'">
        <view class="navigation-bar">
            <view class="back-button" @tap.stop="goBack"><image class="back-icon" src="/static/icons/lucide/back-left.svg" mode="aspectFit"></image></view>
        </view>
 
        <scroll-view class="detail-scroll" scroll-y="true" :show-scrollbar="false">
            <view class="status-panel">
                <view class="status-copy"><text class="status-title" :class="isCompleted ? 'paid-title' : ''">{{statusTitle}}</text><text class="status-desc">{{statusDesc}}</text></view>
                <view class="status-icon-wrap" :class="isCompleted ? 'paid-icon-wrap' : ''"><image class="status-icon" :src="statusIcon" mode="aspectFit"></image></view>
            </view>
            <text class="section-title">订单信息</text>
            <view class="info-card">
                <view class="info-row"><text class="info-label">订单号</text><text class="info-value">{{orderNo}}</text></view>
                <view class="info-row"><text class="info-label">商品金额</text><text class="info-value">{{productAmount}}</text></view>
                <view class="info-row"><text class="info-label">人才折扣</text><text class="info-value">{{talentDiscount}}</text></view>
                <view class="info-row"><text class="info-label">商品优惠</text><text class="info-value">{{productDiscount}}</text></view>
                <view class="info-row"><text class="info-label">实际支付</text><text class="info-value pay-value">{{paidAmount}}</text></view>
                <view class="info-row"><text class="info-label">补贴金额</text><text class="info-value">{{subsidyAmount}}</text></view>
                <view class="info-row"><text class="info-label">下单时间</text><text class="info-value time-value">{{createdAt}}</text></view>
                <view class="info-row last-row"><text class="info-label">核销时间</text><text class="info-value" :class="isCompleted ? 'time-value' : 'muted-value'">{{verificationAt}}</text></view>
            </view>
 
            <text class="section-title benefit-section-title">权益信息</text>
            <view class="benefit-card">
                <view class="benefit-glow"></view>
                <view class="benefit-head"><text class="customer-name">{{customerName}}</text><text class="talent-tag">{{cardLevel}}</text></view>
                <text class="customer-phone">{{customerPhone}}</text>
                <view class="benefit-line"><text class="benefit-label">当前权益</text><text class="benefit-content">{{benefitName}}</text></view>
            </view>
            <view class="bottom-space"></view>
        </scroll-view>
        <view v-if="isCompleted && !isStoreCompleted" class="bottom-actions"><view class="cancel-verification-button" :class="revokeSubmitting ? 'cancel-verification-submitting' : (!canRevokeVerification ? 'cancel-verification-disabled' : '')" @tap="handleRevokeVerification"><text>{{revokeSubmitting ? '撤销中…' : '撤销核销'}}</text></view></view>
    </view>
</template>
 
<script setup lang="uts">
    import { revokeOrderVerification } from '../../services/orders.uts'
    import { loadMerchantOrderDetail, MerchantOrderDetail } from '../../services/orders.uts'
    import { formatErrorMessage } from '../../services/diagnostics.uts'
 
    const orderId = ref('')
    const isCompleted = ref(false)
    const isStoreCompleted = ref(false)
    const canRevokeVerification = ref(false)
    const revokeExpiredMessage = ref('已过可撤销时间')
    const revokeSubmitting = ref(false)
    const statusTitle = ref('订单加载中')
    const statusDesc = ref('正在获取订单详情。')
    const statusIcon = ref('/static/icons/lucide/clock-3.svg')
    const orderNo = ref('')
    const productAmount = ref('')
    const talentDiscount = ref('')
    const productDiscount = ref('')
    const paidAmount = ref('')
    const subsidyAmount = ref('')
    const createdAt = ref('')
    const verificationAt = ref('')
    const customerName = ref('')
    const customerPhone = ref('')
    const cardLevel = ref('人才卡')
    const benefitName = ref('以当前订单权益规则为准')
    const formatCents = (value: number): string => '¥' + (value / 100).toFixed(2)
 
    const applyRemoteOrder = (detail: MerchantOrderDetail) => {
        orderNo.value = detail.orderNo
        productAmount.value = formatCents(detail.orderAmount)
        talentDiscount.value = '-¥' + (detail.talentDiscountAmount / 100).toFixed(2)
        productDiscount.value = '-¥' + (detail.couponAmount / 100).toFixed(2)
        paidAmount.value = formatCents(detail.payAmount)
        subsidyAmount.value = formatCents(detail.subsidyAmount)
        createdAt.value = detail.createdAt.replace('T', ' ')
        verificationAt.value = detail.verifyTime.length > 0 ? detail.verifyTime.replace('T', ' ') : '待核销'
        customerName.value = detail.customerName
        customerPhone.value = detail.customerMobile
        cardLevel.value = detail.cardLevel.length > 0 ? detail.cardLevel : '人才卡'
        benefitName.value = detail.benefitName.length > 0 ? detail.benefitName : '以当前订单权益规则为准'
        isCompleted.value = detail.status == 'completed'
        isStoreCompleted.value = detail.category == 'store' && detail.status == 'completed'
        canRevokeVerification.value = detail.canRevoke
        revokeExpiredMessage.value = detail.revokeReason.length > 0 ? detail.revokeReason : '已过可撤销时间'
        statusTitle.value = isCompleted.value ? '已完成' : '待核销'
        statusDesc.value = isCompleted.value ? '订单已完成核销,相关权益已结算。' : '订单已完成支付,请确认后即可核销。'
        statusIcon.value = isCompleted.value ? '/static/icons/lucide/badge-check.svg' : '/static/icons/lucide/clock-3.svg'
    }
 
    onLoad((options: any) => {
        orderId.value = options.id != null ? options.id as string : ''
        orderNo.value = options.orderNo != null ? options.orderNo as string : orderNo.value
        if (orderNo.value.length == 0) {
            uni.showToast({ title: '订单参数缺失', icon: 'none' })
            return
        }
        loadMerchantOrderDetail(orderNo.value, (detail: MerchantOrderDetail, _traceId: string) => applyRemoteOrder(detail), (error: any) => {
            statusTitle.value = '订单加载失败'
            statusDesc.value = error.message != null ? error.message : '请返回订单列表后重试。'
            uni.showToast({ title: error.message != null ? error.message : '订单详情加载失败', icon: 'none' })
        })
    })
 
    const goBack = () => {
        uni.navigateBack()
    }
 
    const showRevokeExpired = () => {
        uni.showModal({
            title: '无法撤销',
            content: revokeExpiredMessage.value,
            showCancel: false,
            confirmText: '我知道了',
            confirmColor: '#FF6B2C'
        })
    }
 
    const submitRevokeVerification = () => {
        if (revokeSubmitting.value) return
        revokeSubmitting.value = true
        const idempotencyKey = 'revoke-verification-' + orderId.value + '-' + Date.now()
        revokeOrderVerification(orderId.value, idempotencyKey, (result: any, traceId: string) => {
            revokeSubmitting.value = false
            isCompleted.value = false
            canRevokeVerification.value = false
            statusTitle.value = result.orderStatusText != null && result.orderStatusText.length > 0 ? result.orderStatusText as string : '待核销'
            statusDesc.value = '核销已撤销,订单已退回待核销状态。'
            statusIcon.value = '/static/icons/lucide/clock-3.svg'
            verificationAt.value = '待核销'
            uni.showToast({ title: '撤销成功', icon: 'success' })
        }, (error: any) => {
            revokeSubmitting.value = false
            if (error.code == 'REVOCATION_PERIOD_EXPIRED' || error.code == 'SETTLEMENT_PERIOD_EXPIRED') {
                canRevokeVerification.value = false
                revokeExpiredMessage.value = '已过可撤销时间'
                showRevokeExpired()
                return
            }
            let message = formatErrorMessage(error, '撤销失败,请稍后重试')
            if (error.code == 'API_NOT_CONFIGURED') message = '撤销核销接口尚未接入'
            uni.showModal({
                title: '撤销失败',
                content: message,
                showCancel: false,
                confirmText: '我知道了',
                confirmColor: '#FF6B2C'
            })
        })
    }
 
    const handleRevokeVerification = () => {
        if (revokeSubmitting.value) return
        if (!canRevokeVerification.value) {
            return
        }
        uni.showModal({
            title: '撤销核销',
            content: '确认是否撤销核销?撤销后订单将退回待核销状态。',
            cancelText: '暂不撤销',
            confirmText: '确认撤销',
            confirmColor: '#FF6B2C',
            success: (result: any) => {
                if (result.confirm) submitRevokeVerification()
            }
        })
    }
 
</script>
 
<style>
    page { background-color: #F6F5F2; }
    .page { display: flex; flex: 1; flex-direction: column; width: 100%; height: 100vh; overflow: hidden; }
    .pending-page { background-image: linear-gradient(180deg, #FFE3C4 0%, #FFF5EA 390rpx, #F6F5F2 760rpx); }
    .paid-page { background-image: linear-gradient(180deg, #E8EDF4 0%, #F3F5F8 390rpx, #F6F5F2 760rpx); }
    .navigation-bar { display: flex; height: 174rpx; flex: none; position: relative; box-sizing: border-box; }
    .back-button { display: flex; width: 72rpx; height: 72rpx; justify-content: center; align-items: center; position: absolute; top: 82rpx; left: 28rpx; z-index: 2; }
    .back-icon { width: 40rpx; height: 40rpx; }
    .detail-scroll { flex: 1; height: 0; padding: 10rpx 34rpx 0; box-sizing: border-box; }
    .status-panel { display: flex; min-height: 126rpx; flex-direction: row; justify-content: space-between; align-items: flex-start; }
    .status-copy { display: flex; flex: 1; flex-direction: column; }
    .status-title { color: #FF633A; font-size: 50rpx; line-height: 70rpx; font-weight: 700; }
    .paid-title { color: #677386; }
    .status-desc { margin-top: 11rpx; color: #97A0AE; font-size: 25rpx; line-height: 36rpx; }
    .status-icon-wrap { display: flex; width: 60rpx; height: 60rpx; margin-top: 10rpx; justify-content: center; align-items: center; border-radius: 30rpx; background-color: #FFE9D8; box-shadow: 0 10rpx 22rpx rgba(226, 119, 59, 0.10); }
    .paid-icon-wrap { background-color: #E8EDF3; box-shadow: 0 10rpx 22rpx rgba(89, 105, 126, 0.09); }
    .status-icon { width: 32rpx; height: 32rpx; }
    .section-title { display: block; margin-top: 12rpx; color: #293244; font-size: 31rpx; line-height: 44rpx; font-weight: 700; }
    .info-card { padding: 20rpx 30rpx; margin-top: 22rpx; border-radius: 32rpx; background-color: #FFFFFF; box-shadow: 0 16rpx 38rpx rgba(69, 65, 57, 0.06); box-sizing: border-box; }
    .info-row { display: flex; min-height: 57rpx; flex-direction: row; justify-content: space-between; align-items: center; }
    .info-label { color: #A1A9B6; font-size: 27rpx; line-height: 38rpx; }
    .info-value { color: #414B5E; font-size: 27rpx; line-height: 38rpx; font-weight: 600; text-align: right; }
    .pay-value { color: #FF6439; }
    .time-value { font-size: 25rpx; }
    .muted-value { color: #9AA4B3; font-weight: 500; }
    .last-row { min-height: 60rpx; }
    .benefit-section-title { margin-top: 32rpx; }
    .benefit-card { min-height: 238rpx; padding: 34rpx 34rpx 28rpx; margin-top: 22rpx; border-width: 1rpx; border-style: solid; border-color: #E4BE59; border-radius: 38rpx; background-image: linear-gradient(135deg, #C99D32 0%, #F7E69D 55%, #E5BD4E 100%); box-shadow: 0 18rpx 38rpx rgba(188, 139, 37, 0.20); box-sizing: border-box; position: relative; overflow: hidden; }
    .benefit-glow { width: 200rpx; height: 200rpx; border-radius: 100rpx; position: absolute; top: -75rpx; right: -18rpx; background-color: rgba(255, 255, 255, 0.18); }
    .benefit-head { display: flex; flex-direction: row; justify-content: space-between; align-items: center; position: relative; }
    .customer-name { color: #FFFFFF; font-size: 39rpx; line-height: 55rpx; font-weight: 700; }
    .talent-tag { padding: 9rpx 20rpx; border-radius: 24rpx; color: #FFFFFF; font-size: 22rpx; line-height: 31rpx; font-weight: 600; background-color: rgba(149, 104, 22, 0.46); }
    .customer-phone { display: block; margin-top: 15rpx; color: #FFFFFF; font-size: 29rpx; line-height: 41rpx; font-weight: 600; position: relative; }
    .benefit-line { display: flex; margin-top: 22rpx; flex-direction: row; align-items: center; position: relative; }
    .benefit-label { color: rgba(255, 255, 255, 0.78); font-size: 23rpx; line-height: 33rpx; }
    .benefit-content { margin-left: 18rpx; color: #FFFFFF; font-size: 23rpx; line-height: 33rpx; font-weight: 600; }
    .bottom-space { height: 70rpx; }
    .bottom-actions { display: flex; height: 154rpx; padding: 20rpx 96rpx 34rpx; flex: none; background-color: #F6F5F2; box-sizing: border-box; }
    .cancel-verification-button { display: flex; flex: 1; height: 94rpx; justify-content: center; align-items: center; border-radius: 48rpx; color: #FFFFFF; font-size: 29rpx; line-height: 41rpx; font-weight: 600; background-image: linear-gradient(90deg, #FF7625 0%, #FF9630 100%); box-shadow: 0 12rpx 24rpx rgba(255, 107, 36, 0.20); }
    .cancel-verification-submitting { opacity: 0.62; box-shadow: none; }
    .cancel-verification-disabled { background-image: none; background-color: #C8CDD5; box-shadow: none; }
</style>