57543cfade
- 新增 useHjcPayFlow(select/code 两段状态机)与 HjcPayPanel(两处共用): 进页只查单、不发起支付,不预选支付方式;出码后不再有切换控件, 只能显式「更换支付方式」回到选择态(不自动重发、不停查单) - 发起失败留在选择态并保留所选(渠道侧什么都没产生,没有理由锁住用户) - 收银台 pages/tender/pay.vue 只留订单侧职责:取单、终态屏、超时锁、付款成功跳列表 - 购标流程 BuyDocument:下单后停在选择态(不再按默认微信自动发起), 补支付剩余时间与超时锁,超时后回表单步重新下单 - 订单详情:只有已完成订单才显示支付方式渠道名,其余显示「—」; 标签按前缀判,修掉 ALIPAY_QR 被显示成「微信支付」 - useHjcPayStatus:新增结构化 channelClosed——「渠道单已关闭」在选择态说 「重新选择支付方式」、出码态才说「重新获取二维码」,不靠匹配 message 字符串 Refs .scratch/cashier-pay-method-selection/issues/01-pay-method-before-pay.md
176 lines
8.0 KiB
Vue
176 lines
8.0 KiB
Vue
<template>
|
||
<div class="container mx-auto px-4 py-10">
|
||
<div class="mb-6 flex items-center justify-between">
|
||
<h1 class="text-2xl font-bold text-gray-900">收银台</h1>
|
||
<HjcUserBar />
|
||
</div>
|
||
|
||
<div v-if="!loggedIn" class="py-20 text-center">
|
||
<p class="text-gray-500">请先登录后继续支付</p>
|
||
<NuxtLink :to="`/login?redirect=${loginRedirect}`" class="mt-4 inline-block rounded-md bg-blue-600 px-6 py-2 text-white hover:bg-blue-700">去登录</NuxtLink>
|
||
</div>
|
||
|
||
<div v-else-if="!orderNo" class="mx-auto mt-6 max-w-2xl rounded-lg border border-gray-200 p-8 text-center">
|
||
<p class="text-gray-700">缺少订单号,无法发起支付</p>
|
||
<NuxtLink to="/tender/orders" class="mt-6 inline-block rounded-md bg-blue-600 px-6 py-2 text-white hover:bg-blue-700">返回我的订单</NuxtLink>
|
||
</div>
|
||
|
||
<SiteLoading v-else-if="loading" />
|
||
|
||
<div v-else-if="loadError" class="mx-auto mt-6 max-w-2xl rounded-lg border border-gray-200 p-8 text-center">
|
||
<p class="text-red-500">{{ loadError }}</p>
|
||
<NuxtLink to="/tender/orders" class="mt-6 inline-block rounded-md border border-gray-300 px-6 py-2 text-gray-700 hover:bg-gray-50">返回我的订单</NuxtLink>
|
||
</div>
|
||
|
||
<!--
|
||
终态:已支付 / 已取消 / 已退款。
|
||
**这一块必须先于支付面板判定**——进一张已支付的单却先拉起一次统一下单,既无意义,
|
||
又会多撞一次跨端 OUT_TRADE_NO_USED(多一次失败尝试不改变结论)。
|
||
-->
|
||
<div v-else-if="finalState" class="mx-auto mt-6 max-w-2xl rounded-lg border p-8 text-center" :class="finalState.cls">
|
||
<p class="text-xl font-bold">{{ finalState.title }}</p>
|
||
<p class="mt-2 text-sm opacity-80">订单号:{{ orderNo }}</p>
|
||
<NuxtLink to="/tender/orders" class="mt-6 inline-block rounded-md bg-blue-600 px-6 py-2 text-white hover:bg-blue-700">返回我的订单</NuxtLink>
|
||
</div>
|
||
|
||
<!--
|
||
支付时限已到(但后端还没把它落定为已取消)。
|
||
**必须锁死,不给「重新发起支付」**:同一个订单号对同一渠道重复统一下单会撞
|
||
OUT_TRADE_NO_USED,而且渠道侧那张支付单此刻已随支付时限一同失效(ADR 0019)——
|
||
再出一张码要么被渠道拒绝,要么给出一张我们马上就要取消的订单的码。
|
||
超时 = 这张订单终了,想要就重新下单。
|
||
-->
|
||
<div v-else-if="deadlineExpired" class="mx-auto mt-6 max-w-2xl rounded-lg border border-amber-200 bg-amber-50 p-8 text-center text-amber-800">
|
||
<p class="text-xl font-bold">支付已超时</p>
|
||
<p class="mt-2 text-sm opacity-80">该订单因支付超时已取消,如需购买请重新下单。</p>
|
||
<p class="mt-1 text-sm opacity-80">订单号:{{ orderNo }}</p>
|
||
<div class="mt-6 flex items-center justify-center gap-3">
|
||
<NuxtLink :to="projectLink" class="rounded-md bg-blue-600 px-6 py-2 text-white hover:bg-blue-700">重新下单</NuxtLink>
|
||
<NuxtLink to="/tender/orders" class="rounded-md border border-gray-300 px-6 py-2 text-gray-700 hover:bg-gray-50">返回我的订单</NuxtLink>
|
||
</div>
|
||
</div>
|
||
|
||
<!--
|
||
待支付:付款界面全在面板里(先选支付方式、点「立即支付」才发起;出码后只能显式更换)。
|
||
本页只负责订单侧的事——金额与到期时刻都来自上面 by-no 取回的那张订单。
|
||
-->
|
||
<HjcPayPanel
|
||
v-else
|
||
:order-no="orderNo"
|
||
:amount="amount"
|
||
:deadline="order?.payDeadline"
|
||
:project-name="order?.projectName"
|
||
:redirect="route.fullPath"
|
||
@paid="goOrders"
|
||
@timeout="deadlineExpired = true"
|
||
>
|
||
<NuxtLink to="/tender/orders" class="text-sm text-blue-600 hover:underline">← 返回我的订单</NuxtLink>
|
||
</HjcPayPanel>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { hjcIsPayExpired, hjcIsTimeoutCancelled, hjcOrderStatusKey } from '~/utils/orderStatus'
|
||
|
||
/**
|
||
* 收银台:买家为一张**已创建的订单**付款的独立页面(CONTEXT.md 的「收银台」)。
|
||
*
|
||
* 与购标流程(`/buy` 的 BuyDocument 支付步)的分界:购标流程是「先建单、随即付款」,
|
||
* 收银台是事后回到某张待支付订单继续付款。两份实现共用的**付款界面**已经收进
|
||
* `HjcPayPanel`(+ `useHjcPayFlow`);本页只管订单侧的事:取单、终态、超时锁,
|
||
* 以及渠道侧确认收款后去哪。
|
||
*
|
||
* 本页只服务微信外(ADR 0010):不因 UA 在微信内就跳 hjc-h5。
|
||
*/
|
||
const route = useRoute()
|
||
const { isLoggedIn, handleAuthCode } = useHjcAuth()
|
||
|
||
/** 收银台只认订单号(凭它取应付金额),没有自增 id */
|
||
const orderNo = computed(() => String(route.query.orderNo || ''))
|
||
const loginRedirect = computed(() => encodeURIComponent(route.fullPath))
|
||
|
||
const loggedIn = ref(false)
|
||
/** by-no 取单中 */
|
||
const loading = ref(false)
|
||
/** by-no 失败(订单不存在/无权限/网络),与"发起支付失败"分开 */
|
||
const loadError = ref('')
|
||
const order = shallowRef<any | null>(null)
|
||
/** 终态(已完成/已取消/已退款):终态下**绝不渲染支付面板** */
|
||
const finalState = shallowRef<{ title: string; cls: string } | null>(null)
|
||
/**
|
||
* 支付时限已到:进页就过期,或者面板挂着挂着归零(面板的 `timeout` 事件)。
|
||
* 两种情况都锁死页面——不再出码、不再查单。
|
||
*/
|
||
const deadlineExpired = ref(false)
|
||
|
||
/** 金额来自**订单本身**,与"发起支付"解耦:支付一失败页面就会停在 0.00 */
|
||
const amount = ref(0)
|
||
|
||
/** 「重新下单」的目标:回到该项目详情(不是购标流程,购标流程会再建一张新单) */
|
||
const projectLink = computed(() => {
|
||
const id = order.value?.projectId
|
||
return id ? `/tender/${id}` : '/tender'
|
||
})
|
||
|
||
/** 状态口径与订单列表同源(`~/utils/orderStatus`),避免两处各判一次导致分叉 */
|
||
function terminalStateOf(o: any) {
|
||
switch (hjcOrderStatusKey(o)) {
|
||
case 'done':
|
||
return { title: '该订单已完成支付', cls: 'border-green-200 bg-green-50 text-green-700' }
|
||
case 'cancel':
|
||
// 超时取消说清是谁取消的:用户什么都没干,只显示「已取消」会让人以为是自己误触的
|
||
return {
|
||
title: hjcIsTimeoutCancelled(o) ? '该订单因支付超时已取消' : '该订单已取消',
|
||
cls: 'border-gray-200 bg-gray-50 text-gray-600'
|
||
}
|
||
case 'refund':
|
||
return { title: '该订单已退款', cls: 'border-gray-200 bg-gray-50 text-gray-600' }
|
||
default:
|
||
return null
|
||
}
|
||
}
|
||
|
||
/** 渠道侧确认收款后跳订单列表(与 h5 收银台的 redirectTo 列表对齐) */
|
||
async function goOrders() {
|
||
await navigateTo('/tender/orders')
|
||
}
|
||
|
||
loggedIn.value = isLoggedIn()
|
||
|
||
onMounted(async () => {
|
||
if (!loggedIn.value || !orderNo.value) return
|
||
loading.value = true
|
||
try {
|
||
// 客户端发起:这里用裸 $fetch 是安全的(浏览器会带 Cookie)。
|
||
// 服务端渲染期间才必须用 useRequestFetch——本页的取数刻意放在 onMounted,规避那个坑。
|
||
const res: any = await $fetch('/api/tender/order-by-no', { query: { orderNo: orderNo.value } })
|
||
const auth = await handleAuthCode(res, route.fullPath)
|
||
if (auth.handled) {
|
||
loadError.value = auth.message || ''
|
||
return
|
||
}
|
||
if (res?.code !== 0) {
|
||
loadError.value = res?.message || '订单加载失败'
|
||
return
|
||
}
|
||
const o = res.data
|
||
order.value = o
|
||
// 金额来自订单本身,与"发起支付"解耦
|
||
amount.value = Number(o?.totalAmount ?? 0)
|
||
|
||
const terminal = terminalStateOf(o)
|
||
if (terminal) {
|
||
finalState.value = terminal
|
||
return
|
||
}
|
||
// 已过支付时限:锁在"支付已超时"那一屏——面板根本不会挂上,也就不会出码、不会查单。
|
||
// 到期时刻未知时**不算**超时(拿一个假值去锁死收银台更糟)。
|
||
deadlineExpired.value = hjcIsPayExpired(o)
|
||
} catch (e: any) {
|
||
loadError.value = e?.data?.message || e?.message || '订单加载失败'
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
})
|
||
</script>
|