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:
@@ -63,11 +63,31 @@
|
|||||||
</div>
|
</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' : ''">
|
<div :class="payExpired ? 'opacity-30' : ''">
|
||||||
<HjcPayQrcode :value="codeUrl" :size="240" :alt="qrAlt" />
|
<HjcPayQrcode :value="codeUrl" :size="240" :alt="qrAlt" />
|
||||||
@@ -80,7 +100,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<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"
|
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"
|
:disabled="confirming"
|
||||||
@click="confirmPaid"
|
@click="confirmPaid"
|
||||||
@@ -88,6 +108,7 @@
|
|||||||
{{ confirming ? '确认中...' : '我已完成支付,确认订单' }}
|
{{ confirming ? '确认中...' : '我已完成支付,确认订单' }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="!alipayForm"
|
||||||
class="mt-6 w-full rounded-md py-3 font-medium disabled:opacity-50"
|
class="mt-6 w-full rounded-md py-3 font-medium disabled:opacity-50"
|
||||||
:class="codeUrl
|
:class="codeUrl
|
||||||
? 'border border-gray-300 text-gray-700 hover:bg-gray-50'
|
? 'border border-gray-300 text-gray-700 hover:bg-gray-50'
|
||||||
@@ -119,8 +140,8 @@ import { hjcOrderStatusKey } from '~/utils/orderStatus'
|
|||||||
* 两份实现共享的只有 `HjcPayQrcode` 与 `useHjcPayStatus`(重活已经都在那里了)。
|
* 两份实现共享的只有 `HjcPayQrcode` 与 `useHjcPayStatus`(重活已经都在那里了)。
|
||||||
*
|
*
|
||||||
* 本页只服务微信外(ADR 0010):不因 UA 在微信内就跳 hjc-h5。
|
* 本页只服务微信外(ADR 0010):不因 UA 在微信内就跳 hjc-h5。
|
||||||
* 支付方式可选**微信扫码**或**支付宝**;两者都是出码(支付宝默认当面付,
|
* 支付方式可选**微信扫码**或**支付宝**;微信出二维码、支付宝走电脑网站支付
|
||||||
* 与微信 Native 同一交互),渲染与轮询完全复用。
|
* (返回自提交表单,跳转支付宝收银台)。两者共用同一个查单轮询与状态判定。
|
||||||
*/
|
*/
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { isLoggedIn, handleAuthCode } = useHjcAuth()
|
const { isLoggedIn, handleAuthCode } = useHjcAuth()
|
||||||
@@ -146,6 +167,8 @@ const payOptions = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const codeUrl = ref('')
|
const codeUrl = ref('')
|
||||||
|
/** 支付宝电脑网站支付的自提交表单(后端只在该渠道下返回) */
|
||||||
|
const alipayForm = ref('')
|
||||||
const amount = ref(0)
|
const amount = ref(0)
|
||||||
const starting = ref(false)
|
const starting = ref(false)
|
||||||
const refreshing = ref(false)
|
const refreshing = ref(false)
|
||||||
@@ -222,8 +245,14 @@ async function startPay() {
|
|||||||
}
|
}
|
||||||
if (res?.code === 0) {
|
if (res?.code === 0) {
|
||||||
codeUrl.value = res.data?.codeUrl || ''
|
codeUrl.value = res.data?.codeUrl || ''
|
||||||
|
alipayForm.value = res.data?.alipayForm || ''
|
||||||
if (res.data?.amount) amount.value = Number(res.data.amount)
|
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 {
|
} else {
|
||||||
error.value = payErrorMessage(res)
|
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') {
|
async function switchPayType(next: 'wechat' | 'alipay') {
|
||||||
if (next === payType.value) return
|
if (next === payType.value) return
|
||||||
payType.value = next
|
payType.value = next
|
||||||
codeUrl.value = ''
|
codeUrl.value = ''
|
||||||
|
alipayForm.value = ''
|
||||||
error.value = ''
|
error.value = ''
|
||||||
await startPay()
|
await startPay()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,11 +74,31 @@
|
|||||||
</div>
|
</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' : ''">
|
<div :class="payExpired ? 'opacity-30' : ''">
|
||||||
<HjcPayQrcode :value="codeUrl" :size="240" :alt="qrAlt" />
|
<HjcPayQrcode :value="codeUrl" :size="240" :alt="qrAlt" />
|
||||||
@@ -91,7 +111,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<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"
|
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"
|
:disabled="confirming"
|
||||||
@click="confirmPaid"
|
@click="confirmPaid"
|
||||||
@@ -99,6 +119,7 @@
|
|||||||
{{ confirming ? '确认中...' : '我已完成支付,确认订单' }}
|
{{ confirming ? '确认中...' : '我已完成支付,确认订单' }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="!alipayForm"
|
||||||
class="mt-6 w-full rounded-md py-3 font-medium disabled:opacity-50"
|
class="mt-6 w-full rounded-md py-3 font-medium disabled:opacity-50"
|
||||||
:class="codeUrl
|
:class="codeUrl
|
||||||
? 'border border-gray-300 text-gray-700 hover:bg-gray-50'
|
? '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 step = ref<'form' | 'pay' | 'done'>('form')
|
||||||
const orderNo = ref('')
|
const orderNo = ref('')
|
||||||
const codeUrl = ref('')
|
const codeUrl = ref('')
|
||||||
|
/** 支付宝电脑网站支付的自提交表单(后端只在该渠道下返回) */
|
||||||
|
const alipayForm = ref('')
|
||||||
const totalAmount = ref(0)
|
const totalAmount = ref(0)
|
||||||
|
|
||||||
/** 当前支付方式。默认微信,与接入支付宝之前的行为一致 */
|
/** 当前支付方式。默认微信,与接入支付宝之前的行为一致 */
|
||||||
@@ -181,11 +204,36 @@ function formatMoney(n?: number | string) {
|
|||||||
return Number(n ?? 0).toFixed(2)
|
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') {
|
async function switchPayType(next: 'wechat' | 'alipay') {
|
||||||
if (next === payType.value) return
|
if (next === payType.value) return
|
||||||
payType.value = next
|
payType.value = next
|
||||||
codeUrl.value = ''
|
codeUrl.value = ''
|
||||||
|
alipayForm.value = ''
|
||||||
error.value = ''
|
error.value = ''
|
||||||
await refreshQr()
|
await refreshQr()
|
||||||
}
|
}
|
||||||
@@ -231,7 +279,13 @@ async function submit() {
|
|||||||
step.value = 'pay'
|
step.value = 'pay'
|
||||||
if (payRes?.code === 0) {
|
if (payRes?.code === 0) {
|
||||||
codeUrl.value = payRes.data?.codeUrl || ''
|
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 {
|
} else {
|
||||||
error.value = payRes?.message || '发起支付失败,请稍后重试'
|
error.value = payRes?.message || '发起支付失败,请稍后重试'
|
||||||
}
|
}
|
||||||
@@ -296,8 +350,11 @@ async function refreshQr() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
codeUrl.value = res.data?.codeUrl || ''
|
codeUrl.value = res.data?.codeUrl || ''
|
||||||
|
alipayForm.value = res.data?.alipayForm || ''
|
||||||
if (res.data?.amount) totalAmount.value = Number(res.data.amount)
|
if (res.data?.amount) totalAmount.value = Number(res.data.amount)
|
||||||
if (!codeUrl.value) {
|
if (alipayForm.value) {
|
||||||
|
openAlipayForm()
|
||||||
|
} else if (!codeUrl.value) {
|
||||||
error.value = '未获取到支付信息,请稍后重试'
|
error.value = '未获取到支付信息,请稍后重试'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -311,11 +368,24 @@ async function refreshQr() {
|
|||||||
|
|
||||||
loggedIn.value = isLoggedIn()
|
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 {
|
try {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
notFound.value = true
|
notFound.value = true
|
||||||
} else {
|
} 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
|
if (res && res.id) tender.value = res
|
||||||
else notFound.value = true
|
else notFound.value = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,9 +81,27 @@ const listState = shallowRef<Tender[]>([])
|
|||||||
const items = computed(() => listState.value)
|
const items = computed(() => listState.value)
|
||||||
const count = computed(() => (listState as any).__count || 0)
|
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() {
|
async function load() {
|
||||||
page.value = 1
|
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 }
|
query: { page: page.value, limit, keywords: keywords.value || undefined, category: category.value || undefined }
|
||||||
})
|
})
|
||||||
listState.value = res?.list || []
|
listState.value = res?.list || []
|
||||||
@@ -93,7 +111,7 @@ async function load() {
|
|||||||
|
|
||||||
async function loadMore() {
|
async function loadMore() {
|
||||||
page.value += 1
|
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 }
|
query: { page: page.value, limit, keywords: keywords.value || undefined, category: category.value || undefined }
|
||||||
})
|
})
|
||||||
const next = res?.list || []
|
const next = res?.list || []
|
||||||
|
|||||||
Reference in New Issue
Block a user