Files
hjc-web/app/pages/register.vue
T
weicw1996 d0c2dd17cb feat(tender): 登录加图形验证码、注册加短信验证码;改按 body.code 判定 401/403
- 新增 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)
- 下单成功但发起支付失败时停在支付步,避免用户重提交产生重复订单
2026-09-10 23:44:19 +08:00

178 lines
5.9 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="min-h-[60vh] flex items-center justify-center px-4 py-10">
<div class="w-full max-w-2xl rounded-lg border border-gray-200 p-8">
<h1 class="text-2xl font-bold text-center text-gray-900">企业注册</h1>
<p class="mt-2 text-center text-sm text-gray-500">填写企业与经办人信息上传证件后提交审核</p>
<form class="mt-6 space-y-4" @submit.prevent="submitForm">
<HjcEnterpriseForm
ref="entForm"
v-model:form="form"
v-model:materials="materials"
show-password
/>
<!-- 短信验证码发到经办人手机号」(即账号手机号),注册接口的 code 用它校验 -->
<div class="space-y-1">
<label class="block text-sm text-gray-600">短信验证码</label>
<div class="flex items-center gap-2">
<input
v-model="smsCode"
class="w-full rounded-md border border-gray-300 px-4 py-2 text-sm"
placeholder="发送至上方填写的经办人手机号"
autocomplete="off"
/>
<button
type="button"
class="h-[38px] w-32 shrink-0 rounded-md border border-gray-300 text-xs text-gray-600 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="countdown > 0 || sending"
@click="sendCode"
>
{{ countdown > 0 ? `${countdown} 秒后重发` : sending ? '发送中...' : '发送验证码' }}
</button>
</div>
<p class="text-xs text-gray-400">验证码发到经办人手机号」,该号码就是登录账号手机号</p>
</div>
<p class="text-center text-xs text-gray-500">
企业联系电话企业地址选填其余项必填提交后等待平台审核审核通过方可购买标书
</p>
<button
type="submit"
class="w-full rounded-md bg-blue-600 py-3 font-medium text-white hover:bg-blue-700 disabled:opacity-50"
:disabled="loading"
>
{{ loading ? '注册中...' : '注册并登录' }}
</button>
<p v-if="error" class="text-center text-sm text-red-500">{{ error }}</p>
</form>
<p class="mt-4 text-center text-sm text-gray-500">
已注册
<NuxtLink to="/login" class="text-blue-600 hover:underline">去登录</NuxtLink>
</p>
</div>
</div>
</template>
<script setup lang="ts">
const { register, sendSms } = useHjcAuth()
const route = useRoute()
const loading = ref(false)
const error = ref('')
const entForm = ref<any>(null)
/** 短信验证码(注册接口的 code)+ 发送倒计时 */
const smsCode = ref('')
const sending = ref(false)
const countdown = ref(0)
let countdownTimer: ReturnType<typeof setInterval> | null = null
const SMS_COUNTDOWN_SECONDS = 60
/**
* 注册成功后的去向:`redirect` 可能被 encodeURIComponent 过(如 /buy%3Fid%3D3),
* 这里安全解码;仅接受站内路径,避免开放重定向。
*/
function resolveRedirect() {
const raw = (route.query.redirect as string) || ''
if (!raw) return '/tender'
let target = raw
try {
target = decodeURIComponent(raw)
} catch {
target = raw
}
// 必须以单个 '/' 开头:排除 http(s)://、协议相对地址 //evil.com
// 以及反斜杠变体 /\evil.com(部分浏览器把 '\' 归一成 '/',等价于 //evil.com
return target.startsWith('/') && !target.startsWith('//') && !target.includes('\\') ? target : '/tender'
}
const form = ref<Record<string, any>>({
name: '',
password: '',
creditCode: '',
address: '',
contactPhone: '',
contactEmail: '',
agentName: '',
idCardNo: '',
agentEmail: '',
agentPhone: '',
authorizeExpire: ''
})
const materials = ref<any[]>([
{ materialType: 'idcard_front', label: '身份证人像面', materialName: '身份证人像面', fileUrl: '' },
{ materialType: 'idcard_back', label: '身份证国徽面', materialName: '身份证国徽面', fileUrl: '' },
{ materialType: 'handbook', label: '授权委托书', materialName: '授权委托书', fileUrl: '' },
{ materialType: 'license', label: '营业执照', materialName: '营业执照', fileUrl: '' }
])
/** 停止倒计时(倒计时结束或页面卸载时调用) */
function stopCountdown() {
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
countdown.value = 0
}
/** 发送短信验证码到经办人手机号(= 账号手机号) */
async function sendCode() {
error.value = ''
const phone = String(form.value.agentPhone || '').trim()
if (!phone) {
error.value = '请先填写经办人手机号'
return
}
sending.value = true
try {
const res = await sendSms(phone)
if (!res.ok) {
error.value = res.message || '验证码发送失败'
return
}
countdown.value = SMS_COUNTDOWN_SECONDS
countdownTimer = setInterval(() => {
countdown.value -= 1
if (countdown.value <= 0) stopCountdown()
}, 1000)
} finally {
sending.value = false
}
}
onUnmounted(stopCountdown)
async function submitForm() {
error.value = ''
if (!entForm.value?.validate()) return
if (!smsCode.value.trim()) {
error.value = '请输入短信验证码'
return
}
loading.value = true
try {
// buildPayload 返回 { name, ... };注册接口用 enterpriseName 作为登录账号
const { name, ...rest } = entForm.value.buildPayload()
const res = await register({
enterpriseName: name,
password: form.value.password,
// code = 短信验证码(登录接口的 code 是图形验证码,两者不同)
code: smsCode.value.trim(),
...rest
})
if (res.ok) {
return navigateTo(resolveRedirect())
}
error.value = res.message || '注册失败'
} catch (e: any) {
error.value = e?.message || '注册失败'
} finally {
loading.value = false
}
}
</script>