addf
This commit is contained in:
@@ -15,13 +15,13 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import { Check } from '@element-plus/icons-vue'
|
||||
|
||||
const route = useRoute()
|
||||
const scene = ref('auth') // auth: 企业认证, sign: 合同签署
|
||||
|
||||
// 解析路由参数
|
||||
const CERTIFICATION_PAGE = '/profile/certification'
|
||||
|
||||
onMounted(() => {
|
||||
const paramScene = route.params.scene
|
||||
if (paramScene === 'sign') {
|
||||
@@ -29,8 +29,37 @@ onMounted(() => {
|
||||
} else {
|
||||
scene.value = 'auth'
|
||||
}
|
||||
// 向父iframe发送消息
|
||||
window.parent.postMessage({ type: 'CHECK_STATUS', scene: scene.value }, '*')
|
||||
const platform = route.params.platform || ''
|
||||
|
||||
// 企业认证完成:跳出 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(() =>
|
||||
@@ -39,7 +68,7 @@ const successText = computed(() =>
|
||||
const descText = computed(() =>
|
||||
scene.value === 'sign'
|
||||
? '合同已签署,正在为您确认签署状态...'
|
||||
: '企业认证已完成,正在为您确认认证状态...'
|
||||
: '企业认证已完成,正在跳转入驻页面...'
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -64,7 +64,17 @@
|
||||
<el-icon class="mr-2">
|
||||
<DocumentTextIcon />
|
||||
</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 size="large" @click="goToProfile" class="action-btn secondary-btn">
|
||||
<el-icon class="mr-2">
|
||||
|
||||
@@ -14,10 +14,31 @@
|
||||
</div>
|
||||
</template>
|
||||
<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
|
||||
ref="pdfRef"
|
||||
:source="contractUrl"
|
||||
:source="activePreviewUrl"
|
||||
:page="page"
|
||||
:scale="pdfScale"
|
||||
@loaded="handleLoaded"
|
||||
@@ -31,12 +52,18 @@
|
||||
<el-button size="large" @click="fullscreen = true"> 全屏预览 </el-button>
|
||||
</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-button>
|
||||
</div>
|
||||
<!-- 极简全屏预览弹窗,无顶部栏 -->
|
||||
|
||||
<el-dialog
|
||||
v-model="fullscreen"
|
||||
fullscreen
|
||||
@@ -49,7 +76,7 @@
|
||||
>
|
||||
<div class="fullscreen-pdf-simple">
|
||||
<vue-pdf-embed
|
||||
:source="contractUrl"
|
||||
:source="activePreviewUrl"
|
||||
:page="page"
|
||||
:scale="fullscreenScale"
|
||||
class="fullscreen-pdf-embed"
|
||||
@@ -66,16 +93,24 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { DocumentTextIcon } from '@heroicons/vue/24/outline'
|
||||
import VuePdfEmbed from 'vue-pdf-embed'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { certificationApi } from '@/api'
|
||||
|
||||
const props = defineProps({
|
||||
contractUrl: String,
|
||||
loading: Boolean
|
||||
contractUrl: { type: String, default: '' },
|
||||
loading: { type: Boolean, default: false },
|
||||
})
|
||||
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 page = ref(1)
|
||||
const pageCount = ref(0)
|
||||
@@ -86,6 +121,41 @@ const fullscreenScale = ref(1.7)
|
||||
function handleLoaded(pdf) {
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
@@ -126,155 +196,102 @@ function handleLoaded(pdf) {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
.contract-preview-section {
|
||||
padding: 32px 0;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
align-items: stretch;
|
||||
}
|
||||
.preview-iframe-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;
|
||||
flex-direction: column;
|
||||
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 {
|
||||
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;
|
||||
height: calc(100% - 56px);
|
||||
overflow: auto;
|
||||
}
|
||||
.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);
|
||||
gap: 12px;
|
||||
height: 56px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
.pdf-page-info {
|
||||
color: #64748b;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #475569;
|
||||
font-size: 14px;
|
||||
}
|
||||
.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;
|
||||
.sign-btn {
|
||||
align-self: center;
|
||||
min-width: 180px;
|
||||
}
|
||||
.fullscreen-pdf-simple {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fafbfc;
|
||||
overflow: auto;
|
||||
background: #0f172a;
|
||||
}
|
||||
.fullscreen-pdf-embed {
|
||||
width: 100vw;
|
||||
max-width: 1000px;
|
||||
height: calc(100vh - 116px);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: #fff;
|
||||
flex: 1;
|
||||
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;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: #1e293b;
|
||||
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>
|
||||
|
||||
@@ -15,19 +15,24 @@
|
||||
</template>
|
||||
|
||||
<div class="contract-content">
|
||||
<!-- 合同签署iframe -->
|
||||
<!-- 合同签署iframe:必须使用长链 EmbedURL,短链不可嵌入 -->
|
||||
<div class="contract-iframe-section">
|
||||
<div class="iframe-container">
|
||||
<iframe
|
||||
:src="iframeUrl"
|
||||
v-if="activeIframeUrl"
|
||||
:src="activeIframeUrl"
|
||||
frameborder="0"
|
||||
class="contract-iframe"
|
||||
title="合同签署"
|
||||
@load="handleIframeLoad"
|
||||
></iframe>
|
||||
<div v-if="iframeLoading" class="iframe-loading">
|
||||
<div v-if="iframeLoading || refreshing" class="iframe-loading">
|
||||
<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>
|
||||
@@ -36,29 +41,68 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
DocumentTextIcon
|
||||
} from '@heroicons/vue/24/outline'
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { DocumentTextIcon } from '@heroicons/vue/24/outline'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { certificationApi } from '@/api'
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
companyName: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
iframeUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
// 合同签署状态
|
||||
const contractCompleted = ref(false)
|
||||
const activeIframeUrl = ref('')
|
||||
const iframeLoading = ref(true)
|
||||
const refreshing = ref(false)
|
||||
const refreshError = ref('')
|
||||
|
||||
// iframe加载完成回调
|
||||
const handleIframeLoad = () => {
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
@@ -168,6 +212,21 @@ const handleIframeLoad = () => {
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ClockIcon } from '@heroicons/vue/24/outline'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
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"
|
||||
:submit-time="manualReviewSubmitTime"
|
||||
:company-name="enterpriseForm.companyName"
|
||||
@refresh="getCertificationDetails"
|
||||
@refresh="() => getCertificationDetails({ silent: true })"
|
||||
/>
|
||||
|
||||
<PlatformSelect
|
||||
v-if="currentStep === 'platform_select'"
|
||||
@selected="handlePlatformSelected"
|
||||
/>
|
||||
|
||||
<EnterpriseVerify
|
||||
@@ -86,7 +91,7 @@
|
||||
<div v-if="currentStep === 'contract_sign'">
|
||||
<ContractPreview
|
||||
v-if="certificationData?.status === 'enterprise_verified'"
|
||||
:contract-url="stepMeta.ContractURL"
|
||||
:contract-url="stepMeta.contract_url || stepMeta.ContractURL"
|
||||
:loading="contractSignLoading"
|
||||
@start-sign="handleStartContractSign"
|
||||
/>
|
||||
@@ -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()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user