feat(tender): 企业注册与 h5 对齐(一站式表单 + 证件上传 + OCR 回填);修复登录/注册代理路由 404
- 新增 app/components/HjcEnterpriseForm.vue:与 h5 EnterpriseForm 行为一致的共享表单,
字段集 / 必填规则 / OCR 回填 / 提示文案逐条对齐
- register.vue:一站式注册,一次性提交企业 + 经办人 + 授权 + 证件材料;
企业联系电话、企业地址选填,其余(含企业邮箱)必填,4 类证件必传
- tender/qualification.vue:改用共享组件,补「经办人身份证号」,
以真实文件上传 + OCR 取代原「证件 URL 文本框」;
审核中(0)/已通过(1) 只读,驳回(2)/未提交(-1) 可编辑重提
- 新增 server/api/tender/upload.post.ts(multipart 原样转发 → /hjc/auth/upload,匿名,
登录前后均可用)与 server/api/tender/ocr/recognize.post.ts(→ /hjc/ocr/recognize)
- 修正 datetime-local(YYYY-MM-DDTHH:mm)与后端 yyyy-MM-dd HH:mm:ss 的格式转换
- fix: server/api/tender/auth/{login,register}.post.ts 实际路由为 /api/tender/auth/*,
而客户端调用 /api/tender/{login,register},导致登录与注册此前均 404;
已按文件注释与同级接口约定移至扁平路径 server/api/tender/{login,register}.post.ts
- register / login / enterprise-save 三个代理改为原样透传 ApiResult{code,message,data},
useHjcAuth 与资质页按 code===0 判定,避免后端校验失败信息被吞掉
This commit is contained in:
@@ -7,7 +7,12 @@
|
||||
|
||||
<div v-if="!loggedIn" class="py-20 text-center">
|
||||
<p class="text-gray-500">请先登录后管理企业资质</p>
|
||||
<NuxtLink to="/login?redirect=/tender/qualification" class="mt-4 inline-block rounded-md bg-blue-600 px-6 py-2 text-white hover:bg-blue-700">去登录</NuxtLink>
|
||||
<NuxtLink
|
||||
to="/login?redirect=/tender/qualification"
|
||||
class="mt-4 inline-block rounded-md bg-blue-600 px-6 py-2 text-white hover:bg-blue-700"
|
||||
>
|
||||
去登录
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div v-else class="mx-auto max-w-2xl rounded-lg border border-gray-200 p-6">
|
||||
@@ -20,24 +25,18 @@
|
||||
</div>
|
||||
|
||||
<form class="space-y-4" @submit.prevent="submit">
|
||||
<Field label="企业名称"><input v-model="form.name" class="field" /></Field>
|
||||
<Field label="纳税人识别号"><input v-model="form.creditCode" class="field" /></Field>
|
||||
<Field label="企业联系电话"><input v-model="form.contactPhone" class="field" /></Field>
|
||||
<Field label="企业邮箱"><input v-model="form.contactEmail" type="email" class="field" /></Field>
|
||||
<Field label="企业地址"><input v-model="form.address" class="field" /></Field>
|
||||
<Field label="经办人姓名"><input v-model="form.agentName" class="field" /></Field>
|
||||
<Field label="经办人邮箱"><input v-model="form.agentEmail" type="email" class="field" /></Field>
|
||||
<Field label="经办人手机号"><input v-model="form.agentPhone" class="field" /></Field>
|
||||
<Field label="授权到期时间"><input v-model="form.authorizeExpire" type="datetime-local" class="field" /></Field>
|
||||
<HjcEnterpriseForm ref="entForm" v-model:form="form" v-model:materials="materials" :readonly="readonly" />
|
||||
|
||||
<div class="space-y-3">
|
||||
<Field label="经办人身份证正面 URL"><input v-model="materials.idcard_front" class="field" placeholder="/upload/xxx.jpg" /></Field>
|
||||
<Field label="经办人身份证反面 URL"><input v-model="materials.idcard_back" class="field" placeholder="/upload/xxx.jpg" /></Field>
|
||||
<Field label="授权委托书 URL"><input v-model="materials.handbook" class="field" placeholder="/upload/xxx.pdf" /></Field>
|
||||
<Field label="营业执照 URL"><input v-model="materials.license" class="field" placeholder="/upload/xxx.jpg" /></Field>
|
||||
</div>
|
||||
<p v-if="readonly" class="text-center text-xs text-gray-500">
|
||||
资质审核中 / 已通过时不可修改;被驳回后可修改并重新提交。
|
||||
</p>
|
||||
|
||||
<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">
|
||||
<button
|
||||
v-if="!readonly"
|
||||
type="submit"
|
||||
class="w-full rounded-md bg-blue-600 py-3 font-medium text-white 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>
|
||||
@@ -48,95 +47,83 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent, h } from 'vue'
|
||||
|
||||
const Field = defineComponent({
|
||||
props: { label: String },
|
||||
setup(props, { slots }) {
|
||||
return () => h('div', { class: 'space-y-1' }, [
|
||||
h('label', { class: 'block text-sm text-gray-600' }, props.label),
|
||||
slots.default?.()
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
const { isLoggedIn } = useHjcAuth()
|
||||
const loggedIn = ref(false)
|
||||
const authStatus = ref(0)
|
||||
const authStatus = ref(-1)
|
||||
const rejectReason = ref('')
|
||||
const submitting = ref(false)
|
||||
const error = ref('')
|
||||
const tip = ref('')
|
||||
const entForm = ref<any>(null)
|
||||
|
||||
const form = reactive({
|
||||
name: '', creditCode: '', contactPhone: '', contactEmail: '', address: '',
|
||||
agentName: '', agentEmail: '', agentPhone: '', authorizeExpire: ''
|
||||
const form = ref<Record<string, any>>({
|
||||
name: '',
|
||||
creditCode: '',
|
||||
contactPhone: '',
|
||||
contactEmail: '',
|
||||
address: '',
|
||||
agentName: '',
|
||||
idCardNo: '',
|
||||
agentEmail: '',
|
||||
agentPhone: '',
|
||||
authorizeExpire: ''
|
||||
})
|
||||
const materials = reactive({ idcard_front: '', idcard_back: '', handbook: '', license: '' })
|
||||
|
||||
const authStatusText = computed(() => ({ 0: '待审核', 1: '已通过', 2: '已驳回' } as any)[authStatus.value] ?? '待审核')
|
||||
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: '' }
|
||||
])
|
||||
|
||||
const authStatusText = computed(
|
||||
() => ({ 0: '待审核', 1: '已通过', 2: '已驳回' } as Record<number, string>)[authStatus.value] ?? '未提交资质'
|
||||
)
|
||||
const authStatusClass = computed(() => {
|
||||
if (authStatus.value === 1) return 'bg-green-100 text-green-700'
|
||||
if (authStatus.value === 2) return 'bg-red-100 text-red-600'
|
||||
return 'bg-amber-100 text-amber-700'
|
||||
})
|
||||
/** 审核中(0)/已通过(1) 只读;驳回(2)/未提交(-1) 可编辑重提(与 h5 一致) */
|
||||
const readonly = computed(() => authStatus.value === 0 || authStatus.value === 1)
|
||||
|
||||
async function load() {
|
||||
const res: any = await $fetch('/api/tender/enterprise')
|
||||
if (res) {
|
||||
authStatus.value = res.authStatus ?? 0
|
||||
authStatus.value = res.authStatus ?? -1
|
||||
rejectReason.value = res.rejectReason || ''
|
||||
form.name = res.name || ''
|
||||
form.creditCode = res.creditCode || ''
|
||||
form.contactPhone = res.contactPhone || ''
|
||||
form.contactEmail = res.contactEmail || ''
|
||||
form.address = res.address || ''
|
||||
form.agentName = res.agentName || ''
|
||||
form.agentEmail = res.agentEmail || ''
|
||||
form.agentPhone = res.agentPhone || ''
|
||||
form.authorizeExpire = res.authorizeExpire ? res.authorizeExpire.slice(0, 16) : ''
|
||||
const ms: any[] = res.materials || []
|
||||
for (const m of ms) {
|
||||
if (m.materialType && (materials as any)[m.materialType] === '') {
|
||||
(materials as any)[m.materialType] = m.fileUrl || ''
|
||||
}
|
||||
}
|
||||
entForm.value?.fill(res)
|
||||
}
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
error.value = ''
|
||||
tip.value = ''
|
||||
if (!entForm.value?.validate()) return
|
||||
submitting.value = true
|
||||
const materialList = Object.entries(materials)
|
||||
.filter(([, url]) => url)
|
||||
.map(([type, url]) => ({ materialType: type, materialName: type, fileUrl: url }))
|
||||
try {
|
||||
await $fetch('/api/tender/enterprise/save', {
|
||||
const res: any = await $fetch('/api/tender/enterprise/save', {
|
||||
method: 'POST',
|
||||
body: { ...form, materials: materialList }
|
||||
body: entForm.value.buildPayload()
|
||||
})
|
||||
tip.value = '提交成功,等待平台审核'
|
||||
if (res?.code !== 0) {
|
||||
error.value = res?.message || '提交失败'
|
||||
return
|
||||
}
|
||||
tip.value = res?.message || '提交成功,等待平台审核'
|
||||
await load()
|
||||
} catch (e: any) {
|
||||
error.value = e?.data?.message || e?.message || '提交失败'
|
||||
error.value = e?.data?.statusMessage || e?.data?.message || e?.message || '提交失败'
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
loggedIn.value = isLoggedIn()
|
||||
if (loggedIn.value) {
|
||||
await load()
|
||||
}
|
||||
onMounted(async () => {
|
||||
loggedIn.value = isLoggedIn()
|
||||
if (loggedIn.value) {
|
||||
await load()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.field {
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #d1d5db;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user