Files
hjc-web/app/pages/register.vue
T
weicw-dshsa 3c6582c0ec feat(web): 注册与资质表单新增基本户信息与发票类型
- HjcEnterpriseForm.vue:新增账户名称/账号/开户银行/开户支行四个输入与发票类型单选
  (VAT_SPECIAL 电子专票 / VAT_NORMAL 电子普票,无默认值),模板 + validate + buildPayload + fill 四处同步
- 账号只做长度与字符校验(^[0-9]{8,32}$),与后端 validateRegister 一致,不加真实性校验
- 必填提示点名到具体项:请填写基本户账户名称 / 开户银行 / 开户支行,未选发票类型提示「请选择发票类型」
- register.vue、tender/qualification.vue 表单初值补齐 5 个字段
- 回显兼容 bank_account_name 与 bankAccountName 两种键名
2026-10-08 12:20:13 +08:00

197 lines
6.3 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 { siteInfo, fetchSiteInfo } = useSite()
const route = useRoute()
// 先取站点信息,保证标题能带上品牌名(与其它页面一致)
await fetchSiteInfo()
usePageSeo(
{
title: '企业注册',
path: '/register'
},
siteInfo.value
)
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: '',
bankAccountName: '',
bankAccountNo: '',
bankName: '',
bankBranch: '',
// 发票类型:单选、无默认值(未选时提交被拦截)
invoiceType: '',
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>