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:
@@ -0,0 +1,292 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h3 class="border-l-4 border-blue-600 pl-2 text-sm font-semibold text-blue-600">企业信息</h3>
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm text-gray-600">企业名称(登录账号)</label>
|
||||||
|
<input v-model="form.name" class="field" :disabled="readonly" placeholder="请输入企业名称,将作为登录账号" />
|
||||||
|
</div>
|
||||||
|
<div v-if="showPassword" class="space-y-1">
|
||||||
|
<label class="block text-sm text-gray-600">登录密码</label>
|
||||||
|
<input v-model="form.password" type="password" class="field" :disabled="readonly" placeholder="请设置登录密码" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm text-gray-600">纳税人识别号</label>
|
||||||
|
<input v-model="form.creditCode" class="field" :disabled="readonly" placeholder="统一社会信用代码" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm text-gray-600">企业地址(选填)</label>
|
||||||
|
<input v-model="form.address" class="field" :disabled="readonly" placeholder="企业地址" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm text-gray-600">企业联系电话(选填)</label>
|
||||||
|
<input v-model="form.contactPhone" class="field" :disabled="readonly" placeholder="企业联系电话" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm text-gray-600">企业邮箱</label>
|
||||||
|
<input v-model="form.contactEmail" type="email" class="field" :disabled="readonly" placeholder="企业邮箱" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="border-l-4 border-blue-600 pl-2 pt-2 text-sm font-semibold text-blue-600">经办人信息</h3>
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm text-gray-600">经办人姓名</label>
|
||||||
|
<input v-model="form.agentName" class="field" :disabled="readonly" placeholder="经办人姓名" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm text-gray-600">经办人身份证号</label>
|
||||||
|
<input v-model="form.idCardNo" class="field" :disabled="readonly" placeholder="经办人身份证号" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm text-gray-600">经办人邮箱</label>
|
||||||
|
<input v-model="form.agentEmail" type="email" class="field" :disabled="readonly" placeholder="经办人邮箱" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm text-gray-600">经办人手机号</label>
|
||||||
|
<input v-model="form.agentPhone" class="field" :disabled="readonly" placeholder="经办人手机号" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm text-gray-600">授权到期时间</label>
|
||||||
|
<input v-model="form.authorizeExpire" type="datetime-local" class="field" :disabled="readonly" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="border-l-4 border-blue-600 pl-2 pt-2 text-sm font-semibold text-blue-600">
|
||||||
|
证件上传(单张不超过 5M)
|
||||||
|
</h3>
|
||||||
|
<div class="flex flex-wrap gap-6">
|
||||||
|
<div v-for="m in materials" :key="m.materialType" class="w-40 text-center">
|
||||||
|
<div class="mb-2 text-xs text-gray-600">{{ m.label }}</div>
|
||||||
|
<label
|
||||||
|
class="flex h-40 w-40 cursor-pointer items-center justify-center overflow-hidden rounded-md border border-dashed border-gray-300 bg-gray-50"
|
||||||
|
:class="{ 'cursor-not-allowed opacity-60': readonly }"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
class="hidden"
|
||||||
|
:disabled="readonly"
|
||||||
|
@change="onPick(m, $event)"
|
||||||
|
/>
|
||||||
|
<img v-if="m.fileUrl" :src="m.fileUrl" class="h-full w-full object-cover" alt="" />
|
||||||
|
<span v-else class="text-3xl text-gray-400">+</span>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
v-if="m.fileUrl && !readonly"
|
||||||
|
type="button"
|
||||||
|
class="mt-1 text-xs text-red-500 hover:underline"
|
||||||
|
@click="removeImage(m)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="ocrHints.license" class="rounded-md bg-amber-50 px-3 py-2 text-xs text-amber-700">
|
||||||
|
{{ ocrHints.license }}
|
||||||
|
</div>
|
||||||
|
<div v-if="ocrHints.idcard_front" class="rounded-md bg-amber-50 px-3 py-2 text-xs text-amber-700">
|
||||||
|
{{ ocrHints.idcard_front }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 企业信息 + 经办人 + 授权 + 证件上传(含身份证/营业执照 OCR 回填)共享表单。
|
||||||
|
* 注册页与「企业资质」页共用,与 h5 端 components/EnterpriseForm.vue 行为一致。
|
||||||
|
*/
|
||||||
|
const form = defineModel<Record<string, any>>('form', { required: true })
|
||||||
|
const materials = defineModel<any[]>('materials', { required: true })
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
/** 是否显示登录密码字段(仅注册页) */
|
||||||
|
showPassword?: boolean
|
||||||
|
/** 只读(审核中/已通过时禁止编辑与上传) */
|
||||||
|
readonly?: boolean
|
||||||
|
}>(),
|
||||||
|
{ showPassword: false, readonly: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
const ocrHints = reactive<Record<string, string>>({ license: '', idcard_front: '' })
|
||||||
|
|
||||||
|
/** 材料类型:仅身份证人像面与营业执照需要 OCR */
|
||||||
|
const OCR_TYPES = ['license', 'idcard_front']
|
||||||
|
|
||||||
|
async function onPick(m: any, e: Event) {
|
||||||
|
const input = e.target as HTMLInputElement
|
||||||
|
const file = input.files?.[0]
|
||||||
|
input.value = ''
|
||||||
|
if (!file || props.readonly) return
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
uniToast('单张图片不超过 5M')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append('file', file)
|
||||||
|
const res: any = await $fetch('/api/tender/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: fd
|
||||||
|
})
|
||||||
|
m.fileUrl = res?.url || ''
|
||||||
|
if (!m.fileUrl) {
|
||||||
|
uniToast('上传失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
uniToast(e?.data?.statusMessage || e?.data?.message || '上传失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (OCR_TYPES.includes(m.materialType)) {
|
||||||
|
await runOcr(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 营业执照 / 身份证人像面上传后自动 OCR,失败仅提示不阻断 */
|
||||||
|
async function runOcr(m: any) {
|
||||||
|
if (!m.fileUrl) return
|
||||||
|
try {
|
||||||
|
const r: any = await $fetch('/api/tender/ocr/recognize', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { materialType: m.materialType, fileUrl: m.fileUrl }
|
||||||
|
})
|
||||||
|
backfill(m.materialType, r)
|
||||||
|
} catch (e: any) {
|
||||||
|
uniToast(e?.data?.statusMessage || e?.data?.message || '识别失败,请手动填写或重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function backfill(materialType: string, r: any) {
|
||||||
|
if (!r) return
|
||||||
|
const parts: string[] = []
|
||||||
|
if (materialType === 'license') {
|
||||||
|
fillIfBlank('creditCode', r.creditCode)
|
||||||
|
fillIfBlank('address', r.address)
|
||||||
|
if (r.enterpriseName) parts.push(`企业名称:${r.enterpriseName}`)
|
||||||
|
if (r.legalPerson) parts.push(`法定代表人:${r.legalPerson}`)
|
||||||
|
ocrHints.license = parts.length ? `营业执照识别(仅供核对,不覆盖登录账号):${parts.join(';')}` : ''
|
||||||
|
if (parts.length) uniToast('营业执照识别完成')
|
||||||
|
} else if (materialType === 'idcard_front') {
|
||||||
|
fillIfBlank('agentName', r.agentName)
|
||||||
|
fillIfBlank('idCardNo', r.idCardNo)
|
||||||
|
if (r.agentName) parts.push(`姓名:${r.agentName}`)
|
||||||
|
if (r.idCardNo) parts.push(`身份证号:${r.idCardNo}`)
|
||||||
|
ocrHints.idcard_front = parts.length ? `身份证识别:${parts.join(';')}` : ''
|
||||||
|
if (parts.length) uniToast('身份证识别完成')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 仅回填空字段,不覆盖用户已输入内容 */
|
||||||
|
function fillIfBlank(key: string, value?: string) {
|
||||||
|
if (value && !form.value[key]) {
|
||||||
|
form.value[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeImage(m: any) {
|
||||||
|
if (props.readonly) return
|
||||||
|
m.fileUrl = ''
|
||||||
|
if (ocrHints[m.materialType] !== undefined) ocrHints[m.materialType] = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 必填校验:企业联系电话、企业地址选填,其余必填;4 类证件必传 */
|
||||||
|
function validate(): boolean {
|
||||||
|
const required: Array<[string, string]> = [
|
||||||
|
['name', '企业名称'],
|
||||||
|
['creditCode', '纳税人识别号'],
|
||||||
|
['contactEmail', '企业邮箱'],
|
||||||
|
['agentName', '经办人姓名'],
|
||||||
|
['idCardNo', '经办人身份证号'],
|
||||||
|
['agentEmail', '经办人邮箱'],
|
||||||
|
['agentPhone', '经办人手机号'],
|
||||||
|
['authorizeExpire', '授权到期时间']
|
||||||
|
]
|
||||||
|
if (props.showPassword) required.unshift(['password', '登录密码'])
|
||||||
|
for (const [key, label] of required) {
|
||||||
|
if (!form.value[key]) {
|
||||||
|
uniToast(`请填写${label}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (materials.value.some((m: any) => !m.fileUrl)) {
|
||||||
|
uniToast('请上传全部证件')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** datetime-local 取值为 `YYYY-MM-DDTHH:mm`,后端需要 `yyyy-MM-dd HH:mm:ss` */
|
||||||
|
function normalizeDateTime(v: string) {
|
||||||
|
if (!v) return v
|
||||||
|
const s = v.replace('T', ' ')
|
||||||
|
return s.length === 16 ? `${s}:00` : s
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交用载荷(含证件材料,授权到期时间已规范化为后端格式) */
|
||||||
|
function buildPayload() {
|
||||||
|
const f = form.value
|
||||||
|
return {
|
||||||
|
name: f.name,
|
||||||
|
creditCode: f.creditCode,
|
||||||
|
contactPhone: f.contactPhone,
|
||||||
|
contactEmail: f.contactEmail,
|
||||||
|
address: f.address,
|
||||||
|
agentName: f.agentName,
|
||||||
|
agentEmail: f.agentEmail,
|
||||||
|
agentPhone: f.agentPhone,
|
||||||
|
idCardNo: f.idCardNo,
|
||||||
|
authorizeExpire: normalizeDateTime(f.authorizeExpire),
|
||||||
|
materials: materials.value.map((m: any) => ({
|
||||||
|
materialType: m.materialType,
|
||||||
|
materialName: m.materialName,
|
||||||
|
fileUrl: m.fileUrl
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载后端返回的企业资料 → 表单 / 证件 */
|
||||||
|
function fill(data: any) {
|
||||||
|
if (!data) return
|
||||||
|
const f = form.value
|
||||||
|
f.name = data.name || ''
|
||||||
|
f.creditCode = data.creditCode || ''
|
||||||
|
f.contactPhone = data.contactPhone || ''
|
||||||
|
f.contactEmail = data.contactEmail || ''
|
||||||
|
f.address = data.address || ''
|
||||||
|
f.agentName = data.agentName || ''
|
||||||
|
f.idCardNo = data.idCardNo || ''
|
||||||
|
f.agentEmail = data.agentEmail || ''
|
||||||
|
f.agentPhone = data.agentPhone || ''
|
||||||
|
// 后端 `yyyy-MM-dd HH:mm:ss` → datetime-local `yyyy-MM-ddTHH:mm`
|
||||||
|
f.authorizeExpire = data.authorizeExpire ? String(data.authorizeExpire).replace(' ', 'T').slice(0, 16) : ''
|
||||||
|
const ms: any[] = data.materials || []
|
||||||
|
for (const m of ms) {
|
||||||
|
const target = materials.value.find((x: any) => x.materialType === m.materialType)
|
||||||
|
if (target) target.fileUrl = m.fileUrl || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 轻量提示(ant-design-vue message,SSR 下不执行) */
|
||||||
|
function uniToast(msg: string) {
|
||||||
|
if (import.meta.client) message.warning(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ validate, buildPayload, fill })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.field {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.field:disabled {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -13,29 +13,38 @@ export function useHjcAuth() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function login(enterpriseName: string, password: string) {
|
async function login(enterpriseName: string, password: string) {
|
||||||
const res: any = await $fetch('/api/tender/login', {
|
try {
|
||||||
method: 'POST',
|
const res: any = await $fetch('/api/tender/login', {
|
||||||
body: { enterpriseName, password }
|
method: 'POST',
|
||||||
})
|
body: { enterpriseName, password }
|
||||||
if (res?.access_token) {
|
})
|
||||||
token.value = res.access_token
|
// mp-api 统一返回 ApiResult{code,message,data},code===0 成功
|
||||||
user.value = res.user || null
|
if (res?.code === 0 && res.data?.access_token) {
|
||||||
return { ok: true, user: res.user }
|
token.value = res.data.access_token
|
||||||
|
user.value = res.data.user || null
|
||||||
|
return { ok: true, user: res.data.user }
|
||||||
|
}
|
||||||
|
return { ok: false, message: res?.message || '登录失败' }
|
||||||
|
} catch (e: any) {
|
||||||
|
return { ok: false, message: e?.data?.statusMessage || e?.message || '登录失败' }
|
||||||
}
|
}
|
||||||
return { ok: false, message: res?.message || '登录失败' }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function register(payload: Record<string, any>) {
|
async function register(payload: Record<string, any>) {
|
||||||
const res: any = await $fetch('/api/tender/register', {
|
try {
|
||||||
method: 'POST',
|
const res: any = await $fetch('/api/tender/register', {
|
||||||
body: payload
|
method: 'POST',
|
||||||
})
|
body: payload
|
||||||
if (res?.access_token) {
|
})
|
||||||
token.value = res.access_token
|
if (res?.code === 0 && res.data?.access_token) {
|
||||||
user.value = res.user || null
|
token.value = res.data.access_token
|
||||||
return { ok: true, user: res.user }
|
user.value = res.data.user || null
|
||||||
|
return { ok: true, user: res.data.user }
|
||||||
|
}
|
||||||
|
return { ok: false, message: res?.message || '注册失败' }
|
||||||
|
} catch (e: any) {
|
||||||
|
return { ok: false, message: e?.data?.statusMessage || e?.message || '注册失败' }
|
||||||
}
|
}
|
||||||
return { ok: false, message: res?.message || '注册失败' }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
|
|||||||
+46
-31
@@ -1,37 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-[60vh] flex items-center justify-center px-4">
|
<div class="min-h-[60vh] flex items-center justify-center px-4 py-10">
|
||||||
<div class="w-full max-w-md rounded-lg border border-gray-200 p-8">
|
<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>
|
<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">
|
<form class="mt-6 space-y-4" @submit.prevent="submitForm">
|
||||||
<div>
|
<HjcEnterpriseForm
|
||||||
<label class="text-sm text-gray-600">企业名称</label>
|
ref="entForm"
|
||||||
<input v-model="form.enterpriseName" class="mt-1 w-full rounded-md border border-gray-300 px-4 py-2 text-sm" placeholder="企业名称" />
|
v-model:form="form"
|
||||||
</div>
|
v-model:materials="materials"
|
||||||
<div>
|
show-password
|
||||||
<label class="text-sm text-gray-600">登录密码</label>
|
/>
|
||||||
<input v-model="form.password" type="password" class="mt-1 w-full rounded-md border border-gray-300 px-4 py-2 text-sm" placeholder="密码" />
|
|
||||||
</div>
|
<p class="text-center text-xs text-gray-500">
|
||||||
<div>
|
企业联系电话、企业地址选填,其余项必填;提交后等待平台审核,审核通过方可购买标书
|
||||||
<label class="text-sm text-gray-600">纳税人识别号</label>
|
</p>
|
||||||
<input v-model="form.creditCode" class="mt-1 w-full rounded-md border border-gray-300 px-4 py-2 text-sm" placeholder="统一社会信用代码" />
|
|
||||||
</div>
|
<button
|
||||||
<div>
|
type="submit"
|
||||||
<label class="text-sm text-gray-600">企业联系电话</label>
|
class="w-full rounded-md bg-blue-600 py-3 font-medium text-white hover:bg-blue-700 disabled:opacity-50"
|
||||||
<input v-model="form.contactPhone" class="mt-1 w-full rounded-md border border-gray-300 px-4 py-2 text-sm" placeholder="联系电话" />
|
:disabled="loading"
|
||||||
</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="form.agentPhone" class="mt-1 w-full rounded-md border border-gray-300 px-4 py-2 text-sm" placeholder="经办人手机号" />
|
|
||||||
</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="loading">
|
|
||||||
{{ loading ? '注册中...' : '注册并登录' }}
|
{{ loading ? '注册中...' : '注册并登录' }}
|
||||||
</button>
|
</button>
|
||||||
<p v-if="error" class="text-center text-sm text-red-500">{{ error }}</p>
|
<p v-if="error" class="text-center text-sm text-red-500">{{ error }}</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p class="mt-4 text-center text-sm text-gray-500">
|
<p class="mt-4 text-center text-sm text-gray-500">
|
||||||
已注册?
|
已注册?
|
||||||
<NuxtLink to="/login" class="text-blue-600 hover:underline">去登录</NuxtLink>
|
<NuxtLink to="/login" class="text-blue-600 hover:underline">去登录</NuxtLink>
|
||||||
@@ -45,20 +39,41 @@ const { register } = useHjcAuth()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
const form = reactive({
|
const entForm = ref<any>(null)
|
||||||
enterpriseName: '',
|
|
||||||
|
const form = ref<Record<string, any>>({
|
||||||
|
name: '',
|
||||||
password: '',
|
password: '',
|
||||||
creditCode: '',
|
creditCode: '',
|
||||||
|
address: '',
|
||||||
contactPhone: '',
|
contactPhone: '',
|
||||||
contactEmail: '',
|
contactEmail: '',
|
||||||
agentPhone: ''
|
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: '' }
|
||||||
|
])
|
||||||
|
|
||||||
async function submitForm() {
|
async function submitForm() {
|
||||||
error.value = ''
|
error.value = ''
|
||||||
|
if (!entForm.value?.validate()) return
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await register({ ...form })
|
// buildPayload 返回 { name, ... };注册接口用 enterpriseName 作为登录账号
|
||||||
|
const { name, ...rest } = entForm.value.buildPayload()
|
||||||
|
const res = await register({
|
||||||
|
enterpriseName: name,
|
||||||
|
password: form.value.password,
|
||||||
|
...rest
|
||||||
|
})
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
return navigateTo((route.query.redirect as string) || '/tender')
|
return navigateTo((route.query.redirect as string) || '/tender')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,12 @@
|
|||||||
|
|
||||||
<div v-if="!loggedIn" class="py-20 text-center">
|
<div v-if="!loggedIn" class="py-20 text-center">
|
||||||
<p class="text-gray-500">请先登录后管理企业资质</p>
|
<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>
|
||||||
|
|
||||||
<div v-else class="mx-auto max-w-2xl rounded-lg border border-gray-200 p-6">
|
<div v-else class="mx-auto max-w-2xl rounded-lg border border-gray-200 p-6">
|
||||||
@@ -20,24 +25,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="space-y-4" @submit.prevent="submit">
|
<form class="space-y-4" @submit.prevent="submit">
|
||||||
<Field label="企业名称"><input v-model="form.name" class="field" /></Field>
|
<HjcEnterpriseForm ref="entForm" v-model:form="form" v-model:materials="materials" :readonly="readonly" />
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="space-y-3">
|
<p v-if="readonly" class="text-center text-xs text-gray-500">
|
||||||
<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>
|
</p>
|
||||||
<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>
|
|
||||||
|
|
||||||
<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 ? '提交中...' : '提交审核' }}
|
{{ submitting ? '提交中...' : '提交审核' }}
|
||||||
</button>
|
</button>
|
||||||
<p v-if="error" class="text-center text-sm text-red-500">{{ error }}</p>
|
<p v-if="error" class="text-center text-sm text-red-500">{{ error }}</p>
|
||||||
@@ -48,95 +47,83 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { isLoggedIn } = useHjcAuth()
|
||||||
const loggedIn = ref(false)
|
const loggedIn = ref(false)
|
||||||
const authStatus = ref(0)
|
const authStatus = ref(-1)
|
||||||
const rejectReason = ref('')
|
const rejectReason = ref('')
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
const tip = ref('')
|
const tip = ref('')
|
||||||
|
const entForm = ref<any>(null)
|
||||||
|
|
||||||
const form = reactive({
|
const form = ref<Record<string, any>>({
|
||||||
name: '', creditCode: '', contactPhone: '', contactEmail: '', address: '',
|
name: '',
|
||||||
agentName: '', agentEmail: '', agentPhone: '', authorizeExpire: ''
|
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(() => {
|
const authStatusClass = computed(() => {
|
||||||
if (authStatus.value === 1) return 'bg-green-100 text-green-700'
|
if (authStatus.value === 1) return 'bg-green-100 text-green-700'
|
||||||
if (authStatus.value === 2) return 'bg-red-100 text-red-600'
|
if (authStatus.value === 2) return 'bg-red-100 text-red-600'
|
||||||
return 'bg-amber-100 text-amber-700'
|
return 'bg-amber-100 text-amber-700'
|
||||||
})
|
})
|
||||||
|
/** 审核中(0)/已通过(1) 只读;驳回(2)/未提交(-1) 可编辑重提(与 h5 一致) */
|
||||||
|
const readonly = computed(() => authStatus.value === 0 || authStatus.value === 1)
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
const res: any = await $fetch('/api/tender/enterprise')
|
const res: any = await $fetch('/api/tender/enterprise')
|
||||||
if (res) {
|
if (res) {
|
||||||
authStatus.value = res.authStatus ?? 0
|
authStatus.value = res.authStatus ?? -1
|
||||||
rejectReason.value = res.rejectReason || ''
|
rejectReason.value = res.rejectReason || ''
|
||||||
form.name = res.name || ''
|
entForm.value?.fill(res)
|
||||||
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 || ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
error.value = ''
|
error.value = ''
|
||||||
tip.value = ''
|
tip.value = ''
|
||||||
|
if (!entForm.value?.validate()) return
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
const materialList = Object.entries(materials)
|
|
||||||
.filter(([, url]) => url)
|
|
||||||
.map(([type, url]) => ({ materialType: type, materialName: type, fileUrl: url }))
|
|
||||||
try {
|
try {
|
||||||
await $fetch('/api/tender/enterprise/save', {
|
const res: any = await $fetch('/api/tender/enterprise/save', {
|
||||||
method: 'POST',
|
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()
|
await load()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e?.data?.message || e?.message || '提交失败'
|
error.value = e?.data?.statusMessage || e?.data?.message || e?.message || '提交失败'
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loggedIn.value = isLoggedIn()
|
loggedIn.value = isLoggedIn()
|
||||||
if (loggedIn.value) {
|
onMounted(async () => {
|
||||||
await load()
|
loggedIn.value = isLoggedIn()
|
||||||
}
|
if (loggedIn.value) {
|
||||||
|
await load()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.field {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
padding: 8px 16px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { getTenantFromContext } from '../../../utils/tenant'
|
|||||||
* 提交/更新企业资质(信息+证件)
|
* 提交/更新企业资质(信息+证件)
|
||||||
* POST /api/tender/enterprise/save { name, creditCode, ..., materials: [] }
|
* POST /api/tender/enterprise/save { name, creditCode, ..., materials: [] }
|
||||||
* 代理到 mp-api /api/hjc/enterprise/save;需登录态。
|
* 代理到 mp-api /api/hjc/enterprise/save;需登录态。
|
||||||
|
*
|
||||||
|
* 约定:原样透传 ApiResult{code,message,data},由前端依据 code===0 判定成功并展示 message。
|
||||||
*/
|
*/
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
@@ -16,7 +18,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const auth = getHeader(event, 'authorization') || (cookieToken ? `Bearer ${cookieToken}` : null)
|
const auth = getHeader(event, 'authorization') || (cookieToken ? `Bearer ${cookieToken}` : null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await $fetch('/hjc/enterprise/save', {
|
return await $fetch('/hjc/enterprise/save', {
|
||||||
baseURL: config.public.modulesApiBase,
|
baseURL: config.public.modulesApiBase,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -25,13 +27,11 @@ export default defineEventHandler(async (event) => {
|
|||||||
},
|
},
|
||||||
query: { TenantId: ctx.tenantId },
|
query: { TenantId: ctx.tenantId },
|
||||||
body
|
body
|
||||||
}) as any
|
})
|
||||||
const data = res && res.data !== undefined ? res.data : res
|
|
||||||
return data
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: error?.statusCode || error?.response?.status || 502,
|
statusCode: error?.statusCode || error?.response?.status || 502,
|
||||||
statusMessage: error?.statusMessage || 'Failed to save enterprise'
|
statusMessage: error?.data?.message || error?.statusMessage || 'Failed to save enterprise'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { $fetch } from 'ofetch'
|
import { $fetch } from 'ofetch'
|
||||||
import { createError, defineEventHandler, readBody } from 'h3'
|
import { createError, defineEventHandler, readBody } from 'h3'
|
||||||
import { useRuntimeConfig } from '#imports'
|
import { useRuntimeConfig } from '#imports'
|
||||||
import { getTenantFromContext } from '../../../utils/tenant'
|
import { getTenantFromContext } from '../../utils/tenant'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 企业登录
|
* 企业登录
|
||||||
* POST /api/tender/login { enterpriseName, password }
|
* POST /api/tender/login { enterpriseName, password }
|
||||||
* 代理到 mp-api /api/hjc/auth/login;成功返回 LoginResult{ access_token, user }。
|
* 代理到 mp-api /api/hjc/auth/login;原样透传 ApiResult{code,message,data},
|
||||||
|
* 由 useHjcAuth 依据 code===0 判定并展示 message。
|
||||||
*/
|
*/
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
@@ -14,19 +15,17 @@ export default defineEventHandler(async (event) => {
|
|||||||
const body = await readBody(event)
|
const body = await readBody(event)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await $fetch('/hjc/auth/login', {
|
return await $fetch('/hjc/auth/login', {
|
||||||
baseURL: config.public.modulesApiBase,
|
baseURL: config.public.modulesApiBase,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { TenantId: ctx.tenantId },
|
headers: { TenantId: ctx.tenantId },
|
||||||
query: { TenantId: ctx.tenantId },
|
query: { TenantId: ctx.tenantId },
|
||||||
body
|
body
|
||||||
}) as any
|
})
|
||||||
const data = res && res.data !== undefined ? res.data : res
|
|
||||||
return data
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: error?.statusCode || error?.response?.status || 502,
|
statusCode: error?.statusCode || error?.response?.status || 502,
|
||||||
statusMessage: error?.statusMessage || 'Failed to login'
|
statusMessage: error?.data?.message || error?.statusMessage || 'Failed to login'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -4,9 +4,11 @@ import { useRuntimeConfig } from '#imports'
|
|||||||
import { getTenantFromContext } from '../../../utils/tenant'
|
import { getTenantFromContext } from '../../../utils/tenant'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 企业注册
|
* 证件 OCR 识别(身份证人像面 / 营业执照)
|
||||||
* POST /api/tender/register { enterpriseName, password, creditCode, ... }
|
* POST /api/tender/ocr/recognize { materialType: 'idcard_front' | 'license', fileUrl }
|
||||||
* 代理到 mp-api /api/hjc/auth/register;成功返回 LoginResult{ access_token, user }。
|
* 代理到 mp-api /api/hjc/ocr/recognize(匿名放行,注册页在登录前调用)。
|
||||||
|
*
|
||||||
|
* OCR 未配置 AK 时后端返回失败,前端提示「识别失败,请手动填写」,不阻断提交。
|
||||||
*/
|
*/
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
@@ -14,19 +16,18 @@ export default defineEventHandler(async (event) => {
|
|||||||
const body = await readBody(event)
|
const body = await readBody(event)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await $fetch('/hjc/auth/register', {
|
const res = await $fetch('/hjc/ocr/recognize', {
|
||||||
baseURL: config.public.modulesApiBase,
|
baseURL: config.public.modulesApiBase,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { TenantId: ctx.tenantId },
|
headers: { TenantId: ctx.tenantId },
|
||||||
query: { TenantId: ctx.tenantId },
|
query: { TenantId: ctx.tenantId },
|
||||||
body
|
body
|
||||||
}) as any
|
}) as any
|
||||||
const data = res && res.data !== undefined ? res.data : res
|
return res && res.data !== undefined ? res.data : res
|
||||||
return data
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: error?.statusCode || error?.response?.status || 502,
|
statusCode: error?.statusCode || error?.response?.status || 502,
|
||||||
statusMessage: error?.statusMessage || 'Failed to register'
|
statusMessage: error?.data?.message || error?.statusMessage || '识别失败'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { $fetch } from 'ofetch'
|
||||||
|
import { createError, defineEventHandler, readBody } from 'h3'
|
||||||
|
import { useRuntimeConfig } from '#imports'
|
||||||
|
import { getTenantFromContext } from '../../utils/tenant'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 企业注册(一站式:企业 + 经办人 + 授权 + 证件材料)
|
||||||
|
* POST /api/tender/register { enterpriseName, password, creditCode, ..., materials: [] }
|
||||||
|
* 代理到 mp-api /api/hjc/auth/register。
|
||||||
|
*
|
||||||
|
* 约定:mp-api 的业务结果(含校验失败)一律 HTTP 200 + ApiResult{code,message,data},
|
||||||
|
* 这里**原样透传**给前端,由 useHjcAuth 依据 code===0 判定并把 message 展示出来。
|
||||||
|
* 仅网络/HTTP 层错误才抛 createError。
|
||||||
|
*/
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const ctx = getTenantFromContext(event, config)
|
||||||
|
const body = await readBody(event)
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await $fetch('/hjc/auth/register', {
|
||||||
|
baseURL: config.public.modulesApiBase,
|
||||||
|
method: 'POST',
|
||||||
|
headers: { TenantId: ctx.tenantId },
|
||||||
|
query: { TenantId: ctx.tenantId },
|
||||||
|
body
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: error?.statusCode || error?.response?.status || 502,
|
||||||
|
statusMessage: error?.data?.message || error?.statusMessage || 'Failed to register'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { $fetch } from 'ofetch'
|
||||||
|
import { createError, defineEventHandler, getHeader, readRawBody } from 'h3'
|
||||||
|
import { useRuntimeConfig } from '#imports'
|
||||||
|
import { getTenantFromContext } from '../../utils/tenant'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资质证件上传(注册页与资质页共用)
|
||||||
|
* POST /api/tender/upload (multipart/form-data, 字段名 file)
|
||||||
|
* 代理到 mp-api /api/hjc/auth/upload(匿名放行,登录前后均可用)。
|
||||||
|
*
|
||||||
|
* 说明:与 h5 一致走「证件上传」链路;该接口不写 file_record 表,
|
||||||
|
* 上传结果以 URL 存在 hjc_enterprise_material.file_url。
|
||||||
|
*/
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const ctx = getTenantFromContext(event, config)
|
||||||
|
const contentType = getHeader(event, 'content-type') || ''
|
||||||
|
if (!contentType.includes('multipart/form-data')) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: 'Content-Type must be multipart/form-data' })
|
||||||
|
}
|
||||||
|
// 原样转发 multipart 原始字节(含 boundary),避免丢失文件流
|
||||||
|
const raw = await readRawBody(event, false)
|
||||||
|
if (!raw) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: '上传文件为空' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await $fetch('/hjc/auth/upload', {
|
||||||
|
baseURL: config.public.modulesApiBase,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
TenantId: ctx.tenantId,
|
||||||
|
'Content-Type': contentType
|
||||||
|
},
|
||||||
|
query: { TenantId: ctx.tenantId },
|
||||||
|
body: raw
|
||||||
|
}) as any
|
||||||
|
return res && res.data !== undefined ? res.data : res
|
||||||
|
} catch (error: any) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: error?.statusCode || error?.response?.status || 502,
|
||||||
|
statusMessage: error?.data?.message || error?.statusMessage || '上传失败'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user