From a49affbcf23ee5f0e7cbe27da42d348c87d21d15 Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Sun, 26 Jul 2026 23:14:54 +0800 Subject: [PATCH] f --- src/api/index.js | 16 +- .../admin/certification-reviews/index.vue | 59 +++- src/pages/admin/users/index.vue | 109 ++++++++ src/pages/certification/IframeCallback.vue | 39 ++- .../components/ContractPreview.vue | 260 ++++++++++-------- .../certification/components/ContractSign.vue | 87 +++++- .../components/PlatformSelect.vue | 155 +++++++++++ src/pages/certification/index.vue | 194 ++++++++++++- src/router/index.js | 5 + vite.config.js | 12 +- 10 files changed, 770 insertions(+), 166 deletions(-) create mode 100644 src/pages/certification/components/PlatformSelect.vue diff --git a/src/api/index.js b/src/api/index.js index 1faa175..a89cbc6 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -199,8 +199,11 @@ export const certificationApi = { // 申请合同签署 applyContract: () => request.post('/certifications/apply-contract'), - // 获取认证详情 - getCertificationDetails: () => request.get('/certifications/details'), + // 刷新签署 iframe 长链(法大大 EmbedURL 约 10 分钟/单次) + refreshContractSignUrl: () => request.post('/certifications/refresh-contract-sign-url'), + + // 刷新合同预览链接(法大大 get-preview-url) + refreshContractPreviewUrl: () => request.post('/certifications/refresh-contract-preview-url'), // 确认签署状态 confirmSign: (data) => request.post('/certifications/confirm-sign', data), @@ -208,6 +211,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: { @@ -229,6 +238,9 @@ export const certificationApi = { // 管理员代用户完成认证(暂不关联合同) adminCompleteWithoutContract: (data) => request.post('/certifications/admin/complete-without-contract', data), + // 管理员触发重新认证(回退到提交企业信息,保留钱包/API Key/历史记录) + adminResetForResignContract: (data) => request.post('/certifications/admin/reset-for-resign-contract', data), + // 管理端企业审核:列表(按状态机 certification_status 筛选)、详情、通过、拒绝、按用户变更状态 adminListSubmitRecords: (params) => request.get('/certifications/admin/submit-records', { params }), adminGetSubmitRecord: (id) => request.get(`/certifications/admin/submit-records/${id}`), diff --git a/src/pages/admin/certification-reviews/index.vue b/src/pages/admin/certification-reviews/index.vue index 14f341a..09c3691 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 @@ - + + + + @@ -745,6 +807,20 @@ const adminCertForm = reactive({ reason: '' }) +// 管理员重新认证 +const resignContractDialogVisible = ref(false) +const resignContractLoading = ref(false) +const resignContractFormRef = ref(null) +const resignContractForm = reactive({ + reason: '' +}) +const resignContractRules = { + reason: [ + { required: true, message: '请填写操作原因', trigger: 'blur' }, + { min: 2, max: 500, message: '原因长度需在 2-500 字之间', trigger: 'blur' } + ] +} + const adminCertRules = { company_name: [{ required: true, message: '请输入企业名称', trigger: 'blur' }], unified_social_code: [{ required: true, message: '请输入统一社会信用代码', trigger: 'blur' }], @@ -999,6 +1075,39 @@ const handleSubmitAdminCert = async () => { } } +// 打开重新认证弹窗 +const openResignContractDialog = () => { + if (!selectedUser.value?.is_certified) { + ElMessage.warning('仅已认证用户可重新认证') + return + } + resignContractForm.reason = '' + resignContractDialogVisible.value = true +} + +// 提交重新认证 +const handleSubmitResignContract = async () => { + if (!resignContractFormRef.value || !selectedUser.value) return + try { + await resignContractFormRef.value.validate() + resignContractLoading.value = true + await certificationApi.adminResetForResignContract({ + user_id: selectedUser.value.id, + reason: resignContractForm.reason + }) + ElMessage.success('已重置为重新认证,请通知用户前往企业入驻重新填写企业信息') + resignContractDialogVisible.value = false + userDialogVisible.value = false + await loadUsers() + await loadStats() + } catch (error) { + console.error('重新认证失败:', error) + ElMessage.error(error?.response?.data?.message || '重新认证失败') + } finally { + resignContractLoading.value = false + } +} + // 查看合同 const handleViewContract = (contract) => { if (contract.contract_file_url) { diff --git a/src/pages/certification/IframeCallback.vue b/src/pages/certification/IframeCallback.vue index 43d0d2f..5e1d533 100644 --- a/src/pages/certification/IframeCallback.vue +++ b/src/pages/certification/IframeCallback.vue @@ -15,13 +15,13 @@ diff --git a/src/pages/certification/components/ContractPreview.vue b/src/pages/certification/components/ContractPreview.vue index ad6ec8b..38bd095 100644 --- a/src/pages/certification/components/ContractPreview.vue +++ b/src/pages/certification/components/ContractPreview.vue @@ -14,10 +14,31 @@
-
+ +
+ -
+
-

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

+

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

+
+
+

{{ refreshError }}

+ 重新获取链接
@@ -36,29 +41,68 @@ diff --git a/src/pages/certification/index.vue b/src/pages/certification/index.vue index 514aea5..7ef4bc8 100644 --- a/src/pages/certification/index.vue +++ b/src/pages/certification/index.vue @@ -75,7 +75,12 @@ :certification-data="certificationData" :submit-time="manualReviewSubmitTime" :company-name="enterpriseForm.companyName" - @refresh="getCertificationDetails" + @refresh="() => getCertificationDetails({ silent: true })" + /> + + @@ -145,7 +150,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 = [ { @@ -160,6 +167,12 @@ const certificationSteps = [ description: '等待管理员审核企业信息', icon: ClockIcon, }, + { + key: 'platform_select', + title: '选择平台', + description: '选择签署平台(法大大)', + icon: DocumentTextIcon, + }, { key: 'enterprise_verify', title: '企业认证', @@ -224,6 +237,86 @@ const devCurrentStep = ref('enterprise_info') // 合同签署加载状态 const contractSignLoading = ref(false) +// 合同签署状态轮询:进入 contract_applied 后定时拉详情,触发后端 checkAndUpdateSignStatus +let signPollTimer = null +const SIGN_POLL_INTERVAL = 5000 +const signPolling = ref(false) +const autoSelectingPlatform = ref(false) + +const stopSignStatusPolling = () => { + if (signPollTimer) { + clearTimeout(signPollTimer) + signPollTimer = null + } + signPolling.value = false +} + +const startSignStatusPolling = () => { + if (signPollTimer) return + signPolling.value = true + + const tick = async () => { + const status = certificationData.value?.status + if (status !== 'contract_applied') { + stopSignStatusPolling() + return + } + try { + const res = await certificationApi.getCertificationDetails() + if (res?.data) { + certificationData.value = res.data + stepMeta.value = res.data?.metadata || {} + const newStatus = res.data?.status + 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) +} + +// 仅当可见平台为 1 个时可静默 select,否则展示 PlatformSelect +async function maybeGoPlatformSelectOrAuto() { + const meta = certificationData.value?.metadata || {} + if (!meta.need_select_platform) return false + + const platforms = meta.available_platforms || [] + if (platforms.length <= 1 && meta.auto_select_platform && !autoSelectingPlatform.value) { + autoSelectingPlatform.value = true + try { + const res = await certificationApi.selectSignPlatform({ + sign_platform: meta.auto_select_platform, + }) + if (res?.data) { + certificationData.value = res.data + stepMeta.value = res.data.metadata || {} + } + await getCertificationDetails({ silent: true }) + return true + } catch (e) { + ElMessage.error(e?.message || '选择签署平台失败') + } finally { + autoSelectingPlatform.value = false + } + } + + currentStep.value = 'platform_select' + return true +} + // 只补 enterprise_verify 所需 auth_url,不改动原有页面展示数据结构 // 轮询策略:最多 5 秒,每秒 1 次(最多 5 次) const pollAuthUrlOnly = async (maxTries = 5) => { @@ -282,6 +375,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('企业认证成功') @@ -294,10 +400,23 @@ 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() + if (embedUrl) { + stepMeta.value = { + ...(stepMeta.value || {}), + contract_sign_url: embedUrl, + } + } } catch (error) { ElMessage.error(error?.message || '发起签署失败,请稍后重试') } finally { @@ -357,10 +476,11 @@ 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) @@ -393,9 +513,14 @@ 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 } @@ -416,20 +541,46 @@ const setCurrentStepByStatus = async () => { currentStep.value = 'enterprise_info' break case 'info_pending_review': - currentStep.value = 'manual_review' + if (certificationData.value?.metadata?.need_select_platform) { + if (await maybeGoPlatformSelectOrAuto()) return + } 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 + ) { + if (await maybeGoPlatformSelectOrAuto()) return + } else { + currentStep.value = 'enterprise_verify' + } break case 'enterprise_verified': + if ( + !certificationData.value?.metadata?.esign_flow_id && + !certificationData.value?.metadata?.sign_task_ready + ) { + if (await maybeGoPlatformSelectOrAuto()) return + 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 { @@ -530,12 +681,35 @@ 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 () => { + if (route.query?.signCallback) { + try { + await certificationApi.confirmSign() + await getCertificationDetails() + } catch (e) { + console.warn('签署回调确认失败,将轮询状态', e) + pollCertificationDetails(8) + } + } + }) + + 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 7accde7..114ec63 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -365,6 +365,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: '/:pathMatch(.*)*', name: 'NotFound', diff --git a/vite.config.js b/vite.config.js index b35c619..4e210d9 100644 --- a/vite.config.js +++ b/vite.config.js @@ -125,12 +125,12 @@ export default defineConfig({ server: { proxy: { // // 本地开发时将 /api/v1 的请求代理到 8080 端口 - '/api/v1': { - target: 'http://localhost:8080', - changeOrigin: true, - // 保持路径不变 - rewrite: path => path, - }, + // '/api/v1': { + // target: 'http://localhost:8080', + // changeOrigin: true, + // // 保持路径不变 + // rewrite: path => path, + // }, // '/api/v1': { // target: 'http://localhost:8080', // changeOrigin: true,