This commit is contained in:
Mrx
2026-03-17 17:19:00 +08:00
parent 68da50984c
commit 792f8d6abe
9 changed files with 1180 additions and 58 deletions

View File

@@ -22,7 +22,7 @@
label-width="10em"
class="enterprise-form-content"
>
<div class="form-section">
<div class="form-section">
<h3 class="section-title">基本信息</h3>
<!-- 企业名称和OCR识别区域 -->
@@ -42,21 +42,7 @@
<div class="ocr-compact">
<div class="ocr-header-compact">
<el-icon class="text-green-600"><DocumentIcon /></el-icon>
<span class="ocr-title-compact">OCR识别</span>
<el-upload
ref="uploadRef"
:auto-upload="false"
:show-file-list="false"
:on-change="handleFileChange"
:before-upload="beforeUpload"
accept="image/jpeg,image/jpg,image/png,image/webp"
class="ocr-uploader-compact"
>
<el-button type="success" size="small" plain>
<el-icon><ArrowUpTrayIcon /></el-icon>
上传营业执照
</el-button>
</el-upload>
<span class="ocr-title-compact">可进行OCR识别请在下方上传营业执照</span>
</div>
<div v-if="ocrLoading" class="ocr-status-compact">
<el-icon class="is-loading"><Loading /></el-icon>
@@ -84,6 +70,7 @@
</el-col>
</el-row>
<el-row :gutter="16" class="mb-4">
<el-col :span="12">
<el-form-item label="法人姓名" prop="legalPersonName">
@@ -96,21 +83,9 @@
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="法人身份证号" prop="legalPersonID">
<el-input
v-model="form.legalPersonID"
placeholder="请输入法人身份证号"
clearable
size="default"
class="form-input"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16" class="mb-4">
<el-col :span="24">
<el-col :span="12">
<el-form-item label="企业地址" prop="enterpriseAddress">
<el-input
v-model="form.enterpriseAddress"
@@ -122,8 +97,78 @@
</el-form-item>
</el-col>
</el-row>
<!-- 营业执照图片上传对应 BusinessLicenseImageURL -->
<el-row :gutter="16" class="mb-4">
<el-col :span="24">
<el-form-item label="营业执照图片" prop="businessLicenseImageURL">
<el-upload
class="upload-area single-upload-area"
action="#"
list-type="picture-card"
:auto-upload="false"
:file-list="businessLicenseFileList"
:limit="1"
:on-change="handleBusinessLicenseChange"
:on-remove="handleBusinessLicenseRemove"
:before-upload="beforeUpload"
>
<el-icon class="upload-icon"><ArrowUpTrayIcon /></el-icon>
<div class="el-upload__text">上传清晰可辨的营业执照图片</div>
</el-upload>
</el-form-item>
</el-col>
</el-row>
<!-- 办公场地图片上传 -->
<el-row :gutter="16" class="mb-4">
<el-col :span="24">
<el-form-item label="办公场地照片">
<div class="text-xs mb-1 text-blue-500">
请在非 IE 浏览器下上传大小不超过 3M 的图片最多 10 需体现门楣 LOGO办公设备与工作人员
</div>
<div
class="upload-drop-zone"
:class="{ 'is-dragover': officePlaceDragover }"
@dragover.prevent="officePlaceDragover = true"
@dragleave.prevent="officePlaceDragover = false"
@drop.prevent="onOfficePlaceDrop"
>
<el-upload
ref="officePlaceUploadRef"
class="upload-area"
action="#"
list-type="picture-card"
:auto-upload="false"
v-model:file-list="officePlaceFileList"
accept="image/jpeg,image/jpg,image/png,image/webp"
multiple
:limit="10"
:on-change="handleOfficePlaceChange"
:on-remove="handleOfficePlaceRemove"
:before-upload="beforeUpload"
>
<div class="upload-trigger-inner">
<el-icon class="upload-icon"><ArrowUpTrayIcon /></el-icon>
<div class="el-upload__text">上传办公场地环境照片</div>
</div>
</el-upload>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16" class="mb-4">
<el-col :span="12">
<el-form-item label="法人身份证号" prop="legalPersonID">
<el-input
v-model="form.legalPersonID"
placeholder="请输入法人身份证号"
clearable
size="default"
class="form-input"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="法人手机号" prop="legalPersonPhone">
<el-input
@@ -161,6 +206,149 @@
</el-form-item>
</el-col>
</el-row>
<h3 class="section-title">授权代表信息</h3>
<p class="section-desc">授权代表信息用于证明该人员已获得企业授权请确保姓名身份证号手机号及身份证正反面照片真实有效</p>
<!-- 授权代表信息 -->
<el-row :gutter="16" class="mb-4">
<el-col :span="8">
<el-form-item label="授权代表姓名" prop="authorizedRepName">
<el-input
v-model="form.authorizedRepName"
placeholder="请输入授权代表姓名"
clearable
size="default"
class="form-input"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="授权代表身份证号" prop="authorizedRepID">
<el-input
v-model="form.authorizedRepID"
placeholder="请输入授权代表身份证号"
clearable
size="default"
class="form-input"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="授权代表手机号" prop="authorizedRepPhone">
<el-input
v-model="form.authorizedRepPhone"
placeholder="请输入授权代表手机号"
clearable
size="default"
maxlength="11"
class="form-input"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 授权代表身份证图片上传 -->
<el-row :gutter="16" class="mb-4">
<el-col :span="12">
<el-form-item label="身份证人像面" prop="authorizedRepIDImageURLs">
<el-upload
class="upload-area single-upload-area"
action="#"
list-type="picture-card"
:auto-upload="false"
:file-list="authorizedRepIDFrontFileList"
:limit="1"
:on-change="handleAuthorizedRepIDFrontChange"
:on-remove="handleAuthorizedRepIDFrontRemove"
:before-upload="beforeUpload"
>
<el-icon class="upload-icon"><ArrowUpTrayIcon /></el-icon>
<div class="el-upload__text">上传授权代表身份证人像面</div>
</el-upload>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="身份证国徽面" prop="authorizedRepIDImageURLs">
<el-upload
class="upload-area single-upload-area"
action="#"
list-type="picture-card"
:auto-upload="false"
:file-list="authorizedRepIDBackFileList"
:limit="1"
:on-change="handleAuthorizedRepIDBackChange"
:on-remove="handleAuthorizedRepIDBackRemove"
:before-upload="beforeUpload"
>
<el-icon class="upload-icon"><ArrowUpTrayIcon /></el-icon>
<div class="el-upload__text">上传授权代表身份证国徽面</div>
</el-upload>
</el-form-item>
</el-col>
</el-row>
<h3 class="section-title">应用场景填写</h3>
<p class="section-desc">请描述您调用接口的具体业务场景</p>
<!-- 接口用途描述对应 APIUsage -->
<el-row :gutter="16" class="mb-4">
<el-col :span="24">
<el-form-item label="应用场景" prop="apiUsage">
<el-input
v-model="form.apiUsage"
type="textarea"
:rows="4"
placeholder="请描述您调用接口的具体业务场景和用途"
maxlength="500"
show-word-limit
class="form-input"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 应用场景附件图片上传 -->
<el-row :gutter="16" class="mb-4">
<el-col :span="24">
<el-form-item label="应用场景附件">
<div class="text-xs mb-1 text-blue-500">
请在非IE浏览器下上传大小不超过3M的图片要求:不超过10张后台应用截图
</div>
<div
class="upload-drop-zone"
:class="{ 'is-dragover': scenarioDragover }"
@dragover.prevent="scenarioDragover = true"
@dragleave.prevent="scenarioDragover = false"
@drop.prevent="onScenarioDrop"
>
<el-upload
ref="scenarioUploadRef"
class="upload-area"
action="#"
list-type="picture-card"
:auto-upload="false"
v-model:file-list="scenarioFileList"
accept="image/jpeg,image/jpg,image/png,image/webp"
multiple
:limit="10"
:on-change="handleScenarioChange"
:on-remove="handleScenarioRemove"
:before-upload="beforeUpload"
>
<div class="upload-trigger-inner">
<el-icon class="upload-icon"><ArrowUpTrayIcon /></el-icon>
<div class="el-upload__text">上传业务场景相关截图或证明材料</div>
</div>
</el-upload>
</div>
</el-form-item>
</el-col>
</el-row>
</div>
<div class="form-actions">
@@ -190,7 +378,7 @@ import {
CheckIcon,
DocumentIcon
} from '@heroicons/vue/24/outline'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ElMessage, ElMessageBox, genFileId } from 'element-plus'
import { useAliyunCaptcha } from '@/composables/useAliyunCaptcha'
const props = defineProps({
@@ -203,7 +391,17 @@ const props = defineProps({
legalPersonID: '',
legalPersonPhone: '',
enterpriseAddress: '',
legalPersonCode: ''
legalPersonCode: '',
// 扩展:营业执照 & 办公场地 & 场景
businessLicenseImageURL: '',
officePlaceImageURLs: [],
apiUsage: '',
scenarioAttachmentURLs: [],
// 授权代表信息
authorizedRepName: '',
authorizedRepID: '',
authorizedRepPhone: '',
authorizedRepIDImageURLs: []
})
}
})
@@ -224,7 +422,17 @@ const form = ref({
legalPersonID: '',
legalPersonPhone: '',
enterpriseAddress: '',
legalPersonCode: ''
legalPersonCode: '',
// 扩展:营业执照 & 办公场地 & 场景
businessLicenseImageURL: '',
officePlaceImageURLs: [],
apiUsage: '',
scenarioAttachmentURLs: [],
// 授权代表信息
authorizedRepName: '',
authorizedRepID: '',
authorizedRepPhone: '',
authorizedRepIDImageURLs: []
})
// 验证码相关状态
@@ -239,6 +447,17 @@ const submitting = ref(false)
const ocrLoading = ref(false)
const ocrResult = ref(false)
const uploadRef = ref()
const officePlaceUploadRef = ref()
const scenarioUploadRef = ref()
const officePlaceDragover = ref(false)
const scenarioDragover = ref(false)
// 上传文件列表(前端展示用)
const officePlaceFileList = ref([])
const scenarioFileList = ref([])
const businessLicenseFileList = ref([])
const authorizedRepIDFrontFileList = ref([])
const authorizedRepIDBackFileList = ref([])
// 计算属性
const canSendCode = computed(() => {
@@ -326,6 +545,30 @@ const enterpriseRules = {
legalPersonCode: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
{ len: 6, message: '验证码应为6位数字', trigger: 'blur' }
],
// 扩展字段简单校验(可按需加强)
businessLicenseImageURL: [
{ required: true, message: '请上传营业执照图片', trigger: 'change' }
],
apiUsage: [
{ required: true, message: '请填写接口用途', trigger: 'blur' },
{ min: 5, max: 500, message: '接口用途长度应在5-500个字符之间', trigger: 'blur' }
],
// 授权代表信息简单校验(可按需加强)
authorizedRepName: [
{ required: true, message: '请输入授权代表姓名', trigger: 'blur' },
{ min: 2, max: 20, message: '授权代表姓名长度应在2-20个字符之间', trigger: 'blur' }
],
authorizedRepID: [
{ required: true, message: '请输入授权代表身份证号', trigger: 'blur' },
{ validator: validateIDCard, trigger: 'blur' }
],
authorizedRepPhone: [
{ required: true, message: '请输入授权代表手机号', trigger: 'blur' },
{ validator: validatePhone, trigger: 'blur' }
],
authorizedRepIDImageURLs: [
{ required: true, message: '请上传授权代表身份证正反面图片', trigger: 'change' }
]
}
@@ -395,7 +638,7 @@ const beforeUpload = (file) => {
return true
}
// 处理文件变化
// 处理文件变化:触发 OCR并保存营业执照原图 URL若有上传地址
const handleFileChange = async (file) => {
if (!beforeUpload(file.raw)) {
return
@@ -419,6 +662,11 @@ const handleFileChange = async (file) => {
form.value.legalPersonID = ocrData.legal_person_id || ''
form.value.enterpriseAddress = ocrData.address || ''
// 如果后端返回了已保存的营业执照图片URL可以直接写入
if (ocrData.license_image_url) {
form.value.businessLicenseImageURL = ocrData.license_image_url
}
ocrResult.value = true
ElMessage.success('营业执照识别成功,已自动填充表单')
} else {
@@ -432,8 +680,211 @@ const handleFileChange = async (file) => {
}
}
// 判断是否为前端 blob 预览地址(需上传到服务器后替换为真实 URL
const isBlobUrl = (url) => typeof url === 'string' && url.startsWith('blob:')
// 上传单张图片到七牛云,返回可访问 URL
const uploadFileToServer = async (file) => {
const res = await certificationApi.uploadFile(file)
if (!res?.success || !res?.data?.url) {
throw new Error(res?.error?.message || '图片上传失败')
}
return res.data.url
}
// 提交前将 blob 图片全部上传到七牛云,并更新表单中的 URL
const uploadAllBlobFilesAndFillForm = async () => {
const tasks = []
// 营业执照:若当前是 blob 则上传
if (isBlobUrl(form.value.businessLicenseImageURL) && businessLicenseFileList.value.length > 0 && businessLicenseFileList.value[0].raw) {
tasks.push(
uploadFileToServer(businessLicenseFileList.value[0].raw).then((url) => {
form.value.businessLicenseImageURL = url
})
)
}
// 办公场地多图:按顺序上传
if (officePlaceFileList.value.length > 0) {
const files = officePlaceFileList.value.filter((f) => f.raw).map((f) => f.raw)
if (files.length > 0) {
tasks.push(
Promise.all(files.map((f) => uploadFileToServer(f))).then((urls) => {
form.value.officePlaceImageURLs = urls
})
)
}
}
// 应用场景附件多图:按顺序上传
if (scenarioFileList.value.length > 0) {
const files = scenarioFileList.value.filter((f) => f.raw).map((f) => f.raw)
if (files.length > 0) {
tasks.push(
Promise.all(files.map((f) => uploadFileToServer(f))).then((urls) => {
form.value.scenarioAttachmentURLs = urls
})
)
}
}
// 授权代表身份证正反面:人像面 + 国徽面
const frontRaw = authorizedRepIDFrontFileList.value[0]?.raw
const backRaw = authorizedRepIDBackFileList.value[0]?.raw
if (frontRaw || backRaw) {
tasks.push(
(async () => {
const urls = []
if (frontRaw) urls.push(await uploadFileToServer(frontRaw))
if (backRaw) urls.push(await uploadFileToServer(backRaw))
form.value.authorizedRepIDImageURLs = urls
})()
)
}
await Promise.all(tasks)
}
// 从 el-upload 的 fileList 中提取 URL 数组(后端接好上传接口后可用 response.url
const extractUrls = (fileList) => {
return fileList
.map(f => f.url || f.response?.url || f.name)
.filter(Boolean)
}
// 营业执照图片变更(同时触发 OCR 识别)
const handleBusinessLicenseChange = async (file, fileList) => {
businessLicenseFileList.value = fileList
const urls = extractUrls(fileList)
form.value.businessLicenseImageURL = urls[0] || ''
// 使用当前选择的营业执照图片触发 OCR 识别
if (file && file.raw) {
await handleFileChange(file)
}
}
const handleBusinessLicenseRemove = (file, fileList) => {
businessLicenseFileList.value = fileList
const urls = extractUrls(fileList)
form.value.businessLicenseImageURL = urls[0] || ''
}
// 手动清除营业执照图片(预览区域中的“删除”按钮)
const clearBusinessLicense = () => {
businessLicenseFileList.value = []
form.value.businessLicenseImageURL = ''
}
// 授权代表身份证人像面图片变更
const handleAuthorizedRepIDFrontChange = (file, fileList) => {
authorizedRepIDFrontFileList.value = fileList
updateAuthorizedRepIDImageURLs()
}
const handleAuthorizedRepIDFrontRemove = (file, fileList) => {
authorizedRepIDFrontFileList.value = fileList
updateAuthorizedRepIDImageURLs()
}
// 授权代表身份证国徽面图片变更
const handleAuthorizedRepIDBackChange = (file, fileList) => {
authorizedRepIDBackFileList.value = fileList
updateAuthorizedRepIDImageURLs()
}
const handleAuthorizedRepIDBackRemove = (file, fileList) => {
authorizedRepIDBackFileList.value = fileList
updateAuthorizedRepIDImageURLs()
}
// 手动清除授权代表身份证人像面
const clearAuthorizedRepFront = () => {
authorizedRepIDFrontFileList.value = []
updateAuthorizedRepIDImageURLs()
}
// 手动清除授权代表身份证国徽面
const clearAuthorizedRepBack = () => {
authorizedRepIDBackFileList.value = []
updateAuthorizedRepIDImageURLs()
}
// 汇总授权代表身份证正反面图片URL到一个数组字段
const updateAuthorizedRepIDImageURLs = () => {
const frontUrl = extractUrls(authorizedRepIDFrontFileList.value)[0] || ''
const backUrl = extractUrls(authorizedRepIDBackFileList.value)[0] || ''
const urls = []
if (frontUrl) urls.push(frontUrl)
if (backUrl) urls.push(backUrl)
form.value.authorizedRepIDImageURLs = urls
}
// 办公场地:拖放文件时通过 handleStart 加入列表
const onOfficePlaceDrop = (e) => {
officePlaceDragover.value = false
const upload = officePlaceUploadRef.value
if (!upload) return
const files = Array.from(e.dataTransfer?.files || []).filter(
(f) => f.type && /^image\/(jpeg|jpg|png|webp)$/i.test(f.type)
)
const limit = 10
const remain = Math.max(0, limit - officePlaceFileList.value.length)
const toAdd = files.slice(0, remain)
for (const file of toAdd) {
if (!beforeUpload(file)) continue
if (typeof file.uid === 'undefined') file.uid = genFileId()
upload.handleStart(file)
}
if (files.length > remain && remain > 0) ElMessage.warning(`最多上传 ${limit} 张,已忽略多余文件`)
}
// 应用场景:拖放文件时通过 handleStart 加入列表
const onScenarioDrop = (e) => {
scenarioDragover.value = false
const upload = scenarioUploadRef.value
if (!upload) return
const files = Array.from(e.dataTransfer?.files || []).filter(
(f) => f.type && /^image\/(jpeg|jpg|png|webp)$/i.test(f.type)
)
const limit = 10
const remain = Math.max(0, limit - scenarioFileList.value.length)
const toAdd = files.slice(0, remain)
for (const file of toAdd) {
if (!beforeUpload(file)) continue
if (typeof file.uid === 'undefined') file.uid = genFileId()
upload.handleStart(file)
}
if (files.length > remain && remain > 0) ElMessage.warning(`最多上传 ${limit} 张,已忽略多余文件`)
}
// 办公场地图片变更
const handleOfficePlaceChange = (file, fileList) => {
officePlaceFileList.value = fileList
form.value.officePlaceImageURLs = extractUrls(fileList)
}
const handleOfficePlaceRemove = (file, fileList) => {
officePlaceFileList.value = fileList
form.value.officePlaceImageURLs = extractUrls(fileList)
}
// 应用场景附件图片变更
const handleScenarioChange = (file, fileList) => {
scenarioFileList.value = fileList
form.value.scenarioAttachmentURLs = extractUrls(fileList)
}
const handleScenarioRemove = (file, fileList) => {
scenarioFileList.value = fileList
form.value.scenarioAttachmentURLs = extractUrls(fileList)
}
// 提交表单
const submitForm = async () => {
if (submitting.value) return
submitting.value = true
try {
await enterpriseFormRef.value.validate()
@@ -450,12 +901,42 @@ const submitForm = async () => {
}
)
submitting.value = true
// 先将所有 blob 图片上传到七牛云,再提交企业信息
try {
await uploadAllBlobFilesAndFillForm()
} catch (err) {
ElMessage.error(err?.message || '图片上传失败,请重试')
submitting.value = false
return
}
// Mock API 调用
await new Promise(resolve => setTimeout(resolve, 2000))
// 调用后端提交接口
const payload = {
company_name: form.value.companyName,
unified_social_code: form.value.unifiedSocialCode,
legal_person_name: form.value.legalPersonName,
legal_person_id: form.value.legalPersonID,
legal_person_phone: form.value.legalPersonPhone,
enterprise_address: form.value.enterpriseAddress,
verification_code: form.value.legalPersonCode,
// 扩展字段
business_license_image_url: form.value.businessLicenseImageURL,
office_place_image_urls: form.value.officePlaceImageURLs,
api_usage: form.value.apiUsage,
scenario_attachment_urls: form.value.scenarioAttachmentURLs,
// 授权代表信息
authorized_rep_name: form.value.authorizedRepName,
authorized_rep_id: form.value.authorizedRepID,
authorized_rep_phone: form.value.authorizedRepPhone,
authorized_rep_id_image_urls: form.value.authorizedRepIDImageURLs
}
emit('submit', form.value)
const res = await certificationApi.submitEnterpriseInfo(payload)
if (!res.success) {
throw new Error(res?.error?.message || '提交企业信息失败')
}
emit('submit', { formData: form.value, response: res })
} catch (error) {
// 用户点击取消或关闭对话框,不处理
@@ -609,6 +1090,13 @@ onUnmounted(() => {
border-radius: 2px;
}
.section-desc {
font-size: 14px;
color: #64748b;
margin: 0 0 20px 0;
line-height: 1.6;
}
/* 表单输入框 */
.form-input :deep(.el-input__wrapper) {
border-radius: 8px;
@@ -633,6 +1121,45 @@ onUnmounted(() => {
padding: 8px 12px;
}
/* 拖放区域:包裹 picture-card 上传,支持拖拽图片到整块区域 */
.upload-drop-zone {
width: 100%;
border-radius: 8px;
transition: background 0.2s, border-color 0.2s;
}
.upload-drop-zone.is-dragover {
background: var(--el-color-primary-light-9, #ecf5ff);
outline: 2px dashed var(--el-color-primary);
outline-offset: -2px;
}
/* 上传区域基础样式 */
.upload-area {
width: 100%;
}
/* 保证 picture-card 触发区域整块可点击、可拖拽 */
.upload-trigger-inner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
min-height: 148px;
cursor: pointer;
gap: 8px;
}
.upload-icon {
font-size: 28px;
}
/* 当已有一张图片时,隐藏单图上传的“+ 选择文件”入口 */
.single-upload-area :deep(.el-upload-list__item + .el-upload--picture-card) {
display: none;
}
/* 验证码按钮 */
.code-btn {
min-width: 100px;