From 480c46f15be37b9de64e8d8981a5fe7a1e3ee28e Mon Sep 17 00:00:00 2001 From: Mrxs <18278715334@163.com> Date: Tue, 21 Jul 2026 15:53:43 +0800 Subject: [PATCH] addf --- src/api/index.js | 12 + .../admin/certification-reviews/index.vue | 60 +++- src/pages/certification/IframeCallback.vue | 39 ++- .../components/CertificationComplete.vue | 12 +- .../components/ContractPreview.vue | 273 ++++++++++-------- .../certification/components/ContractSign.vue | 87 +++++- .../components/ManualReviewPending.vue | 1 + .../components/PlatformSelect.vue | 152 ++++++++++ src/pages/certification/index.vue | 168 ++++++++++- src/router/index.js | 5 + 10 files changed, 641 insertions(+), 168 deletions(-) create mode 100644 src/pages/certification/components/PlatformSelect.vue diff --git a/src/api/index.js b/src/api/index.js index 2f4b01e..4607858 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -178,6 +178,12 @@ export const certificationApi = { // 申请合同签署 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'), @@ -187,6 +193,12 @@ export const certificationApi = { // 确认认证状态 confirmAuth: (data) => request.post('/certifications/confirm-auth', data), + // 可选签署平台列表 + getSignPlatforms: () => request.get('/certifications/sign-platforms'), + + // 选择签署平台并生成企业认证链接 + selectSignPlatform: (data) => request.post('/certifications/select-sign-platform', data), + // OCR营业执照识别 recognizeBusinessLicense: (formData) => request.post('/certifications/ocr/business-license', formData, { headers: { diff --git a/src/pages/admin/certification-reviews/index.vue b/src/pages/admin/certification-reviews/index.vue index bfba7f4..08aaf41 100644 --- a/src/pages/admin/certification-reviews/index.vue +++ b/src/pages/admin/certification-reviews/index.vue @@ -2,7 +2,7 @@
@@ -59,13 +59,18 @@ - + + + +
-
+ +
+ -
+
-

正在加载合同签署页面...

+

{{ refreshing ? '正在获取签署页面...' : '正在加载合同签署页面...' }}

+
+
+

{{ refreshError }}

+ 重新获取链接
@@ -36,29 +41,68 @@ diff --git a/src/pages/certification/index.vue b/src/pages/certification/index.vue index 935999d..69a97b3 100644 --- a/src/pages/certification/index.vue +++ b/src/pages/certification/index.vue @@ -73,7 +73,12 @@ :certification-data="certificationData" :submit-time="manualReviewSubmitTime" :company-name="enterpriseForm.companyName" - @refresh="getCertificationDetails" + @refresh="() => getCertificationDetails({ silent: true })" + /> + + @@ -143,7 +148,9 @@ import ContractSign from './components/ContractSign.vue' import EnterpriseInfo from './components/EnterpriseInfo.vue' import EnterpriseVerify from './components/EnterpriseVerify.vue' import ManualReviewPending from './components/ManualReviewPending.vue' +import PlatformSelect from './components/PlatformSelect.vue' const router = useRouter() +const route = useRoute() const userStore = useUserStore() const certificationSteps = [ { @@ -158,6 +165,12 @@ const certificationSteps = [ description: '等待管理员审核企业信息', icon: ClockIcon, }, + { + key: 'platform_select', + title: '选择平台', + description: '选择 e签宝 或法大大', + icon: DocumentTextIcon, + }, { key: 'enterprise_verify', title: '企业认证', @@ -222,6 +235,58 @@ const devCurrentStep = ref('enterprise_info') // 合同签署加载状态 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,不改动原有页面展示数据结构 // 轮询策略:最多 5 秒,每秒 1 次(最多 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 () => { try { ElMessage.success('企业认证成功') @@ -292,10 +370,24 @@ const handleEnterpriseVerifySubmit = async () => { const handleStartContractSign = async () => { try { contractSignLoading.value = true - // 调用后端接口,发起合同签署 - await certificationApi.applyContract() + const res = 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('合同签署流程已启动') await getCertificationDetails() + // details 库内可能是短链,保留刚申请到的 iframe 长链 + if (embedUrl) { + stepMeta.value = { + ...(stepMeta.value || {}), + contract_sign_url: embedUrl, + } + } } catch (error) { ElMessage.error(error?.message || '发起签署失败,请稍后重试') } finally { @@ -355,10 +447,12 @@ const goToProfile = () => { router.push('/profile') } -// 获取认证详情 -const getCertificationDetails = async () => { +// 获取认证详情(silent=true 时不展示全页 loading,用于人工审核轮询) +// 返回是否成功,便于调用方在失败时不要把步骤重置到「填写企业信息」 +const getCertificationDetails = async (opts = {}) => { + const silent = opts?.silent === true try { - loading.value = true + if (!silent) loading.value = true const res = await certificationApi.getCertificationDetails() certificationData.value = res.data console.log('s', res) @@ -391,9 +485,15 @@ const getCertificationDetails = async () => { // 步骤特定元数据 stepMeta.value = res.data?.metadata || {} await setCurrentStepByStatus() + return true } catch (error) { console.error('获取认证详情失败:', error) ElMessage.error('获取认证详情失败') + // 已有数据时保持当前步骤,避免失败时默认落到「填写企业信息」 + if (certificationData.value) { + await setCurrentStepByStatus() + } + return false } finally { loading.value = false } @@ -414,20 +514,43 @@ const setCurrentStepByStatus = async () => { currentStep.value = 'enterprise_info' break 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 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 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': + currentStep.value = 'contract_sign' + // 进入签署中:启动轮询,自动推进到完成/失败 + startSignStatusPolling() + break case 'contract_rejected': case 'contract_expired': currentStep.value = 'contract_sign' + // 失败终态:停止轮询 + stopSignStatusPolling() break case 'contract_signed': case 'completed': currentStep.value = 'completed' + // 完成:停止轮询 + stopSignStatusPolling() // 认证完成时重新获取用户信息 if (previousStatus !== 'completed') { try { @@ -528,12 +651,37 @@ async function handleIframeMessage(event) { // 初始化 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) - 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(() => { window.removeEventListener('message', handleIframeMessage) + stopSignStatusPolling() }) diff --git a/src/router/index.js b/src/router/index.js index b3d60f8..911b5d4 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -320,6 +320,11 @@ const routes = [ component: () => import('@/pages/certification/IframeCallback.vue'), meta: { requiresAuth: false }, }, + { + path: '/certification/callback/:platform/:scene', + component: () => import('@/pages/certification/IframeCallback.vue'), + meta: { requiresAuth: false }, + }, { path: '/legal/:doc', name: 'LegalReader',