yeg
2026-09-17 e3f412c5474311a471345ea508db4875c9ec648e
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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
<template>
    <view class="page">
        <view class="content">
            <view class="page-inner">
                <view class="top-panel">
                    <view class="custom-nav">
                        <view class="safe-space" :style="'height:' + navTop + 'px'"></view>
                        <view class="nav-row" :style="'height:' + navHeight + 'px;padding-right:' + navRight + 'px'">
                            <view class="back-button" @tap="goBack" hover-class="back-button-active">
                                <image src="/static/news-assets/arrow-left.svg"></image>
                            </view>
                            <text class="nav-title">订单支付</text>
                            <view class="capsule-space" :style="'width:' + navWidth + 'px'"></view>
                        </view>
                    </view>
 
                    <view class="product-summary">
                        <image class="product-image" :src="productImage" mode="aspectFill"></image>
                        <view class="product-copy">
                            <text class="product-name">{{productName}}</text>
                            <view class="product-price"><text class="price-symbol">¥</text><text>{{salePriceText}}</text></view>
                        </view>
                    </view>
                </view>
 
                <view class="price-card">
                    <view class="price-item"><text class="price-label">人才折扣</text><text class="talent-discount">{{productDiscount}}</text></view>
                    <view class="price-item coupon-item" @tap="chooseCoupon" hover-class="price-item-active">
                        <text class="price-label">商品优惠</text>
                        <view class="coupon-value"><text>{{couponText}}</text><text class="chevron">›</text></view>
                    </view>
                    <view class="price-item"><text class="price-label">原价</text><text class="price-value">¥ {{originalAmountText}}</text></view>
                    <view class="price-item"><text class="price-label">优惠金额</text><text class="price-value">-¥ {{couponAmountText}}</text></view>
                    <view class="price-item"><text class="price-label">用户实付</text><text class="price-value strong">¥ {{actualPayAmountText}}</text></view>
                    <view class="price-item"><text class="price-label">补贴金额</text><text class="price-value">¥ {{subsidyAmountText}}</text></view>
                </view>
 
                <view class="actual-payment">
                    <view class="actual-price"><text class="actual-symbol">¥</text><text>{{actualPayAmountText}}</text></view>
                    <text class="actual-label">实际支付</text>
                </view>
 
                <view class="confirm-button" :class="isSubmitting ? 'confirm-button-disabled' : ''" hover-class="confirm-button-active" @tap="confirmPayment">
                    <text>{{isSubmitting ? '支付中...' : '确认支付'}}</text>
                </view>
                <view class="safe-bottom"></view>
            </view>
        </view>
    </view>
</template>
 
<script setup lang="uts">
    import { safeBack } from '../../utils/navigation.uts'
    import { abandonPendingOrderPayment, createAppOrder, createPendingOrderPayment, getOrderCouponQuote } from '../../services/portal.uts'
    import { resolveApiUrl } from '../../services/http.uts'
    import { getSession } from '../../services/session.uts'
 
    const navTop = ref(28)
    const navHeight = ref(32)
    const navWidth = ref(88)
    const navRight = ref(10)
    const isSubmitting = ref(false)
    const merchantId = ref('')
    const productId = ref('')
    const orderId = ref('')
    const talentId = ref('')
    const couponText = ref('无')
    const couponAmount = ref(0.0)
    const productName = ref('【川味盛宴】川菜12人餐')
    const productImage = ref('/static/food-assets/food-1.jpg')
    const salePrice = ref(138.0)
    const originalAmount = ref(266.0)
    const productDiscount = ref('6折')
    const salePriceText = computed(() => salePrice.value.toFixed(2))
    const originalAmountText = computed(() => originalAmount.value.toFixed(2))
    const couponAmountText = computed(() => couponAmount.value.toFixed(2))
    const subsidyAmountText = ref('0.00')
    const actualPayAmountText = ref('0.00')
    const createOrderIdempotencyKey = ref('')
    let quoteSequence = 0
 
    const applyProductById = (id: string) => {
        if (id == 'food-set-002') { productName.value = '【川香四溢】川菜4人餐'; productImage.value = '/static/food-assets/food-2.jpg'; salePrice.value = 118.0; originalAmount.value = 198.0; productDiscount.value = '6折'; return }
        if (id == 'food-set-003') { productName.value = '【欢聚时光】招牌菜6人餐'; salePrice.value = 168.0; originalAmount.value = 248.0; productDiscount.value = '6.8折'; return }
        if (id == 'food-set-004') { productName.value = '【暖心小聚】特色菜双人餐'; productImage.value = '/static/food-assets/food-2.jpg'; salePrice.value = 88.0; originalAmount.value = 126.0; productDiscount.value = '7折'; return }
        if (id == 'food-set-005') { productName.value = '【家庭聚餐】经典川菜8人餐'; salePrice.value = 238.0; originalAmount.value = 366.0; productDiscount.value = '6.5折'; return }
        if (id == 'food-set-006') { productName.value = '【人气必点】招牌菜4人餐'; productImage.value = '/static/food-assets/food-2.jpg'; salePrice.value = 128.0; originalAmount.value = 188.0; productDiscount.value = '6.8折'; return }
        if (id == 'food-set-007') { productName.value = '【周末欢聚】特色菜10人餐'; salePrice.value = 298.0; originalAmount.value = 498.0; productDiscount.value = '6折'; return }
        if (id == 'food-set-008') { productName.value = '【一人食】暖心工作餐'; productImage.value = '/static/food-assets/food-2.jpg'; salePrice.value = 39.0; originalAmount.value = 49.0; productDiscount.value = '8折'; return }
    }
 
    onLoad((options: any) => {
        // #ifdef MP-WEIXIN
        const capsule = uni.getMenuButtonBoundingClientRect()
        const systemInfo = uni.getSystemInfoSync()
        navTop.value = capsule.top
        navHeight.value = capsule.height
        navWidth.value = capsule.width
        navRight.value = systemInfo.windowWidth - capsule.right
        // #endif
        if (options != null && options['merchantId'] != null) merchantId.value = options['merchantId'] as string
        if (options != null && options['productId'] != null) productId.value = options['productId'] as string
        if (options != null && options['orderId'] != null) orderId.value = options['orderId'] as string
        const session = getSession()
        if (session != null && session.talentId != null) talentId.value = '' + session.talentId
        applyProductById(productId.value)
        if (options != null && options['name'] != null) productName.value = decodeURIComponent(options['name'] as string)
        if (options != null && options['image'] != null) productImage.value = decodeURIComponent(options['image'] as string)
        if (options != null && options['price'] != null) {
            const parsedPrice = parseFloat(options['price'] as string)
            if (!isNaN(parsedPrice)) salePrice.value = parsedPrice
        }
        if (options != null && options['originalPrice'] != null) {
            const parsedOriginalPrice = parseFloat(options['originalPrice'] as string)
            if (!isNaN(parsedOriginalPrice)) originalAmount.value = parsedOriginalPrice
        }
        if (options != null && options['discount'] != null) productDiscount.value = decodeURIComponent(options['discount'] as string)
        if (orderId.value == '') orderId.value = 'order-draft-' + merchantId.value + '-' + productId.value
        createOrderIdempotencyKey.value = 'create-order-' + Date.now() + '-' + Math.floor(Math.random() * 1000000)
        loadOrderQuote('')
    })
 
    onShow(() => {
        const selectedCouponId: any = uni.getStorageSync(couponStorageKey())
        loadOrderQuote(selectedCouponId != null ? '' + selectedCouponId : '')
    })
 
    const goBack = () => safeBack('/pages/benefits/benefits')
    const couponStorageKey = () : string => 'selectedOrderCouponId:' + orderId.value
    const validOrderContext = () : boolean => /^\d+$/.test(merchantId.value) && /^\d+$/.test(productId.value) && orderId.value != ''
    const toAmount = (value: any) : number => {
        const parsed = Number(value)
        return isNaN(parsed) ? 0.0 : parsed
    }
    const loadOrderQuote = (couponId: string) => {
        if (!validOrderContext()) return
        const sequence = quoteSequence + 1
        quoteSequence = sequence
        getOrderCouponQuote(merchantId.value, productId.value, orderId.value, couponId, talentId.value, (data: any) => {
            if (sequence != quoteSequence || data == null) return
            productName.value = data.productName != null ? data.productName as string : productName.value
            if (data.coverUrl != null && data.coverUrl != '') productImage.value = resolveApiUrl(data.coverUrl as string)
            salePrice.value = toAmount(data.productAmount)
            originalAmount.value = toAmount(data.originalAmount)
            couponAmount.value = toAmount(data.couponAmount)
            couponText.value = data.couponLabel != null ? data.couponLabel as string : '无'
            productDiscount.value = data.discountText != null ? data.discountText as string : productDiscount.value
            subsidyAmountText.value = toAmount(data.subsidyAmount).toFixed(2)
            actualPayAmountText.value = toAmount(data.payAmount).toFixed(2)
        }, (error) => {
            if (sequence != quoteSequence) return
            if (couponId != '') {
                uni.removeStorageSync(couponStorageKey())
                couponText.value = '无'
                uni.showToast({ title: error.message, icon: 'none' })
                loadOrderQuote('')
            }
        })
    }
    const chooseCoupon = () => uni.navigateTo({
        url: '/pages/coupon-select/coupon-select?source=payment-confirm'
            + '&merchantId=' + encodeURIComponent(merchantId.value)
            + '&productId=' + encodeURIComponent(productId.value)
            + '&orderId=' + encodeURIComponent(orderId.value)
    })
    const confirmPayment = () => {
        if (isSubmitting.value) return
        if (!validOrderContext()) {
            uni.showToast({ title: '订单信息不完整,请重新选择商品', icon: 'none' })
            return
        }
        const session = getSession()
        const talentId = session != null && session.talentId != null ? '' + session.talentId : ''
        if (session == null || talentId == '') {
            uni.showToast({ title: '请先登录后再支付', icon: 'none' })
            uni.navigateTo({ url: '/pages/login/login' })
            return
        }
        isSubmitting.value = true
        const selectedCouponId: any = uni.getStorageSync(couponStorageKey())
        const couponId = selectedCouponId != null ? '' + selectedCouponId : ''
        createAppOrder(merchantId.value, productId.value, couponId, createOrderIdempotencyKey.value, (data: any) => {
            uni.removeStorageSync(couponStorageKey())
            const realOrderNo = data != null && data.orderNo != null ? '' + data.orderNo : ''
            if (realOrderNo == '') {
                isSubmitting.value = false
                uni.showToast({ title: '订单创建失败,请稍后重试', icon: 'none' })
                return
            }
            if (data != null && data.paid === true) {
                isSubmitting.value = false
                uni.redirectTo({ url: '/pages/payment-success/payment-success?orderId=' + encodeURIComponent(realOrderNo) })
                return
            }
            // 一次点击即支付:用户既然点了「确认支付」,就不要再让他到第二页点同一个按钮——
            // 建单之后立刻发起预支付并拉起收银台,只有放弃或失败才落到待支付页。
            startPayment(realOrderNo, talentId, data != null && data.priceVersion != null ? Number(data.priceVersion) : 1)
        }, (error) => {
            isSubmitting.value = false
            uni.showToast({ title: error.message, icon: 'none' })
        })
    }
    const startPayment = (orderNo: string, talentId: string, priceVersion: number) => {
        createPendingOrderPayment(orderNo, talentId, priceVersion, (data: any) => {
            if (data != null && data.paymentStatus == 'PAID') {
                openPaymentSuccess(orderNo)
                return
            }
            if (data != null && data.paymentStatus == 'PAYMENT_PROCESSING') {
                // 关单中或结果未定:落到待支付页由它等结论(那里会显示「支付处理中」并退避轮询)。
                openPendingPayment(orderNo, talentId, true)
                return
            }
            const params: any = data != null ? data.paymentParams : null
            if (params == null || params.timeStamp == null || params.nonceStr == null || params.package == null || params.paySign == null) {
                uni.showToast({ title: '支付参数获取失败,请在待支付页重试', icon: 'none' })
                openPendingPayment(orderNo, talentId, false)
                return
            }
            presentCashier(orderNo, talentId, params)
        }, (error: any) => {
            uni.showToast({ title: error.message, icon: 'none' })
            openPendingPayment(orderNo, talentId, false)
        })
    }
    const presentCashier = (orderNo: string, talentId: string, params: any) => {
        // #ifdef MP-WEIXIN
        uni.requestPayment({
            provider: 'wxpay',
            timeStamp: '' + params.timeStamp,
            nonceStr: '' + params.nonceStr,
            package: '' + params.package,
            signType: params.signType != null ? '' + params.signType : 'RSA',
            paySign: '' + params.paySign,
            success: () => openPendingPayment(orderNo, talentId, true),
            fail: (error: any) => {
                const message = error != null && error.errMsg != null ? '' + error.errMsg : ''
                if (message.indexOf('cancel') >= 0) {
                    // 用户关掉了收银台:告诉服务端一声,让它立刻尝试关单(服务端不据此判定收款)。
                    abandonPendingOrderPayment(orderNo, talentId, '', () => openPendingPayment(orderNo, talentId, false), () => openPendingPayment(orderNo, talentId, false))
                    return
                }
                uni.showToast({ title: '支付未完成,请在待支付页继续', icon: 'none' })
                openPendingPayment(orderNo, talentId, false)
            }
        })
        // #endif
        // #ifndef MP-WEIXIN
        uni.showToast({ title: '请在微信小程序中完成支付', icon: 'none' })
        openPendingPayment(orderNo, talentId, false)
        // #endif
    }
    const openPendingPayment = (orderNo: string, talentId: string, waitingPayment: boolean) => {
        isSubmitting.value = false
        uni.redirectTo({
            url: '/pages/pending-payment/pending-payment?orderId=' + encodeURIComponent(orderNo)
                + '&talentId=' + encodeURIComponent(talentId)
                + (waitingPayment ? '&waiting=1' : '')
        })
    }
    const openPaymentSuccess = (orderNo: string) => {
        isSubmitting.value = false
        uni.redirectTo({ url: '/pages/payment-success/payment-success?orderId=' + encodeURIComponent(orderNo) })
    }
</script>
 
<style>
    .page { width: 100%; height: 100vh; background-color: #F7F7F7; }
    .content { width: 100%; height: 100%; }
    .page-inner { min-height: 100%; background-color: #F7F7F7; }
    .top-panel { padding-bottom: 30rpx; background-image: linear-gradient(180deg, #FFF0B8 0%, #FFF8E5 45%, #F7F7F7 100%); }
    .custom-nav { width: 100%; }
    .safe-space { height: 28px; }
    .nav-row { display: flex; height: 32px; box-sizing: border-box; padding-left: 26rpx; padding-right: 10px; position: relative; flex-direction: row; align-items: center; justify-content: space-between; }
    .back-button { display: flex; width: 60rpx; height: 60rpx; margin-left: -8rpx; justify-content: center; align-items: center; }
    .back-button image { width: 38rpx; height: 38rpx; }
    .back-button-active { opacity: .55; }
    .nav-title { position: absolute; left: 50%; top: 50%; color: #181A1E; font-size: 36rpx; font-weight: 700; transform: translate(-50%, -50%); white-space: nowrap; }
    .capsule-space { width: 88px; height: 100%; flex-shrink: 0; }
    .product-summary { display: flex; margin-top: 34rpx; padding: 0 30rpx; flex-direction: row; align-items: flex-start; }
    .product-image { width: 184rpx; height: 184rpx; border-radius: 17rpx; flex-shrink: 0; background-color: #E8E8E8; }
    .product-copy { display: flex; min-width: 0; flex: 1; min-height: 184rpx; padding-left: 29rpx; flex-direction: column; }
    .product-name { color: #27282B; font-size: 31rpx; font-weight: 600; line-height: 43rpx; }
    .product-price { display: flex; margin-top: 35rpx; color: #17181B; font-size: 34rpx; font-weight: 700; flex-direction: row; align-items: baseline; }
    .price-symbol { margin-right: 3rpx; font-size: 28rpx; }
    .price-card { margin: 0 24rpx; padding: 22rpx 28rpx; border-radius: 17rpx; background-color: #FFFFFF; }
    .price-item { display: flex; min-height: 66rpx; flex-direction: row; align-items: center; justify-content: space-between; }
    .price-label { color: #6A6D73; font-size: 27rpx; }
    .price-value { color: #33363B; font-size: 27rpx; }
    .price-value.strong { color: #25272B; font-weight: 700; }
    .talent-discount { color: #FF6E21; font-size: 28rpx; }
    .coupon-item { margin: 0 -12rpx; padding: 0 12rpx; border-radius: 10rpx; }
    .price-item-active { background-color: #F7F7F7; }
    .coupon-value { display: flex; color: #A5A8AD; font-size: 26rpx; flex-direction: row; align-items: center; }
    .chevron { margin-left: 16rpx; color: #62666C; font-size: 38rpx; line-height: 32rpx; }
    .actual-payment { display: flex; margin-top: 120rpx; flex-direction: column; align-items: center; }
    .actual-price { display: flex; color: #17191D; font-size: 57rpx; font-weight: 800; line-height: 68rpx; flex-direction: row; align-items: baseline; }
    .actual-symbol { margin-right: 5rpx; font-size: 29rpx; }
    .actual-label { margin-top: 8rpx; color: #666970; font-size: 29rpx; }
    .confirm-button { display: flex; width: 514rpx; height: 86rpx; margin: 146rpx auto 0; border-radius: 45rpx; background-image: linear-gradient(90deg, #FF4C32 0%, #FF841C 100%); justify-content: center; align-items: center; }
    .confirm-button text { color: #FFFFFF; font-size: 30rpx; font-weight: 600; }
    .confirm-button-active { opacity: .82; transform: scale(.99); }
    .confirm-button-disabled { opacity: .62; transform: none; }
    .safe-bottom { height: 44rpx; height: calc(44rpx + env(safe-area-inset-bottom)); }
</style>