addf
This commit is contained in:
@@ -178,6 +178,12 @@ export const certificationApi = {
|
|||||||
// 申请合同签署
|
// 申请合同签署
|
||||||
applyContract: () => request.post('/certifications/apply-contract'),
|
applyContract: () => request.post('/certifications/apply-contract'),
|
||||||
|
|
||||||
|
// 刷新签署 iframe 长链(法大大 EmbedURL 约 10 分钟/单次)
|
||||||
|
refreshContractSignUrl: () => request.post('/certifications/refresh-contract-sign-url'),
|
||||||
|
|
||||||
|
// 刷新合同预览链接(法大大 get-preview-url)
|
||||||
|
refreshContractPreviewUrl: () => request.post('/certifications/refresh-contract-preview-url'),
|
||||||
|
|
||||||
// 获取认证详情
|
// 获取认证详情
|
||||||
getCertificationDetails: () => request.get('/certifications/details'),
|
getCertificationDetails: () => request.get('/certifications/details'),
|
||||||
|
|
||||||
@@ -187,6 +193,12 @@ export const certificationApi = {
|
|||||||
// 确认认证状态
|
// 确认认证状态
|
||||||
confirmAuth: (data) => request.post('/certifications/confirm-auth', data),
|
confirmAuth: (data) => request.post('/certifications/confirm-auth', data),
|
||||||
|
|
||||||
|
// 可选签署平台列表
|
||||||
|
getSignPlatforms: () => request.get('/certifications/sign-platforms'),
|
||||||
|
|
||||||
|
// 选择签署平台并生成企业认证链接
|
||||||
|
selectSignPlatform: (data) => request.post('/certifications/select-sign-platform', data),
|
||||||
|
|
||||||
// OCR营业执照识别
|
// OCR营业执照识别
|
||||||
recognizeBusinessLicense: (formData) => request.post('/certifications/ocr/business-license', formData, {
|
recognizeBusinessLicense: (formData) => request.post('/certifications/ocr/business-license', formData, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="certification-reviews-page">
|
<div class="certification-reviews-page">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1 class="page-title">企业审核</h1>
|
<h1 class="page-title">企业审核</h1>
|
||||||
<p class="page-subtitle">审核用户提交的企业信息,通过后可进入企业认证流程</p>
|
<p class="page-subtitle">审核用户提交的企业信息;通过后由用户选择签署平台(e签宝/法大大)再继续认证</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
@@ -59,13 +59,18 @@
|
|||||||
<el-table-column prop="company_name" label="企业名称" min-width="180" show-overflow-tooltip />
|
<el-table-column prop="company_name" label="企业名称" min-width="180" show-overflow-tooltip />
|
||||||
<el-table-column prop="unified_social_code" label="统一社会信用代码" width="200" />
|
<el-table-column prop="unified_social_code" label="统一社会信用代码" width="200" />
|
||||||
<el-table-column prop="legal_person_name" label="法人姓名" width="100" />
|
<el-table-column prop="legal_person_name" label="法人姓名" width="100" />
|
||||||
<el-table-column prop="certification_status" label="认证状态" width="100">
|
<el-table-column prop="certification_status" label="认证状态" width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="statusTagType(row)" size="small">
|
<el-tag :type="statusTagType(row)" size="small">
|
||||||
{{ certificationStatusDisplay(row?.certification_status) }}
|
{{ certificationStatusDisplay(row) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="签署平台" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ signPlatformDisplay(row?.sign_platform) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="240" fixed="right">
|
<el-table-column label="操作" width="240" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click="openDetail(row.id)">查看详情</el-button>
|
<el-button link type="primary" @click="openDetail(row.id)">查看详情</el-button>
|
||||||
@@ -83,10 +88,13 @@
|
|||||||
<div class="mobile-card-header">
|
<div class="mobile-card-header">
|
||||||
<div class="mobile-company">{{ row.company_name || '-' }}</div>
|
<div class="mobile-company">{{ row.company_name || '-' }}</div>
|
||||||
<el-tag :type="statusTagType(row)" size="small">
|
<el-tag :type="statusTagType(row)" size="small">
|
||||||
{{ certificationStatusDisplay(row?.certification_status) }}
|
{{ certificationStatusDisplay(row) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="mobile-card-info"><span>提交时间:</span>{{ formatDate(row.submit_at) }}</div>
|
<div class="mobile-card-info"><span>提交时间:</span>{{ formatDate(row.submit_at) }}</div>
|
||||||
|
<div class="mobile-card-info">
|
||||||
|
<span>签署平台:</span>{{ signPlatformDisplay(row.sign_platform) }}
|
||||||
|
</div>
|
||||||
<div class="mobile-card-info"><span>法人:</span>{{ row.legal_person_name || '-' }}</div>
|
<div class="mobile-card-info"><span>法人:</span>{{ row.legal_person_name || '-' }}</div>
|
||||||
<div class="mobile-card-info">
|
<div class="mobile-card-info">
|
||||||
<span>手机号:</span>{{ row.legal_person_phone || '-' }}
|
<span>手机号:</span>{{ row.legal_person_phone || '-' }}
|
||||||
@@ -162,8 +170,11 @@
|
|||||||
<dt>认证状态</dt>
|
<dt>认证状态</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<el-tag :type="statusTagType(detail)" size="small">
|
<el-tag :type="statusTagType(detail)" size="small">
|
||||||
{{ certificationStatusDisplay(detail?.certification_status) }}
|
{{ certificationStatusDisplay(detail) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
|
<span v-if="detail?.sign_platform" class="detail-platform">
|
||||||
|
签署平台:{{ signPlatformDisplay(detail.sign_platform) }}
|
||||||
|
</span>
|
||||||
</dd>
|
</dd>
|
||||||
<template v-if="detail.failure_reason">
|
<template v-if="detail.failure_reason">
|
||||||
<dt>失败原因</dt>
|
<dt>失败原因</dt>
|
||||||
@@ -363,12 +374,34 @@ const CERTIFICATION_STATUS_LABELS = {
|
|||||||
completed: '已完成',
|
completed: '已完成',
|
||||||
}
|
}
|
||||||
|
|
||||||
function certificationStatusDisplay(certificationStatus) {
|
function signPlatformDisplay(platform) {
|
||||||
if (!certificationStatus) return '-'
|
if (!platform) return '-'
|
||||||
return CERTIFICATION_STATUS_LABELS[certificationStatus] || certificationStatus
|
if (platform === 'esign') return 'e签宝'
|
||||||
|
if (platform === 'fadada') return '法大大'
|
||||||
|
return platform
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 审核已通过但仍等用户选平台时,状态机仍是 info_pending_review,用审核字段辅助展示 */
|
||||||
|
function certificationStatusDisplay(row) {
|
||||||
|
if (!row) return '-'
|
||||||
|
if (
|
||||||
|
row.certification_status === 'info_pending_review' &&
|
||||||
|
row.manual_review_status === 'approved'
|
||||||
|
) {
|
||||||
|
return '待选平台'
|
||||||
|
}
|
||||||
|
const status = typeof row === 'string' ? row : row.certification_status
|
||||||
|
if (!status) return '-'
|
||||||
|
return CERTIFICATION_STATUS_LABELS[status] || status
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusTagType(row) {
|
function statusTagType(row) {
|
||||||
|
if (
|
||||||
|
row?.certification_status === 'info_pending_review' &&
|
||||||
|
row?.manual_review_status === 'approved'
|
||||||
|
) {
|
||||||
|
return 'success'
|
||||||
|
}
|
||||||
const status = row?.certification_status
|
const status = row?.certification_status
|
||||||
const m = {
|
const m = {
|
||||||
pending: 'info',
|
pending: 'info',
|
||||||
@@ -386,12 +419,14 @@ function statusTagType(row) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否展示通过/拒绝:须当前认证为待审核,且本条提交记录为「校验通过」(verified)。
|
* 是否展示通过/拒绝:须当前认证为待审核、人工审核尚未通过,且本条为「校验通过」(verified)。
|
||||||
* 历史失败记录 (failed) 与认证状态无关字段共用同一 certification_status,否则会误显按钮。
|
|
||||||
*/
|
*/
|
||||||
function canShowApproveReject(row) {
|
function canShowApproveReject(row) {
|
||||||
if (!row) return false
|
if (!row) return false
|
||||||
if (row.certification_status !== 'info_pending_review') return false
|
if (row.certification_status !== 'info_pending_review') return false
|
||||||
|
if (row.manual_review_status === 'approved' || row.manual_review_status === 'rejected') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return row.status === 'verified'
|
return row.status === 'verified'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,6 +701,11 @@ onBeforeUnmount(() => {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
.detail-platform {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
.detail-mono {
|
.detail-mono {
|
||||||
font-family: ui-monospace, monospace;
|
font-family: ui-monospace, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -15,13 +15,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
import { Check } from '@element-plus/icons-vue'
|
import { Check } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const scene = ref('auth') // auth: 企业认证, sign: 合同签署
|
const scene = ref('auth') // auth: 企业认证, sign: 合同签署
|
||||||
|
|
||||||
// 解析路由参数
|
const CERTIFICATION_PAGE = '/profile/certification'
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const paramScene = route.params.scene
|
const paramScene = route.params.scene
|
||||||
if (paramScene === 'sign') {
|
if (paramScene === 'sign') {
|
||||||
@@ -29,8 +29,37 @@ onMounted(() => {
|
|||||||
} else {
|
} else {
|
||||||
scene.value = 'auth'
|
scene.value = 'auth'
|
||||||
}
|
}
|
||||||
// 向父iframe发送消息
|
const platform = route.params.platform || ''
|
||||||
window.parent.postMessage({ type: 'CHECK_STATUS', scene: scene.value }, '*')
|
|
||||||
|
// 企业认证完成:跳出 iframe,顶层回到入驻页(法大大 redirect 常发生在 iframe 内)
|
||||||
|
if (scene.value === 'auth') {
|
||||||
|
const target = `${window.location.origin}${CERTIFICATION_PAGE}${window.location.search || ''}`
|
||||||
|
if (window.top && window.top !== window.self) {
|
||||||
|
window.top.location.href = target
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.location.replace(CERTIFICATION_PAGE + (window.location.search || ''))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 签署完成:同样顶层跳出到入驻页,由主页 confirmSign 推进到「完成」
|
||||||
|
if (scene.value === 'sign') {
|
||||||
|
const qs = new URLSearchParams(window.location.search || '')
|
||||||
|
qs.set('signCallback', '1')
|
||||||
|
if (platform) qs.set('platform', String(platform))
|
||||||
|
const target = `${window.location.origin}${CERTIFICATION_PAGE}?${qs.toString()}`
|
||||||
|
if (window.top && window.top !== window.self) {
|
||||||
|
window.top.location.href = target
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.location.replace(`${CERTIFICATION_PAGE}?${qs.toString()}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
window.parent.postMessage(
|
||||||
|
{ type: 'CHECK_STATUS', scene: scene.value, platform },
|
||||||
|
'*'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const successText = computed(() =>
|
const successText = computed(() =>
|
||||||
@@ -39,7 +68,7 @@ const successText = computed(() =>
|
|||||||
const descText = computed(() =>
|
const descText = computed(() =>
|
||||||
scene.value === 'sign'
|
scene.value === 'sign'
|
||||||
? '合同已签署,正在为您确认签署状态...'
|
? '合同已签署,正在为您确认签署状态...'
|
||||||
: '企业认证已完成,正在为您确认认证状态...'
|
: '企业认证已完成,正在跳转入驻页面...'
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,17 @@
|
|||||||
<el-icon class="mr-2">
|
<el-icon class="mr-2">
|
||||||
<DocumentTextIcon />
|
<DocumentTextIcon />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
查看合同
|
查看已签合同
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="contractUrl"
|
||||||
|
size="large"
|
||||||
|
tag="a"
|
||||||
|
:href="contractUrl"
|
||||||
|
target="_blank"
|
||||||
|
class="action-btn secondary-btn"
|
||||||
|
>
|
||||||
|
下载签署文档
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="large" @click="goToProfile" class="action-btn secondary-btn">
|
<el-button size="large" @click="goToProfile" class="action-btn secondary-btn">
|
||||||
<el-icon class="mr-2">
|
<el-icon class="mr-2">
|
||||||
|
|||||||
@@ -14,10 +14,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="contract-preview-section">
|
<div class="contract-preview-section">
|
||||||
<div v-if="contractUrl" class="pdf-viewer-wrapper">
|
<!-- 法大大:EUI 预览页 iframe(get-preview-url),不用 PDF 直链 -->
|
||||||
|
<div v-if="previewMode === 'iframe'" class="preview-iframe-wrapper">
|
||||||
|
<iframe
|
||||||
|
v-if="activePreviewUrl"
|
||||||
|
:src="activePreviewUrl"
|
||||||
|
class="preview-iframe"
|
||||||
|
title="合同预览"
|
||||||
|
frameborder="0"
|
||||||
|
@load="previewLoading = false"
|
||||||
|
/>
|
||||||
|
<div v-if="previewLoading || refreshing" class="preview-loading">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<p>{{ refreshing ? '正在获取预览...' : '正在加载预览...' }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="previewError" class="preview-error">
|
||||||
|
<p>{{ previewError }}</p>
|
||||||
|
<el-button type="primary" size="small" @click="loadPreview">重新获取预览</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- e签宝等:PDF 直链 -->
|
||||||
|
<div v-else-if="activePreviewUrl" class="pdf-viewer-wrapper">
|
||||||
<vue-pdf-embed
|
<vue-pdf-embed
|
||||||
ref="pdfRef"
|
ref="pdfRef"
|
||||||
:source="contractUrl"
|
:source="activePreviewUrl"
|
||||||
:page="page"
|
:page="page"
|
||||||
:scale="pdfScale"
|
:scale="pdfScale"
|
||||||
@loaded="handleLoaded"
|
@loaded="handleLoaded"
|
||||||
@@ -31,12 +52,18 @@
|
|||||||
<el-button size="large" @click="fullscreen = true"> 全屏预览 </el-button>
|
<el-button size="large" @click="fullscreen = true"> 全屏预览 </el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" @click="emit('start-sign')" :loading="props.loading">
|
|
||||||
|
<div v-else-if="!refreshing" class="preview-empty">
|
||||||
|
<p>暂无预览内容</p>
|
||||||
|
<el-button type="primary" size="small" @click="loadPreview">获取预览</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-button type="primary" @click="emit('start-sign')" :loading="props.loading" class="sign-btn">
|
||||||
<el-icon class="mr-2"><DocumentTextIcon /></el-icon>
|
<el-icon class="mr-2"><DocumentTextIcon /></el-icon>
|
||||||
开始签署
|
立即签署
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<!-- 极简全屏预览弹窗,无顶部栏 -->
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="fullscreen"
|
v-model="fullscreen"
|
||||||
fullscreen
|
fullscreen
|
||||||
@@ -49,7 +76,7 @@
|
|||||||
>
|
>
|
||||||
<div class="fullscreen-pdf-simple">
|
<div class="fullscreen-pdf-simple">
|
||||||
<vue-pdf-embed
|
<vue-pdf-embed
|
||||||
:source="contractUrl"
|
:source="activePreviewUrl"
|
||||||
:page="page"
|
:page="page"
|
||||||
:scale="fullscreenScale"
|
:scale="fullscreenScale"
|
||||||
class="fullscreen-pdf-embed"
|
class="fullscreen-pdf-embed"
|
||||||
@@ -66,16 +93,24 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
import { DocumentTextIcon } from '@heroicons/vue/24/outline'
|
import { DocumentTextIcon } from '@heroicons/vue/24/outline'
|
||||||
import VuePdfEmbed from 'vue-pdf-embed'
|
import VuePdfEmbed from 'vue-pdf-embed'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { certificationApi } from '@/api'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
contractUrl: String,
|
contractUrl: { type: String, default: '' },
|
||||||
loading: Boolean
|
loading: { type: Boolean, default: false },
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['start-sign'])
|
const emit = defineEmits(['start-sign'])
|
||||||
|
|
||||||
|
const activePreviewUrl = ref('')
|
||||||
|
const previewMode = ref('iframe')
|
||||||
|
const previewLoading = ref(true)
|
||||||
|
const refreshing = ref(false)
|
||||||
|
const previewError = ref('')
|
||||||
|
|
||||||
const pdfRef = ref(null)
|
const pdfRef = ref(null)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const pageCount = ref(0)
|
const pageCount = ref(0)
|
||||||
@@ -86,6 +121,41 @@ const fullscreenScale = ref(1.7)
|
|||||||
function handleLoaded(pdf) {
|
function handleLoaded(pdf) {
|
||||||
pageCount.value = pdf.numPages
|
pageCount.value = pdf.numPages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadPreview = async () => {
|
||||||
|
refreshing.value = true
|
||||||
|
previewError.value = ''
|
||||||
|
previewLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await certificationApi.refreshContractPreviewUrl()
|
||||||
|
const url = res?.data?.preview_url || ''
|
||||||
|
const mode = res?.data?.preview_mode || 'iframe'
|
||||||
|
if (!url) {
|
||||||
|
throw new Error('未获取到预览链接')
|
||||||
|
}
|
||||||
|
previewMode.value = mode
|
||||||
|
activePreviewUrl.value = url
|
||||||
|
} catch (e) {
|
||||||
|
previewError.value = e?.message || '获取预览失败'
|
||||||
|
// 兼容旧数据:仅当返回的是非法大大 PDF 直链时可回退
|
||||||
|
if (props.contractUrl && !String(props.contractUrl).includes('fadada.com')) {
|
||||||
|
previewMode.value = 'pdf'
|
||||||
|
activePreviewUrl.value = props.contractUrl
|
||||||
|
previewError.value = ''
|
||||||
|
} else {
|
||||||
|
ElMessage.error(previewError.value)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
refreshing.value = false
|
||||||
|
if (previewMode.value === 'pdf') {
|
||||||
|
previewLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadPreview()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -126,155 +196,102 @@ function handleLoaded(pdf) {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
.contract-preview-section {
|
.contract-preview-section {
|
||||||
padding: 32px 0;
|
display: flex;
|
||||||
text-align: center;
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
.preview-iframe-wrapper,
|
||||||
.pdf-viewer-wrapper {
|
.pdf-viewer-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 640px;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.preview-iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.preview-loading,
|
||||||
|
.preview-error,
|
||||||
|
.preview-empty {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 16px;
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
color: #64748b;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.preview-error {
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
.loading-spinner {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border: 3px solid #e2e8f0;
|
||||||
|
border-top-color: #3b82f6;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.pdf-embed {
|
.pdf-embed {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 600px;
|
height: calc(100% - 56px);
|
||||||
min-height: 600px;
|
overflow: auto;
|
||||||
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 {
|
.pdf-controls {
|
||||||
margin: 24px 0 0 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 24px;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 16px;
|
gap: 12px;
|
||||||
}
|
height: 56px;
|
||||||
.pdf-btn {
|
border-top: 1px solid #e2e8f0;
|
||||||
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 {
|
.pdf-page-info {
|
||||||
color: #64748b;
|
color: #475569;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
.contract-sign-btn {
|
.sign-btn {
|
||||||
min-width: 220px;
|
align-self: center;
|
||||||
height: 52px;
|
min-width: 180px;
|
||||||
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 {
|
.fullscreen-pdf-simple {
|
||||||
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
background: #0f172a;
|
||||||
justify-content: center;
|
|
||||||
background: #fafbfc;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
}
|
||||||
.fullscreen-pdf-embed {
|
.fullscreen-pdf-embed {
|
||||||
width: 100vw;
|
flex: 1;
|
||||||
max-width: 1000px;
|
|
||||||
height: calc(100vh - 116px);
|
|
||||||
border: none;
|
|
||||||
border-radius: 0;
|
|
||||||
background: #fff;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
box-shadow: none;
|
|
||||||
}
|
}
|
||||||
.fullscreen-pdf-controls {
|
.fullscreen-pdf-controls {
|
||||||
width: 100%;
|
|
||||||
height: 68px;
|
|
||||||
background-color: #e2e8f0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 24px;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 18px;
|
gap: 12px;
|
||||||
}
|
padding: 12px;
|
||||||
.fullscreen-close-btn {
|
background: #1e293b;
|
||||||
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;
|
color: #fff;
|
||||||
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.22);
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.preview-iframe-wrapper,
|
||||||
|
.pdf-viewer-wrapper {
|
||||||
|
height: 420px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -15,19 +15,24 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="contract-content">
|
<div class="contract-content">
|
||||||
<!-- 合同签署iframe -->
|
<!-- 合同签署iframe:必须使用长链 EmbedURL,短链不可嵌入 -->
|
||||||
<div class="contract-iframe-section">
|
<div class="contract-iframe-section">
|
||||||
<div class="iframe-container">
|
<div class="iframe-container">
|
||||||
<iframe
|
<iframe
|
||||||
:src="iframeUrl"
|
v-if="activeIframeUrl"
|
||||||
|
:src="activeIframeUrl"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
class="contract-iframe"
|
class="contract-iframe"
|
||||||
title="合同签署"
|
title="合同签署"
|
||||||
@load="handleIframeLoad"
|
@load="handleIframeLoad"
|
||||||
></iframe>
|
></iframe>
|
||||||
<div v-if="iframeLoading" class="iframe-loading">
|
<div v-if="iframeLoading || refreshing" class="iframe-loading">
|
||||||
<div class="loading-spinner"></div>
|
<div class="loading-spinner"></div>
|
||||||
<p>正在加载合同签署页面...</p>
|
<p>{{ refreshing ? '正在获取签署页面...' : '正在加载合同签署页面...' }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="refreshError" class="iframe-error">
|
||||||
|
<p>{{ refreshError }}</p>
|
||||||
|
<el-button type="primary" size="small" @click="refreshEmbedUrl">重新获取链接</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,29 +41,68 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import { ref, watch, onMounted } from 'vue'
|
||||||
DocumentTextIcon
|
import { DocumentTextIcon } from '@heroicons/vue/24/outline'
|
||||||
} from '@heroicons/vue/24/outline'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { certificationApi } from '@/api'
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
companyName: {
|
companyName: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: '',
|
||||||
},
|
},
|
||||||
iframeUrl: {
|
iframeUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: '',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 合同签署状态
|
const activeIframeUrl = ref('')
|
||||||
const contractCompleted = ref(false)
|
|
||||||
const iframeLoading = ref(true)
|
const iframeLoading = ref(true)
|
||||||
|
const refreshing = ref(false)
|
||||||
|
const refreshError = ref('')
|
||||||
|
|
||||||
// iframe加载完成回调
|
|
||||||
const handleIframeLoad = () => {
|
const handleIframeLoad = () => {
|
||||||
iframeLoading.value = false
|
iframeLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshEmbedUrl = async () => {
|
||||||
|
refreshing.value = true
|
||||||
|
refreshError.value = ''
|
||||||
|
iframeLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await certificationApi.refreshContractSignUrl()
|
||||||
|
const url = res?.data?.contract_sign_url || ''
|
||||||
|
if (!url) {
|
||||||
|
throw new Error('未获取到可嵌入的签署链接')
|
||||||
|
}
|
||||||
|
activeIframeUrl.value = url
|
||||||
|
} catch (e) {
|
||||||
|
refreshError.value = e?.message || '获取签署链接失败,请重试'
|
||||||
|
// 兼容:刷新失败时若父组件已有链接则暂用(可能是短链,法大大会失败)
|
||||||
|
if (!activeIframeUrl.value && props.iframeUrl) {
|
||||||
|
activeIframeUrl.value = props.iframeUrl
|
||||||
|
}
|
||||||
|
ElMessage.error(refreshError.value)
|
||||||
|
} finally {
|
||||||
|
refreshing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
refreshEmbedUrl()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.iframeUrl,
|
||||||
|
(url) => {
|
||||||
|
// 申请合同刚返回的长链可先展示,随后仍会以 refresh 为准
|
||||||
|
if (url && !activeIframeUrl.value) {
|
||||||
|
activeIframeUrl.value = url
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -168,6 +212,21 @@ const handleIframeLoad = () => {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.iframe-error {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
z-index: 2;
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
.iframe-actions {
|
.iframe-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ClockIcon } from '@heroicons/vue/24/outline'
|
import { ClockIcon } from '@heroicons/vue/24/outline'
|
||||||
|
import { onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
certificationData: {
|
certificationData: {
|
||||||
|
|||||||
152
src/pages/certification/components/PlatformSelect.vue
Normal file
152
src/pages/certification/components/PlatformSelect.vue
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<template>
|
||||||
|
<div class="platform-select">
|
||||||
|
<div class="platform-select__header">
|
||||||
|
<h2 class="platform-select__title">选择签署平台</h2>
|
||||||
|
<p class="platform-select__desc">
|
||||||
|
企业信息已审核通过,请选择企业认证与合同签署使用的平台。选定后不可更改。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="platform-select__grid">
|
||||||
|
<button
|
||||||
|
v-for="item in platforms"
|
||||||
|
:key="item.platform"
|
||||||
|
type="button"
|
||||||
|
class="platform-card"
|
||||||
|
:class="{ 'is-active': selected === item.platform, 'is-disabled': !item.available }"
|
||||||
|
:disabled="!item.available || submitting"
|
||||||
|
@click="selected = item.platform"
|
||||||
|
>
|
||||||
|
<span class="platform-card__name">{{ item.name }}</span>
|
||||||
|
<span class="platform-card__desc">{{ item.description }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="platform-select__actions">
|
||||||
|
<el-button type="primary" size="large" :loading="submitting" :disabled="!selected" @click="confirm">
|
||||||
|
确认并开始企业认证
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { certificationApi } from '@/api/index.js'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const emit = defineEmits(['selected'])
|
||||||
|
|
||||||
|
const platforms = ref([])
|
||||||
|
const selected = ref('')
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const res = await certificationApi.getSignPlatforms()
|
||||||
|
platforms.value = res?.data?.platforms || []
|
||||||
|
if (platforms.value.length === 1) {
|
||||||
|
selected.value = platforms.value[0].platform
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e?.message || '获取签署平台失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function confirm() {
|
||||||
|
if (!selected.value) return
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const res = await certificationApi.selectSignPlatform({ sign_platform: selected.value })
|
||||||
|
ElMessage.success('已选择签署平台')
|
||||||
|
emit('selected', res?.data)
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e?.message || '选择签署平台失败')
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.platform-select {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 32px 20px 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-select__header {
|
||||||
|
margin-bottom: 28px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-select__title {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-select__desc {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-select__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 24px 20px;
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-card:hover:not(.is-disabled) {
|
||||||
|
border-color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-card.is-active {
|
||||||
|
border-color: #2563eb;
|
||||||
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-card.is-disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-card__name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-card__desc {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-select__actions {
|
||||||
|
margin-top: 28px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.platform-select__grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -73,7 +73,12 @@
|
|||||||
:certification-data="certificationData"
|
:certification-data="certificationData"
|
||||||
:submit-time="manualReviewSubmitTime"
|
:submit-time="manualReviewSubmitTime"
|
||||||
:company-name="enterpriseForm.companyName"
|
:company-name="enterpriseForm.companyName"
|
||||||
@refresh="getCertificationDetails"
|
@refresh="() => getCertificationDetails({ silent: true })"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PlatformSelect
|
||||||
|
v-if="currentStep === 'platform_select'"
|
||||||
|
@selected="handlePlatformSelected"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EnterpriseVerify
|
<EnterpriseVerify
|
||||||
@@ -86,7 +91,7 @@
|
|||||||
<div v-if="currentStep === 'contract_sign'">
|
<div v-if="currentStep === 'contract_sign'">
|
||||||
<ContractPreview
|
<ContractPreview
|
||||||
v-if="certificationData?.status === 'enterprise_verified'"
|
v-if="certificationData?.status === 'enterprise_verified'"
|
||||||
:contract-url="stepMeta.ContractURL"
|
:contract-url="stepMeta.contract_url || stepMeta.ContractURL"
|
||||||
:loading="contractSignLoading"
|
:loading="contractSignLoading"
|
||||||
@start-sign="handleStartContractSign"
|
@start-sign="handleStartContractSign"
|
||||||
/>
|
/>
|
||||||
@@ -143,7 +148,9 @@ import ContractSign from './components/ContractSign.vue'
|
|||||||
import EnterpriseInfo from './components/EnterpriseInfo.vue'
|
import EnterpriseInfo from './components/EnterpriseInfo.vue'
|
||||||
import EnterpriseVerify from './components/EnterpriseVerify.vue'
|
import EnterpriseVerify from './components/EnterpriseVerify.vue'
|
||||||
import ManualReviewPending from './components/ManualReviewPending.vue'
|
import ManualReviewPending from './components/ManualReviewPending.vue'
|
||||||
|
import PlatformSelect from './components/PlatformSelect.vue'
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const certificationSteps = [
|
const certificationSteps = [
|
||||||
{
|
{
|
||||||
@@ -158,6 +165,12 @@ const certificationSteps = [
|
|||||||
description: '等待管理员审核企业信息',
|
description: '等待管理员审核企业信息',
|
||||||
icon: ClockIcon,
|
icon: ClockIcon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'platform_select',
|
||||||
|
title: '选择平台',
|
||||||
|
description: '选择 e签宝 或法大大',
|
||||||
|
icon: DocumentTextIcon,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'enterprise_verify',
|
key: 'enterprise_verify',
|
||||||
title: '企业认证',
|
title: '企业认证',
|
||||||
@@ -222,6 +235,58 @@ const devCurrentStep = ref('enterprise_info')
|
|||||||
// 合同签署加载状态
|
// 合同签署加载状态
|
||||||
const contractSignLoading = ref(false)
|
const contractSignLoading = ref(false)
|
||||||
|
|
||||||
|
// 合同签署状态轮询:进入 contract_applied 后定时拉详情,触发后端 checkAndUpdateSignStatus
|
||||||
|
// 状态一旦变为 contract_signed / completed 或失败状态立即停止
|
||||||
|
let signPollTimer = null
|
||||||
|
const SIGN_POLL_INTERVAL = 5000 // 5 秒/次
|
||||||
|
const signPolling = ref(false)
|
||||||
|
|
||||||
|
const stopSignStatusPolling = () => {
|
||||||
|
if (signPollTimer) {
|
||||||
|
clearTimeout(signPollTimer)
|
||||||
|
signPollTimer = null
|
||||||
|
}
|
||||||
|
signPolling.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const startSignStatusPolling = () => {
|
||||||
|
// 已在轮询则不重复启动
|
||||||
|
if (signPollTimer) return
|
||||||
|
signPolling.value = true
|
||||||
|
|
||||||
|
const tick = async () => {
|
||||||
|
// 兜底:状态已离开 contract_applied 则停止
|
||||||
|
const status = certificationData.value?.status
|
||||||
|
if (status !== 'contract_applied') {
|
||||||
|
stopSignStatusPolling()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 静默拉详情,GetCertification 内会自动调用后端 checkAndUpdateSignStatus
|
||||||
|
const res = await certificationApi.getCertificationDetails()
|
||||||
|
if (res?.data) {
|
||||||
|
certificationData.value = res.data
|
||||||
|
stepMeta.value = res.data?.metadata || {}
|
||||||
|
const newStatus = res.data?.status
|
||||||
|
// 签署完成或失败:交给 setCurrentStepByStatus 切页,并停止轮询
|
||||||
|
if (newStatus === 'contract_signed' || newStatus === 'completed' ||
|
||||||
|
newStatus === 'contract_rejected' || newStatus === 'contract_expired') {
|
||||||
|
stopSignStatusPolling()
|
||||||
|
await setCurrentStepByStatus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('合同签署状态轮询失败:', e)
|
||||||
|
}
|
||||||
|
// 还在轮询则排下一次
|
||||||
|
if (signPolling.value) {
|
||||||
|
signPollTimer = setTimeout(tick, SIGN_POLL_INTERVAL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
signPollTimer = setTimeout(tick, SIGN_POLL_INTERVAL)
|
||||||
|
}
|
||||||
|
|
||||||
// 只补 enterprise_verify 所需 auth_url,不改动原有页面展示数据结构
|
// 只补 enterprise_verify 所需 auth_url,不改动原有页面展示数据结构
|
||||||
// 轮询策略:最多 5 秒,每秒 1 次(最多 5 次)
|
// 轮询策略:最多 5 秒,每秒 1 次(最多 5 次)
|
||||||
const pollAuthUrlOnly = async (maxTries = 5) => {
|
const pollAuthUrlOnly = async (maxTries = 5) => {
|
||||||
@@ -280,6 +345,19 @@ const handleEnterpriseSubmit = async (payload) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handlePlatformSelected = async (data) => {
|
||||||
|
if (data) {
|
||||||
|
certificationData.value = data
|
||||||
|
stepMeta.value = data.metadata || {}
|
||||||
|
}
|
||||||
|
await getCertificationDetails()
|
||||||
|
if (stepMeta.value?.auth_url) {
|
||||||
|
currentStep.value = 'enterprise_verify'
|
||||||
|
} else if (certificationData.value) {
|
||||||
|
await setCurrentStepByStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleEnterpriseVerifySubmit = async () => {
|
const handleEnterpriseVerifySubmit = async () => {
|
||||||
try {
|
try {
|
||||||
ElMessage.success('企业认证成功')
|
ElMessage.success('企业认证成功')
|
||||||
@@ -292,10 +370,24 @@ const handleEnterpriseVerifySubmit = async () => {
|
|||||||
const handleStartContractSign = async () => {
|
const handleStartContractSign = async () => {
|
||||||
try {
|
try {
|
||||||
contractSignLoading.value = true
|
contractSignLoading.value = true
|
||||||
// 调用后端接口,发起合同签署
|
const res = await certificationApi.applyContract()
|
||||||
await certificationApi.applyContract()
|
const embedUrl = res?.data?.contract_sign_url
|
||||||
|
if (embedUrl) {
|
||||||
|
stepMeta.value = {
|
||||||
|
...(stepMeta.value || {}),
|
||||||
|
contract_sign_url: embedUrl,
|
||||||
|
contract_sign_short_url: res?.data?.contract_sign_short_url || '',
|
||||||
|
}
|
||||||
|
}
|
||||||
ElMessage.success('合同签署流程已启动')
|
ElMessage.success('合同签署流程已启动')
|
||||||
await getCertificationDetails()
|
await getCertificationDetails()
|
||||||
|
// details 库内可能是短链,保留刚申请到的 iframe 长链
|
||||||
|
if (embedUrl) {
|
||||||
|
stepMeta.value = {
|
||||||
|
...(stepMeta.value || {}),
|
||||||
|
contract_sign_url: embedUrl,
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error?.message || '发起签署失败,请稍后重试')
|
ElMessage.error(error?.message || '发起签署失败,请稍后重试')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -355,10 +447,12 @@ const goToProfile = () => {
|
|||||||
router.push('/profile')
|
router.push('/profile')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取认证详情
|
// 获取认证详情(silent=true 时不展示全页 loading,用于人工审核轮询)
|
||||||
const getCertificationDetails = async () => {
|
// 返回是否成功,便于调用方在失败时不要把步骤重置到「填写企业信息」
|
||||||
|
const getCertificationDetails = async (opts = {}) => {
|
||||||
|
const silent = opts?.silent === true
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
if (!silent) loading.value = true
|
||||||
const res = await certificationApi.getCertificationDetails()
|
const res = await certificationApi.getCertificationDetails()
|
||||||
certificationData.value = res.data
|
certificationData.value = res.data
|
||||||
console.log('s', res)
|
console.log('s', res)
|
||||||
@@ -391,9 +485,15 @@ const getCertificationDetails = async () => {
|
|||||||
// 步骤特定元数据
|
// 步骤特定元数据
|
||||||
stepMeta.value = res.data?.metadata || {}
|
stepMeta.value = res.data?.metadata || {}
|
||||||
await setCurrentStepByStatus()
|
await setCurrentStepByStatus()
|
||||||
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取认证详情失败:', error)
|
console.error('获取认证详情失败:', error)
|
||||||
ElMessage.error('获取认证详情失败')
|
ElMessage.error('获取认证详情失败')
|
||||||
|
// 已有数据时保持当前步骤,避免失败时默认落到「填写企业信息」
|
||||||
|
if (certificationData.value) {
|
||||||
|
await setCurrentStepByStatus()
|
||||||
|
}
|
||||||
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -414,20 +514,43 @@ const setCurrentStepByStatus = async () => {
|
|||||||
currentStep.value = 'enterprise_info'
|
currentStep.value = 'enterprise_info'
|
||||||
break
|
break
|
||||||
case 'info_pending_review':
|
case 'info_pending_review':
|
||||||
currentStep.value = 'manual_review'
|
if (certificationData.value?.metadata?.need_select_platform) {
|
||||||
|
currentStep.value = 'platform_select'
|
||||||
|
} else {
|
||||||
|
currentStep.value = 'manual_review'
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case 'info_submitted':
|
case 'info_submitted':
|
||||||
currentStep.value = 'enterprise_verify'
|
if (!certificationData.value?.metadata?.auth_url && certificationData.value?.metadata?.need_select_platform) {
|
||||||
|
currentStep.value = 'platform_select'
|
||||||
|
} else {
|
||||||
|
currentStep.value = 'enterprise_verify'
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case 'enterprise_verified':
|
case 'enterprise_verified':
|
||||||
|
// 无签署任务时不应进入签署合同(创建失败脏数据),留在选平台
|
||||||
|
if (!certificationData.value?.metadata?.esign_flow_id && !certificationData.value?.metadata?.sign_task_ready) {
|
||||||
|
currentStep.value = 'platform_select'
|
||||||
|
} else {
|
||||||
|
currentStep.value = 'contract_sign'
|
||||||
|
}
|
||||||
|
break
|
||||||
case 'contract_applied':
|
case 'contract_applied':
|
||||||
|
currentStep.value = 'contract_sign'
|
||||||
|
// 进入签署中:启动轮询,自动推进到完成/失败
|
||||||
|
startSignStatusPolling()
|
||||||
|
break
|
||||||
case 'contract_rejected':
|
case 'contract_rejected':
|
||||||
case 'contract_expired':
|
case 'contract_expired':
|
||||||
currentStep.value = 'contract_sign'
|
currentStep.value = 'contract_sign'
|
||||||
|
// 失败终态:停止轮询
|
||||||
|
stopSignStatusPolling()
|
||||||
break
|
break
|
||||||
case 'contract_signed':
|
case 'contract_signed':
|
||||||
case 'completed':
|
case 'completed':
|
||||||
currentStep.value = 'completed'
|
currentStep.value = 'completed'
|
||||||
|
// 完成:停止轮询
|
||||||
|
stopSignStatusPolling()
|
||||||
// 认证完成时重新获取用户信息
|
// 认证完成时重新获取用户信息
|
||||||
if (previousStatus !== 'completed') {
|
if (previousStatus !== 'completed') {
|
||||||
try {
|
try {
|
||||||
@@ -528,12 +651,37 @@ async function handleIframeMessage(event) {
|
|||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 若法大大等第三方在 iframe 内回跳到本页,立即顶层打开入驻页
|
||||||
|
if (window.top && window.top !== window.self) {
|
||||||
|
const target = `${window.location.origin}/profile/certification${window.location.search || ''}`
|
||||||
|
window.top.location.href = target
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('message', handleIframeMessage)
|
window.addEventListener('message', handleIframeMessage)
|
||||||
getCertificationDetails()
|
getCertificationDetails().then(async () => {
|
||||||
|
// 签署完成顶层回跳:推进状态到 completed
|
||||||
|
if (route.query?.signCallback) {
|
||||||
|
try {
|
||||||
|
await certificationApi.confirmSign()
|
||||||
|
await getCertificationDetails()
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('签署回调确认失败,将轮询状态', e)
|
||||||
|
pollCertificationDetails(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 从法大大 redirect 带回的 query(authResult / clientCorpId 等)时主动同步状态
|
||||||
|
const q = route.query || {}
|
||||||
|
if (q.authResult || q.openCorpId || q.clientCorpId) {
|
||||||
|
pollCertificationDetails(8)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('message', handleIframeMessage)
|
window.removeEventListener('message', handleIframeMessage)
|
||||||
|
stopSignStatusPolling()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -320,6 +320,11 @@ const routes = [
|
|||||||
component: () => import('@/pages/certification/IframeCallback.vue'),
|
component: () => import('@/pages/certification/IframeCallback.vue'),
|
||||||
meta: { requiresAuth: false },
|
meta: { requiresAuth: false },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/certification/callback/:platform/:scene',
|
||||||
|
component: () => import('@/pages/certification/IframeCallback.vue'),
|
||||||
|
meta: { requiresAuth: false },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/legal/:doc',
|
path: '/legal/:doc',
|
||||||
name: 'LegalReader',
|
name: 'LegalReader',
|
||||||
|
|||||||
Reference in New Issue
Block a user