Merge branch 'main' of http://1.117.67.95:3000/ZhangRongHong/tyapi-frontend
This commit is contained in:
@@ -366,6 +366,7 @@
|
|||||||
"useThrottleFn": true,
|
"useThrottleFn": true,
|
||||||
"useThrottledRefHistory": true,
|
"useThrottledRefHistory": true,
|
||||||
"useTimeAgo": true,
|
"useTimeAgo": true,
|
||||||
|
"useTimeAgoIntl": true,
|
||||||
"useTimeout": true,
|
"useTimeout": true,
|
||||||
"useTimeoutFn": true,
|
"useTimeoutFn": true,
|
||||||
"useTimeoutPoll": true,
|
"useTimeoutPoll": true,
|
||||||
|
|||||||
1
auto-imports.d.ts
vendored
1
auto-imports.d.ts
vendored
@@ -817,6 +817,7 @@ declare module 'vue' {
|
|||||||
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
|
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
|
||||||
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
|
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
|
||||||
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
|
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
|
||||||
|
readonly useTimeAgoIntl: UnwrapRef<typeof import('@vueuse/core')['useTimeAgoIntl']>
|
||||||
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
|
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
|
||||||
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
|
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
|
||||||
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
|
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
|
||||||
|
|||||||
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -55,6 +55,7 @@ declare module 'vue' {
|
|||||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
ElRow: typeof import('element-plus/es')['ElRow']
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
|
ElSegmented: typeof import('element-plus/es')['ElSegmented']
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||||
|
|||||||
@@ -194,8 +194,27 @@ export const certificationApi = {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// 上传认证图片到七牛云(企业信息中的营业执照、办公场地、场景附件、授权代表身份证等)
|
||||||
|
uploadFile: (file) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
return request.post('/certifications/upload', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
// 管理员代用户完成认证(暂不关联合同)
|
// 管理员代用户完成认证(暂不关联合同)
|
||||||
adminCompleteWithoutContract: (data) => request.post('/certifications/admin/complete-without-contract', data)
|
adminCompleteWithoutContract: (data) => request.post('/certifications/admin/complete-without-contract', data),
|
||||||
|
|
||||||
|
// 管理端企业审核:列表(按状态机 certification_status 筛选)、详情、通过、拒绝、按用户变更状态
|
||||||
|
adminListSubmitRecords: (params) => request.get('/certifications/admin/submit-records', { params }),
|
||||||
|
adminGetSubmitRecord: (id) => request.get(`/certifications/admin/submit-records/${id}`),
|
||||||
|
adminApproveSubmitRecord: (id, data) => request.post(`/certifications/admin/submit-records/${id}/approve`, data || {}),
|
||||||
|
adminRejectSubmitRecord: (id, data) => request.post(`/certifications/admin/submit-records/${id}/reject`, data),
|
||||||
|
// 管理端按用户变更认证状态(以状态机为准:info_submitted=通过 / info_rejected=拒绝)
|
||||||
|
adminTransitionCertificationStatus: (data) => request.post('/certifications/admin/transition-status', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// API相关接口
|
// API相关接口
|
||||||
|
|||||||
@@ -659,3 +659,31 @@ export function adminGetTodayCertifiedEnterprises(params = {}) {
|
|||||||
params
|
params
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================ 管理员安全可视化接口 ================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可疑IP列表
|
||||||
|
* @param {Object} params - 查询参数
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
export function adminGetSuspiciousIPList(params = {}) {
|
||||||
|
return request({
|
||||||
|
url: '/admin/security/suspicious-ip/list',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可疑IP地球请求流
|
||||||
|
* @param {Object} params - 查询参数
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
export function adminGetSuspiciousIPGeoStream(params = {}) {
|
||||||
|
return request({
|
||||||
|
url: '/admin/security/suspicious-ip/geo-stream',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export const getUserAccessibleMenuItems = (userType = 'user') => {
|
|||||||
icon: Setting,
|
icon: Setting,
|
||||||
children: [
|
children: [
|
||||||
{ name: '系统统计', path: '/admin/statistics', icon: ChartBar },
|
{ name: '系统统计', path: '/admin/statistics', icon: ChartBar },
|
||||||
|
{ name: '企业审核', path: '/admin/certification-reviews', icon: ShieldCheck },
|
||||||
{ name: '产品管理', path: '/admin/products', icon: Cube },
|
{ name: '产品管理', path: '/admin/products', icon: Cube },
|
||||||
{ name: '用户管理', path: '/admin/users', icon: Users },
|
{ name: '用户管理', path: '/admin/users', icon: Users },
|
||||||
{ name: '分类管理', path: '/admin/categories', icon: Tag },
|
{ name: '分类管理', path: '/admin/categories', icon: Tag },
|
||||||
|
|||||||
582
src/pages/admin/certification-reviews/index.vue
Normal file
582
src/pages/admin/certification-reviews/index.vue
Normal file
@@ -0,0 +1,582 @@
|
|||||||
|
<template>
|
||||||
|
<div class="certification-reviews-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">企业审核</h1>
|
||||||
|
<p class="page-subtitle">审核用户提交的企业信息,通过后可进入企业认证流程</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-select
|
||||||
|
v-model="filterStatus"
|
||||||
|
placeholder="认证状态"
|
||||||
|
clearable
|
||||||
|
style="width: 140px"
|
||||||
|
@change="loadList"
|
||||||
|
>
|
||||||
|
<el-option label="全部" value="" />
|
||||||
|
<el-option label="待审核" value="info_pending_review" />
|
||||||
|
<el-option label="已通过" value="info_submitted" />
|
||||||
|
<el-option label="已拒绝" value="info_rejected" />
|
||||||
|
</el-select>
|
||||||
|
<el-input
|
||||||
|
v-model="filterCompanyName"
|
||||||
|
placeholder="企业名称"
|
||||||
|
clearable
|
||||||
|
style="width: 180px"
|
||||||
|
@keyup.enter="loadList"
|
||||||
|
/>
|
||||||
|
<el-input
|
||||||
|
v-model="filterLegalPersonPhone"
|
||||||
|
placeholder="法人手机号"
|
||||||
|
clearable
|
||||||
|
style="width: 140px"
|
||||||
|
@keyup.enter="loadList"
|
||||||
|
/>
|
||||||
|
<el-input
|
||||||
|
v-model="filterLegalPersonName"
|
||||||
|
placeholder="法人姓名"
|
||||||
|
clearable
|
||||||
|
style="width: 120px"
|
||||||
|
@keyup.enter="loadList"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" @click="loadList">查询</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="list"
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
class="reviews-table"
|
||||||
|
>
|
||||||
|
<el-table-column prop="submit_at" label="提交时间" width="170">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.submit_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="user_id" label="用户ID" width="280" 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="legal_person_name" label="法人姓名" width="100" />
|
||||||
|
<el-table-column prop="certification_status" label="认证状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="statusTagType(row)" size="small">
|
||||||
|
{{ certificationStatusDisplay(row?.certification_status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="240" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click="openDetail(row.id)">查看详情</el-button>
|
||||||
|
<template v-if="canShowApproveReject(row)">
|
||||||
|
<el-button link type="success" @click="handleApprove(row)">通过</el-button>
|
||||||
|
<el-button link type="danger" @click="handleReject(row)">拒绝</el-button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination-wrap">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="page"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
:total="total"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
@size-change="loadList"
|
||||||
|
@current-change="loadList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 详情抽屉 -->
|
||||||
|
<el-drawer
|
||||||
|
v-model="drawerVisible"
|
||||||
|
title="企业信息详情"
|
||||||
|
size="560"
|
||||||
|
direction="rtl"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span class="drawer-title">企业信息详情</span>
|
||||||
|
<div class="drawer-actions">
|
||||||
|
<template v-if="detail && canShowApproveReject(detail)">
|
||||||
|
<el-button type="success" size="small" @click="approveFromDrawer">通过</el-button>
|
||||||
|
<el-button type="danger" size="small" @click="rejectFromDrawer">拒绝</el-button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-if="detail" class="detail-content">
|
||||||
|
<section class="detail-section">
|
||||||
|
<h4 class="detail-section-title">基本信息</h4>
|
||||||
|
<dl class="detail-dl">
|
||||||
|
<dt>企业名称</dt>
|
||||||
|
<dd>{{ detail.company_name }}</dd>
|
||||||
|
<dt>统一社会信用代码</dt>
|
||||||
|
<dd class="detail-mono">{{ detail.unified_social_code }}</dd>
|
||||||
|
<dt>法人姓名</dt>
|
||||||
|
<dd>{{ detail.legal_person_name }}</dd>
|
||||||
|
<dt>法人身份证号</dt>
|
||||||
|
<dd class="detail-mono">{{ detail.legal_person_id }}</dd>
|
||||||
|
<dt>法人手机号</dt>
|
||||||
|
<dd>{{ detail.legal_person_phone }}</dd>
|
||||||
|
<dt>企业地址</dt>
|
||||||
|
<dd class="detail-long">{{ detail.enterprise_address || '-' }}</dd>
|
||||||
|
<dt>提交时间</dt>
|
||||||
|
<dd>{{ formatDate(detail.submit_at) }}</dd>
|
||||||
|
<dt>认证状态</dt>
|
||||||
|
<dd>
|
||||||
|
<el-tag :type="statusTagType(detail)" size="small">
|
||||||
|
{{ certificationStatusDisplay(detail?.certification_status) }}
|
||||||
|
</el-tag>
|
||||||
|
</dd>
|
||||||
|
<template v-if="detail.failure_reason">
|
||||||
|
<dt>失败原因</dt>
|
||||||
|
<dd class="detail-long detail-error">{{ detail.failure_reason }}</dd>
|
||||||
|
</template>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="detail-section">
|
||||||
|
<h4 class="detail-section-title">授权代表</h4>
|
||||||
|
<dl class="detail-dl">
|
||||||
|
<dt>姓名</dt>
|
||||||
|
<dd>{{ (detail.authorized_rep_name ?? detail.authorizedRepName) || '-' }}</dd>
|
||||||
|
<dt>身份证号</dt>
|
||||||
|
<dd class="detail-mono">{{ (detail.authorized_rep_id ?? detail.authorizedRepId) || '-' }}</dd>
|
||||||
|
<dt>手机号</dt>
|
||||||
|
<dd>{{ (detail.authorized_rep_phone ?? detail.authorizedRepPhone) || '-' }}</dd>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="detail-section">
|
||||||
|
<h4 class="detail-section-title">应用场景说明</h4>
|
||||||
|
<div class="detail-long-block">{{ detail.api_usage || '无' }}</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="image-section">
|
||||||
|
<h4>营业执照</h4>
|
||||||
|
<div v-if="detail.business_license_image_url" class="image-list">
|
||||||
|
<a :href="detail.business_license_image_url" target="_blank" rel="noopener" class="image-link">
|
||||||
|
<img v-if="isImageUrl(detail.business_license_image_url)" :src="detail.business_license_image_url" alt="营业执照" class="thumb" />
|
||||||
|
<span v-else>查看链接</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p v-else class="text-gray-500 text-sm">无</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="image-section">
|
||||||
|
<h4>办公场地照片</h4>
|
||||||
|
<div v-if="officePlaceUrls.length" class="image-list">
|
||||||
|
<a v-for="(url, i) in officePlaceUrls" :key="i" :href="url" target="_blank" rel="noopener" class="image-link">
|
||||||
|
<img v-if="isImageUrl(url)" :src="url" :alt="`场地${i + 1}`" class="thumb" />
|
||||||
|
<span v-else>链接{{ i + 1 }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p v-else class="text-gray-500 text-sm">无</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="image-section">
|
||||||
|
<h4>应用场景附件</h4>
|
||||||
|
<div v-if="scenarioUrls.length" class="image-list">
|
||||||
|
<a v-for="(url, i) in scenarioUrls" :key="i" :href="url" target="_blank" rel="noopener" class="image-link">
|
||||||
|
<img v-if="isImageUrl(url)" :src="url" :alt="`场景${i + 1}`" class="thumb" />
|
||||||
|
<span v-else>链接{{ i + 1 }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p v-else class="text-gray-500 text-sm">无</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="image-section">
|
||||||
|
<h4>授权代表身份证</h4>
|
||||||
|
<div v-if="authorizedRepIdUrls.length" class="image-list">
|
||||||
|
<a v-for="(url, i) in authorizedRepIdUrls" :key="i" :href="url" target="_blank" rel="noopener" class="image-link">
|
||||||
|
<img v-if="isImageUrl(url)" :src="url" :alt="i === 0 ? '人像面' : '国徽面'" class="thumb" />
|
||||||
|
<span v-else>{{ i === 0 ? '人像面' : '国徽面' }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p v-else class="text-gray-500 text-sm">无</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-drawer>
|
||||||
|
|
||||||
|
<!-- 通过弹窗 -->
|
||||||
|
<el-dialog v-model="approveDialogVisible" title="审核通过" width="400px">
|
||||||
|
<el-form label-width="80">
|
||||||
|
<el-form-item label="审核备注">
|
||||||
|
<el-input v-model="approveRemark" type="textarea" :rows="3" placeholder="选填" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="approveDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="success" :loading="actionLoading" @click="confirmApprove">确认通过</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 拒绝弹窗 -->
|
||||||
|
<el-dialog v-model="rejectDialogVisible" title="审核拒绝" width="400px">
|
||||||
|
<el-form label-width="80">
|
||||||
|
<el-form-item label="拒绝原因" required>
|
||||||
|
<el-input v-model="rejectRemark" type="textarea" :rows="3" placeholder="必填" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="rejectDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="danger" :loading="actionLoading" :disabled="!rejectRemark.trim()" @click="confirmReject">确认拒绝</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { certificationApi } from '@/api/index.js'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const list = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const filterStatus = ref('')
|
||||||
|
const filterCompanyName = ref('')
|
||||||
|
const filterLegalPersonPhone = ref('')
|
||||||
|
const filterLegalPersonName = ref('')
|
||||||
|
|
||||||
|
const drawerVisible = ref(false)
|
||||||
|
const detail = ref(null)
|
||||||
|
const approveDialogVisible = ref(false)
|
||||||
|
const rejectDialogVisible = ref(false)
|
||||||
|
const approveRemark = ref('')
|
||||||
|
const rejectRemark = ref('')
|
||||||
|
const actionLoading = ref(false)
|
||||||
|
const pendingRecordId = ref('')
|
||||||
|
const pendingUserId = ref('')
|
||||||
|
|
||||||
|
function formatDate(val) {
|
||||||
|
if (!val) return '-'
|
||||||
|
try {
|
||||||
|
const d = new Date(val)
|
||||||
|
return Number.isNaN(d.getTime()) ? val : d.toLocaleString('zh-CN')
|
||||||
|
} catch {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 以状态机为准:认证状态展示与是否可操作(全流程口径)
|
||||||
|
const CERTIFICATION_STATUS_LABELS = {
|
||||||
|
pending: '待认证',
|
||||||
|
info_pending_review: '待审核',
|
||||||
|
info_submitted: '已通过',
|
||||||
|
info_rejected: '已拒绝',
|
||||||
|
enterprise_verified: '已企业认证',
|
||||||
|
contract_applied: '已申请合同',
|
||||||
|
contract_signed: '已签署合同',
|
||||||
|
contract_rejected: '合同拒签',
|
||||||
|
contract_expired: '合同超时',
|
||||||
|
completed: '已完成'
|
||||||
|
}
|
||||||
|
|
||||||
|
function certificationStatusDisplay(certificationStatus) {
|
||||||
|
if (!certificationStatus) return '-'
|
||||||
|
return CERTIFICATION_STATUS_LABELS[certificationStatus] || certificationStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusTagType(row) {
|
||||||
|
const status = row?.certification_status
|
||||||
|
const m = {
|
||||||
|
pending: 'info',
|
||||||
|
info_pending_review: 'warning',
|
||||||
|
info_submitted: 'success',
|
||||||
|
info_rejected: 'danger',
|
||||||
|
enterprise_verified: 'success',
|
||||||
|
contract_applied: 'info',
|
||||||
|
contract_signed: 'success',
|
||||||
|
contract_rejected: 'danger',
|
||||||
|
contract_expired: 'warning',
|
||||||
|
completed: 'success'
|
||||||
|
}
|
||||||
|
return m[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
function canShowApproveReject(row) {
|
||||||
|
if (!row) return false
|
||||||
|
return row.certification_status === 'info_pending_review'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否可作为图片展示(含七牛云等无扩展名的 CDN URL)
|
||||||
|
function isImageUrl(url) {
|
||||||
|
if (!url || typeof url !== 'string') return false
|
||||||
|
if (url.startsWith('blob:') || url.startsWith('data:')) return true
|
||||||
|
if (url.startsWith('https://file.tianyuanapi.com')) return true
|
||||||
|
return /\.(jpe?g|png|webp|gif)(\?|$)/i.test(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const officePlaceUrls = computed(() => {
|
||||||
|
if (!detail.value?.office_place_image_urls) return []
|
||||||
|
try {
|
||||||
|
const v = detail.value.office_place_image_urls
|
||||||
|
return typeof v === 'string' ? JSON.parse(v) : v
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const scenarioUrls = computed(() => {
|
||||||
|
if (!detail.value?.scenario_attachment_urls) return []
|
||||||
|
try {
|
||||||
|
const v = detail.value.scenario_attachment_urls
|
||||||
|
return typeof v === 'string' ? JSON.parse(v) : v
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const authorizedRepIdUrls = computed(() => {
|
||||||
|
if (!detail.value?.authorized_rep_id_image_urls) return []
|
||||||
|
try {
|
||||||
|
const v = detail.value.authorized_rep_id_image_urls
|
||||||
|
return typeof v === 'string' ? JSON.parse(v) : v
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadList() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await certificationApi.adminListSubmitRecords({
|
||||||
|
page: page.value,
|
||||||
|
page_size: pageSize.value,
|
||||||
|
certification_status: filterStatus.value || undefined,
|
||||||
|
company_name: filterCompanyName.value || undefined,
|
||||||
|
legal_person_phone: filterLegalPersonPhone.value || undefined,
|
||||||
|
legal_person_name: filterLegalPersonName.value || undefined
|
||||||
|
})
|
||||||
|
const data = res?.data
|
||||||
|
list.value = data?.items ?? []
|
||||||
|
total.value = data?.total ?? 0
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e?.message || '加载列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openDetail(id) {
|
||||||
|
try {
|
||||||
|
const res = await certificationApi.adminGetSubmitRecord(id)
|
||||||
|
detail.value = res?.data ?? res
|
||||||
|
drawerVisible.value = true
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e?.message || '加载详情失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleApprove(row) {
|
||||||
|
pendingRecordId.value = row.id
|
||||||
|
pendingUserId.value = row.user_id
|
||||||
|
approveRemark.value = ''
|
||||||
|
approveDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function approveFromDrawer() {
|
||||||
|
if (!detail.value?.id) return
|
||||||
|
pendingRecordId.value = detail.value.id
|
||||||
|
pendingUserId.value = detail.value.user_id
|
||||||
|
approveRemark.value = ''
|
||||||
|
approveDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmApprove() {
|
||||||
|
if (!pendingUserId.value) return
|
||||||
|
actionLoading.value = true
|
||||||
|
try {
|
||||||
|
await certificationApi.adminTransitionCertificationStatus({
|
||||||
|
user_id: pendingUserId.value,
|
||||||
|
target_status: 'info_submitted',
|
||||||
|
remark: approveRemark.value || ''
|
||||||
|
})
|
||||||
|
ElMessage.success('已通过')
|
||||||
|
approveDialogVisible.value = false
|
||||||
|
drawerVisible.value = false
|
||||||
|
detail.value = null
|
||||||
|
pendingRecordId.value = ''
|
||||||
|
pendingUserId.value = ''
|
||||||
|
loadList()
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e?.message || '操作失败')
|
||||||
|
} finally {
|
||||||
|
actionLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReject(row) {
|
||||||
|
pendingRecordId.value = row.id
|
||||||
|
pendingUserId.value = row.user_id
|
||||||
|
rejectRemark.value = ''
|
||||||
|
rejectDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function rejectFromDrawer() {
|
||||||
|
if (!detail.value?.id) return
|
||||||
|
pendingRecordId.value = detail.value.id
|
||||||
|
pendingUserId.value = detail.value.user_id
|
||||||
|
rejectRemark.value = ''
|
||||||
|
rejectDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmReject() {
|
||||||
|
if (!pendingUserId.value || !rejectRemark.value?.trim()) return
|
||||||
|
actionLoading.value = true
|
||||||
|
try {
|
||||||
|
await certificationApi.adminTransitionCertificationStatus({
|
||||||
|
user_id: pendingUserId.value,
|
||||||
|
target_status: 'info_rejected',
|
||||||
|
remark: rejectRemark.value.trim()
|
||||||
|
})
|
||||||
|
ElMessage.success('已拒绝')
|
||||||
|
rejectDialogVisible.value = false
|
||||||
|
drawerVisible.value = false
|
||||||
|
detail.value = null
|
||||||
|
pendingRecordId.value = ''
|
||||||
|
pendingUserId.value = ''
|
||||||
|
loadList()
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e?.message || '操作失败')
|
||||||
|
} finally {
|
||||||
|
actionLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.certification-reviews-page {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.page-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #64748b;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.pagination-wrap {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.drawer-title {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
.drawer-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.detail-content {
|
||||||
|
padding-right: 12px;
|
||||||
|
max-height: calc(100vh - 60px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.detail-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.detail-section-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #475569;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
.detail-dl {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 110px 1fr;
|
||||||
|
gap: 8px 16px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.detail-dl dt {
|
||||||
|
margin: 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.detail-dl dd {
|
||||||
|
margin: 0;
|
||||||
|
color: #1e293b;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.detail-mono {
|
||||||
|
font-family: ui-monospace, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.detail-long {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.detail-long-block {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #334155;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
max-height: 160px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.detail-error {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
.image-section {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.image-section h4 {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #475569;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
.image-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.image-link {
|
||||||
|
display: block;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 100px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
.image-link .thumb {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="list-page-card">
|
<div class="list-page-card">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div class="text-sm text-gray-500">统计视图切换</div>
|
||||||
|
<el-segmented
|
||||||
|
v-model="viewMode"
|
||||||
|
:options="[
|
||||||
|
{ label: '统计仪表盘', value: 'dashboard' },
|
||||||
|
{ label: '请求流可视化', value: 'request-globe' }
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<RequestFlowGlobe v-if="viewMode === 'request-globe'" />
|
||||||
|
<template v-else>
|
||||||
<!-- 加载状态 -->
|
<!-- 加载状态 -->
|
||||||
<div v-if="loading" class="text-center py-8">
|
<div v-if="loading" class="text-center py-8">
|
||||||
<el-icon size="32" class="animate-spin">
|
<el-icon size="32" class="animate-spin">
|
||||||
@@ -466,6 +478,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -480,6 +493,7 @@ import {
|
|||||||
adminGetUserCallRanking,
|
adminGetUserCallRanking,
|
||||||
adminGetUserDomainStatistics
|
adminGetUserDomainStatistics
|
||||||
} from '@/api/statistics'
|
} from '@/api/statistics'
|
||||||
|
import RequestFlowGlobe from '@/pages/admin/statistics/components/RequestFlowGlobe.vue'
|
||||||
import DanmakuBar from '@/components/common/DanmakuBar.vue'
|
import DanmakuBar from '@/components/common/DanmakuBar.vue'
|
||||||
import { Check, Loading, Money, Refresh, TrendCharts, User } from '@element-plus/icons-vue'
|
import { Check, Loading, Money, Refresh, TrendCharts, User } from '@element-plus/icons-vue'
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
@@ -488,6 +502,7 @@ import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const viewMode = ref('dashboard')
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
|||||||
337
src/pages/admin/statistics/components/RequestFlowGlobe.vue
Normal file
337
src/pages/admin/statistics/components/RequestFlowGlobe.vue
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
<template>
|
||||||
|
<div class="request-flow-page">
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="datetimerange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始时间"
|
||||||
|
end-placeholder="结束时间"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
class="toolbar-item"
|
||||||
|
/>
|
||||||
|
<el-input-number v-model="topN" :min="10" :max="1000" :step="10" class="toolbar-item" />
|
||||||
|
<el-button type="primary" :loading="loading" @click="loadData">刷新请求流</el-button>
|
||||||
|
<el-button v-if="selectedIP || selectedPath" @click="clearFilter">清空筛选</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div ref="chartRef" class="chart"></div>
|
||||||
|
<div class="side-list">
|
||||||
|
<h4>TOP 可疑来源</h4>
|
||||||
|
<div v-if="rows.length === 0" class="empty">暂无数据</div>
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in rows.slice(0, 20)"
|
||||||
|
:key="`${item.ip}-${item.path}-${index}`"
|
||||||
|
class="row clickable"
|
||||||
|
@click="selectFlow(item)"
|
||||||
|
>
|
||||||
|
<div class="name">{{ item.from_name }} -> {{ item.path }}</div>
|
||||||
|
<div class="value">{{ item.value }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-card">
|
||||||
|
<div class="table-title">
|
||||||
|
可疑IP明细
|
||||||
|
<span v-if="selectedIP || selectedPath" class="hint">
|
||||||
|
(筛选:{{ selectedIP || '-' }} / {{ selectedPath || '-' }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<el-table :data="listRows" border size="small" height="360">
|
||||||
|
<el-table-column prop="ip" label="IP" width="150" />
|
||||||
|
<el-table-column prop="path" label="接口路径" min-width="240" />
|
||||||
|
<el-table-column prop="method" label="方法" width="90" />
|
||||||
|
<el-table-column prop="request_count" label="次数" width="90" />
|
||||||
|
<el-table-column prop="trigger_reason" label="触发原因" width="160" />
|
||||||
|
<el-table-column prop="created_at" label="时间" width="180" />
|
||||||
|
</el-table>
|
||||||
|
<div class="pager">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="page"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
:total="total"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
@current-change="loadList"
|
||||||
|
@size-change="handlePageSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { adminGetSuspiciousIPGeoStream, adminGetSuspiciousIPList } from '@/api/statistics'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const topN = ref(200)
|
||||||
|
const dateRange = ref([])
|
||||||
|
const rows = ref([])
|
||||||
|
const listRows = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(20)
|
||||||
|
const selectedIP = ref('')
|
||||||
|
const selectedPath = ref('')
|
||||||
|
const chartRef = ref(null)
|
||||||
|
let chart = null
|
||||||
|
|
||||||
|
const getDefaultRange = () => {
|
||||||
|
const now = new Date()
|
||||||
|
const start = new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
||||||
|
const format = (d) => {
|
||||||
|
const y = d.getFullYear()
|
||||||
|
const m = `${d.getMonth() + 1}`.padStart(2, '0')
|
||||||
|
const day = `${d.getDate()}`.padStart(2, '0')
|
||||||
|
const h = `${d.getHours()}`.padStart(2, '0')
|
||||||
|
const mi = `${d.getMinutes()}`.padStart(2, '0')
|
||||||
|
const s = `${d.getSeconds()}`.padStart(2, '0')
|
||||||
|
return `${y}-${m}-${day} ${h}:${mi}:${s}`
|
||||||
|
}
|
||||||
|
return [format(start), format(now)]
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderChart = () => {
|
||||||
|
if (!chartRef.value) return
|
||||||
|
if (!chart) chart = echarts.init(chartRef.value)
|
||||||
|
|
||||||
|
const lineData = rows.value.map(item => ({
|
||||||
|
coords: [
|
||||||
|
[item.from_lng, item.from_lat],
|
||||||
|
[item.to_lng, item.to_lat]
|
||||||
|
],
|
||||||
|
value: item.value
|
||||||
|
}))
|
||||||
|
|
||||||
|
const pointData = rows.value.map(item => ({
|
||||||
|
name: item.from_name,
|
||||||
|
value: [item.from_lng, item.from_lat, item.value]
|
||||||
|
}))
|
||||||
|
|
||||||
|
chart.setOption({
|
||||||
|
backgroundColor: '#040b1b',
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item'
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'value',
|
||||||
|
min: -180,
|
||||||
|
max: 180,
|
||||||
|
axisLabel: { color: '#6b7280' },
|
||||||
|
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.08)' } }
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
min: -90,
|
||||||
|
max: 90,
|
||||||
|
axisLabel: { color: '#6b7280' },
|
||||||
|
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.08)' } }
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '请求流',
|
||||||
|
type: 'lines',
|
||||||
|
coordinateSystem: 'cartesian2d',
|
||||||
|
zlevel: 2,
|
||||||
|
effect: {
|
||||||
|
show: true,
|
||||||
|
period: 4,
|
||||||
|
symbol: 'arrow',
|
||||||
|
symbolSize: 6
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
width: 1,
|
||||||
|
color: '#4cc9f0',
|
||||||
|
curveness: 0.2
|
||||||
|
},
|
||||||
|
data: lineData
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '来源点',
|
||||||
|
type: 'scatter',
|
||||||
|
coordinateSystem: 'cartesian2d',
|
||||||
|
symbolSize: (val) => Math.max(6, Math.min(18, (val[2] || 1) / 2)),
|
||||||
|
itemStyle: {
|
||||||
|
color: '#f72585'
|
||||||
|
},
|
||||||
|
data: pointData
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
chart.off('click')
|
||||||
|
chart.on('click', (params) => {
|
||||||
|
if (params.seriesName === '来源点' && params.dataIndex >= 0) {
|
||||||
|
const item = rows.value[params.dataIndex]
|
||||||
|
if (item) selectFlow(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectFlow = (item) => {
|
||||||
|
selectedIP.value = item.ip || ''
|
||||||
|
selectedPath.value = item.path || ''
|
||||||
|
page.value = 1
|
||||||
|
loadList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearFilter = () => {
|
||||||
|
selectedIP.value = ''
|
||||||
|
selectedPath.value = ''
|
||||||
|
page.value = 1
|
||||||
|
loadList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadList = async () => {
|
||||||
|
try {
|
||||||
|
if (!dateRange.value || dateRange.value.length !== 2) {
|
||||||
|
dateRange.value = getDefaultRange()
|
||||||
|
}
|
||||||
|
const res = await adminGetSuspiciousIPList({
|
||||||
|
page: page.value,
|
||||||
|
page_size: pageSize.value,
|
||||||
|
start_time: dateRange.value[0],
|
||||||
|
end_time: dateRange.value[1],
|
||||||
|
ip: selectedIP.value || undefined,
|
||||||
|
path: selectedPath.value || undefined
|
||||||
|
})
|
||||||
|
listRows.value = res.data?.items || []
|
||||||
|
total.value = res.data?.total || 0
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载可疑IP明细失败:', error)
|
||||||
|
ElMessage.error('加载可疑IP明细失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageSizeChange = (size) => {
|
||||||
|
pageSize.value = size
|
||||||
|
page.value = 1
|
||||||
|
loadList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
if (!dateRange.value || dateRange.value.length !== 2) {
|
||||||
|
dateRange.value = getDefaultRange()
|
||||||
|
}
|
||||||
|
const res = await adminGetSuspiciousIPGeoStream({
|
||||||
|
start_time: dateRange.value[0],
|
||||||
|
end_time: dateRange.value[1],
|
||||||
|
top_n: topN.value
|
||||||
|
})
|
||||||
|
rows.value = res.data || []
|
||||||
|
await nextTick()
|
||||||
|
renderChart()
|
||||||
|
loadList()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载请求流失败:', error)
|
||||||
|
ElMessage.error('加载请求流失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
dateRange.value = getDefaultRange()
|
||||||
|
loadData()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (chart) {
|
||||||
|
chart.dispose()
|
||||||
|
chart = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.request-flow-page {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.toolbar-item {
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 320px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.chart {
|
||||||
|
height: 620px;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.side-list {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
max-height: 620px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.clickable:hover {
|
||||||
|
background: #f9fafb;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
color: #374151;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
.table-card {
|
||||||
|
margin-top: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
.table-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
margin-left: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
.pager {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -181,17 +181,17 @@
|
|||||||
<span v-if="field.required" class="text-red-500 ml-1">*</span>
|
<span v-if="field.required" class="text-red-500 ml-1">*</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<!-- 图片上传字段(photo_data) -->
|
<!-- Base64字段上传(根据后端校验规则自动识别) -->
|
||||||
<div v-if="field.name === 'photo_data' || field.name === 'vlphoto_data' && field.type === 'textarea'" class="space-y-2">
|
<div v-if="isBase64UploadField(field)" class="space-y-2">
|
||||||
<div class="flex gap-2 mb-2">
|
<div class="flex gap-2 mb-2">
|
||||||
<el-upload
|
<el-upload
|
||||||
:auto-upload="false"
|
:auto-upload="false"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
accept="image/jpeg,image/jpg,image/png,image/bmp"
|
:accept="getUploadAcceptByField(field)"
|
||||||
:on-change="(file) => handleImageUpload(file, field.name)"
|
:on-change="(file) => handleImageUpload(file, field.name, field)"
|
||||||
class="flex-1">
|
class="flex-1">
|
||||||
<el-button type="primary" size="small">
|
<el-button type="primary" size="small">
|
||||||
<i class="el-icon-upload"></i> 上传图片(JPG/BMP/PNG)
|
<i class="el-icon-upload"></i> {{ getUploadButtonTextByField(field) }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
<el-button v-if="formData[field.name]" type="danger" size="small" @click="clearImageData(field.name)">
|
<el-button v-if="formData[field.name]" type="danger" size="small" @click="clearImageData(field.name)">
|
||||||
@@ -423,7 +423,7 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<!-- 解密加载状态 -->
|
<!-- 解密加载状态 -->
|
||||||
<div v-else-if="debugging && debugResult && debugResult.success && debugResult.response?.data?.data"
|
<div v-else-if="debugging && debugResult && debugResult.success && debugResult.response?.data?.data"
|
||||||
class="mt-3 pt-3 border-t border-gray-200">
|
class="mt-3 pt-3 border-t border-gray-200">
|
||||||
<div class="flex items-center gap-2 text-sm text-gray-500">
|
<div class="flex items-center gap-2 text-sm text-gray-500">
|
||||||
<svg class="animate-spin h-4 w-4 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
<svg class="animate-spin h-4 w-4 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
@@ -749,12 +749,12 @@ watch(
|
|||||||
if (newProductId === oldProductId) {
|
if (newProductId === oldProductId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果正在选择产品,不重复执行
|
// 如果正在选择产品,不重复执行
|
||||||
if (isSelectingProduct.value) {
|
if (isSelectingProduct.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newProductId && userProducts.value.length > 0) {
|
if (newProductId && userProducts.value.length > 0) {
|
||||||
await autoSelectProduct(newProductId)
|
await autoSelectProduct(newProductId)
|
||||||
} else if (!newProductId && userProducts.value.length > 0 && !selectedProduct.value) {
|
} else if (!newProductId && userProducts.value.length > 0 && !selectedProduct.value) {
|
||||||
@@ -771,13 +771,13 @@ const autoSelectProduct = async (productId) => {
|
|||||||
console.log('正在选择产品,跳过重复请求')
|
console.log('正在选择产品,跳过重复请求')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果已经选择了相同的产品,不重复选择
|
// 如果已经选择了相同的产品,不重复选择
|
||||||
if (lastSelectedProductId.value === productId && selectedProduct.value) {
|
if (lastSelectedProductId.value === productId && selectedProduct.value) {
|
||||||
console.log('产品已选择,跳过重复选择:', productId)
|
console.log('产品已选择,跳过重复选择:', productId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果用户产品列表为空,等待加载完成
|
// 如果用户产品列表为空,等待加载完成
|
||||||
if (!userProducts.value.length) {
|
if (!userProducts.value.length) {
|
||||||
console.log('等待用户产品列表加载完成...')
|
console.log('等待用户产品列表加载完成...')
|
||||||
@@ -883,13 +883,13 @@ const loadApiKeys = async () => {
|
|||||||
const selectProduct = async (product) => {
|
const selectProduct = async (product) => {
|
||||||
// 防止重复选择相同产品
|
// 防止重复选择相同产品
|
||||||
const productId = product.product_id || product.id
|
const productId = product.product_id || product.id
|
||||||
if (selectedProduct.value &&
|
if (selectedProduct.value &&
|
||||||
(selectedProduct.value.id === productId || selectedProduct.value.product_id === productId) &&
|
(selectedProduct.value.id === productId || selectedProduct.value.product_id === productId) &&
|
||||||
!isSelectingProduct.value) {
|
!isSelectingProduct.value) {
|
||||||
console.log('产品已选择,跳过重复加载:', productId)
|
console.log('产品已选择,跳过重复加载:', productId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保API密钥已经加载
|
// 确保API密钥已经加载
|
||||||
if (!debugForm.accessId || !debugForm.secretKey) {
|
if (!debugForm.accessId || !debugForm.secretKey) {
|
||||||
ElMessage.warning('正在加载API密钥,请稍候...')
|
ElMessage.warning('正在加载API密钥,请稍候...')
|
||||||
@@ -934,24 +934,51 @@ const loadFormConfig = async (apiCode) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理图片上传并转换为base64
|
const getFieldValidationText = (field) => {
|
||||||
const handleImageUpload = (file, fieldName) => {
|
return typeof field?.validation === 'string' ? field.validation : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBase64ImageOnlyField = (field) => {
|
||||||
|
return getFieldValidationText(field).includes('Base64图片')
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBase64UploadField = (field) => {
|
||||||
|
if (field?.type !== 'textarea') return false
|
||||||
|
const validationText = getFieldValidationText(field)
|
||||||
|
return validationText.includes('Base64图片') || validationText.includes('Base64编码') || validationText.toLowerCase().includes('base64')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUploadAcceptByField = (field) => {
|
||||||
|
if (isBase64ImageOnlyField(field)) {
|
||||||
|
return 'image/jpeg,image/jpg,image/png,image/bmp'
|
||||||
|
}
|
||||||
|
return 'image/jpeg,image/jpg,image/png,image/bmp,application/pdf,.pdf'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUploadButtonTextByField = (field) => {
|
||||||
|
return isBase64ImageOnlyField(field) ? '上传图片(JPG/BMP/PNG)' : '上传文件(JPG/BMP/PNG/PDF)'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理文件上传并转换为base64(支持按字段规则限制类型)
|
||||||
|
const handleImageUpload = (file, fieldName, field) => {
|
||||||
const fileObj = file.raw || file
|
const fileObj = file.raw || file
|
||||||
|
|
||||||
// 验证文件类型
|
// 验证文件类型
|
||||||
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/bmp']
|
const allowedTypes = isBase64ImageOnlyField(field)
|
||||||
|
? ['image/jpeg', 'image/jpg', 'image/png', 'image/bmp']
|
||||||
|
: ['image/jpeg', 'image/jpg', 'image/png', 'image/bmp', 'application/pdf']
|
||||||
if (!allowedTypes.includes(fileObj.type)) {
|
if (!allowedTypes.includes(fileObj.type)) {
|
||||||
ElMessage.error('只支持 JPG、BMP、PNG 格式的图片')
|
ElMessage.error(isBase64ImageOnlyField(field) ? '只支持 JPG、BMP、PNG 格式的图片' : '只支持 JPG、BMP、PNG、PDF 格式的文件')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证文件大小(限制为5MB)
|
// 验证文件大小(限制为5MB)
|
||||||
const maxSize = 5 * 1024 * 1024 // 5MB
|
const maxSize = 5 * 1024 * 1024 // 5MB
|
||||||
if (fileObj.size > maxSize) {
|
if (fileObj.size > maxSize) {
|
||||||
ElMessage.error('图片大小不能超过 5MB')
|
ElMessage.error('文件大小不能超过 5MB')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取文件并转换为base64
|
// 读取文件并转换为base64
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
@@ -959,13 +986,13 @@ const handleImageUpload = (file, fieldName) => {
|
|||||||
// 移除 data:image/xxx;base64, 前缀,只保留纯base64数据
|
// 移除 data:image/xxx;base64, 前缀,只保留纯base64数据
|
||||||
const base64Data = base64String.includes(',') ? base64String.split(',')[1] : base64String
|
const base64Data = base64String.includes(',') ? base64String.split(',')[1] : base64String
|
||||||
formData.value[fieldName] = base64Data
|
formData.value[fieldName] = base64Data
|
||||||
ElMessage.success('图片上传成功,已转换为base64')
|
ElMessage.success('文件上传成功,已转换为base64')
|
||||||
}
|
}
|
||||||
reader.onerror = () => {
|
reader.onerror = () => {
|
||||||
ElMessage.error('图片读取失败,请重试')
|
ElMessage.error('文件读取失败,请重试')
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(fileObj)
|
reader.readAsDataURL(fileObj)
|
||||||
|
|
||||||
return false // 阻止自动上传
|
return false // 阻止自动上传
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1142,7 +1169,7 @@ const convertFieldTypes = (data) => {
|
|||||||
if (!formFields.value || formFields.value.length === 0) {
|
if (!formFields.value || formFields.value.length === 0) {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
const processedData = { ...data }
|
const processedData = { ...data }
|
||||||
formFields.value.forEach(field => {
|
formFields.value.forEach(field => {
|
||||||
const value = processedData[field.name]
|
const value = processedData[field.name]
|
||||||
@@ -1150,7 +1177,7 @@ const convertFieldTypes = (data) => {
|
|||||||
if (value === '' || value === null || value === undefined) {
|
if (value === '' || value === null || value === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据字段类型进行转换
|
// 根据字段类型进行转换
|
||||||
if (field.type === 'number') {
|
if (field.type === 'number') {
|
||||||
// 将字符串转换为数字(整数)
|
// 将字符串转换为数字(整数)
|
||||||
@@ -1160,7 +1187,7 @@ const convertFieldTypes = (data) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return processedData
|
return processedData
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1197,7 +1224,7 @@ const encryptWithAES = async (data, secretKey) => {
|
|||||||
|
|
||||||
// 解析JSON字符串(如果是字符串)
|
// 解析JSON字符串(如果是字符串)
|
||||||
let parsedData = typeof data === 'string' ? JSON.parse(data) : data
|
let parsedData = typeof data === 'string' ? JSON.parse(data) : data
|
||||||
|
|
||||||
// 根据字段类型进行类型转换
|
// 根据字段类型进行类型转换
|
||||||
parsedData = convertFieldTypes(parsedData)
|
parsedData = convertFieldTypes(parsedData)
|
||||||
|
|
||||||
@@ -1282,7 +1309,7 @@ const handleDebug = async () => {
|
|||||||
debugResult.value = null
|
debugResult.value = null
|
||||||
decryptedData.value = null
|
decryptedData.value = null
|
||||||
await nextTick() // 确保DOM更新
|
await nextTick() // 确保DOM更新
|
||||||
|
|
||||||
const startTime = new Date()
|
const startTime = new Date()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
label-width="10em"
|
label-width="10em"
|
||||||
class="enterprise-form-content"
|
class="enterprise-form-content"
|
||||||
>
|
>
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h3 class="section-title">基本信息</h3>
|
<h3 class="section-title">基本信息</h3>
|
||||||
|
|
||||||
<!-- 企业名称和OCR识别区域 -->
|
<!-- 企业名称和OCR识别区域 -->
|
||||||
@@ -42,21 +42,7 @@
|
|||||||
<div class="ocr-compact">
|
<div class="ocr-compact">
|
||||||
<div class="ocr-header-compact">
|
<div class="ocr-header-compact">
|
||||||
<el-icon class="text-green-600"><DocumentIcon /></el-icon>
|
<el-icon class="text-green-600"><DocumentIcon /></el-icon>
|
||||||
<span class="ocr-title-compact">OCR识别</span>
|
<span class="ocr-title-compact">可进行OCR识别,请在下方上传营业执照</span>
|
||||||
<el-upload
|
|
||||||
ref="uploadRef"
|
|
||||||
:auto-upload="false"
|
|
||||||
:show-file-list="false"
|
|
||||||
:on-change="handleFileChange"
|
|
||||||
:before-upload="beforeUpload"
|
|
||||||
accept="image/jpeg,image/jpg,image/png,image/webp"
|
|
||||||
class="ocr-uploader-compact"
|
|
||||||
>
|
|
||||||
<el-button type="success" size="small" plain>
|
|
||||||
<el-icon><ArrowUpTrayIcon /></el-icon>
|
|
||||||
上传营业执照
|
|
||||||
</el-button>
|
|
||||||
</el-upload>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="ocrLoading" class="ocr-status-compact">
|
<div v-if="ocrLoading" class="ocr-status-compact">
|
||||||
<el-icon class="is-loading"><Loading /></el-icon>
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
@@ -84,6 +70,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
|
|
||||||
<el-row :gutter="16" class="mb-4">
|
<el-row :gutter="16" class="mb-4">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="法人姓名" prop="legalPersonName">
|
<el-form-item label="法人姓名" prop="legalPersonName">
|
||||||
@@ -96,21 +83,9 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="法人身份证号" prop="legalPersonID">
|
|
||||||
<el-input
|
|
||||||
v-model="form.legalPersonID"
|
|
||||||
placeholder="请输入法人身份证号"
|
|
||||||
clearable
|
|
||||||
size="default"
|
|
||||||
class="form-input"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<el-row :gutter="16" class="mb-4">
|
|
||||||
<el-col :span="24">
|
<el-col :span="12">
|
||||||
<el-form-item label="企业地址" prop="enterpriseAddress">
|
<el-form-item label="企业地址" prop="enterpriseAddress">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.enterpriseAddress"
|
v-model="form.enterpriseAddress"
|
||||||
@@ -122,8 +97,70 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
<!-- 营业执照图片上传(保留) -->
|
||||||
|
<el-row :gutter="16" class="mb-4">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="营业执照图片" prop="businessLicenseImageURL">
|
||||||
|
<el-upload
|
||||||
|
class="upload-area single-upload-area"
|
||||||
|
action="#"
|
||||||
|
list-type="picture-card"
|
||||||
|
:auto-upload="false"
|
||||||
|
:file-list="businessLicenseFileList"
|
||||||
|
:limit="1"
|
||||||
|
:on-change="handleBusinessLicenseChange"
|
||||||
|
:on-remove="handleBusinessLicenseRemove"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
>
|
||||||
|
<el-icon class="upload-icon"><ArrowUpTrayIcon /></el-icon>
|
||||||
|
<div class="el-upload__text">上传清晰可辨的营业执照图片</div>
|
||||||
|
</el-upload>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<!-- 暂时隐藏:办公场地图片上传 -->
|
||||||
|
<!-- <el-row :gutter="16" class="mb-4">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="办公场地照片">
|
||||||
|
<div class="text-xs mb-1 text-blue-500">
|
||||||
|
请在非 IE 浏览器下上传大小不超过 1M 的图片,最多 10 张,需体现门楣 LOGO、办公设备与工作人员。
|
||||||
|
</div>
|
||||||
|
<el-upload
|
||||||
|
ref="officePlaceUploadRef"
|
||||||
|
class="upload-area"
|
||||||
|
action="#"
|
||||||
|
list-type="picture-card"
|
||||||
|
:auto-upload="false"
|
||||||
|
v-model:file-list="officePlaceFileList"
|
||||||
|
accept="image/jpeg,image/jpg,image/png,image/webp"
|
||||||
|
multiple
|
||||||
|
:limit="10"
|
||||||
|
:on-change="handleOfficePlaceChange"
|
||||||
|
:on-remove="handleOfficePlaceRemove"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
>
|
||||||
|
<div class="upload-trigger-inner">
|
||||||
|
<el-icon class="upload-icon"><ArrowUpTrayIcon /></el-icon>
|
||||||
|
<div class="el-upload__text">上传办公场地环境照片</div>
|
||||||
|
</div>
|
||||||
|
</el-upload>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row> -->
|
||||||
|
|
||||||
|
|
||||||
<el-row :gutter="16" class="mb-4">
|
<el-row :gutter="16" class="mb-4">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="法人身份证号" prop="legalPersonID">
|
||||||
|
<el-input
|
||||||
|
v-model="form.legalPersonID"
|
||||||
|
placeholder="请输入法人身份证号"
|
||||||
|
clearable
|
||||||
|
size="default"
|
||||||
|
class="form-input"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="法人手机号" prop="legalPersonPhone">
|
<el-form-item label="法人手机号" prop="legalPersonPhone">
|
||||||
<el-input
|
<el-input
|
||||||
@@ -161,6 +198,137 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 暂时隐藏:授权代表信息(姓名、身份证号、手机号、授权代表身份证) -->
|
||||||
|
<!-- <h3 class="section-title">授权代表信息</h3>
|
||||||
|
<p class="section-desc">授权代表信息用于证明该人员已获得企业授权,请确保姓名、身份证号、手机号及身份证正反面照片真实有效。</p>
|
||||||
|
|
||||||
|
<el-row :gutter="16" class="mb-4">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="授权代表姓名" prop="authorizedRepName">
|
||||||
|
<el-input
|
||||||
|
v-model="form.authorizedRepName"
|
||||||
|
placeholder="请输入授权代表姓名"
|
||||||
|
clearable
|
||||||
|
size="default"
|
||||||
|
class="form-input"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="授权代表身份证号" prop="authorizedRepID">
|
||||||
|
<el-input
|
||||||
|
v-model="form.authorizedRepID"
|
||||||
|
placeholder="请输入授权代表身份证号"
|
||||||
|
clearable
|
||||||
|
size="default"
|
||||||
|
class="form-input"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="授权代表手机号" prop="authorizedRepPhone">
|
||||||
|
<el-input
|
||||||
|
v-model="form.authorizedRepPhone"
|
||||||
|
placeholder="请输入授权代表手机号"
|
||||||
|
clearable
|
||||||
|
size="default"
|
||||||
|
maxlength="11"
|
||||||
|
class="form-input"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="16" class="mb-4">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="身份证人像面" prop="authorizedRepIDImageURLs">
|
||||||
|
<el-upload
|
||||||
|
class="upload-area single-upload-area"
|
||||||
|
action="#"
|
||||||
|
list-type="picture-card"
|
||||||
|
:auto-upload="false"
|
||||||
|
:file-list="authorizedRepIDFrontFileList"
|
||||||
|
:limit="1"
|
||||||
|
:on-change="handleAuthorizedRepIDFrontChange"
|
||||||
|
:on-remove="handleAuthorizedRepIDFrontRemove"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
>
|
||||||
|
<el-icon class="upload-icon"><ArrowUpTrayIcon /></el-icon>
|
||||||
|
<div class="el-upload__text">上传授权代表身份证人像面</div>
|
||||||
|
</el-upload>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="身份证国徽面" prop="authorizedRepIDImageURLs">
|
||||||
|
<el-upload
|
||||||
|
class="upload-area single-upload-area"
|
||||||
|
action="#"
|
||||||
|
list-type="picture-card"
|
||||||
|
:auto-upload="false"
|
||||||
|
:file-list="authorizedRepIDBackFileList"
|
||||||
|
:limit="1"
|
||||||
|
:on-change="handleAuthorizedRepIDBackChange"
|
||||||
|
:on-remove="handleAuthorizedRepIDBackRemove"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
>
|
||||||
|
<el-icon class="upload-icon"><ArrowUpTrayIcon /></el-icon>
|
||||||
|
<div class="el-upload__text">上传授权代表身份证国徽面</div>
|
||||||
|
</el-upload>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row> -->
|
||||||
|
|
||||||
|
<!-- 暂时隐藏:应用场景说明、应用场景附件 -->
|
||||||
|
<!-- <h3 class="section-title">应用场景填写</h3>
|
||||||
|
<p class="section-desc">请描述您调用接口的具体业务场景</p>
|
||||||
|
|
||||||
|
<el-row :gutter="16" class="mb-4">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="应用场景" prop="apiUsage">
|
||||||
|
<el-input
|
||||||
|
v-model="form.apiUsage"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="请描述您调用接口的具体业务场景和用途"
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
class="form-input"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="16" class="mb-4">
|
||||||
|
<el-col :span="24">
|
||||||
|
|
||||||
|
<el-form-item label="应用场景附件">
|
||||||
|
<div class="text-xs mb-1 text-blue-500">
|
||||||
|
请在非 IE 浏览器下上传大小不超过 1M 的图片,最多 10 张后台应用截图。
|
||||||
|
</div>
|
||||||
|
<el-upload
|
||||||
|
ref="scenarioUploadRef"
|
||||||
|
class="upload-area"
|
||||||
|
action="#"
|
||||||
|
list-type="picture-card"
|
||||||
|
:auto-upload="false"
|
||||||
|
v-model:file-list="scenarioFileList"
|
||||||
|
accept="image/jpeg,image/jpg,image/png,image/webp"
|
||||||
|
multiple
|
||||||
|
:limit="10"
|
||||||
|
:on-change="handleScenarioChange"
|
||||||
|
:on-remove="handleScenarioRemove"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
>
|
||||||
|
<div class="upload-trigger-inner">
|
||||||
|
<el-icon class="upload-icon"><ArrowUpTrayIcon /></el-icon>
|
||||||
|
<div class="el-upload__text">上传业务场景相关截图或证明材料</div>
|
||||||
|
</div>
|
||||||
|
</el-upload>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
@@ -203,7 +371,17 @@ const props = defineProps({
|
|||||||
legalPersonID: '',
|
legalPersonID: '',
|
||||||
legalPersonPhone: '',
|
legalPersonPhone: '',
|
||||||
enterpriseAddress: '',
|
enterpriseAddress: '',
|
||||||
legalPersonCode: ''
|
legalPersonCode: '',
|
||||||
|
// 扩展:营业执照 & 办公场地 & 场景
|
||||||
|
businessLicenseImageURL: '',
|
||||||
|
officePlaceImageURLs: [],
|
||||||
|
apiUsage: '',
|
||||||
|
scenarioAttachmentURLs: [],
|
||||||
|
// 授权代表信息
|
||||||
|
authorizedRepName: '',
|
||||||
|
authorizedRepID: '',
|
||||||
|
authorizedRepPhone: '',
|
||||||
|
authorizedRepIDImageURLs: []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -224,7 +402,17 @@ const form = ref({
|
|||||||
legalPersonID: '',
|
legalPersonID: '',
|
||||||
legalPersonPhone: '',
|
legalPersonPhone: '',
|
||||||
enterpriseAddress: '',
|
enterpriseAddress: '',
|
||||||
legalPersonCode: ''
|
legalPersonCode: '',
|
||||||
|
// 扩展:营业执照 & 办公场地 & 场景
|
||||||
|
businessLicenseImageURL: '',
|
||||||
|
officePlaceImageURLs: [],
|
||||||
|
apiUsage: '',
|
||||||
|
scenarioAttachmentURLs: [],
|
||||||
|
// 授权代表信息
|
||||||
|
authorizedRepName: '',
|
||||||
|
authorizedRepID: '',
|
||||||
|
authorizedRepPhone: '',
|
||||||
|
authorizedRepIDImageURLs: []
|
||||||
})
|
})
|
||||||
|
|
||||||
// 验证码相关状态
|
// 验证码相关状态
|
||||||
@@ -239,6 +427,15 @@ const submitting = ref(false)
|
|||||||
const ocrLoading = ref(false)
|
const ocrLoading = ref(false)
|
||||||
const ocrResult = ref(false)
|
const ocrResult = ref(false)
|
||||||
const uploadRef = ref()
|
const uploadRef = ref()
|
||||||
|
const officePlaceUploadRef = ref()
|
||||||
|
const scenarioUploadRef = ref()
|
||||||
|
|
||||||
|
// 上传文件列表(前端展示用)
|
||||||
|
const officePlaceFileList = ref([])
|
||||||
|
const scenarioFileList = ref([])
|
||||||
|
const businessLicenseFileList = ref([])
|
||||||
|
const authorizedRepIDFrontFileList = ref([])
|
||||||
|
const authorizedRepIDBackFileList = ref([])
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const canSendCode = computed(() => {
|
const canSendCode = computed(() => {
|
||||||
@@ -326,7 +523,30 @@ const enterpriseRules = {
|
|||||||
legalPersonCode: [
|
legalPersonCode: [
|
||||||
{ required: true, message: '请输入验证码', trigger: 'blur' },
|
{ required: true, message: '请输入验证码', trigger: 'blur' },
|
||||||
{ len: 6, message: '验证码应为6位数字', trigger: 'blur' }
|
{ len: 6, message: '验证码应为6位数字', trigger: 'blur' }
|
||||||
]
|
],
|
||||||
|
businessLicenseImageURL: [
|
||||||
|
{ required: true, message: '请上传营业执照图片', trigger: 'change' }
|
||||||
|
],
|
||||||
|
// 暂时隐藏的表单项,校验已关闭,恢复显示时请还原
|
||||||
|
// apiUsage: [
|
||||||
|
// { required: true, message: '请填写接口用途', trigger: 'blur' },
|
||||||
|
// { min: 5, max: 500, message: '接口用途长度应在5-500个字符之间', trigger: 'blur' }
|
||||||
|
// ],
|
||||||
|
// authorizedRepName: [
|
||||||
|
// { required: true, message: '请输入授权代表姓名', trigger: 'blur' },
|
||||||
|
// { min: 2, max: 20, message: '授权代表姓名长度应在2-20个字符之间', trigger: 'blur' }
|
||||||
|
// ],
|
||||||
|
// authorizedRepID: [
|
||||||
|
// { required: true, message: '请输入授权代表身份证号', trigger: 'blur' },
|
||||||
|
// { validator: validateIDCard, trigger: 'blur' }
|
||||||
|
// ],
|
||||||
|
// authorizedRepPhone: [
|
||||||
|
// { required: true, message: '请输入授权代表手机号', trigger: 'blur' },
|
||||||
|
// { validator: validatePhone, trigger: 'blur' }
|
||||||
|
// ],
|
||||||
|
// authorizedRepIDImageURLs: [
|
||||||
|
// { required: true, message: '请上传授权代表身份证正反面图片', trigger: 'change' }
|
||||||
|
// ]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听props变化
|
// 监听props变化
|
||||||
@@ -382,20 +602,21 @@ const startCountdown = () => {
|
|||||||
const beforeUpload = (file) => {
|
const beforeUpload = (file) => {
|
||||||
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
|
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
|
||||||
const isValidType = allowedTypes.includes(file.type)
|
const isValidType = allowedTypes.includes(file.type)
|
||||||
const isValidSize = file.size / 1024 / 1024 < 5
|
const maxSizeMB = 1
|
||||||
|
const isValidSize = file.size / 1024 / 1024 < maxSizeMB
|
||||||
|
|
||||||
if (!isValidType) {
|
if (!isValidType) {
|
||||||
ElMessage.error('只支持 JPG、PNG、WEBP 格式的图片')
|
ElMessage.error('只支持 JPG、PNG、WEBP 格式的图片')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (!isValidSize) {
|
if (!isValidSize) {
|
||||||
ElMessage.error('图片大小不能超过 5MB')
|
ElMessage.error(`图片大小不能超过 ${maxSizeMB}MB`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理文件变化
|
// 处理文件变化:触发 OCR,并保存营业执照原图 URL(若有上传地址)
|
||||||
const handleFileChange = async (file) => {
|
const handleFileChange = async (file) => {
|
||||||
if (!beforeUpload(file.raw)) {
|
if (!beforeUpload(file.raw)) {
|
||||||
return
|
return
|
||||||
@@ -419,6 +640,11 @@ const handleFileChange = async (file) => {
|
|||||||
form.value.legalPersonID = ocrData.legal_person_id || ''
|
form.value.legalPersonID = ocrData.legal_person_id || ''
|
||||||
form.value.enterpriseAddress = ocrData.address || ''
|
form.value.enterpriseAddress = ocrData.address || ''
|
||||||
|
|
||||||
|
// 如果后端返回了已保存的营业执照图片URL,可以直接写入
|
||||||
|
if (ocrData.license_image_url) {
|
||||||
|
form.value.businessLicenseImageURL = ocrData.license_image_url
|
||||||
|
}
|
||||||
|
|
||||||
ocrResult.value = true
|
ocrResult.value = true
|
||||||
ElMessage.success('营业执照识别成功,已自动填充表单')
|
ElMessage.success('营业执照识别成功,已自动填充表单')
|
||||||
} else {
|
} else {
|
||||||
@@ -432,8 +658,180 @@ const handleFileChange = async (file) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 上传单张图片到七牛云,返回可访问 URL
|
||||||
|
const uploadFileToServer = async (file) => {
|
||||||
|
const res = await certificationApi.uploadFile(file)
|
||||||
|
if (!res?.success || !res?.data?.url) {
|
||||||
|
throw new Error(res?.error?.message || '图片上传失败')
|
||||||
|
}
|
||||||
|
return res.data.url
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择后立即上传:服务器 URL 存到 response.url,保留 file.url 为 blob 以便预览(避免服务器证书等问题导致预览失败)
|
||||||
|
const uploadFileOnceSelected = async (file) => {
|
||||||
|
if (!file?.raw) return null
|
||||||
|
if (file.response?.url) return file.response.url // 已上传过,不重复上传
|
||||||
|
file.status = 'uploading'
|
||||||
|
try {
|
||||||
|
const url = await uploadFileToServer(file.raw)
|
||||||
|
file.status = 'success'
|
||||||
|
if (file.response === undefined) file.response = {}
|
||||||
|
file.response.url = url
|
||||||
|
// 不覆盖 file.url,保留 blob 预览地址,避免服务器证书无效时预览失败
|
||||||
|
return url
|
||||||
|
} catch (err) {
|
||||||
|
file.status = 'fail'
|
||||||
|
ElMessage.error(err?.message || '图片上传失败')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交前仅从 fileList 同步 URL 到表单,并检查是否全部已上传(选择即上传,提交时不再批量上传)
|
||||||
|
// 注:营业执照、办公场地、应用场景、授权代表身份证等表单项已暂时隐藏,仅同步 URL,不再强制校验
|
||||||
|
const syncFormUrlsAndCheckReady = () => {
|
||||||
|
form.value.businessLicenseImageURL = extractUrls(businessLicenseFileList.value)[0] || ''
|
||||||
|
form.value.officePlaceImageURLs = extractUrls(officePlaceFileList.value)
|
||||||
|
form.value.scenarioAttachmentURLs = extractUrls(scenarioFileList.value)
|
||||||
|
updateAuthorizedRepIDImageURLs()
|
||||||
|
|
||||||
|
const hasUploading = (list) => list.some((f) => f.status === 'uploading')
|
||||||
|
const hasUnfinished = (list) => list.some((f) => f.raw && !f.response?.url)
|
||||||
|
if (hasUploading(businessLicenseFileList.value) || hasUnfinished(businessLicenseFileList.value)) return false
|
||||||
|
// 以下上传项已暂时隐藏,不再参与“未上传完成”的拦截
|
||||||
|
// if (hasUploading(officePlaceFileList.value) || hasUnfinished(officePlaceFileList.value)) return false
|
||||||
|
// if (hasUploading(scenarioFileList.value) || hasUnfinished(scenarioFileList.value)) return false
|
||||||
|
// if (hasUploading(authorizedRepIDFrontFileList.value) || hasUnfinished(authorizedRepIDFrontFileList.value)) return false
|
||||||
|
// if (hasUploading(authorizedRepIDBackFileList.value) || hasUnfinished(authorizedRepIDBackFileList.value)) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 el-upload 的 fileList 中提取 URL 数组,优先用服务器 URL(response.url),提交用
|
||||||
|
const extractUrls = (fileList) => {
|
||||||
|
return fileList
|
||||||
|
.map(f => f.response?.url || f.url || f.name)
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 营业执照图片变更:先 OCR 识别,再选择即上传
|
||||||
|
const handleBusinessLicenseChange = async (file, fileList) => {
|
||||||
|
businessLicenseFileList.value = fileList
|
||||||
|
const urls = extractUrls(fileList)
|
||||||
|
form.value.businessLicenseImageURL = urls[0] || ''
|
||||||
|
|
||||||
|
if (file && file.raw) {
|
||||||
|
await handleFileChange(file)
|
||||||
|
// OCR 若未返回服务器 URL,则选择后立即上传(未上传过才上传)
|
||||||
|
if (!file.response?.url) {
|
||||||
|
const url = await uploadFileOnceSelected(file)
|
||||||
|
if (url) form.value.businessLicenseImageURL = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBusinessLicenseRemove = (file, fileList) => {
|
||||||
|
businessLicenseFileList.value = fileList
|
||||||
|
const urls = extractUrls(fileList)
|
||||||
|
form.value.businessLicenseImageURL = urls[0] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动清除营业执照图片(预览区域中的“删除”按钮)
|
||||||
|
const clearBusinessLicense = () => {
|
||||||
|
businessLicenseFileList.value = []
|
||||||
|
form.value.businessLicenseImageURL = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 授权代表身份证人像面图片变更:选择即上传
|
||||||
|
const handleAuthorizedRepIDFrontChange = async (file, fileList) => {
|
||||||
|
authorizedRepIDFrontFileList.value = fileList
|
||||||
|
if (file?.raw && !file.response?.url) {
|
||||||
|
const url = await uploadFileOnceSelected(file)
|
||||||
|
if (url) {
|
||||||
|
authorizedRepIDFrontFileList.value = authorizedRepIDFrontFileList.value.map((f) =>
|
||||||
|
f.uid === file.uid ? { ...f, status: 'success', response: { url }, url: f.url } : f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateAuthorizedRepIDImageURLs()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAuthorizedRepIDFrontRemove = (file, fileList) => {
|
||||||
|
authorizedRepIDFrontFileList.value = fileList
|
||||||
|
updateAuthorizedRepIDImageURLs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 授权代表身份证国徽面图片变更:选择即上传
|
||||||
|
const handleAuthorizedRepIDBackChange = async (file, fileList) => {
|
||||||
|
authorizedRepIDBackFileList.value = fileList
|
||||||
|
if (file?.raw && !file.response?.url) {
|
||||||
|
const url = await uploadFileOnceSelected(file)
|
||||||
|
if (url) {
|
||||||
|
authorizedRepIDBackFileList.value = authorizedRepIDBackFileList.value.map((f) =>
|
||||||
|
f.uid === file.uid ? { ...f, status: 'success', response: { url }, url: f.url } : f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateAuthorizedRepIDImageURLs()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAuthorizedRepIDBackRemove = (file, fileList) => {
|
||||||
|
authorizedRepIDBackFileList.value = fileList
|
||||||
|
updateAuthorizedRepIDImageURLs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动清除授权代表身份证人像面
|
||||||
|
const clearAuthorizedRepFront = () => {
|
||||||
|
authorizedRepIDFrontFileList.value = []
|
||||||
|
updateAuthorizedRepIDImageURLs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动清除授权代表身份证国徽面
|
||||||
|
const clearAuthorizedRepBack = () => {
|
||||||
|
authorizedRepIDBackFileList.value = []
|
||||||
|
updateAuthorizedRepIDImageURLs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 汇总授权代表身份证正反面图片URL到一个数组字段
|
||||||
|
const updateAuthorizedRepIDImageURLs = () => {
|
||||||
|
const frontUrl = extractUrls(authorizedRepIDFrontFileList.value)[0] || ''
|
||||||
|
const backUrl = extractUrls(authorizedRepIDBackFileList.value)[0] || ''
|
||||||
|
const urls = []
|
||||||
|
if (frontUrl) urls.push(frontUrl)
|
||||||
|
if (backUrl) urls.push(backUrl)
|
||||||
|
form.value.authorizedRepIDImageURLs = urls
|
||||||
|
}
|
||||||
|
|
||||||
|
// 办公场地图片变更:选择即上传
|
||||||
|
const handleOfficePlaceChange = async (file, fileList) => {
|
||||||
|
officePlaceFileList.value = fileList
|
||||||
|
if (file?.raw && !file.response?.url) {
|
||||||
|
await uploadFileOnceSelected(file)
|
||||||
|
}
|
||||||
|
form.value.officePlaceImageURLs = extractUrls(fileList)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOfficePlaceRemove = (file, fileList) => {
|
||||||
|
officePlaceFileList.value = fileList
|
||||||
|
form.value.officePlaceImageURLs = extractUrls(fileList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用场景附件图片变更:选择即上传
|
||||||
|
const handleScenarioChange = async (file, fileList) => {
|
||||||
|
scenarioFileList.value = fileList
|
||||||
|
if (file?.raw && !file.response?.url) {
|
||||||
|
await uploadFileOnceSelected(file)
|
||||||
|
}
|
||||||
|
form.value.scenarioAttachmentURLs = extractUrls(fileList)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleScenarioRemove = (file, fileList) => {
|
||||||
|
scenarioFileList.value = fileList
|
||||||
|
form.value.scenarioAttachmentURLs = extractUrls(fileList)
|
||||||
|
}
|
||||||
|
|
||||||
// 提交表单
|
// 提交表单
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
|
if (submitting.value) return
|
||||||
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
await enterpriseFormRef.value.validate()
|
await enterpriseFormRef.value.validate()
|
||||||
|
|
||||||
@@ -450,12 +848,40 @@ const submitForm = async () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
submitting.value = true
|
// 选择即上传:提交时不再上传,仅同步 URL 并校验是否均已上传完成
|
||||||
|
if (!syncFormUrlsAndCheckReady()) {
|
||||||
|
ElMessage.warning('请等待所有图片上传完成后再提交')
|
||||||
|
submitting.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Mock API 调用
|
// 调用后端提交接口
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
const payload = {
|
||||||
|
company_name: form.value.companyName,
|
||||||
|
unified_social_code: form.value.unifiedSocialCode,
|
||||||
|
legal_person_name: form.value.legalPersonName,
|
||||||
|
legal_person_id: form.value.legalPersonID,
|
||||||
|
legal_person_phone: form.value.legalPersonPhone,
|
||||||
|
enterprise_address: form.value.enterpriseAddress,
|
||||||
|
verification_code: form.value.legalPersonCode,
|
||||||
|
// 扩展字段
|
||||||
|
business_license_image_url: form.value.businessLicenseImageURL,
|
||||||
|
office_place_image_urls: form.value.officePlaceImageURLs,
|
||||||
|
api_usage: form.value.apiUsage,
|
||||||
|
scenario_attachment_urls: form.value.scenarioAttachmentURLs,
|
||||||
|
// 授权代表信息
|
||||||
|
authorized_rep_name: form.value.authorizedRepName,
|
||||||
|
authorized_rep_id: form.value.authorizedRepID,
|
||||||
|
authorized_rep_phone: form.value.authorizedRepPhone,
|
||||||
|
authorized_rep_id_image_urls: form.value.authorizedRepIDImageURLs
|
||||||
|
}
|
||||||
|
|
||||||
emit('submit', form.value)
|
const res = await certificationApi.submitEnterpriseInfo(payload)
|
||||||
|
if (!res.success) {
|
||||||
|
throw new Error(res?.error?.message || '提交企业信息失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('submit', { formData: form.value, response: res })
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 用户点击取消或关闭对话框,不处理
|
// 用户点击取消或关闭对话框,不处理
|
||||||
@@ -609,6 +1035,13 @@ onUnmounted(() => {
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #64748b;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
/* 表单输入框 */
|
/* 表单输入框 */
|
||||||
.form-input :deep(.el-input__wrapper) {
|
.form-input :deep(.el-input__wrapper) {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -633,6 +1066,33 @@ onUnmounted(() => {
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 上传区域基础样式 */
|
||||||
|
.upload-area {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保证 picture-card 触发区域整块可点击、可拖拽 */
|
||||||
|
.upload-trigger-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 148px;
|
||||||
|
cursor: pointer;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 当已有一张图片时,隐藏单图上传的“+ 选择文件”入口 */
|
||||||
|
.single-upload-area :deep(.el-upload-list__item + .el-upload--picture-card) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* 验证码按钮 */
|
/* 验证码按钮 */
|
||||||
.code-btn {
|
.code-btn {
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
|
|||||||
115
src/pages/certification/components/ManualReviewPending.vue
Normal file
115
src/pages/certification/components/ManualReviewPending.vue
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<el-card class="step-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="header-icon">
|
||||||
|
<el-icon class="text-amber-600">
|
||||||
|
<ClockIcon />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="header-content">
|
||||||
|
<h2 class="header-title">人工审核</h2>
|
||||||
|
<p class="header-subtitle">您的企业信息已提交,请等待管理员审核</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="manual-review-content">
|
||||||
|
<div class="review-status-box">
|
||||||
|
<el-icon class="status-icon"><ClockIcon /></el-icon>
|
||||||
|
<p class="status-text">我们正在审核您提交的企业信息,请耐心等待。</p>
|
||||||
|
<p v-if="submitTime" class="submit-time">提交时间:{{ submitTime }}</p>
|
||||||
|
<p v-if="companyName" class="company-name">企业名称:{{ companyName }}</p>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" class="refresh-btn" :loading="refreshing" @click="handleRefresh">
|
||||||
|
刷新状态
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ClockIcon } from '@heroicons/vue/24/outline'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
certificationData: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
submitTime: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
companyName: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['refresh'])
|
||||||
|
|
||||||
|
const refreshing = ref(false)
|
||||||
|
const pollTimer = ref(null)
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
refreshing.value = true
|
||||||
|
try {
|
||||||
|
emit('refresh')
|
||||||
|
} finally {
|
||||||
|
refreshing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 人工审核阶段:自动轮询状态,审核通过后会在父组件中自动切换步骤
|
||||||
|
pollTimer.value = window.setInterval(() => {
|
||||||
|
emit('refresh')
|
||||||
|
}, 5000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (pollTimer.value) {
|
||||||
|
window.clearInterval(pollTimer.value)
|
||||||
|
pollTimer.value = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.manual-review-content {
|
||||||
|
padding: 24px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-status-box {
|
||||||
|
background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
color: #d97706;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #92400e;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-time,
|
||||||
|
.company-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #b45309;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -68,6 +68,15 @@
|
|||||||
@submit="handleEnterpriseSubmit"
|
@submit="handleEnterpriseSubmit"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 暂时隐藏:第二步人工审核 -->
|
||||||
|
<!-- <ManualReviewPending
|
||||||
|
v-if="currentStep === 'manual_review'"
|
||||||
|
:certification-data="certificationData"
|
||||||
|
:submit-time="manualReviewSubmitTime"
|
||||||
|
:company-name="enterpriseForm.companyName"
|
||||||
|
@refresh="getCertificationDetails"
|
||||||
|
/> -->
|
||||||
|
|
||||||
<EnterpriseVerify
|
<EnterpriseVerify
|
||||||
v-if="currentStep === 'enterprise_verify'"
|
v-if="currentStep === 'enterprise_verify'"
|
||||||
:enterprise-data="enterpriseForm"
|
:enterprise-data="enterpriseForm"
|
||||||
@@ -120,6 +129,7 @@ import { useUserStore } from '@/stores/user'
|
|||||||
import {
|
import {
|
||||||
BuildingOfficeIcon,
|
BuildingOfficeIcon,
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
|
ClockIcon,
|
||||||
CodeBracketIcon,
|
CodeBracketIcon,
|
||||||
DocumentTextIcon,
|
DocumentTextIcon,
|
||||||
UserIcon,
|
UserIcon,
|
||||||
@@ -133,9 +143,10 @@ import ContractRejected from './components/ContractRejected.vue'
|
|||||||
import ContractSign from './components/ContractSign.vue'
|
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'
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
// 认证步骤配置
|
// 认证步骤配置(暂时隐藏第二步「人工审核」,恢复时取消注释 manual_review 并还原 setCurrentStepByStatus 中 info_pending_review 分支)
|
||||||
const certificationSteps = [
|
const certificationSteps = [
|
||||||
{
|
{
|
||||||
key: 'enterprise_info',
|
key: 'enterprise_info',
|
||||||
@@ -143,6 +154,12 @@ const certificationSteps = [
|
|||||||
description: '填写企业基本信息和法人信息',
|
description: '填写企业基本信息和法人信息',
|
||||||
icon: BuildingOfficeIcon,
|
icon: BuildingOfficeIcon,
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// key: 'manual_review',
|
||||||
|
// title: '人工审核',
|
||||||
|
// description: '等待管理员审核企业信息',
|
||||||
|
// icon: ClockIcon,
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
key: 'enterprise_verify',
|
key: 'enterprise_verify',
|
||||||
title: '企业认证',
|
title: '企业认证',
|
||||||
@@ -176,6 +193,18 @@ const currentStepIndex = computed(() => {
|
|||||||
// 步骤特定元数据
|
// 步骤特定元数据
|
||||||
const stepMeta = ref({}) // 用于存储当前步骤的metadata
|
const stepMeta = ref({}) // 用于存储当前步骤的metadata
|
||||||
|
|
||||||
|
// 人工审核步骤的提交时间展示
|
||||||
|
const manualReviewSubmitTime = computed(() => {
|
||||||
|
const at = certificationData.value?.metadata?.enterprise_info?.submit_at ?? certificationData.value?.info_submitted_at
|
||||||
|
if (!at) return ''
|
||||||
|
try {
|
||||||
|
const d = new Date(at)
|
||||||
|
return Number.isNaN(d.getTime()) ? '' : d.toLocaleString('zh-CN')
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const enterpriseForm = ref({
|
const enterpriseForm = ref({
|
||||||
companyName: '',
|
companyName: '',
|
||||||
@@ -188,35 +217,66 @@ const enterpriseForm = ref({
|
|||||||
enterpriseEmail: '',
|
enterpriseEmail: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 开发模式控制
|
//
|
||||||
const isDevelopment = ref(false)
|
const isDevelopment = ref(false)
|
||||||
const devCurrentStep = ref('enterprise_info')
|
const devCurrentStep = ref('enterprise_info')
|
||||||
|
|
||||||
// 合同签署加载状态
|
// 合同签署加载状态
|
||||||
const contractSignLoading = ref(false)
|
const contractSignLoading = ref(false)
|
||||||
|
|
||||||
// 事件处理
|
// 只补 enterprise_verify 所需 auth_url,不改动原有页面展示数据结构
|
||||||
const handleEnterpriseSubmit = async (formData) => {
|
// 轮询策略:最多 5 秒,每秒 1 次(最多 5 次)
|
||||||
|
const pollAuthUrlOnly = async (maxTries = 5) => {
|
||||||
|
for (let i = 0; i < maxTries; i++) {
|
||||||
|
const res = await certificationApi.getCertificationDetails()
|
||||||
|
const status = res?.data?.status
|
||||||
|
const authUrl = res?.data?.metadata?.auth_url
|
||||||
|
|
||||||
|
// 仅同步状态,避免覆盖已有展示字段
|
||||||
|
if (status && certificationData.value) {
|
||||||
|
certificationData.value.status = status
|
||||||
|
await setCurrentStepByStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authUrl) {
|
||||||
|
// 保留原 metadata,仅更新链接字段
|
||||||
|
stepMeta.value = {
|
||||||
|
...(stepMeta.value || {}),
|
||||||
|
auth_url: authUrl,
|
||||||
|
}
|
||||||
|
return authUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 事件处理:优先用提交接口返回的认证数据更新步骤,确保进入「人工审核」页,避免依赖二次请求
|
||||||
|
const handleEnterpriseSubmit = async (payload) => {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
// 字段映射
|
const nextAction = payload?.response?.data?.metadata?.next_action
|
||||||
const payload = {
|
if (nextAction) {
|
||||||
company_name: formData.companyName,
|
ElMessage.success(nextAction)
|
||||||
unified_social_code: formData.unifiedSocialCode,
|
} else {
|
||||||
legal_person_name: formData.legalPersonName,
|
ElMessage.success('企业信息提交成功,请等待管理员审核')
|
||||||
legal_person_id: formData.legalPersonID,
|
}
|
||||||
legal_person_phone: formData.legalPersonPhone,
|
if (payload?.response?.data?.status) {
|
||||||
enterprise_address: formData.enterpriseAddress,
|
certificationData.value = payload.response.data
|
||||||
enterprise_email: formData.enterpriseEmail,
|
stepMeta.value = payload.response.data?.metadata || {}
|
||||||
verification_code: formData.legalPersonCode,
|
await setCurrentStepByStatus()
|
||||||
|
if (currentStep.value === 'enterprise_verify' && !stepMeta.value?.auth_url) {
|
||||||
|
await pollAuthUrlOnly()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await getCertificationDetails()
|
||||||
|
if (currentStep.value === 'enterprise_verify' && !stepMeta.value?.auth_url) {
|
||||||
|
await pollAuthUrlOnly()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await certificationApi.submitEnterpriseInfo(payload)
|
|
||||||
ElMessage.success('企业信息提交成功')
|
|
||||||
// 提交成功后刷新认证详情
|
|
||||||
await getCertificationDetails()
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error?.message || '提交失败,请检查表单信息')
|
ElMessage.error(error?.message || '获取认证状态失败,请刷新页面')
|
||||||
// 提交失败时不刷新认证详情,保持用户填写的信息
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -355,6 +415,10 @@ const setCurrentStepByStatus = async () => {
|
|||||||
case 'pending':
|
case 'pending':
|
||||||
currentStep.value = 'enterprise_info'
|
currentStep.value = 'enterprise_info'
|
||||||
break
|
break
|
||||||
|
case 'info_pending_review':
|
||||||
|
// 暂时跳过人工审核展示,直接进入企业认证步骤
|
||||||
|
currentStep.value = 'enterprise_verify'
|
||||||
|
break
|
||||||
case 'info_submitted':
|
case 'info_submitted':
|
||||||
currentStep.value = 'enterprise_verify'
|
currentStep.value = 'enterprise_verify'
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -304,6 +304,12 @@ const routes = [
|
|||||||
name: 'AdminPurchaseRecords',
|
name: 'AdminPurchaseRecords',
|
||||||
component: () => import('@/pages/admin/purchase-records/index.vue'),
|
component: () => import('@/pages/admin/purchase-records/index.vue'),
|
||||||
meta: { title: '购买记录管理' }
|
meta: { title: '购买记录管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'certification-reviews',
|
||||||
|
name: 'AdminCertificationReviews',
|
||||||
|
component: () => import('@/pages/admin/certification-reviews/index.vue'),
|
||||||
|
meta: { title: '企业审核' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user