Files
hjc-web/app/pages/tender/pay.vue
T
weicw-dshsa 57543cfade feat(order): 收银台与购标流程改为先选支付方式再发起支付
- 新增 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
2026-09-30 09:16:36 +08:00

176 lines
8.0 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>
<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>