d0c2dd17cb
- 新增 captcha / sms / logout 三个 Nuxt server 代理 - 修复代理把 ApiResult 拍扁成 data 的问题(my-orders、enterprise、order、pay、 mark-paid):登录态失败被前端当成「没有订单 / 未提交资质」,无法区分 - 新增 handleAuthCode 统一处理:401 → 清凭据并跳 /login?redirect=…; 403 → 只提示「没有访问权限」,不清 token、不跳登录页 - 登录页与注册页分别接入图形验证码与短信验证码(含 60s 倒计时) - 修复回跳开放重定向:redirect 参数安全解码且只接受站内路径 (拒绝 //evil.com、https://… 及反斜杠变体 /\evil.com) - 下单成功但发起支付失败时停在支付步,避免用户重提交产生重复订单
185 lines
7.4 KiB
Vue
185 lines
7.4 KiB
Vue
<template>
|
||
<div class="container mx-auto px-4 py-10">
|
||
<NuxtLink to="/tender" class="text-sm text-blue-600 hover:underline">← 返回标书中心</NuxtLink>
|
||
|
||
<!-- 未登录提示 -->
|
||
<div v-if="!loggedIn" class="mx-auto mt-6 max-w-xl rounded-lg border border-gray-200 p-8 text-center">
|
||
<p class="text-lg text-gray-700">购买标书需要先登录企业账号</p>
|
||
<div class="mt-6 flex gap-4 justify-center">
|
||
<NuxtLink :to="`/login?redirect=${redirect}`" class="rounded-md bg-blue-600 px-6 py-2 text-white hover:bg-blue-700">去登录</NuxtLink>
|
||
<NuxtLink :to="`/register?redirect=${redirect}`" class="rounded-md border border-gray-300 px-6 py-2 text-gray-700 hover:bg-gray-50">企业注册</NuxtLink>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 表单 -->
|
||
<div v-else-if="tender && step === 'form'" class="mx-auto mt-6 max-w-2xl rounded-lg border border-gray-200 p-6">
|
||
<h1 class="text-xl font-bold text-gray-900">购买标书</h1>
|
||
<div class="mt-4 rounded-md bg-gray-50 p-4 text-sm">
|
||
<p class="font-semibold text-gray-800">{{ tender.projectName }}</p>
|
||
<p class="mt-1 text-gray-500">项目编号:{{ tender.projectNo }}</p>
|
||
<p class="mt-2 text-blue-600">
|
||
信息服务费:<span class="text-2xl font-bold">¥{{ formatMoney(tender.tenderPrice) }}</span>
|
||
</p>
|
||
</div>
|
||
|
||
<form class="mt-6 space-y-4" @submit.prevent="submit">
|
||
<div>
|
||
<label class="text-sm text-gray-600">购买联系人</label>
|
||
<input v-model="form.contactName" class="mt-1 w-full rounded-md border border-gray-300 px-4 py-2 text-sm" placeholder="姓名" />
|
||
</div>
|
||
<div>
|
||
<label class="text-sm text-gray-600">联系电话</label>
|
||
<input v-model="form.contactPhone" class="mt-1 w-full rounded-md border border-gray-300 px-4 py-2 text-sm" placeholder="手机号" />
|
||
</div>
|
||
<div>
|
||
<label class="text-sm text-gray-600">联系邮箱</label>
|
||
<input v-model="form.contactEmail" type="email" class="mt-1 w-full rounded-md border border-gray-300 px-4 py-2 text-sm" placeholder="邮箱" />
|
||
</div>
|
||
<div>
|
||
<label class="text-sm text-gray-600">购买数量</label>
|
||
<input v-model.number="form.quantity" type="number" min="1" class="mt-1 w-full rounded-md border border-gray-300 px-4 py-2 text-sm" />
|
||
</div>
|
||
|
||
<button type="submit" class="w-full rounded-md bg-blue-600 py-3 text-white font-medium hover:bg-blue-700 disabled:opacity-50" :disabled="submitting">
|
||
{{ submitting ? '提交中...' : '提交订单并用微信支付' }}
|
||
</button>
|
||
<p v-if="error" class="text-center text-sm text-red-500">{{ error }}</p>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- 支付 -->
|
||
<div v-else-if="step === 'pay'" class="mx-auto mt-6 max-w-2xl rounded-lg border border-gray-200 p-8 text-center">
|
||
<h1 class="text-xl font-bold text-gray-900">请使用微信扫码支付</h1>
|
||
<p class="mt-2 text-sm text-gray-500">订单号:{{ orderNo }}</p>
|
||
<p class="my-6 text-2xl font-bold text-blue-600">¥{{ formatMoney(totalAmount) }}</p>
|
||
<div class="rounded-md bg-gray-50 p-4 text-sm break-all text-gray-700">
|
||
扫码链接:<a :href="codeUrl" target="_blank" class="text-blue-600 hover:underline">{{ codeUrl }}</a>
|
||
</div>
|
||
<button class="mt-6 w-full rounded-md bg-blue-600 py-3 text-white font-medium hover:bg-blue-700" :disabled="marking" @click="markPaid">
|
||
{{ marking ? '确认中...' : '支付已完成,确认订单' }}
|
||
</button>
|
||
<p v-if="error" class="mt-3 text-sm text-red-500">{{ error }}</p>
|
||
</div>
|
||
|
||
<!-- 完成 -->
|
||
<div v-else-if="step === 'done'" class="mx-auto mt-6 max-w-2xl rounded-lg border border-green-200 bg-green-50 p-8 text-center">
|
||
<p class="text-xl font-bold text-green-700">购买成功</p>
|
||
<p class="mt-2 text-sm text-green-600">订单号:{{ orderNo }},标书已可查看/下载</p>
|
||
<NuxtLink to="/tender" class="mt-6 inline-block rounded-md bg-green-600 px-6 py-2 text-white hover:bg-green-700">返回标书中心</NuxtLink>
|
||
</div>
|
||
|
||
<div v-else-if="notFound" class="py-20 text-center text-gray-500">项目不存在</div>
|
||
<SiteLoading v-else />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import type { Tender } from '~/types/tender'
|
||
|
||
const route = useRoute()
|
||
const id = route.query.id as string
|
||
const { isLoggedIn, handleAuthCode } = useHjcAuth()
|
||
|
||
const loggedIn = ref(false)
|
||
const tender = shallowRef<Tender | null>(null)
|
||
const notFound = ref(false)
|
||
const submitting = ref(false)
|
||
const marking = ref(false)
|
||
const error = ref('')
|
||
const step = ref<'form' | 'pay' | 'done'>('form')
|
||
const orderNo = ref('')
|
||
const codeUrl = ref('')
|
||
const totalAmount = ref(0)
|
||
|
||
const form = reactive({ contactName: '', contactPhone: '', contactEmail: '', quantity: 1 })
|
||
const redirect = computed(() => `/buy?id=${id}`)
|
||
|
||
function formatMoney(n?: number | string) {
|
||
return Number(n ?? 0).toFixed(2)
|
||
}
|
||
|
||
async function submit() {
|
||
error.value = ''
|
||
submitting.value = true
|
||
try {
|
||
// 代理原样透传 ApiResult:按 body.code 判定(HTTP 状态码恒为 200,不可用于判定)
|
||
const order: any = await $fetch('/api/tender/order', {
|
||
method: 'POST',
|
||
body: {
|
||
projectId: Number(id),
|
||
quantity: form.quantity || 1,
|
||
contactName: form.contactName,
|
||
contactPhone: form.contactPhone,
|
||
contactEmail: form.contactEmail
|
||
}
|
||
})
|
||
const orderAuth = await handleAuthCode(order, redirect.value)
|
||
if (orderAuth.handled) {
|
||
// 401 已清凭据并跳登录页;403 只提示「没有访问权限」,不清 token
|
||
error.value = orderAuth.message || ''
|
||
return
|
||
}
|
||
if (order?.code !== 0) {
|
||
error.value = order?.message || '下单失败'
|
||
return
|
||
}
|
||
orderNo.value = order.data?.orderNo || ''
|
||
totalAmount.value = order.data?.totalAmount || 0
|
||
// 发起支付
|
||
const pay: any = await $fetch('/api/tender/pay', { method: 'POST', body: { orderNo: orderNo.value } })
|
||
const payAuth = await handleAuthCode(pay, redirect.value)
|
||
if (payAuth.handled) {
|
||
error.value = payAuth.message || ''
|
||
return
|
||
}
|
||
// 订单已创建:即使发起支付失败也停在支付步骤,避免用户重提交产生重复订单
|
||
step.value = 'pay'
|
||
if (pay?.code === 0) {
|
||
codeUrl.value = pay.data?.codeUrl || ''
|
||
} else {
|
||
error.value = pay?.message || '发起支付失败,请稍后重试'
|
||
}
|
||
} catch (e: any) {
|
||
error.value = e?.data?.message || e?.message || '下单失败'
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
async function markPaid() {
|
||
error.value = ''
|
||
marking.value = true
|
||
try {
|
||
const res: any = await $fetch('/api/tender/mark-paid', { method: 'PUT', body: { orderNo: orderNo.value } })
|
||
const auth = await handleAuthCode(res, redirect.value)
|
||
if (auth.handled) {
|
||
error.value = auth.message || ''
|
||
return
|
||
}
|
||
if (res?.code !== 0) {
|
||
error.value = res?.message || '确认失败'
|
||
return
|
||
}
|
||
step.value = 'done'
|
||
} catch (e: any) {
|
||
error.value = e?.data?.message || e?.message || '确认失败'
|
||
} finally {
|
||
marking.value = false
|
||
}
|
||
}
|
||
|
||
loggedIn.value = isLoggedIn()
|
||
|
||
try {
|
||
if (!id) {
|
||
notFound.value = true
|
||
} else {
|
||
const res: any = await $fetch(`/api/tender/detail?id=${id}`)
|
||
if (res && res.id) tender.value = res
|
||
else notFound.value = true
|
||
}
|
||
} catch {
|
||
notFound.value = true
|
||
}
|
||
</script>
|