feat(tender): 阶段3b 企业登录/注册 + 购买→支付→确认 闭环

- useHjcAuth:token 存 cookie(hjc_token),login/register/isLoggedIn
- 页面 /login /register(企业名称+密码)
- server/api/tender/{auth/login,auth/register,pay,mark-paid} 代理;order 代理从 cookie 读取 hjc_token 透传 Authorization
- BuyDocument:未登录跳登录;下单→pay(codeUrl)→确认 mark-paid→成功
- types 增 HjcUser
This commit is contained in:
2026-09-08 22:44:30 +08:00
parent 2828ba6718
commit 0fbf122a57
10 changed files with 389 additions and 25 deletions
+68 -22
View File
@@ -2,7 +2,17 @@
<div class="container mx-auto px-4 py-10">
<NuxtLink to="/tender" class="text-sm text-blue-600 hover:underline"> 返回标书中心</NuxtLink>
<div v-if="tender" class="mx-auto mt-6 max-w-2xl rounded-lg border border-gray-200 p-6">
<!-- 未登录提示 -->
<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>
@@ -30,19 +40,34 @@
<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 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="tip" class="text-center text-sm text-gray-500">{{ tip }}</p>
<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>
@@ -53,18 +78,21 @@ import type { Tender } from '~/types/tender'
const route = useRoute()
const id = route.query.id as string
const { isLoggedIn } = useHjcAuth()
const loggedIn = ref(false)
const tender = shallowRef<Tender | null>(null)
const notFound = ref(false)
const submitting = ref(false)
const tip = ref('')
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 form = reactive({ contactName: '', contactPhone: '', contactEmail: '', quantity: 1 })
const redirect = computed(() => `/buy?id=${id}`)
function formatMoney(n?: number | string) {
return Number(n ?? 0).toFixed(2)
@@ -72,10 +100,9 @@ function formatMoney(n?: number | string) {
async function submit() {
error.value = ''
tip.value = ''
submitting.value = true
try {
const res: any = await $fetch('/api/tender/order', {
const order: any = await $fetch('/api/tender/order', {
method: 'POST',
body: {
projectId: Number(id),
@@ -85,11 +112,15 @@ async function submit() {
contactEmail: form.contactEmail
}
})
if (res && res.orderNo) {
tip.value = `下单成功,订单号:${res.orderNo},请扫码支付`
// TODO(阶段3b): 调 /api/tender/pay 获取微信二维码(需登录态)
if (order && order.orderNo) {
orderNo.value = order.orderNo
totalAmount.value = order.totalAmount || 0
// 发起支付
const pay: any = await $fetch('/api/tender/pay', { method: 'POST', body: { orderNo: order.orderNo } })
codeUrl.value = pay?.codeUrl || ''
step.value = 'pay'
} else {
error.value = res?.message || '下单失败'
error.value = order?.message || '下单失败'
}
} catch (e: any) {
error.value = e?.data?.message || e?.message || '下单失败'
@@ -98,6 +129,21 @@ async function submit() {
}
}
async function markPaid() {
error.value = ''
marking.value = true
try {
await $fetch('/api/tender/mark-paid', { method: 'PUT', body: { orderNo: orderNo.value } })
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