revert: PC 支付宝改回电脑网站支付(自提交表单跳转)

配合后端把支付宝默认渠道改回电脑网站支付(当面付需单独签约并提交
资料审核,实测被网关以 ACQ.ACCESS_FORBIDDEN 拒绝)。

改动(tender/pay.vue 收银台 + template-07/BuyDocument.vue 购标流程):
- 恢复 alipayForm 这套处理:后端返回自提交表单时用 document.write
  写进新窗口跳转支付宝收银台,原页面留在原标签页继续轮询查单;
  弹窗被拦截时给出可行动提示,按钮可重试。
- 模板恢复「表单」与「二维码」两支:支付宝走表单、微信走 codeUrl。
- 切换支付方式时把两种产物都清掉,避免上一支的残留留在页面上。
- 文件头注释同步:微信出码、支付宝跳转。

前端不传 payType 之外的值即可——裸 alipay 现在由后端解析为网页支付,
所以「微信支付 / 支付宝」选择器保持不变。

验证:eslint 0 error;npm run build:dev 通过。
This commit is contained in:
2026-09-21 21:49:28 +08:00
parent 1b4dbda28e
commit 755866e28b
3 changed files with 160 additions and 18 deletions
+62 -8
View File
@@ -63,11 +63,31 @@
</div>
<!--
两个渠道都是出码:微信 Native 与支付宝当面付(默认渠道)返回同一形态的 codeUrl,
渲染与轮询完全复用。支付宝不跳转账单——当面付需要在开放平台签约「当面付」,
与「电脑网站支付」是不同的产品签约(见 ADR 0010)。
支付宝走**电脑网站支付**:后端返回一段自提交表单,写进新页面即跳转支付宝收银台
(当面付扫码需单独签约并提交资料审核,见 ADR 0010 补记,故不作默认)。
微信仍是出码:两者在模板里各占一支。
-->
<template v-if="codeUrl">
<template v-if="alipayForm">
<div class="rounded-md bg-blue-50 px-4 py-6 text-sm text-blue-700">
已为你打开支付宝收银台。若新页面没有自动打开,请点击下方按钮。
</div>
<button
class="mt-6 w-full rounded-md bg-blue-600 py-3 font-medium text-white hover:bg-blue-700 disabled:opacity-50"
:disabled="confirming"
@click="openAlipayForm"
>
前往支付宝付款
</button>
<button
class="mt-3 w-full rounded-md border border-gray-300 py-3 font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
:disabled="confirming"
@click="confirmPaid"
>
{{ confirming ? '确认中...' : '我已完成支付,确认订单' }}
</button>
</template>
<template v-else-if="codeUrl">
<!-- 过期只是置灰提示,仍留在页面上:万一用户是刚过点就扫的,还能看见自己扫的是哪张码 -->
<div :class="payExpired ? 'opacity-30' : ''">
<HjcPayQrcode :value="codeUrl" :size="240" :alt="qrAlt" />
@@ -80,7 +100,7 @@
</div>
<button
v-if="codeUrl"
v-if="codeUrl && !alipayForm"
class="mt-6 w-full rounded-md bg-blue-600 py-3 font-medium text-white hover:bg-blue-700 disabled:opacity-50"
:disabled="confirming"
@click="confirmPaid"
@@ -88,6 +108,7 @@
{{ confirming ? '确认中...' : '我已完成支付,确认订单' }}
</button>
<button
v-if="!alipayForm"
class="mt-6 w-full rounded-md py-3 font-medium disabled:opacity-50"
:class="codeUrl
? 'border border-gray-300 text-gray-700 hover:bg-gray-50'
@@ -119,8 +140,8 @@ import { hjcOrderStatusKey } from '~/utils/orderStatus'
* 两份实现共享的只有 `HjcPayQrcode` 与 `useHjcPayStatus`(重活已经都在那里了)。
*
* 本页只服务微信外(ADR 0010):不因 UA 在微信内就跳 hjc-h5。
* 支付方式可选**微信扫码**或**支付宝**;两者都是出码(支付宝默认当面付,
* 与微信 Native 同一交互),渲染与轮询完全复用。
* 支付方式可选**微信扫码**或**支付宝**;微信出二维码、支付宝走电脑网站支付
* (返回自提交表单,跳转支付宝收银台)。两者共用同一个查单轮询与状态判定。
*/
const route = useRoute()
const { isLoggedIn, handleAuthCode } = useHjcAuth()
@@ -146,6 +167,8 @@ const payOptions = [
]
const codeUrl = ref('')
/** 支付宝电脑网站支付的自提交表单(后端只在该渠道下返回) */
const alipayForm = ref('')
const amount = ref(0)
const starting = ref(false)
const refreshing = ref(false)
@@ -222,8 +245,14 @@ async function startPay() {
}
if (res?.code === 0) {
codeUrl.value = res.data?.codeUrl || ''
alipayForm.value = res.data?.alipayForm || ''
if (res.data?.amount) amount.value = Number(res.data.amount)
if (!codeUrl.value) error.value = '未获取到支付信息,请稍后重试'
if (!codeUrl.value && !alipayForm.value) {
error.value = '未获取到支付信息,请稍后重试'
} else if (alipayForm.value) {
// 网页支付:立刻把表单送去新页面,用户不必再点一次
openAlipayForm()
}
} else {
error.value = payErrorMessage(res)
}
@@ -237,11 +266,36 @@ async function startPay() {
}
}
/**
* 把支付宝返回的自提交表单写进一个新页面。
*
* 用 `document.write` 而不是 `v-html`:表单里带一段自动提交的脚本,
* 而 `v-html` 插入的脚本标签**不会执行**(HTML 规范如此),表单永远不会自动提交。
* 写进新窗口则脚本正常执行,且原收银台页面保留在原标签页继续轮询查单。
*
* 弹窗被拦截时不报"失败"——按钮复用本函数,用户允许弹窗后再点一次即可。
*
* 注意:本注释不要写出脚本标签的字面量——SFC 的 script 块会被它提前闭合。
*/
function openAlipayForm() {
if (!alipayForm.value) return
const w = window.open('', '_blank')
if (!w) {
error.value = '浏览器拦截了新窗口,请允许弹出窗口后点「前往支付宝付款」'
return
}
w.document.open()
w.document.write(alipayForm.value)
w.document.close()
error.value = ''
}
/** 切换支付方式:清掉上一张码再重新发起,避免两种支付信息同时留在页面上 */
async function switchPayType(next: 'wechat' | 'alipay') {
if (next === payType.value) return
payType.value = next
codeUrl.value = ''
alipayForm.value = ''
error.value = ''
await startPay()
}
@@ -74,11 +74,31 @@
</div>
<!--
两个渠道都是出码:微信 Native 与支付宝当面付(默认渠道)返回同一形态的 codeUrl,
渲染与轮询完全复用。支付宝不跳转账单——当面付需要在开放平台签约「当面付」,
与「电脑网站支付」是不同的产品签约(见 ADR 0010)。
支付宝走**电脑网站支付**:后端返回一段自提交表单,写进新页面即跳转支付宝收银台
(当面付扫码需单独签约并提交资料审核,见 ADR 0010 补记,故不作默认)。
微信仍是出码:两者在模板里各占一支。
-->
<template v-if="codeUrl">
<template v-if="alipayForm">
<div class="rounded-md bg-blue-50 px-4 py-6 text-sm text-blue-700">
已为你打开支付宝收银台。若新页面没有自动打开,请点击下方按钮。
</div>
<button
class="mt-6 w-full rounded-md bg-blue-600 py-3 font-medium text-white hover:bg-blue-700 disabled:opacity-50"
:disabled="confirming"
@click="openAlipayForm"
>
前往支付宝付款
</button>
<button
class="mt-3 w-full rounded-md border border-gray-300 py-3 font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
:disabled="confirming"
@click="confirmPaid"
>
{{ confirming ? '确认中...' : '我已完成支付,确认订单' }}
</button>
</template>
<template v-else-if="codeUrl">
<!-- 过期只是置灰提示,仍留在页面上:万一用户是刚过点就扫的,还能看见自己扫的是哪张码 -->
<div :class="payExpired ? 'opacity-30' : ''">
<HjcPayQrcode :value="codeUrl" :size="240" :alt="qrAlt" />
@@ -91,7 +111,7 @@
</div>
<button
v-if="codeUrl"
v-if="codeUrl && !alipayForm"
class="mt-6 w-full rounded-md bg-blue-600 py-3 text-white font-medium hover:bg-blue-700 disabled:opacity-50"
:disabled="confirming"
@click="confirmPaid"
@@ -99,6 +119,7 @@
{{ confirming ? '确认中...' : '我已完成支付,确认订单' }}
</button>
<button
v-if="!alipayForm"
class="mt-6 w-full rounded-md py-3 font-medium disabled:opacity-50"
:class="codeUrl
? 'border border-gray-300 text-gray-700 hover:bg-gray-50'
@@ -142,6 +163,8 @@ const error = ref('')
const step = ref<'form' | 'pay' | 'done'>('form')
const orderNo = ref('')
const codeUrl = ref('')
/** 支付宝电脑网站支付的自提交表单(后端只在该渠道下返回) */
const alipayForm = ref('')
const totalAmount = ref(0)
/** 当前支付方式。默认微信,与接入支付宝之前的行为一致 */
@@ -181,11 +204,36 @@ function formatMoney(n?: number | string) {
return Number(n ?? 0).toFixed(2)
}
/**
* 把支付宝返回的自提交表单写进一个新页面。
*
* 用 `document.write` 而不是 `v-html`:表单里带一段自动提交的脚本,
* 而 `v-html` 插入的脚本标签**不会执行**(HTML 规范如此),表单永远不会自动提交。
* 写进新窗口则脚本正常执行,且原页面保留在原标签页继续轮询查单。
*
* 弹窗被拦截时不报"失败"——按钮复用本函数,用户允许弹窗后再点一次即可。
*
* 注意:本注释不要写出脚本标签的字面量——SFC 的 script 块会被它提前闭合。
*/
function openAlipayForm() {
if (!alipayForm.value) return
const w = window.open('', '_blank')
if (!w) {
error.value = '浏览器拦截了新窗口,请允许弹出窗口后点「前往支付宝付款」'
return
}
w.document.open()
w.document.write(alipayForm.value)
w.document.close()
error.value = ''
}
/** 切换支付方式:清掉上一张码再重新发起,避免两种支付信息同时留在页面上 */
async function switchPayType(next: 'wechat' | 'alipay') {
if (next === payType.value) return
payType.value = next
codeUrl.value = ''
alipayForm.value = ''
error.value = ''
await refreshQr()
}
@@ -231,7 +279,13 @@ async function submit() {
step.value = 'pay'
if (payRes?.code === 0) {
codeUrl.value = payRes.data?.codeUrl || ''
if (!codeUrl.value) error.value = '未获取到支付信息,请稍后重试'
alipayForm.value = payRes.data?.alipayForm || ''
if (alipayForm.value) {
// 网页支付:立刻把表单送去新页面,用户不必再点一次
openAlipayForm()
} else if (!codeUrl.value) {
error.value = '未获取到支付信息,请稍后重试'
}
} else {
error.value = payRes?.message || '发起支付失败,请稍后重试'
}
@@ -296,8 +350,11 @@ async function refreshQr() {
return
}
codeUrl.value = res.data?.codeUrl || ''
alipayForm.value = res.data?.alipayForm || ''
if (res.data?.amount) totalAmount.value = Number(res.data.amount)
if (!codeUrl.value) {
if (alipayForm.value) {
openAlipayForm()
} else if (!codeUrl.value) {
error.value = '未获取到支付信息,请稍后重试'
return
}
@@ -311,11 +368,24 @@ async function refreshQr() {
loggedIn.value = isLoggedIn()
/**
* 必须用 useRequestFetch 取数,不能用裸 `$fetch`。
*
* 下面这段在 setup 顶层执行(SSR 期间就会跑):裸 `$fetch` 在服务端渲染时**不会**
* 转发浏览器原始请求头给站内 `/api` 代理,代理收到的是 host=localhost 的「新请求」,
* 租户中间件解析不到租户 → 上游按空租户查询 → SSR 拿不到数据而渲染 notFound 分支;
* 客户端水合时却拿到了真实数据,两棵树不匹配触发 hydration mismatch。
* (同一个坑的表现见 TenderDetail.vue / TenderList.vue 的注释。)
*
* 客户端侧 `useRequestFetch()` 等价于 `globalThis.$fetch`,行为不变。
*/
const requestFetch = useRequestFetch()
try {
if (!id) {
notFound.value = true
} else {
const res: any = await $fetch(`/api/tender/detail?id=${id}`)
const res: any = await requestFetch(`/api/tender/detail?id=${id}`)
if (res && res.id) tender.value = res
else notFound.value = true
}
+20 -2
View File
@@ -81,9 +81,27 @@ const listState = shallowRef<Tender[]>([])
const items = computed(() => listState.value)
const count = computed(() => (listState as any).__count || 0)
/**
* 必须用 useRequestFetch 取数,不能用裸 `$fetch`。
*
* 本组件在 setup 顶层就 `await load()`(SSR 期间执行):裸 `$fetch` 走 Nuxt 全局 ofetch,
* 服务端渲染时**不会**把浏览器原始请求头(Cookie、X-Forwarded-* 等)转发给站内 `/api` 代理,
* 代理收到的是 host=localhost、无 Cookie 的「新请求」——租户中间件按 localhost 走本地分支,
* 解析不到租户 → 上游按空租户查询 → SSR 渲染出「暂无标书项目」空列表。
* 客户端水合时浏览器重新请求拿到真实数据,两棵树分支错位(空状态 vs 网格列表),
* 卡片被插进残留的空状态 div 里 → 标书列表区域样式错乱(inline 排布、边框断裂)。
*
* `useRequestFetch` 会转发原始请求头:SSR 期间代理能拿到真实 Host 与 Cookie,
* 与客户端刷新后的行为一致。(同一个坑的另外两种表现见 TenderDetail.vue 与
* app/pages/tender/orders/index.vue 的注释。)
*
* 客户端侧 `useRequestFetch()` 等价于 `globalThis.$fetch`,行为不变。
*/
const requestFetch = useRequestFetch()
async function load() {
page.value = 1
const res: any = await $fetch('/api/tender/list', {
const res: any = await requestFetch('/api/tender/list', {
query: { page: page.value, limit, keywords: keywords.value || undefined, category: category.value || undefined }
})
listState.value = res?.list || []
@@ -93,7 +111,7 @@ async function load() {
async function loadMore() {
page.value += 1
const res: any = await $fetch('/api/tender/list', {
const res: any = await requestFetch('/api/tender/list', {
query: { page: page.value, limit, keywords: keywords.value || undefined, category: category.value || undefined }
})
const next = res?.list || []