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,91 @@
<template>
<div class="iframe-callback-page">
<div class="callback-content">
<div class="callback-icon">
<el-icon :size="48" color="#10b981"><Check /></el-icon>
</div>
<h2 class="callback-title">{{ successText }}</h2>
<p class="callback-desc">{{ descText }}</p>
<div class="callback-loading">
<span class="loading-spinner"></span>
<span>正在确认结果请稍候...</span>
</div>
</div>
</div>
</template>
<script setup>
import { Check, Loading } from '@element-plus/icons-vue'
const route = useRoute()
const scene = ref('auth') // auth: 企业认证, sign: 合同签署
// 解析路由参数
onMounted(() => {
const paramScene = route.params.scene
if (paramScene === 'sign') {
scene.value = 'sign'
} else {
scene.value = 'auth'
}
// 向父iframe发送消息
window.parent.postMessage({ type: 'CHECK_STATUS', scene: scene.value }, '*')
})
const successText = computed(() =>
scene.value === 'sign' ? '合同签署成功' : '企业认证成功'
)
const descText = computed(() =>
scene.value === 'sign'
? '合同已签署,正在为您确认签署状态...'
: '企业认证已完成,正在为您确认认证状态...'
)
</script>
<style scoped>
.iframe-callback-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #f8fafc;
}
.callback-content {
background: #fff;
border-radius: 20px;
box-shadow: 0 8px 32px rgba(16, 185, 129, 0.08);
padding: 48px 32px;
text-align: center;
min-width: 320px;
}
.callback-icon {
margin-bottom: 16px;
}
.callback-title {
font-size: 28px;
font-weight: 700;
color: #10b981;
margin-bottom: 8px;
}
.callback-desc {
font-size: 16px;
color: #64748b;
margin-bottom: 32px;
}
.callback-loading {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
color: #64748b;
font-size: 16px;
}
.loading-spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>

View File

@@ -0,0 +1,333 @@
# 企业入驻流程
## 📋 概述
企业入驻流程是一个基于Vue 3 + Element Plus + Tailwind CSS构建的步骤式认证系统用于引导用户完成企业信息填写、认证验证、合同签署等入驻流程。
## 🏗️ 架构设计
### 目录结构
```
certification/
├── index.vue # 主页面容器
├── components/ # 子组件目录
│ ├── EnterpriseInfo.vue # 企业信息填写
│ ├── EnterpriseVerify.vue # 企业认证验证
│ ├── ContractSign.vue # 合同签署
│ └── CertificationComplete.vue # 完成页面
└── README.md # 本文档
```
### 技术栈
- **前端框架**: Vue 3 (Composition API)
- **UI组件库**: Element Plus
- **样式框架**: Tailwind CSS v4
- **图标库**: Heroicons
- **状态管理**: Pinia (用户状态)
- **路由**: Vue Router
## 🔄 流程步骤
### 1. 填写企业信息 (`enterprise_info`)
**组件**: `EnterpriseInfo.vue`
**功能特性**:
- 企业基本信息收集
- 法人信息验证
- 手机号验证码验证
- 实时表单验证
**表单字段**:
- 企业名称 (2-100字符)
- 统一社会信用代码 (18位)
- 法人姓名 (2-20字符)
- 法人身份证号 (18位)
- 法人手机号 (11位)
- 验证码 (6位数字)
**验证规则**:
```javascript
// 统一社会信用代码验证
const pattern = /^[0-9A-HJ-NPQRTUWXY]{18}$/
// 身份证号验证
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]$/
// 手机号验证
const pattern = /^1[3-9]\d{9}$/
```
### 2. 企业认证 (`enterprise_verify`)
**组件**: `EnterpriseVerify.vue`
**功能特性**:
- 集成e签宝企业认证服务
- iframe嵌入认证页面
- 认证状态监控
- 加载状态管理
**集成服务**:
- 认证URL: `https://smlt.esign.cn/Z1Dc9Ts`
- 支持iframe加载
- 认证完成回调处理
### 3. 签署合同 (`contract_sign`)
**组件**: `ContractSign.vue`
**功能特性**:
- 集成电子合同签署服务
- iframe嵌入签署页面
- 签署状态监控
- 完成确认机制
**集成服务**:
- 签署URL: `https://smlt.esign.cn/95wqyN2`
- 支持iframe加载
- 签署完成回调处理
### 4. 完成入驻 (`completed`)
**组件**: `CertificationComplete.vue`
**功能特性**:
- 入驻成功展示
- 企业信息汇总
- 后续操作引导
- 状态标签显示
**操作入口**:
- 前往控制台
- 返回账户中心
## 🎨 UI/UX 设计
### 视觉设计原则
- **现代化**: 毛玻璃效果、渐变背景、圆角设计
- **一致性**: 统一的色彩系统和间距规范
- **可访问性**: 清晰的视觉层次和状态反馈
### 响应式设计
```css
/* 桌面端 */
@media (min-width: 1200px) {
.iframe-container { height: 700px; }
}
/* 移动端 */
@media (max-width: 768px) {
.iframe-container { height: 400px; }
.form-section { padding: 20px; }
}
```
### 状态指示
- **未开始**: 灰色图标,等待状态
- **进行中**: 蓝色图标,脉冲动画
- **已完成**: 绿色图标,完成状态
## 🔧 技术实现
### 状态管理
```javascript
// 主要状态
const currentStep = ref('enterprise_info')
const certificationData = ref(null)
const enterpriseForm = ref({
companyName: '',
unifiedSocialCode: '',
legalPersonName: '',
legalPersonID: '',
legalPersonPhone: '',
legalPersonCode: ''
})
```
### 组件通信
```javascript
// 事件定义
const emit = defineEmits(['submit', 'apply', 'check-status', 'go-dashboard', 'go-profile'])
// 数据传递
const props = defineProps({
formData: Object,
enterpriseData: Object,
companyName: String,
certificationData: Object
})
```
### 表单验证
```javascript
// 自定义验证器
const validateUnifiedSocialCode = (rule, value, callback) => {
const pattern = /^[0-9A-HJ-NPQRTUWXY]{18}$/
if (!pattern.test(value)) {
callback(new Error('统一社会信用代码格式不正确'))
return
}
callback()
}
```
## 🚀 开发特性
### 开发模式
```javascript
// 开发模式控制器
const isDevelopment = ref(false)
const devCurrentStep = ref('enterprise_info')
// 步骤切换功能
const switchToStep = (stepKey) => {
currentStep.value = stepKey
}
const resetToFirstStep = () => {
currentStep.value = 'enterprise_info'
}
```
### 错误处理
- 表单验证错误提示
- API调用异常处理
- 网络错误友好提示
- 加载状态管理
### 性能优化
- 组件懒加载
- 表单数据缓存
- 防抖处理
- 内存清理
## 📱 移动端适配
### 布局调整
- 垂直布局适配
- 按钮全宽显示
- 步骤图标缩小
- 间距优化
### 交互优化
- 触摸友好的按钮尺寸
- 简化的表单布局
- 优化的iframe高度
- 响应式步骤条
## 🔌 第三方集成
### e签宝服务
- **企业认证**: 统一身份认证
- **合同签署**: 电子合同服务
- **iframe集成**: 无缝嵌入体验
### 短信服务
- **验证码发送**: 手机号验证
- **倒计时功能**: 防止重复发送
- **错误处理**: 发送失败重试
## 📊 状态流转
### 认证状态映射
```javascript
const setCurrentStepByStatus = () => {
switch (certificationData.value.status) {
case 'not_started':
case 'pending':
currentStep.value = 'enterprise_info'
break
case 'info_submitted':
currentStep.value = 'enterprise_verify'
break
case 'enterprise_verified':
case 'contract_applied':
case 'contract_pending':
case 'contract_approved':
currentStep.value = 'contract_sign'
break
case 'contract_signed':
case 'completed':
currentStep.value = 'completed'
break
default:
currentStep.value = 'enterprise_info'
}
}
```
## 🧪 测试指南
### 开发模式测试
1. 启用开发模式控制器
2. 使用步骤选择器切换不同步骤
3. 测试表单验证和提交
4. 验证响应式布局
### 功能测试
1. 表单验证测试
2. 验证码发送测试
3. iframe加载测试
4. 状态切换测试
### 兼容性测试
1. 不同浏览器测试
2. 移动端适配测试
3. 网络异常测试
4. 性能压力测试
## 📝 使用说明
### 基本使用
```vue
<template>
<div class="certification-page">
<!-- 步骤条 -->
<CustomSteps :steps="certificationSteps" :current-step="currentStep" />
<!-- 内容区域 -->
<EnterpriseInfo v-if="currentStep === 'enterprise_info'" @submit="handleSubmit" />
<EnterpriseVerify v-if="currentStep === 'enterprise_verify'" @submit="handleVerify" />
<ContractSign v-if="currentStep === 'contract_sign'" @apply="handleApply" />
<CertificationComplete v-if="currentStep === 'completed'" @go-dashboard="goToDashboard" />
</div>
</template>
```
### 配置步骤
```javascript
const certificationSteps = [
{
key: 'enterprise_info',
title: '填写企业信息',
description: '填写企业基本信息和法人信息',
icon: BuildingOfficeIcon
},
// ... 其他步骤
]
```
## 🔮 未来规划
### 功能增强
- [ ] 支持多语言国际化
- [ ] 增加进度保存功能
- [ ] 支持离线填写
- [ ] 增加数据导出功能
### 技术优化
- [ ] 引入TypeScript支持
- [ ] 增加单元测试覆盖
- [ ] 优化包体积
- [ ] 增加PWA支持
### 用户体验
- [ ] 增加引导提示
- [ ] 优化加载动画
- [ ] 增加操作确认
- [ ] 支持快捷键操作
## 📞 技术支持
如有问题或建议请联系开发团队或提交Issue。
---
*最后更新: 2024年*

View File

@@ -0,0 +1,538 @@
<template>
<el-card class="step-card">
<template #header>
<div class="card-header">
<div class="header-icon">
<el-icon class="text-green-600">
<CheckCircleIcon />
</el-icon>
</div>
<div class="header-content">
<h2 class="header-title">入驻完成</h2>
<p class="header-subtitle">恭喜您完成企业入驻现在可以使用完整的API服务功能</p>
</div>
</div>
</template>
<div class="completion-content">
<div class="success-card">
<div class="success-icon">
<div class="icon-wrapper">
<el-icon class="text-green-500">
<CheckCircleIcon />
</el-icon>
</div>
</div>
<div class="success-text">
<h2 class="success-title">恭喜企业入驻已完成</h2>
<p class="success-desc">您的企业已完成入驻现在可以使用完整的API服务功能</p>
<div class="completion-info">
<h3 class="info-title">入驻信息</h3>
<div class="info-grid">
<div class="info-item">
<label class="info-label">入驻状态</label>
<div class="info-value">
<el-tag type="success" size="large" class="status-tag">已入驻</el-tag>
</div>
</div>
<div class="info-item">
<label class="info-label">入驻时间</label>
<div class="info-value">
{{ formatDate(certificationData.completed_at || formatDate(new Date())) }}
</div>
</div>
<div class="info-item">
<label class="info-label">企业名称</label>
<div class="info-value">{{ enterpriseInfo.company_name || '' }}</div>
</div>
<div class="info-item">
<label class="info-label">统一社会信用代码</label>
<div class="info-value">{{ enterpriseInfo.unified_social_code || '' }}</div>
</div>
</div>
</div>
<div class="completion-actions">
<el-button
type="primary"
size="large"
@click="showContract"
class="action-btn primary-btn"
>
<el-icon class="mr-2">
<DocumentTextIcon />
</el-icon>
查看合同
</el-button>
<el-button size="large" @click="goToProfile" class="action-btn secondary-btn">
<el-icon class="mr-2">
<UserIcon />
</el-icon>
返回账户中心
</el-button>
</div>
</div>
</div>
</div>
<!-- 合同查看弹窗 -->
<el-dialog
v-model="contractDialogVisible"
title="合同查看"
width="70%"
:close-on-click-modal="false"
:close-on-press-escape="true"
class="contract-dialog"
>
<div class="contract-container">
<iframe
v-if="contractUrl"
:src="contractUrl"
class="contract-iframe"
frameborder="0"
allowfullscreen
></iframe>
<div v-else class="no-contract">
<el-icon class="text-gray-400" size="48">
<DocumentIcon />
</el-icon>
<p>暂无合同文件</p>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="contractDialogVisible = false">关闭</el-button>
</div>
</template>
</el-dialog>
</el-card>
</template>
<script setup>
import { useUserStore } from '@/stores/user'
import { CheckCircleIcon, DocumentIcon, DocumentTextIcon, UserIcon } from '@heroicons/vue/24/outline'
import { ElMessage } from 'element-plus'
import { computed, onMounted, ref } from 'vue'
const userStore = useUserStore()
const { enterpriseForm, certificationData } = defineProps({
enterpriseForm: {
type: Object,
default: () => ({}),
},
certificationData: {
type: Object,
default: () => ({}),
},
})
const emit = defineEmits(['go-dashboard', 'go-profile'])
// 合同弹窗状态
const contractDialogVisible = ref(false)
// 响应式获取企业信息
const enterpriseInfo = computed(() => {
return userStore.userInfo?.enterprise_info || {}
})
// 获取合同URL
const contractUrl = computed(() => {
return certificationData?.metadata?.contract_url || ''
})
// 显示合同
const showContract = () => {
if (contractUrl.value) {
contractDialogVisible.value = true
} else {
ElMessage.warning('暂无合同文件')
}
}
// 跳转到控制台
const goToDashboard = () => {
emit('go-dashboard')
}
// 跳转到账户中心
const goToProfile = () => {
emit('go-profile')
}
// 格式化日期
const formatDate = (date) => {
return new Date(date).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
}
// 格式化手机号
const formatPhone = (phone) => {
if (!phone) return ''
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}
// 组件挂载时重新获取用户信息
onMounted(async () => {
try {
await userStore.fetchUserProfile()
console.log('认证完成页面:已重新获取用户信息')
} catch (error) {
console.error('认证完成页面:重新获取用户信息失败:', error)
}
})
</script>
<style scoped>
.step-card {
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
border: none;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
/* 卡片头部 */
.card-header {
display: flex;
align-items: center;
gap: 16px;
padding: 8px 0;
}
.header-icon {
width: 48px;
height: 48px;
border-radius: 12px;
background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.header-content {
flex: 1;
}
.header-title {
font-size: 24px;
font-weight: 700;
color: #1e293b;
margin: 0 0 4px 0;
}
.header-subtitle {
font-size: 14px;
color: #64748b;
margin: 0;
font-weight: 500;
}
/* 完成内容 */
.completion-content {
text-align: center;
padding: 40px 0;
}
.success-card {
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
border-radius: 24px;
padding: 48px;
border: 1px solid rgba(34, 197, 94, 0.2);
box-shadow: 0 16px 48px rgba(34, 197, 94, 0.15);
max-width: 800px;
margin: 0 auto;
}
.success-icon {
margin-bottom: 32px;
}
.icon-wrapper {
width: 120px;
height: 120px;
border-radius: 50%;
background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
font-size: 60px;
box-shadow: 0 16px 32px rgba(34, 197, 94, 0.3);
animation: successPulse 2s ease-in-out infinite;
}
.success-text {
max-width: 700px;
margin: 0 auto;
}
.success-title {
font-size: 36px;
font-weight: 700;
color: #166534;
margin: 0 0 16px 0;
background: linear-gradient(135deg, #166534 0%, #16a34a 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.success-desc {
font-size: 18px;
color: #16a34a;
margin: 0 0 40px 0;
line-height: 1.6;
}
/* 认证信息 */
.completion-info {
text-align: left;
margin: 40px 0;
background: white;
border-radius: 16px;
padding: 32px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
border: 1px solid #e2e8f0;
}
.info-title {
font-size: 20px;
font-weight: 600;
color: #1e293b;
margin: 0 0 24px 0;
display: flex;
align-items: center;
gap: 8px;
}
.info-title::before {
content: '';
width: 4px;
height: 20px;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border-radius: 2px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 24px;
}
.info-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.info-label {
font-size: 14px;
color: #64748b;
font-weight: 500;
}
.info-value {
font-size: 16px;
color: #1e293b;
font-weight: 600;
word-break: break-all;
}
.status-tag {
font-weight: 600;
border-radius: 8px;
padding: 6px 16px;
font-size: 14px;
}
/* 操作按钮 */
.completion-actions {
display: flex;
gap: 20px;
justify-content: center;
flex-wrap: wrap;
margin-top: 40px;
}
.action-btn {
min-width: 180px;
height: 56px;
font-size: 16px;
font-weight: 600;
border-radius: 12px;
transition: all 0.3s ease;
}
.primary-btn {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border: none;
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.3);
color: white;
}
.primary-btn:hover {
transform: translateY(-2px);
box-shadow: 0 12px 32px rgba(16, 185, 129, 0.4);
}
.secondary-btn {
border: 2px solid #e2e8f0;
background: white;
color: #64748b;
}
.secondary-btn:hover {
border-color: #10b981;
color: #10b981;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.15);
}
/* 合同弹窗样式 */
.contract-dialog {
border-radius: 16px;
}
.contract-container {
height: 70vh;
border-radius: 8px;
overflow: hidden;
background: #f8fafc;
}
.contract-iframe {
width: 100%;
height: 100%;
border: none;
background: white;
}
.no-contract {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #64748b;
font-size: 16px;
}
.no-contract p {
margin-top: 16px;
font-weight: 500;
}
.dialog-footer {
text-align: right;
padding-top: 16px;
}
/* 动画效果 */
@keyframes successPulse {
0%,
100% {
transform: scale(1);
box-shadow: 0 16px 32px rgba(34, 197, 94, 0.3);
}
50% {
transform: scale(1.05);
box-shadow: 0 20px 40px rgba(34, 197, 94, 0.4);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.completion-content {
padding: 20px 0;
}
.success-card {
padding: 32px 24px;
margin: 0 16px;
}
.success-title {
font-size: 28px;
}
.success-desc {
font-size: 16px;
}
.icon-wrapper {
width: 80px;
height: 80px;
font-size: 40px;
}
.card-header {
flex-direction: column;
text-align: center;
gap: 12px;
}
.header-icon {
width: 40px;
height: 40px;
font-size: 20px;
}
.header-title {
font-size: 20px;
}
.completion-info {
padding: 24px;
margin: 24px 0;
}
.info-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.completion-actions {
flex-direction: column;
align-items: center;
gap: 16px;
}
.action-btn {
min-width: 100%;
}
.contract-container {
height: 60vh;
}
}
@media (max-width: 480px) {
.success-card {
padding: 24px 16px;
}
.success-title {
font-size: 24px;
}
.icon-wrapper {
width: 60px;
height: 60px;
font-size: 30px;
}
.contract-container {
height: 50vh;
}
}
</style>

View File

@@ -0,0 +1,303 @@
<template>
<div class="contract-expired-page">
<div class="expired-content">
<div class="expired-icon">
<el-icon :size="64" color="#f59e0b"><Clock /></el-icon>
</div>
<h2 class="expired-title">合同签署已过期</h2>
<div class="expired-info">
<h3 class="info-title">过期说明</h3>
<p class="info-text">合同签署时间已超过规定期限需要重新申请签署</p>
</div>
<div class="expired-details">
<div class="detail-item">
<span class="detail-label">合同生成时间</span>
<span class="detail-value">{{ formatTime(contractCreatedAt) }}</span>
</div>
<div class="detail-item">
<span class="detail-label">签署期限</span>
<span class="detail-value">50分钟</span>
</div>
<div class="detail-item">
<span class="detail-label">过期时间</span>
<span class="detail-value">{{ formatTime(expiredAt) }}</span>
</div>
</div>
<div class="expired-actions">
<el-button
type="primary"
size="large"
@click="reapplyContract"
:loading="reapplyLoading"
class="reapply-btn"
>
<el-icon><Refresh /></el-icon>
重新申请签署
</el-button>
<el-button
type="info"
size="large"
@click="goBack"
class="back-btn"
>
返回上一步
</el-button>
</div>
<div class="expired-tips">
<h4>温馨提示</h4>
<ul>
<li>合同签署有效期为50分钟请及时完成签署</li>
<li>重新申请后您将获得新的签署链接</li>
<li>建议在网络良好的环境下完成签署操作</li>
</ul>
</div>
</div>
</div>
</template>
<script setup>
import { Clock, Refresh } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
contractCreatedAt: {
type: String,
default: ''
}
})
const emit = defineEmits(['reapply-contract', 'go-back'])
const reapplyLoading = ref(false)
const expiredAt = computed(() => {
if (!props.contractCreatedAt) return ''
const created = new Date(props.contractCreatedAt)
const expired = new Date(created.getTime() + 50 * 60 * 1000) // 50分钟后过期
return expired.toISOString()
})
const formatTime = (timeStr) => {
if (!timeStr) return '未知'
const date = new Date(timeStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
const reapplyContract = async () => {
try {
reapplyLoading.value = true
emit('reapply-contract')
} catch (error) {
ElMessage.error('重新申请失败,请稍后重试')
} finally {
reapplyLoading.value = false
}
}
const goBack = () => {
emit('go-back')
}
</script>
<style scoped>
.contract-expired-page {
min-height: 600px;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 20px;
}
.expired-content {
background: #fff;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
padding: 48px 40px;
text-align: center;
max-width: 600px;
width: 100%;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.expired-icon {
margin-bottom: 24px;
}
.expired-title {
font-size: 32px;
font-weight: 700;
color: #f59e0b;
margin-bottom: 32px;
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.expired-info {
background: #fffbeb;
border: 1px solid #fed7aa;
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
text-align: left;
}
.info-title {
font-size: 18px;
font-weight: 600;
color: #d97706;
margin-bottom: 12px;
}
.info-text {
font-size: 16px;
color: #92400e;
line-height: 1.6;
margin: 0;
}
.expired-details {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 24px;
margin-bottom: 32px;
text-align: left;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
font-size: 14px;
}
.detail-item:last-child {
margin-bottom: 0;
}
.detail-label {
color: #64748b;
font-weight: 500;
}
.detail-value {
color: #1e293b;
font-weight: 600;
}
.expired-actions {
display: flex;
gap: 16px;
justify-content: center;
margin-bottom: 32px;
}
.reapply-btn {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border: none;
border-radius: 12px;
padding: 12px 32px;
font-weight: 600;
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}
.reapply-btn:hover {
background: linear-gradient(135deg, #059669 0%, #047857 100%);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4);
}
.back-btn {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 12px 32px;
font-weight: 600;
color: #64748b;
}
.back-btn:hover {
background: #f1f5f9;
border-color: #cbd5e1;
color: #475569;
}
.expired-tips {
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 12px;
padding: 24px;
text-align: left;
}
.expired-tips h4 {
font-size: 16px;
font-weight: 600;
color: #0369a1;
margin-bottom: 12px;
}
.expired-tips ul {
margin: 0;
padding-left: 20px;
}
.expired-tips li {
font-size: 14px;
color: #0c4a6e;
line-height: 1.6;
margin-bottom: 8px;
}
.expired-tips li:last-child {
margin-bottom: 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.contract-expired-page {
padding: 20px 16px;
}
.expired-content {
padding: 32px 24px;
}
.expired-title {
font-size: 24px;
}
.expired-actions {
flex-direction: column;
align-items: center;
}
.reapply-btn,
.back-btn {
width: 100%;
max-width: 280px;
}
.detail-item {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
}
</style>

View File

@@ -0,0 +1,280 @@
<template>
<el-card class="step-card">
<template #header>
<div class="card-header">
<div class="header-icon">
<el-icon class="text-blue-600">
<DocumentTextIcon />
</el-icon>
</div>
<div class="header-content">
<h2 class="header-title">合同预览</h2>
<p class="header-subtitle">请仔细阅读合同内容确认无误后再进行签署</p>
</div>
</div>
</template>
<div class="contract-preview-section">
<div v-if="contractUrl" class="pdf-viewer-wrapper">
<vue-pdf-embed
ref="pdfRef"
:source="contractUrl"
:page="page"
:scale="pdfScale"
@loaded="handleLoaded"
class="pdf-embed"
/>
<div class="pdf-controls">
<el-button size="large" :disabled="page <= 1" @click="page--"> 上一页 </el-button>
<span class="pdf-page-info"> {{ page }} / {{ pageCount }} </span>
<el-button size="large" :disabled="page >= pageCount" @click="page++"> 下一页 </el-button>
<el-divider direction="vertical" />
<el-button size="large" @click="fullscreen = true"> 全屏预览 </el-button>
</div>
</div>
<el-button type="primary" @click="emit('start-sign')" :loading="props.loading">
<el-icon class="mr-2"><DocumentTextIcon /></el-icon>
开始签署
</el-button>
</div>
<!-- 极简全屏预览弹窗无顶部栏 -->
<el-dialog
v-model="fullscreen"
fullscreen
append-to-body
:close-on-click-modal="true"
class="fullscreen-dialog"
:show-close="false"
lock-scroll
:header="false"
>
<div class="fullscreen-pdf-simple">
<vue-pdf-embed
:source="contractUrl"
:page="page"
:scale="fullscreenScale"
class="fullscreen-pdf-embed"
/>
<div class="fullscreen-pdf-controls">
<el-button size="large" :disabled="page <= 1" @click="page--">上一页</el-button>
<span class="pdf-page-info"> {{ page }} / {{ pageCount }} </span>
<el-button size="large" :disabled="page >= pageCount" @click="page++">下一页</el-button>
<el-button type="primary" size="large" @click="fullscreen = false"> 关闭预览 </el-button>
</div>
</div>
</el-dialog>
</el-card>
</template>
<script setup>
import { DocumentTextIcon } from '@heroicons/vue/24/outline'
import VuePdfEmbed from 'vue-pdf-embed'
const props = defineProps({
contractUrl: String,
loading: Boolean
})
const emit = defineEmits(['start-sign'])
const pdfRef = ref(null)
const page = ref(1)
const pageCount = ref(0)
const pdfScale = ref(1.3)
const fullscreen = ref(false)
const fullscreenScale = ref(1.7)
function handleLoaded(pdf) {
pageCount.value = pdf.numPages
}
</script>
<style scoped>
.step-card {
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
border: none;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.card-header {
display: flex;
align-items: center;
gap: 16px;
padding: 8px 0;
}
.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;
}
.header-content {
flex: 1;
}
.header-title {
font-size: 24px;
font-weight: 700;
color: #1e293b;
margin: 0 0 4px 0;
}
.header-subtitle {
font-size: 14px;
color: #64748b;
margin: 0;
font-weight: 500;
}
.contract-preview-section {
padding: 32px 0;
text-align: center;
}
.pdf-viewer-wrapper {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 16px;
}
.pdf-embed {
width: 100%;
max-width: 600px;
min-height: 600px;
max-height: 600px;
border: 1px solid #e5e7eb;
border-radius: 16px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
background: #fafbfc;
overflow-y: auto;
overflow-x: auto;
scrollbar-width: thin;
scrollbar-color: #cbd5e1 #f1f5f9;
}
.pdf-embed::-webkit-scrollbar {
width: 8px;
}
.pdf-embed::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
.pdf-embed::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 4px;
}
.pdf-controls {
margin: 24px 0 0 0;
display: flex;
align-items: center;
gap: 24px;
justify-content: center;
font-size: 16px;
}
.pdf-btn {
border-radius: 12px;
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
color: #fff;
font-weight: 600;
border: none;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.1);
transition:
background 0.2s,
color 0.2s,
box-shadow 0.2s;
min-width: 110px;
height: 44px;
font-size: 16px;
}
.pdf-btn:disabled {
background: #e2e8f0;
color: #94a3b8;
box-shadow: none;
}
.pdf-btn:hover:not(:disabled) {
background: #2563eb;
color: #fff;
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.18);
}
.pdf-page-info {
color: #64748b;
font-size: 16px;
font-weight: 500;
}
.contract-sign-btn {
min-width: 220px;
height: 52px;
font-size: 18px;
font-weight: 700;
border-radius: 14px;
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
border: none;
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.18);
transition:
background 0.2s,
color 0.2s,
box-shadow 0.2s;
margin-top: 32px;
}
.contract-sign-btn:hover:not(:disabled) {
background: #2563eb;
color: #fff;
box-shadow: 0 12px 32px rgba(59, 130, 246, 0.22);
}
.contract-sign-btn:disabled {
background: #e2e8f0;
color: #94a3b8;
box-shadow: none;
}
.fullscreen-dialog >>> .el-dialog__body {
padding: 0;
}
.fullscreen-pdf-simple {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #fafbfc;
overflow: auto;
}
.fullscreen-pdf-embed {
width: 100vw;
max-width: 1000px;
height: calc(100vh - 116px);
border: none;
border-radius: 0;
background: #fff;
overflow: auto;
box-shadow: none;
}
.fullscreen-pdf-controls {
width: 100%;
height: 68px;
background-color: #e2e8f0;
display: flex;
align-items: center;
gap: 24px;
justify-content: center;
font-size: 18px;
}
.fullscreen-close-btn {
min-width: 120px;
height: 44px;
font-size: 16px;
font-weight: 700;
border-radius: 12px;
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
border: none;
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.18);
margin-left: 32px;
transition:
background 0.2s,
color 0.2s,
box-shadow 0.2s;
}
.fullscreen-close-btn:hover {
background: #2563eb;
color: #fff;
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.22);
}
</style>

View File

@@ -0,0 +1,223 @@
<template>
<div class="contract-rejected-page">
<div class="rejected-content">
<div class="rejected-icon">
<el-icon :size="64" color="#ef4444"><Close /></el-icon>
</div>
<h2 class="rejected-title">合同签署被拒绝</h2>
<div class="rejected-reason">
<h3 class="reason-title">拒绝原因</h3>
<p class="reason-text">{{ failureMessage || '用户主动拒绝签署合同' }}</p>
</div>
<div class="rejected-actions">
<el-button
type="primary"
size="large"
@click="contactCustomerService"
class="contact-btn"
>
<el-icon><Service /></el-icon>
联系客服
</el-button>
<el-button
type="info"
size="large"
@click="goBack"
class="back-btn"
>
返回上一步
</el-button>
</div>
<div class="rejected-tips">
<h4>温馨提示</h4>
<ul>
<li>如果您对合同内容有疑问请联系客服咨询</li>
<li>客服将协助您解决相关问题并重新申请签署</li>
<li>您也可以稍后重新申请合同签署</li>
</ul>
</div>
</div>
</div>
</template>
<script setup>
import { Close, Service } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
failureMessage: {
type: String,
default: ''
}
})
const emit = defineEmits(['contact-service', 'go-back'])
const contactCustomerService = () => {
// 这里可以打开客服聊天窗口或跳转到客服页面
ElMessage.success('正在为您连接客服...')
emit('contact-service')
}
const goBack = () => {
emit('go-back')
}
</script>
<style scoped>
.contract-rejected-page {
min-height: 600px;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 20px;
}
.rejected-content {
background: #fff;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
padding: 48px 40px;
text-align: center;
max-width: 600px;
width: 100%;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.rejected-icon {
margin-bottom: 24px;
}
.rejected-title {
font-size: 32px;
font-weight: 700;
color: #ef4444;
margin-bottom: 32px;
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.rejected-reason {
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 12px;
padding: 24px;
margin-bottom: 32px;
text-align: left;
}
.reason-title {
font-size: 18px;
font-weight: 600;
color: #dc2626;
margin-bottom: 12px;
}
.reason-text {
font-size: 16px;
color: #7f1d1d;
line-height: 1.6;
margin: 0;
}
.rejected-actions {
display: flex;
gap: 16px;
justify-content: center;
margin-bottom: 32px;
}
.contact-btn {
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
border: none;
border-radius: 12px;
padding: 12px 32px;
font-weight: 600;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.contact-btn:hover {
background: linear-gradient(135deg, #1d4ed8 0%, #1e40af 100%);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4);
}
.back-btn {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 12px 32px;
font-weight: 600;
color: #64748b;
}
.back-btn:hover {
background: #f1f5f9;
border-color: #cbd5e1;
color: #475569;
}
.rejected-tips {
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 12px;
padding: 24px;
text-align: left;
}
.rejected-tips h4 {
font-size: 16px;
font-weight: 600;
color: #0369a1;
margin-bottom: 12px;
}
.rejected-tips ul {
margin: 0;
padding-left: 20px;
}
.rejected-tips li {
font-size: 14px;
color: #0c4a6e;
line-height: 1.6;
margin-bottom: 8px;
}
.rejected-tips li:last-child {
margin-bottom: 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.contract-rejected-page {
padding: 20px 16px;
}
.rejected-content {
padding: 32px 24px;
}
.rejected-title {
font-size: 24px;
}
.rejected-actions {
flex-direction: column;
align-items: center;
}
.contact-btn,
.back-btn {
width: 100%;
max-width: 280px;
}
}
</style>

View File

@@ -0,0 +1,259 @@
<template>
<el-card class="step-card">
<template #header>
<div class="card-header">
<div class="header-icon">
<el-icon class="text-purple-600">
<DocumentTextIcon />
</el-icon>
</div>
<div class="header-content">
<h2 class="header-title">签署合同</h2>
<p class="header-subtitle">请完成合同签署流程</p>
</div>
</div>
</template>
<div class="contract-content">
<!-- 合同签署iframe -->
<div class="contract-iframe-section">
<div class="iframe-container">
<iframe
:src="iframeUrl"
frameborder="0"
class="contract-iframe"
title="合同签署"
@load="handleIframeLoad"
></iframe>
<div v-if="iframeLoading" class="iframe-loading">
<div class="loading-spinner"></div>
<p>正在加载合同签署页面...</p>
</div>
</div>
</div>
</div>
</el-card>
</template>
<script setup>
import {
CheckIcon,
DocumentTextIcon
} from '@heroicons/vue/24/outline'
import { ElMessage } from 'element-plus'
defineProps({
companyName: {
type: String,
default: ''
},
iframeUrl: {
type: String,
default: ''
}
})
// 合同签署状态
const contractCompleted = ref(false)
const iframeLoading = ref(true)
// iframe加载完成回调
const handleIframeLoad = () => {
iframeLoading.value = false
}
</script>
<style scoped>
.step-card {
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
border: none;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
/* 卡片头部 */
.card-header {
display: flex;
align-items: center;
gap: 16px;
padding: 8px 0;
}
.header-icon {
width: 48px;
height: 48px;
border-radius: 12px;
background: linear-gradient(135deg, #f3e8ff 0%, #e9d5ff 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.header-content {
flex: 1;
}
.header-title {
font-size: 24px;
font-weight: 700;
color: #1e293b;
margin: 0 0 4px 0;
}
.header-subtitle {
font-size: 14px;
color: #64748b;
margin: 0;
font-weight: 500;
}
/* 合同内容 */
.contract-content {
max-width: 100%;
margin: 0 auto;
padding: 20px 0;
}
/* 合同签署iframe */
.contract-iframe-section {
padding: 20px;
}
.iframe-container {
position: relative;
width: 100%;
height: 600px; /* 调整iframe高度 */
border-radius: 12px;
overflow: hidden;
border: 1px solid #e2e8f0;
background: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.contract-iframe {
width: 100%;
height: 100%;
border: none;
}
.iframe-loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
border-radius: 12px;
z-index: 10;
}
.iframe-loading .loading-spinner {
width: 48px;
height: 48px;
border: 4px solid #e2e8f0;
border-top: 4px solid #8b5cf6;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.iframe-loading p {
font-size: 16px;
color: #64748b;
font-weight: 500;
}
.iframe-actions {
display: flex;
justify-content: center;
gap: 16px;
margin-top: 24px;
}
.contract-complete-btn {
min-width: 180px;
border-radius: 12px;
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
border: none;
font-weight: 500;
transition: all 0.3s ease;
}
.contract-complete-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
}
.contract-complete-btn:disabled {
background: #e2e8f0;
color: #64748b;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* 动画 */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.contract-content {
max-width: 100%;
padding: 10px 0;
}
.contract-iframe-section {
padding: 15px;
}
.iframe-container {
height: 400px; /* 移动端调整iframe高度 */
}
.iframe-actions {
flex-direction: column;
gap: 12px;
}
.contract-complete-btn {
min-width: 100%;
}
.card-header {
flex-direction: column;
text-align: center;
gap: 12px;
}
.header-icon {
width: 40px;
height: 40px;
font-size: 20px;
}
.header-title {
font-size: 20px;
}
}
/* 大屏幕优化 */
@media (min-width: 1200px) {
.iframe-container {
height: 700px; /* 大屏幕增加iframe高度 */
}
}
</style>

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>

View File

@@ -0,0 +1,296 @@
<template>
<el-card class="step-card">
<template #header>
<div class="card-header">
<div class="header-icon">
<el-icon class="text-green-600">
<ShieldCheckIcon />
</el-icon>
</div>
<div class="header-content">
<h2 class="header-title">企业认证</h2>
<p class="header-subtitle">请完成企业认证流程</p>
</div>
</div>
</template>
<div class="verify-content">
<!-- 企业认证iframe -->
<div class="esign-iframe-section">
<div class="iframe-container">
<iframe
:src="authUrl"
frameborder="0"
class="esign-iframe"
title="企业认证"
@load="handleIframeLoad"
></iframe>
<div v-if="iframeLoading" class="iframe-loading">
<div class="loading-spinner"></div>
<p>正在加载企业认证页面...</p>
</div>
</div>
</div>
</div>
</el-card>
</template>
<script setup>
import {
CheckIcon,
ShieldCheckIcon
} from '@heroicons/vue/24/outline'
import { ElMessage } from 'element-plus'
const props = defineProps({
enterpriseData: {
type: Object,
required: true
},
authUrl: {
type: String,
default: ''
}
})
const emit = defineEmits(['submit'])
// 企业认证状态
const esignCompleted = ref(false)
const iframeLoading = ref(true)
// 企业认证完成回调
const handleEsignComplete = async () => {
try {
esignCompleted.value = true
ElMessage.success('企业认证完成')
emit('submit')
} catch (error) {
console.error('企业认证失败:', error)
ElMessage.error('企业认证失败,请重试')
}
}
// 刷新iframe
const refreshIframe = () => {
const iframe = document.querySelector('.esign-iframe')
if (iframe) {
iframe.src = iframe.src // 刷新iframe
}
}
// iframe加载完成回调
const handleIframeLoad = () => {
iframeLoading.value = false
}
</script>
<style scoped>
.step-card {
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
border: none;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
/* 卡片头部 */
.card-header {
display: flex;
align-items: center;
gap: 16px;
padding: 8px 0;
}
.header-icon {
width: 48px;
height: 48px;
border-radius: 12px;
background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.header-content {
flex: 1;
}
.header-title {
font-size: 24px;
font-weight: 700;
color: #1e293b;
margin: 0 0 4px 0;
}
.header-subtitle {
font-size: 14px;
color: #64748b;
margin: 0;
font-weight: 500;
}
/* 认证内容 */
.verify-content {
max-width: 100%;
margin: 0 auto;
padding: 20px 0;
}
/* 企业认证iframe */
.esign-iframe-section {
padding: 20px;
}
.iframe-container {
position: relative;
width: 100%;
height: 600px; /* 调整iframe高度 */
border-radius: 12px;
overflow: hidden;
border: 1px solid #e2e8f0;
background: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.esign-iframe {
width: 100%;
height: 100%;
border: none;
}
.iframe-loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
border-radius: 12px;
z-index: 10;
}
.iframe-loading .loading-spinner {
width: 48px;
height: 48px;
border: 4px solid #e2e8f0;
border-top: 4px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.iframe-loading p {
font-size: 16px;
color: #64748b;
font-weight: 500;
}
.iframe-actions {
display: flex;
justify-content: center;
gap: 16px;
margin-top: 24px;
}
.esign-complete-btn {
min-width: 180px;
border-radius: 12px;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border: none;
font-weight: 500;
transition: all 0.3s ease;
}
.esign-complete-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}
.esign-complete-btn:disabled {
background: #e2e8f0;
color: #64748b;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.refresh-btn {
min-width: 120px;
border-radius: 12px;
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
border: none;
font-weight: 500;
transition: all 0.3s ease;
}
.refresh-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
/* 动画 */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.verify-content {
max-width: 100%;
padding: 10px 0;
}
.esign-iframe-section {
padding: 15px;
}
.iframe-container {
height: 400px; /* 移动端调整iframe高度 */
}
.iframe-actions {
flex-direction: column;
gap: 12px;
}
.esign-complete-btn,
.refresh-btn {
min-width: 100%;
}
.card-header {
flex-direction: column;
text-align: center;
gap: 12px;
}
.header-icon {
width: 40px;
height: 40px;
font-size: 20px;
}
.header-title {
font-size: 20px;
}
}
/* 大屏幕优化 */
@media (min-width: 1200px) {
.iframe-container {
height: 700px; /* 大屏幕增加iframe高度 */
}
}
</style>

View File

@@ -0,0 +1,730 @@
<template>
<div class="certification-page">
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<div class="loading-content">
<div class="loading-spinner"></div>
<p class="loading-text">正在加载认证信息...</p>
</div>
</div>
<!-- 认证步骤条 -->
<div v-if="!loading && currentStep !== 'completed'" class="steps-container">
<div class="steps-header">
<h3 class="steps-title">企业入驻流程</h3>
<p class="steps-subtitle">完成企业入驻开启更多API服务功能</p>
</div>
<!-- 开发模式步骤控制器 -->
<div v-if="isDevelopment" class="dev-controller">
<div class="dev-controller-content">
<div class="dev-controller-label">
<el-icon class="dev-icon"><CodeBracketIcon /></el-icon>
<span>开发模式 - 步骤控制器</span>
</div>
<div class="dev-controller-controls">
<el-select
v-model="devCurrentStep"
placeholder="选择步骤"
size="small"
class="dev-step-select"
@change="handleDevStepChange"
>
<el-option
v-for="step in certificationSteps"
:key="step.key"
:label="step.title"
:value="step.key"
/>
</el-select>
<el-button
type="primary"
size="small"
@click="switchToStep(devCurrentStep)"
:disabled="!devCurrentStep"
class="dev-switch-btn"
>
切换到该步骤
</el-button>
<el-button type="info" size="small" @click="resetToFirstStep" class="dev-reset-btn">
重置到第一步
</el-button>
</div>
</div>
</div>
<CustomSteps
:steps="certificationSteps"
:current-step="currentStep"
class="certification-steps"
/>
</div>
<!-- 认证内容区域 -->
<div v-if="!loading" class="certification-content">
<EnterpriseInfo
v-if="currentStep === 'enterprise_info'"
:form-data="enterpriseForm"
@submit="handleEnterpriseSubmit"
/>
<EnterpriseVerify
v-if="currentStep === 'enterprise_verify'"
:enterprise-data="enterpriseForm"
:auth-url="stepMeta.auth_url"
@submit="handleEnterpriseVerifySubmit"
/>
<div v-if="currentStep === 'contract_sign'">
<ContractPreview
v-if="certificationData?.status === 'enterprise_verified'"
:contract-url="stepMeta.ContractURL"
:loading="contractSignLoading"
@start-sign="handleStartContractSign"
/>
<ContractSign
v-else-if="certificationData?.status === 'contract_applied'"
:company-name="enterpriseForm.companyName"
:iframe-url="stepMeta.contract_sign_url"
/>
<ContractRejected
v-else-if="certificationData?.status === 'contract_rejected'"
:failure-message="certificationData?.failure_message"
@contact-service="handleContactService"
@go-back="handleGoBackFromRejected"
/>
<ContractExpired
v-else-if="certificationData?.status === 'contract_expired'"
:contract-created-at="
certificationData?.contract_info?.generated_at || certificationData?.contract_applied_at
"
@reapply-contract="handleReapplyContract"
@go-back="handleGoBackFromExpired"
/>
</div>
<CertificationComplete
v-if="currentStep === 'completed'"
:certification-data="certificationData"
@go-dashboard="goToDashboard"
@go-profile="goToProfile"
/>
</div>
</div>
</template>
<script setup>
import { certificationApi } from '@/api/index.js'
import CustomSteps from '@/components/common/CustomSteps.vue'
import { useUserStore } from '@/stores/user'
import {
BuildingOfficeIcon,
CheckCircleIcon,
CodeBracketIcon,
DocumentTextIcon,
UserIcon,
} from '@heroicons/vue/24/outline'
import { ElMessage } from 'element-plus'
import CertificationComplete from './components/CertificationComplete.vue'
import ContractExpired from './components/ContractExpired.vue'
import ContractPreview from './components/ContractPreview.vue'
import ContractRejected from './components/ContractRejected.vue'
import ContractSign from './components/ContractSign.vue'
import EnterpriseInfo from './components/EnterpriseInfo.vue'
import EnterpriseVerify from './components/EnterpriseVerify.vue'
const router = useRouter()
const userStore = useUserStore()
// 认证步骤配置
const certificationSteps = [
{
key: 'enterprise_info',
title: '填写企业信息',
description: '填写企业基本信息和法人信息',
icon: BuildingOfficeIcon,
},
{
key: 'enterprise_verify',
title: '企业认证',
description: '完成企业认证流程',
icon: UserIcon,
},
{
key: 'contract_sign',
title: '签署合同',
description: '签署认证合同',
icon: DocumentTextIcon,
},
{
key: 'completed',
title: '完成',
description: '认证流程完成',
icon: CheckCircleIcon,
},
]
// 认证状态数据
const certificationData = ref(null)
const loading = ref(false)
// 当前步骤状态
const currentStep = ref('enterprise_info')
const currentStepIndex = computed(() => {
return certificationSteps.findIndex((step) => step.key === currentStep.value)
})
// 步骤特定元数据
const stepMeta = ref({}) // 用于存储当前步骤的metadata
// 表单数据
const enterpriseForm = ref({
companyName: '',
unifiedSocialCode: '',
legalPersonName: '',
legalPersonID: '',
legalPersonPhone: '',
legalPersonCode: '',
enterpriseAddress: '',
enterpriseEmail: '',
})
// 开发模式控制
const isDevelopment = ref(false)
const devCurrentStep = ref('enterprise_info')
// 合同签署加载状态
const contractSignLoading = ref(false)
// 事件处理
const handleEnterpriseSubmit = async (formData) => {
try {
loading.value = true
// 字段映射
const payload = {
company_name: formData.companyName,
unified_social_code: formData.unifiedSocialCode,
legal_person_name: formData.legalPersonName,
legal_person_id: formData.legalPersonID,
legal_person_phone: formData.legalPersonPhone,
enterprise_address: formData.enterpriseAddress,
enterprise_email: formData.enterpriseEmail,
verification_code: formData.legalPersonCode,
}
await certificationApi.submitEnterpriseInfo(payload)
ElMessage.success('企业信息提交成功')
// 提交成功后刷新认证详情
await getCertificationDetails()
} catch (error) {
ElMessage.error(error?.message || '提交失败,请检查表单信息')
// 提交失败时不刷新认证详情,保持用户填写的信息
} finally {
loading.value = false
}
}
const handleEnterpriseVerifySubmit = async () => {
try {
ElMessage.success('企业认证成功')
currentStep.value = 'contract_sign'
} catch {
ElMessage.error('企业认证失败,请重试')
}
}
const handleStartContractSign = async () => {
try {
contractSignLoading.value = true
// 调用后端接口,发起合同签署
await certificationApi.applyContract()
ElMessage.success('合同签署流程已启动')
await getCertificationDetails()
} catch (error) {
ElMessage.error(error?.message || '发起签署失败,请稍后重试')
} finally {
contractSignLoading.value = false
}
}
const handleContactService = () => {
// 这里可以打开客服聊天窗口或跳转到客服页面
ElMessage.success('正在为您连接客服...')
// 可以添加具体的客服逻辑,比如打开聊天窗口
}
const handleGoBackFromRejected = () => {
// 从拒签状态返回,重置到企业认证状态
currentStep.value = 'enterprise_verify'
}
const handleGoBackFromExpired = () => {
// 从过期状态返回,重置到企业认证状态
currentStep.value = 'enterprise_verify'
}
const handleReapplyContract = async () => {
try {
contractSignLoading.value = true
// 重新申请合同签署
await certificationApi.applyContract()
ElMessage.success('合同重新申请成功')
await getCertificationDetails()
} catch (error) {
ElMessage.error(error?.message || '重新申请失败,请稍后重试')
} finally {
contractSignLoading.value = false
}
}
const handleDevStepChange = (value) => {
devCurrentStep.value = value
}
const switchToStep = (stepKey) => {
currentStep.value = stepKey
}
const resetToFirstStep = () => {
currentStep.value = 'enterprise_info'
}
// 跳转到控制台
const goToDashboard = () => {
router.push('/products')
}
// 跳转到账户中心
const goToProfile = () => {
router.push('/profile')
}
// 获取认证详情
const getCertificationDetails = async () => {
try {
loading.value = true
const res = await certificationApi.getCertificationDetails()
certificationData.value = res.data
console.log('s', res)
// 回显企业信息 - 修复字段路径问题
if (res.data && res.data.metadata?.enterprise_info) {
const enterpriseInfo = res.data.metadata.enterprise_info
// 只有当后端有数据时才覆盖,避免清空用户刚填写的信息
if (enterpriseInfo.company_name) {
enterpriseForm.value.companyName = enterpriseInfo.company_name
}
if (enterpriseInfo.unified_social_code) {
enterpriseForm.value.unifiedSocialCode = enterpriseInfo.unified_social_code
}
if (enterpriseInfo.legal_person_name) {
enterpriseForm.value.legalPersonName = enterpriseInfo.legal_person_name
}
if (enterpriseInfo.legal_person_id) {
enterpriseForm.value.legalPersonID = enterpriseInfo.legal_person_id
}
if (enterpriseInfo.legal_person_phone) {
enterpriseForm.value.legalPersonPhone = enterpriseInfo.legal_person_phone
}
if (enterpriseInfo.enterprise_address) {
enterpriseForm.value.enterpriseAddress = enterpriseInfo.enterprise_address
}
if (enterpriseInfo.enterprise_email) {
enterpriseForm.value.enterpriseEmail = enterpriseInfo.enterprise_email
}
}
// 步骤特定元数据
stepMeta.value = res.data?.metadata || {}
await setCurrentStepByStatus()
} catch (error) {
console.error('获取认证详情失败:', error)
ElMessage.error('获取认证详情失败')
} finally {
loading.value = false
}
}
// 根据认证状态设置当前步骤
const setCurrentStepByStatus = async () => {
if (!certificationData.value) {
currentStep.value = 'enterprise_info'
return
}
const previousStatus = currentStep.value
const newStatus = certificationData.value.status
switch (newStatus) {
case 'pending':
currentStep.value = 'enterprise_info'
break
case 'info_submitted':
currentStep.value = 'enterprise_verify'
break
case 'enterprise_verified':
case 'contract_applied':
case 'contract_rejected':
case 'contract_expired':
currentStep.value = 'contract_sign'
break
case 'contract_signed':
case 'completed':
currentStep.value = 'completed'
// 认证完成时重新获取用户信息
if (previousStatus !== 'completed') {
try {
await userStore.fetchUserProfile()
console.log('认证完成,已重新获取用户信息')
} catch (error) {
console.error('重新获取用户信息失败:', error)
}
}
break
case 'info_rejected':
currentStep.value = 'enterprise_info'
break
default:
currentStep.value = 'enterprise_info'
}
}
// 认证详情轮询用于iframe回调后确认状态变化
async function pollCertificationDetails(maxTries = 5) {
let tries = 0
let lastStatus = certificationData.value?.status
let changed = false
while (tries < maxTries && !changed) {
try {
// 不显示loading
const res = await certificationApi.getCertificationDetails()
const newStatus = res.data?.status
if (newStatus !== lastStatus) {
certificationData.value = res.data
// 回显企业信息 - 修复字段路径问题
if (res.data && res.data.metadata?.enterprise_info) {
const enterpriseInfo = res.data.metadata.enterprise_info
// 只有当后端有数据时才覆盖,避免清空用户刚填写的信息
if (enterpriseInfo.company_name) {
enterpriseForm.value.companyName = enterpriseInfo.company_name
}
if (enterpriseInfo.unified_social_code) {
enterpriseForm.value.unifiedSocialCode = enterpriseInfo.unified_social_code
}
if (enterpriseInfo.legal_person_name) {
enterpriseForm.value.legalPersonName = enterpriseInfo.legal_person_name
}
if (enterpriseInfo.legal_person_id) {
enterpriseForm.value.legalPersonID = enterpriseInfo.legal_person_id
}
if (enterpriseInfo.legal_person_phone) {
enterpriseForm.value.legalPersonPhone = enterpriseInfo.legal_person_phone
}
}
stepMeta.value = res.data?.metadata || {}
await setCurrentStepByStatus()
changed = true
break
}
} catch (error) {
// 可选:打印错误
console.warn('轮询认证详情失败:', error)
}
tries++
const delay = Math.min(tries, 5) * 1000 // 1s,2s,3s,4s,5s
await new Promise((resolve) => setTimeout(resolve, delay))
}
}
// Iframe 回调消息监听
const handledScenes = new Set()
async function handleIframeMessage(event) {
// 这里可以加安全校验event.origin === '你的主站点origin'
// 目前只打印消息,后续可补充具体逻辑
const { type, scene } = event.data || {}
if (type === 'CHECK_STATUS') {
if (handledScenes.has(scene)) {
console.log('该scene已处理忽略重复消息:', scene)
return
}
console.log('收到iframe消息:', event)
handledScenes.add(scene)
console.log('需要检查后端状态scene:', scene)
try {
if (scene === 'sign') {
// 合同签署场景,调用 confirmSign 接口
const response = await certificationApi.confirmSign()
console.log('confirmSign 接口返回:', response)
// 确认签署状态后,重新获取完整的认证详情
await getCertificationDetails()
} else {
// 企业认证场景,使用原有的轮询方式
pollCertificationDetails()
}
} catch (error) {
console.error('检查状态失败:', error)
ElMessage.error('检查状态失败,请稍后重试')
}
}
}
// 初始化
onMounted(() => {
window.addEventListener('message', handleIframeMessage)
getCertificationDetails()
})
onBeforeUnmount(() => {
window.removeEventListener('message', handleIframeMessage)
})
</script>
<style scoped>
.certification-page {
min-height: 100vh;
position: relative;
}
/* 加载状态 */
.loading-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
padding: 60px;
margin: 40px auto;
text-align: center;
min-height: 600px;
display: flex;
align-items: center;
justify-content: center;
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
}
.loading-spinner {
width: 48px;
height: 48px;
border: 4px solid #e2e8f0;
border-top: 4px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-text {
font-size: 16px;
color: #64748b;
font-weight: 500;
}
/* 步骤条容器 */
.steps-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
padding: 36px;
margin: 0 auto 32px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.steps-header {
text-align: center;
margin-bottom: 24px;
}
.steps-title {
font-size: 28px;
font-weight: 700;
color: #1e293b;
margin-bottom: 6px;
background: linear-gradient(135deg, #1e293b 0%, #475569 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.steps-subtitle {
font-size: 16px;
color: #64748b;
font-weight: 500;
}
.certification-steps {
margin-bottom: 0;
}
.certification-content {
min-height: 600px;
margin: 0 auto;
}
/* 开发模式控制器样式 */
.dev-controller {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
padding: 24px;
margin-bottom: 48px;
border: 1px solid rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
gap: 20px;
}
.dev-controller-content {
display: flex;
align-items: center;
gap: 20px;
}
.dev-controller-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
color: #1e293b;
}
.dev-controller-controls {
display: flex;
align-items: center;
gap: 10px;
}
.dev-step-select :deep(.el-input__inner) {
border-radius: 8px;
border-color: #e2e8f0;
background-color: #f8fafc;
}
.dev-step-select :deep(.el-input__inner:focus) {
border-color: #3b82f6;
box-shadow: 0 0 0 2px #3b82f6;
}
.dev-switch-btn :deep(.el-button--small) {
border-radius: 8px;
background-color: #3b82f6;
border-color: #3b82f6;
}
.dev-switch-btn :deep(.el-button--small:hover) {
background-color: #1d4ed8;
border-color: #1d4ed8;
}
.dev-reset-btn :deep(.el-button--small) {
border-radius: 8px;
background-color: #e2e8f0;
border-color: #e2e8f0;
color: #64748b;
}
.dev-reset-btn :deep(.el-button--small:hover) {
background-color: #cbd5e1;
border-color: #cbd5e1;
color: #475569;
}
.dev-icon {
width: 20px;
height: 20px;
color: #3b82f6;
}
/* 动画效果 */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.certification-page {
padding: 20px 16px;
}
.steps-container {
padding: 32px 24px;
margin-bottom: 24px;
}
.steps-title {
font-size: 24px;
}
.steps-subtitle {
font-size: 14px;
}
.certification-steps :deep(.el-step__icon) {
width: 40px;
height: 40px;
}
.certification-steps :deep(.el-step__title) {
font-size: 14px;
}
.certification-steps :deep(.el-step__description) {
font-size: 12px;
}
.loading-container {
margin: 20px 16px;
padding: 40px 24px;
}
.dev-controller {
flex-direction: column;
align-items: flex-start;
gap: 15px;
padding: 16px;
}
.dev-controller-content {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.dev-controller-label {
font-size: 14px;
}
.dev-controller-controls {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.dev-step-select :deep(.el-input__inner) {
width: 100%;
}
.dev-switch-btn,
.dev-reset-btn {
width: 100%;
}
}
@media (max-width: 480px) {
.certification-steps :deep(.el-step__icon) {
width: 36px;
height: 36px;
}
.certification-steps :deep(.el-step__icon.is-text) {
font-size: 16px;
}
}
</style>