first commit

This commit is contained in:
2025-11-24 16:06:44 +08:00
commit e57d497751
165 changed files with 59349 additions and 0 deletions

View File

@@ -0,0 +1,725 @@
<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"
>
<div class="form-section">
<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>
<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>
</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>
<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>
<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-form-item label="企业地址" prop="enterpriseAddress">
<el-input
v-model="form.enterpriseAddress"
placeholder="请输入企业地址"
clearable
size="default"
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="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>
</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'
import { ElMessage } from 'element-plus'
const props = defineProps({
formData: {
type: Object,
default: () => ({
companyName: '',
unifiedSocialCode: '',
legalPersonName: '',
legalPersonID: '',
legalPersonPhone: '',
enterpriseAddress: '',
legalPersonCode: ''
})
}
})
const emit = defineEmits(['submit'])
const userStore = useUserStore()
// 表单引用
const enterpriseFormRef = ref()
// 表单数据
const form = ref({
companyName: '',
unifiedSocialCode: '',
legalPersonName: '',
legalPersonID: '',
legalPersonPhone: '',
enterpriseAddress: '',
legalPersonCode: ''
})
// 验证码相关状态
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()
// 计算属性
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' }
]
}
// 监听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 {
const result = await userStore.sendCode(form.value.legalPersonPhone, 'certification')
if (result.success) {
ElMessage.success('验证码发送成功')
startCountdown()
}
} catch (error) {
console.error('验证码发送失败:', error)
ElMessage.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)
const isValidSize = file.size / 1024 / 1024 < 5
if (!isValidType) {
ElMessage.error('只支持 JPG、PNG、WEBP 格式的图片')
return false
}
if (!isValidSize) {
ElMessage.error('图片大小不能超过 5MB')
return false
}
return true
}
// 处理文件变化
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 || ''
ocrResult.value = true
ElMessage.success('营业执照识别成功,已自动填充表单')
} else {
ElMessage.error('营业执照识别失败,请重试')
}
} catch (error) {
console.error('OCR识别失败:', error)
ElMessage.error('营业执照识别失败,请重试')
} finally {
ocrLoading.value = false
}
}
// 提交表单
const submitForm = async () => {
try {
await enterpriseFormRef.value.validate()
submitting.value = true
// Mock API 调用
await new Promise(resolve => setTimeout(resolve, 2000))
emit('submit', form.value)
} catch (error) {
console.error('表单验证失败:', error)
} 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;
}
/* 表单输入框 */
.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;
}
/* 验证码按钮 */
.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>