Files
tyapi-frontend/src/pages/certification/components/EnterpriseInfo.vue

1262 lines
37 KiB
Vue
Raw Normal View History

2025-11-24 16:06:44 +08:00
<template>
<el-card class="step-card">
<template #header>
<div class="card-header">
<div class="header-icon">
<el-icon class="text-blue-600">
<BuildingOfficeIcon />
</el-icon>
</div>
<div class="header-content">
<h2 class="header-title">企业信息</h2>
<p class="header-subtitle">请填写企业基本信息确保信息真实有效</p>
</div>
</div>
</template>
<div class="enterprise-form">
<el-form
ref="enterpriseFormRef"
:model="form"
:rules="enterpriseRules"
label-width="10em"
class="enterprise-form-content"
>
2026-03-17 17:19:00 +08:00
<div class="form-section">
2025-11-24 16:06:44 +08:00
<h3 class="section-title">基本信息</h3>
<!-- 企业名称和OCR识别区域 -->
<el-row :gutter="16" class="mb-4">
<el-col :span="16">
<el-form-item label="企业名称" prop="companyName">
<el-input
v-model="form.companyName"
placeholder="请输入企业名称"
clearable
size="default"
class="form-input"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<div class="ocr-compact">
<div class="ocr-header-compact">
<el-icon class="text-green-600"><DocumentIcon /></el-icon>
2026-03-17 17:19:00 +08:00
<span class="ocr-title-compact">可进行OCR识别请在下方上传营业执照</span>
2025-11-24 16:06:44 +08:00
</div>
<div v-if="ocrLoading" class="ocr-status-compact">
<el-icon class="is-loading"><Loading /></el-icon>
<span>识别中...</span>
</div>
<div v-if="ocrResult" class="ocr-status-compact success">
<el-icon class="text-green-600"><CheckIcon /></el-icon>
<span>识别成功</span>
</div>
</div>
</el-col>
</el-row>
<el-row :gutter="16" class="mb-4">
<el-col :span="24">
<el-form-item label="统一社会信用代码" prop="unifiedSocialCode">
<el-input
v-model="form.unifiedSocialCode"
placeholder="请输入统一社会信用代码"
clearable
size="default"
class="form-input"
/>
</el-form-item>
</el-col>
</el-row>
2026-03-17 17:19:00 +08:00
2025-11-24 16:06:44 +08:00
<el-row :gutter="16" class="mb-4">
<el-col :span="12">
<el-form-item label="法人姓名" prop="legalPersonName">
<el-input
v-model="form.legalPersonName"
placeholder="请输入法人姓名"
clearable
size="default"
class="form-input"
/>
</el-form-item>
</el-col>
2026-03-17 17:19:00 +08:00
2025-11-24 16:06:44 +08:00
<el-col :span="12">
2026-03-17 17:19:00 +08:00
<el-form-item label="企业地址" prop="enterpriseAddress">
2025-11-24 16:06:44 +08:00
<el-input
2026-03-17 17:19:00 +08:00
v-model="form.enterpriseAddress"
placeholder="请输入企业地址"
2025-11-24 16:06:44 +08:00
clearable
size="default"
class="form-input"
/>
</el-form-item>
</el-col>
</el-row>
2026-03-17 17:19:00 +08:00
<!-- 营业执照图片上传对应 BusinessLicenseImageURL -->
2025-11-24 16:06:44 +08:00
<el-row :gutter="16" class="mb-4">
<el-col :span="24">
2026-03-17 17:19:00 +08:00
<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">
2026-03-17 18:30:23 +08:00
请在非 IE 浏览器下上传大小不超过 1M 的图片最多 10 需体现门楣 LOGO办公设备与工作人员
2026-03-17 17:19:00 +08:00
</div>
2026-03-18 11:00:35 +08:00
<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"
2026-03-17 17:19:00 +08:00
>
2026-03-18 11:00:35 +08:00
<div class="upload-trigger-inner">
<el-icon class="upload-icon"><ArrowUpTrayIcon /></el-icon>
<div class="el-upload__text">上传办公场地环境照片</div>
</div>
</el-upload>
2026-03-17 17:19:00 +08:00
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16" class="mb-4">
<el-col :span="12">
<el-form-item label="法人身份证号" prop="legalPersonID">
2025-11-24 16:06:44 +08:00
<el-input
2026-03-17 17:19:00 +08:00
v-model="form.legalPersonID"
placeholder="请输入法人身份证号"
2025-11-24 16:06:44 +08:00
clearable
size="default"
class="form-input"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="法人手机号" prop="legalPersonPhone">
<el-input
v-model="form.legalPersonPhone"
placeholder="请输入法人手机号"
clearable
size="default"
maxlength="11"
class="form-input"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="验证码" prop="legalPersonCode">
<div class="flex gap-2">
<el-input
v-model="form.legalPersonCode"
placeholder="请输入验证码"
clearable
size="default"
maxlength="6"
class="form-input"
/>
<el-button
type="primary"
size="default"
:disabled="!canSendCode || sendingCode"
@click="sendCode"
:loading="sendingCode"
class="code-btn"
>
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
</el-button>
</div>
</el-form-item>
</el-col>
</el-row>
2026-03-17 17:19:00 +08:00
<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">
2026-03-17 18:30:23 +08:00
请在非 IE 浏览器下上传大小不超过 1M 的图片最多 10 张后台应用截图
2026-03-17 17:19:00 +08:00
</div>
2026-03-18 11:00:35 +08:00
<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"
2026-03-17 17:19:00 +08:00
>
2026-03-18 11:00:35 +08:00
<div class="upload-trigger-inner">
<el-icon class="upload-icon"><ArrowUpTrayIcon /></el-icon>
<div class="el-upload__text">上传业务场景相关截图或证明材料</div>
</div>
</el-upload>
2026-03-17 17:19:00 +08:00
</el-form-item>
</el-col>
</el-row>
2025-11-24 16:06:44 +08:00
</div>
<div class="form-actions">
<el-button
type="primary"
size="default"
@click="submitForm"
:loading="submitting"
class="submit-btn"
>
<el-icon class="mr-1"><CheckIcon /></el-icon>
提交企业信息
</el-button>
</div>
</el-form>
</div>
</el-card>
</template>
<script setup>
import { certificationApi } from '@/api'
import { useUserStore } from '@/stores/user'
import { Loading } from '@element-plus/icons-vue'
import {
ArrowUpTrayIcon,
BuildingOfficeIcon,
CheckIcon,
DocumentIcon
} from '@heroicons/vue/24/outline'
2026-03-18 11:00:35 +08:00
import { ElMessage, ElMessageBox } from 'element-plus'
2026-02-27 14:49:21 +08:00
import { useAliyunCaptcha } from '@/composables/useAliyunCaptcha'
2025-11-24 16:06:44 +08:00
const props = defineProps({
formData: {
type: Object,
default: () => ({
companyName: '',
unifiedSocialCode: '',
legalPersonName: '',
legalPersonID: '',
legalPersonPhone: '',
enterpriseAddress: '',
2026-03-17 17:19:00 +08:00
legalPersonCode: '',
// 扩展:营业执照 & 办公场地 & 场景
businessLicenseImageURL: '',
officePlaceImageURLs: [],
apiUsage: '',
scenarioAttachmentURLs: [],
// 授权代表信息
authorizedRepName: '',
authorizedRepID: '',
authorizedRepPhone: '',
authorizedRepIDImageURLs: []
2025-11-24 16:06:44 +08:00
})
}
})
const emit = defineEmits(['submit'])
const userStore = useUserStore()
2026-02-27 14:49:21 +08:00
const { runWithCaptcha } = useAliyunCaptcha()
2025-11-24 16:06:44 +08:00
// 表单引用
const enterpriseFormRef = ref()
// 表单数据
const form = ref({
companyName: '',
unifiedSocialCode: '',
legalPersonName: '',
legalPersonID: '',
legalPersonPhone: '',
enterpriseAddress: '',
2026-03-17 17:19:00 +08:00
legalPersonCode: '',
// 扩展:营业执照 & 办公场地 & 场景
businessLicenseImageURL: '',
officePlaceImageURLs: [],
apiUsage: '',
scenarioAttachmentURLs: [],
// 授权代表信息
authorizedRepName: '',
authorizedRepID: '',
authorizedRepPhone: '',
authorizedRepIDImageURLs: []
2025-11-24 16:06:44 +08:00
})
// 验证码相关状态
const sendingCode = ref(false)
const countdown = ref(0)
let countdownTimer = null
// 加载状态
const submitting = ref(false)
// OCR相关状态
const ocrLoading = ref(false)
const ocrResult = ref(false)
const uploadRef = ref()
2026-03-17 17:19:00 +08:00
const officePlaceUploadRef = ref()
const scenarioUploadRef = ref()
// 上传文件列表(前端展示用)
const officePlaceFileList = ref([])
const scenarioFileList = ref([])
const businessLicenseFileList = ref([])
const authorizedRepIDFrontFileList = ref([])
const authorizedRepIDBackFileList = ref([])
2025-11-24 16:06:44 +08:00
// 计算属性
const canSendCode = computed(() => {
return form.value.legalPersonPhone && form.value.legalPersonPhone.length === 11 && countdown.value === 0
})
// 统一社会信用代码验证函数
const validateUnifiedSocialCode = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入统一社会信用代码'))
return
}
// 统一社会信用代码格式18位由数字和大写字母组成
const pattern = /^[0-9A-HJ-NPQRTUWXY]{18}$/
if (!pattern.test(value)) {
callback(new Error('统一社会信用代码格式不正确应为18位数字和大写字母组合'))
return
}
callback()
}
// 身份证号验证函数
const validateIDCard = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入法人身份证号'))
return
}
// 身份证号格式18位最后一位可能是X
const pattern = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/
if (!pattern.test(value)) {
callback(new Error('身份证号格式不正确'))
return
}
callback()
}
// 手机号验证函数
const validatePhone = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入法人手机号'))
return
}
// 手机号格式11位数字
const pattern = /^1[3-9]\d{9}$/
if (!pattern.test(value)) {
callback(new Error('手机号格式不正确'))
return
}
callback()
}
// 表单验证规则
const enterpriseRules = {
companyName: [
{ required: true, message: '请输入企业名称', trigger: 'blur' },
{ min: 2, max: 100, message: '企业名称长度应在2-100个字符之间', trigger: 'blur' }
],
unifiedSocialCode: [
{ required: true, message: '请输入统一社会信用代码', trigger: 'blur' },
{ validator: validateUnifiedSocialCode, trigger: 'blur' }
],
legalPersonName: [
{ required: true, message: '请输入法人姓名', trigger: 'blur' },
{ min: 2, max: 20, message: '法人姓名长度应在2-20个字符之间', trigger: 'blur' },
{ pattern: /^[\u4e00-\u9fa5a-zA-Z\s·]+$/, message: '法人姓名只能包含中文、英文和间隔号', trigger: 'blur' }
],
legalPersonID: [
{ required: true, message: '请输入法人身份证号', trigger: 'blur' },
{ validator: validateIDCard, trigger: 'blur' }
],
legalPersonPhone: [
{ required: true, message: '请输入法人手机号', trigger: 'blur' },
{ validator: validatePhone, trigger: 'blur' }
],
enterpriseAddress: [
{ required: true, message: '请输入企业地址', trigger: 'blur' },
{ min: 5, max: 200, message: '企业地址长度应在5-200个字符之间', trigger: 'blur' }
],
legalPersonCode: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
{ len: 6, message: '验证码应为6位数字', trigger: 'blur' }
2026-03-17 17:19:00 +08:00
],
// 扩展字段简单校验(可按需加强)
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' }
2025-11-24 16:06:44 +08:00
]
}
// 监听props变化
watch(() => props.formData, (newData) => {
if (newData) {
// 同步父组件传递的数据到本地表单
Object.keys(newData).forEach(key => {
if (newData[key] && Object.prototype.hasOwnProperty.call(form.value, key)) {
form.value[key] = newData[key]
}
})
}
}, { immediate: true, deep: true })
// 发送验证码
const sendCode = async () => {
if (!canSendCode.value) return
sendingCode.value = true
try {
2026-02-27 14:49:21 +08:00
await runWithCaptcha(
async (captchaVerifyParam) => {
return await userStore.sendCode(form.value.legalPersonPhone, 'certification', captchaVerifyParam)
},
(res) => {
if (res.success) {
ElMessage.success('验证码发送成功')
startCountdown()
} else {
ElMessage.error(res?.error?.message || '验证码发送失败')
}
}
)
2025-11-24 16:06:44 +08:00
} catch (error) {
console.error('验证码发送失败:', error)
} finally {
sendingCode.value = false
}
}
// 开始倒计时
const startCountdown = () => {
countdown.value = 60
countdownTimer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(countdownTimer)
}
}, 1000)
}
// OCR文件上传前验证
const beforeUpload = (file) => {
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
const isValidType = allowedTypes.includes(file.type)
2026-03-17 18:30:23 +08:00
const maxSizeMB = 1
const isValidSize = file.size / 1024 / 1024 < maxSizeMB
2025-11-24 16:06:44 +08:00
if (!isValidType) {
ElMessage.error('只支持 JPG、PNG、WEBP 格式的图片')
return false
}
if (!isValidSize) {
2026-03-17 18:30:23 +08:00
ElMessage.error(`图片大小不能超过 ${maxSizeMB}MB`)
2025-11-24 16:06:44 +08:00
return false
}
return true
}
2026-03-17 17:19:00 +08:00
// 处理文件变化:触发 OCR并保存营业执照原图 URL若有上传地址
2025-11-24 16:06:44 +08:00
const handleFileChange = async (file) => {
if (!beforeUpload(file.raw)) {
return
}
ocrLoading.value = true
ocrResult.value = false
try {
const formData = new FormData()
formData.append('image', file.raw)
const result = await certificationApi.recognizeBusinessLicense(formData)
if (result.success && result.data) {
// 自动填充表单
const ocrData = result.data
form.value.companyName = ocrData.company_name || ''
form.value.unifiedSocialCode = ocrData.unified_social_code || ''
form.value.legalPersonName = ocrData.legal_person_name || ''
form.value.legalPersonID = ocrData.legal_person_id || ''
form.value.enterpriseAddress = ocrData.address || ''
2026-03-17 17:19:00 +08:00
// 如果后端返回了已保存的营业执照图片URL可以直接写入
if (ocrData.license_image_url) {
form.value.businessLicenseImageURL = ocrData.license_image_url
}
2025-11-24 16:06:44 +08:00
ocrResult.value = true
ElMessage.success('营业执照识别成功,已自动填充表单')
} else {
ElMessage.error('营业执照识别失败,请重试')
}
} catch (error) {
console.error('OCR识别失败:', error)
ElMessage.error('营业执照识别失败,请重试')
} finally {
ocrLoading.value = false
}
}
2026-03-17 17:19:00 +08:00
// 上传单张图片到七牛云,返回可访问 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
}
2026-03-18 11:00:35 +08:00
// 选择后立即上传:服务器 URL 存到 response.url保留 file.url 为 blob 以便预览(避免服务器证书等问题导致预览失败)
2026-03-17 18:30:23 +08:00
const uploadFileOnceSelected = async (file) => {
if (!file?.raw) return null
2026-03-18 11:00:35 +08:00
if (file.response?.url) return file.response.url // 已上传过,不重复上传
2026-03-17 18:30:23 +08:00
file.status = 'uploading'
try {
const url = await uploadFileToServer(file.raw)
file.status = 'success'
if (file.response === undefined) file.response = {}
file.response.url = url
2026-03-18 11:00:35 +08:00
// 不覆盖 file.url保留 blob 预览地址,避免服务器证书无效时预览失败
2026-03-17 18:30:23 +08:00
return url
} catch (err) {
file.status = 'fail'
ElMessage.error(err?.message || '图片上传失败')
return null
}
}
2026-03-18 11:00:35 +08:00
// 提交前仅从 fileList 同步 URL 到表单,并检查是否全部已上传(选择即上传,提交时不再批量上传)
const syncFormUrlsAndCheckReady = () => {
form.value.businessLicenseImageURL = extractUrls(businessLicenseFileList.value)[0] || ''
form.value.officePlaceImageURLs = extractUrls(officePlaceFileList.value)
form.value.scenarioAttachmentURLs = extractUrls(scenarioFileList.value)
updateAuthorizedRepIDImageURLs()
2026-03-17 17:19:00 +08:00
2026-03-18 11:00:35 +08:00
const hasUploading = (list) => list.some((f) => f.status === 'uploading')
const hasUnfinished = (list) => list.some((f) => f.raw && !f.response?.url)
if (hasUploading(businessLicenseFileList.value) || hasUnfinished(businessLicenseFileList.value)) return false
if (hasUploading(officePlaceFileList.value) || hasUnfinished(officePlaceFileList.value)) return false
if (hasUploading(scenarioFileList.value) || hasUnfinished(scenarioFileList.value)) return false
if (hasUploading(authorizedRepIDFrontFileList.value) || hasUnfinished(authorizedRepIDFrontFileList.value)) return false
if (hasUploading(authorizedRepIDBackFileList.value) || hasUnfinished(authorizedRepIDBackFileList.value)) return false
return true
2026-03-17 17:19:00 +08:00
}
2026-03-18 11:00:35 +08:00
// 从 el-upload 的 fileList 中提取 URL 数组,优先用服务器 URLresponse.url提交用
2026-03-17 17:19:00 +08:00
const extractUrls = (fileList) => {
return fileList
2026-03-18 11:00:35 +08:00
.map(f => f.response?.url || f.url || f.name)
2026-03-17 17:19:00 +08:00
.filter(Boolean)
}
2026-03-17 18:30:23 +08:00
// 营业执照图片变更:先 OCR 识别,再选择即上传
2026-03-17 17:19:00 +08:00
const handleBusinessLicenseChange = async (file, fileList) => {
businessLicenseFileList.value = fileList
const urls = extractUrls(fileList)
form.value.businessLicenseImageURL = urls[0] || ''
if (file && file.raw) {
await handleFileChange(file)
2026-03-18 11:00:35 +08:00
// OCR 若未返回服务器 URL则选择后立即上传未上传过才上传
if (!file.response?.url) {
2026-03-17 18:30:23 +08:00
const url = await uploadFileOnceSelected(file)
if (url) form.value.businessLicenseImageURL = url
}
2026-03-17 17:19:00 +08:00
}
}
const handleBusinessLicenseRemove = (file, fileList) => {
businessLicenseFileList.value = fileList
const urls = extractUrls(fileList)
form.value.businessLicenseImageURL = urls[0] || ''
}
// 手动清除营业执照图片(预览区域中的“删除”按钮)
const clearBusinessLicense = () => {
businessLicenseFileList.value = []
form.value.businessLicenseImageURL = ''
}
2026-03-17 18:30:23 +08:00
// 授权代表身份证人像面图片变更:选择即上传
const handleAuthorizedRepIDFrontChange = async (file, fileList) => {
2026-03-17 17:19:00 +08:00
authorizedRepIDFrontFileList.value = fileList
2026-03-18 11:00:35 +08:00
if (file?.raw && !file.response?.url) {
2026-03-18 16:04:49 +08:00
const url = await uploadFileOnceSelected(file)
if (url) {
authorizedRepIDFrontFileList.value = authorizedRepIDFrontFileList.value.map((f) =>
f.uid === file.uid ? { ...f, status: 'success', response: { url }, url: f.url } : f
)
}
2026-03-17 18:30:23 +08:00
}
2026-03-17 17:19:00 +08:00
updateAuthorizedRepIDImageURLs()
}
const handleAuthorizedRepIDFrontRemove = (file, fileList) => {
authorizedRepIDFrontFileList.value = fileList
updateAuthorizedRepIDImageURLs()
}
2026-03-17 18:30:23 +08:00
// 授权代表身份证国徽面图片变更:选择即上传
const handleAuthorizedRepIDBackChange = async (file, fileList) => {
2026-03-17 17:19:00 +08:00
authorizedRepIDBackFileList.value = fileList
2026-03-18 11:00:35 +08:00
if (file?.raw && !file.response?.url) {
2026-03-18 16:04:49 +08:00
const url = await uploadFileOnceSelected(file)
if (url) {
authorizedRepIDBackFileList.value = authorizedRepIDBackFileList.value.map((f) =>
f.uid === file.uid ? { ...f, status: 'success', response: { url }, url: f.url } : f
)
}
2026-03-17 18:30:23 +08:00
}
2026-03-17 17:19:00 +08:00
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
}
2026-03-17 18:30:23 +08:00
// 办公场地图片变更:选择即上传
const handleOfficePlaceChange = async (file, fileList) => {
2026-03-17 17:19:00 +08:00
officePlaceFileList.value = fileList
2026-03-18 11:00:35 +08:00
if (file?.raw && !file.response?.url) {
2026-03-17 18:30:23 +08:00
await uploadFileOnceSelected(file)
}
2026-03-17 17:19:00 +08:00
form.value.officePlaceImageURLs = extractUrls(fileList)
}
const handleOfficePlaceRemove = (file, fileList) => {
officePlaceFileList.value = fileList
form.value.officePlaceImageURLs = extractUrls(fileList)
}
2026-03-17 18:30:23 +08:00
// 应用场景附件图片变更:选择即上传
const handleScenarioChange = async (file, fileList) => {
2026-03-17 17:19:00 +08:00
scenarioFileList.value = fileList
2026-03-18 11:00:35 +08:00
if (file?.raw && !file.response?.url) {
2026-03-17 18:30:23 +08:00
await uploadFileOnceSelected(file)
}
2026-03-17 17:19:00 +08:00
form.value.scenarioAttachmentURLs = extractUrls(fileList)
}
const handleScenarioRemove = (file, fileList) => {
scenarioFileList.value = fileList
form.value.scenarioAttachmentURLs = extractUrls(fileList)
}
2025-11-24 16:06:44 +08:00
// 提交表单
const submitForm = async () => {
2026-03-17 17:19:00 +08:00
if (submitting.value) return
submitting.value = true
2025-11-24 16:06:44 +08:00
try {
await enterpriseFormRef.value.validate()
2026-02-27 14:49:21 +08:00
// 显示确认对话框
await ElMessageBox.confirm(
'提交的信息必须为法人真实信息(包括手机号),如信息有误请联系客服。',
'提交确认',
{
confirmButtonText: '确认提交',
cancelButtonText: '取消',
type: 'warning',
distinguishCancelAndClose: true,
customClass: 'submit-confirm-dialog'
}
)
2026-03-18 11:00:35 +08:00
// 选择即上传:提交时不再上传,仅同步 URL 并校验是否均已上传完成
if (!syncFormUrlsAndCheckReady()) {
ElMessage.warning('请等待所有图片上传完成后再提交')
2026-03-17 17:19:00 +08:00
submitting.value = false
return
}
// 调用后端提交接口
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
}
2025-11-24 16:06:44 +08:00
2026-03-17 17:19:00 +08:00
const res = await certificationApi.submitEnterpriseInfo(payload)
if (!res.success) {
throw new Error(res?.error?.message || '提交企业信息失败')
}
2025-11-24 16:06:44 +08:00
2026-03-17 17:19:00 +08:00
emit('submit', { formData: form.value, response: res })
2025-11-24 16:06:44 +08:00
} catch (error) {
2026-02-27 14:49:21 +08:00
// 用户点击取消或关闭对话框,不处理
if (error !== 'cancel' && error !== 'close') {
console.error('表单验证失败:', error)
}
2025-11-24 16:06:44 +08:00
} finally {
submitting.value = false
}
}
// 组件卸载时清理定时器
onUnmounted(() => {
if (countdownTimer) {
clearInterval(countdownTimer)
}
})
</script>
<style scoped>
.step-card {
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 0, 0, 0.06);
background: white;
overflow: hidden;
}
/* 卡片头部 */
.card-header {
display: flex;
align-items: center;
gap: 12px;
padding: 0;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
}
.header-icon {
width: 48px;
height: 48px;
border-radius: 12px;
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: #2563eb;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.15);
}
.header-content {
flex: 1;
}
.header-title {
font-size: 20px;
font-weight: 700;
color: #1e293b;
margin: 0 0 4px 0;
letter-spacing: -0.025em;
}
.header-subtitle {
font-size: 14px;
color: #64748b;
margin: 0;
font-weight: 500;
}
/* OCR紧凑样式 */
.ocr-compact {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 8px;
padding: 8px 12px;
background: linear-gradient(135deg, #f0fdf4 0%, #ecfdf5 100%);
border-radius: 8px;
border: 1px solid rgba(34, 197, 94, 0.2);
height: 100%;
min-height: 40px;
justify-content: center;
}
.ocr-header-compact {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
flex-wrap: wrap;
}
.ocr-title-compact {
font-size: 12px;
font-weight: 600;
color: #166534;
white-space: nowrap;
}
.ocr-uploader-compact {
flex-shrink: 0;
}
.ocr-status-compact {
display: flex;
align-items: center;
gap: 3px;
font-size: 10px;
color: #166534;
font-weight: 500;
margin-top: 2px;
}
.ocr-status-compact.success {
color: #16a34a;
}
/* 表单内容 */
.enterprise-form-content {
max-width: 100%;
margin: 0 auto;
padding: 0;
}
.form-section {
margin-bottom: 24px;
padding: 24px;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-radius: 12px;
border: 1px solid rgba(226, 232, 240, 0.8);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
margin: 0 0 20px 0;
display: flex;
align-items: center;
gap: 8px;
padding-bottom: 12px;
border-bottom: 2px solid #e2e8f0;
}
.section-title::before {
content: '';
width: 4px;
height: 16px;
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
border-radius: 2px;
}
2026-03-17 17:19:00 +08:00
.section-desc {
font-size: 14px;
color: #64748b;
margin: 0 0 20px 0;
line-height: 1.6;
}
2025-11-24 16:06:44 +08:00
/* 表单输入框 */
.form-input :deep(.el-input__wrapper) {
border-radius: 8px;
border: 1px solid #e2e8f0;
background: white;
transition: all 0.3s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.form-input :deep(.el-input__wrapper:hover) {
border-color: #3b82f6;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.15);
}
.form-input :deep(.el-input__wrapper.is-focus) {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-input :deep(.el-input__inner) {
font-size: 14px;
padding: 8px 12px;
}
2026-03-17 17:19:00 +08:00
/* 上传区域基础样式 */
.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;
}
2025-11-24 16:06:44 +08:00
/* 验证码按钮 */
.code-btn {
min-width: 100px;
border-radius: 8px;
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
border: none;
font-weight: 500;
transition: all 0.3s ease;
}
.code-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.code-btn:disabled {
background: #e2e8f0;
color: #94a3b8;
cursor: not-allowed;
}
/* 表单操作 */
.form-actions {
text-align: center;
padding: 24px;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-radius: 12px;
margin-top: 24px;
border: 1px solid rgba(226, 232, 240, 0.8);
}
.submit-btn {
min-width: 200px;
height: 44px;
font-size: 16px;
font-weight: 600;
border-radius: 12px;
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
border: none;
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3);
transition: all 0.3s ease;
}
.submit-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.4);
}
.submit-btn:active {
transform: translateY(0);
}
/* 响应式设计 */
@media (max-width: 768px) {
.enterprise-form-content {
max-width: 100%;
padding: 8px 0;
}
.form-section {
padding: 16px;
margin-bottom: 16px;
}
/* 移动端OCR紧凑样式 */
.ocr-compact {
padding: 6px 8px;
min-height: 36px;
gap: 4px;
}
.ocr-header-compact {
gap: 4px;
margin-bottom: 2px;
}
.ocr-title-compact {
font-size: 11px;
}
.ocr-status-compact {
font-size: 9px;
gap: 2px;
margin-top: 1px;
}
.card-header {
flex-direction: column;
text-align: center;
gap: 8px;
}
.header-icon {
width: 36px;
height: 36px;
font-size: 18px;
}
.header-title {
font-size: 18px;
}
.submit-btn {
min-width: 100%;
}
.upload-area {
padding: 16px;
}
.upload-icon {
font-size: 24px;
}
}
</style>
2026-02-27 14:49:21 +08:00
<style>
/* 提交确认对话框样式 */
.submit-confirm-dialog {
max-width: 420px;
}
.submit-confirm-dialog .el-message-box__message {
font-size: 15px;
line-height: 1.6;
color: #606266;
padding: 10px 0;
}
.submit-confirm-dialog .el-message-box__title {
font-size: 18px;
font-weight: 600;
color: #303133;
}
.submit-confirm-dialog .el-message-box__btns {
padding-top: 20px;
}
.submit-confirm-dialog .el-button--warning {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
border: none;
font-weight: 500;
}
.submit-confirm-dialog .el-button--warning:hover {
background: linear-gradient(135deg, #d97706 0%, #b45309 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(217, 119, 6, 0.3);
}
.submit-confirm-dialog .el-button--default {
border: 1px solid #dcdfe6;
color: #606266;
font-weight: 500;
}
.submit-confirm-dialog .el-button--default:hover {
color: #409eff;
border-color: #c6e2ff;
background-color: #ecf5ff;
}
</style>