feat(F006): 企业详情页专业UI重构 + 审核交互全链路 (审核通过/驳回+原因+本会话留痕时间线, 列表页单行/批量审核接通, 修 enabled/showEnterprise query传参bug)

This commit is contained in:
zou 2026-08-18 13:20:34 +08:00
parent fc7d9f9179
commit 8be7ca9dc2
2 changed files with 572 additions and 93 deletions

View File

@ -61,6 +61,16 @@ const selectedIds = ref<string[]>([])
const detailVisible = ref(false)
const detailRow = ref<Enterprise | null>(null)
// ========== 单行审核 ==========
const auditVisible = ref(false)
const auditBusy = ref(false)
const auditForm = reactive({ id: '', result: 2, failReason: '' })
// ========== 批量审核 ==========
const batchAuditVisible = ref(false)
const batchAuditBusy = ref(false)
const batchAuditForm = reactive({ result: 2, failReason: '' })
const filters = reactive({
enterpriseName: '',
industry: '',
@ -160,12 +170,60 @@ function onPageChange(p: number) { page.value = p; load() }
function onLimitChange(l: number) { limit.value = l; page.value = 1; load() }
function onSelectionChange(rows: any[]) { selectedIds.value = rows.map((r: any) => r.id) }
// ========== 审核交互 (后端 GET /user/enterprise/checkStatus?id=&status=&failReason=, 支持逗号批量) ==========
function onAudit(row: Enterprise) {
auditForm.id = row.id
auditForm.result = 2
auditForm.failReason = ''
auditVisible.value = true
}
async function submitAudit() {
if (auditForm.result === 3 && !auditForm.failReason.trim()) { ElMessage.warning('请填写驳回原因'); return }
auditBusy.value = true
try {
await httpGet('/user/enterprise/checkStatus', {
id: auditForm.id, status: auditForm.result,
failReason: auditForm.result === 3 ? auditForm.failReason.trim() : ''
})
ElMessage.success(auditForm.result === 2 ? '审核通过' : '已驳回')
auditVisible.value = false
load()
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '审核操作失败') }
finally { auditBusy.value = false }
}
async function openBatchAudit() {
if (!selectedIds.value.length) { ElMessage.warning('请先选择企业'); return }
batchAuditForm.result = 2
batchAuditForm.failReason = ''
batchAuditVisible.value = true
}
async function submitBatchAudit() {
if (batchAuditForm.result === 3 && !batchAuditForm.failReason.trim()) { ElMessage.warning('请填写驳回原因'); return }
try {
await ElMessageBox.confirm(
`确认对已选 ${selectedIds.value.length} 家企业执行「${batchAuditForm.result === 2 ? '审核通过' : '驳回'}」?` +
(batchAuditForm.result === 2 ? '通过后将通过站内消息与短信通知企业。' : ''),
'批量审核确认', { type: 'warning' }
)
} catch { return }
batchAuditBusy.value = true
try {
await httpGet('/user/enterprise/checkStatus', {
id: selectedIds.value.join(','), status: batchAuditForm.result,
failReason: batchAuditForm.result === 3 ? batchAuditForm.failReason.trim() : ''
})
ElMessage.success(`已对 ${selectedIds.value.length} 家企业完成审核`)
batchAuditVisible.value = false
load()
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '批量审核失败') }
finally { batchAuditBusy.value = false }
}
// ========== 批量操作 ==========
function importHandle() { ElMessage.info('导入功能待实现') }
function batchAuditHandle() {
if (!selectedIds.value.length) { ElMessage.warning('请先选择企业'); return }
ElMessage.info('批量审核功能待实现')
}
function batchLabelHandle() {
if (!selectedIds.value.length) { ElMessage.warning('请先选择企业'); return }
ElMessage.info('批量设置标签待实现')
@ -195,20 +253,21 @@ function changeAdminHandle(_row: Enterprise) { ElMessage.info('更换管理员
async function handleMore(cmd: string, row: Enterprise) {
try {
switch (cmd) {
case 'audit': ElMessage.info('审核待实现'); break
case 'audit': onAudit(row); break
case 'label': ElMessage.info('标签待实现'); break
case 'rating': ElMessage.info('评级待实现'); break
case 'enabled':
await httpPost('/user/enterprise/enabled', { id: row.id, enabled: row.enabled === 1 ? 0 : 1 })
ElMessage.success(row.enabled === 1 ? '已禁用' : '已启用'); load()
// 后端 @RequestParam 从 query 读, body 不绑定 (2026-08-18 实测修正)
await httpPost(`/user/enterprise/enabled?id=${row.id}`)
ElMessage.success(row.enabled === 1 ? '已启用' : '已禁用'); load()
break
case 'showFlag':
await httpPost('/user/enterprise/showEnterprise', { id: row.id, showFlag: row.showFlag === 1 ? 0 : 1 })
await httpPost(`/user/enterprise/showEnterprise?id=${row.id}`)
ElMessage.success(row.showFlag === 1 ? '已显示' : '已隐藏'); load()
break
case 'homeShow':
await httpPost('/user/enterprise/homeShow', { id: row.id, homeShow: row.homeShow === 1 ? 0 : 1 })
ElMessage.success(row.homeShow === 1 ? '已取消推荐' : '已推荐'); load()
await httpGet('/user/enterprise/homeShow', { id: row.id, homeShow: row.homeShow === 1 ? 0 : 1 })
ElMessage.success(row.homeShow === 1 ? '已推荐' : '已取消推荐'); load()
break
case 'assign': ElMessage.info('分配业务员待实现'); break
case 'deleteRow':
@ -250,7 +309,7 @@ onMounted(() => load())
<!-- 批量操作栏 -->
<div class="rr-batch-bar">
<el-button size="small" type="success" @click="importHandle">导入</el-button>
<el-button size="small" type="danger" @click="batchAuditHandle">批量审核</el-button>
<el-button size="small" type="danger" @click="openBatchAudit">批量审核</el-button>
<el-button size="small" @click="batchLabelHandle">批量设置标签</el-button>
<el-button size="small" type="warning" @click="batchAssignSalesperson">批量分配业务员</el-button>
<el-button size="small" type="danger" @click="deleteHandle">删除</el-button>
@ -382,6 +441,54 @@ onMounted(() => load())
<el-button @click="detailVisible=false">关闭</el-button>
</template>
</el-dialog>
<!-- 单行审核弹窗 -->
<el-dialog v-model="auditVisible" title="企业认证审核" width="480px" :close-on-click-modal="false" destroy-on-close>
<div style="background:var(--bg,#f6f8fb);border:1px solid var(--border);border-radius:8px;padding:10px 14px;font-size:13px;color:var(--ink-2);margin-bottom:18px">
正在审核:<strong style="color:var(--ink)">{{ auditForm.id ? (list.find(r => r.id === auditForm.id)?.enterpriseName || '--') : '--' }}</strong>
</div>
<el-form label-width="80px" label-position="left">
<el-form-item label="审核结果">
<el-radio-group v-model="auditForm.result">
<el-radio :value="2">审核通过</el-radio>
<el-radio :value="3">审核驳回</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="auditForm.result === 3" label="驳回原因" required>
<el-input v-model="auditForm.failReason" type="textarea" :rows="4"
placeholder="请填写驳回原因,将随站内消息与短信通知企业" maxlength="200" show-word-limit />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="auditVisible=false">取消</el-button>
<el-button type="primary" :loading="auditBusy" @click="submitAudit">确定提交</el-button>
</template>
</el-dialog>
<!-- 批量审核弹窗 -->
<el-dialog v-model="batchAuditVisible" title="批量审核企业" width="480px" :close-on-click-modal="false" destroy-on-close>
<el-form label-width="80px" label-position="left">
<el-form-item label="审核结果">
<el-radio-group v-model="batchAuditForm.result">
<el-radio :value="2">全部审核通过</el-radio>
<el-radio :value="3">全部驳回</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="batchAuditForm.result === 3" label="驳回原因" required>
<el-input v-model="batchAuditForm.failReason" type="textarea" :rows="4"
placeholder="请填写驳回原因,将随站内消息与短信通知企业" maxlength="200" show-word-limit />
</el-form-item>
<el-form-item v-else label="通过说明">
<div style="font-size:12px;color:var(--ink-3);line-height:1.7;background:var(--bg,#f6f8fb);border-radius:8px;padding:10px 12px;width:100%">
已选 <strong style="color:var(--primary)">{{ selectedIds.length }}</strong> 家企业将通过认证,并通过站内消息与短信通知企业。
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="batchAuditVisible=false">取消</el-button>
<el-button type="primary" :loading="batchAuditBusy" @click="submitBatchAudit">确定提交</el-button>
</template>
</el-dialog>
</div>
</template>

View File

@ -1,21 +1,34 @@
<script setup lang="ts">
/**
* F006 企业详情页 - 独立页面
* F006 企业详情页 - 专业 UI 重构版 (2026-08-18)
*
* 参考老 vue user/enterprise/enterprise-detail.vue 设计 + 新系统主题规范
* 数据源:
* - GET /user/enterprise/getInfo?enterpriseId= 企业全量信息(80+字段)
* - GET /user/enterprise/getInfo?enterpriseId= 企业全量信息(80+字段, 含聚合评分/商品数/入驻时长)
* - GET /user/performancescorerecord/page?enterpriseId= 履约评分记录
* 3 Tab: 基本信息 / 资质认证 / 履约情况
* - GET /user/enterprise/checkStatus?id=&status=&failReason= 审核(2通过/3驳回, 支持批量逗号)
* - POST /user/enterprise/enabled?id= 禁用/启用切换
* - POST /user/enterprise/showEnterprise?id= 显示/隐藏切换
* - GET /user/enterprise/homeShow?id=&homeShow= 首页推荐切换
*
* 结构: 状态横幅(审核操作) → 头部信息卡 → 认证流程时间线 → 3Tab(基本信息/资质认证/履约情况)
* 审核留痕: 后端无审核历史表, 时间线展示「入驻提交时间(真实)+当前审核结果(真实)
* +本会话审核操作(前端本地记录, 不伪造历史)」
*/
import { ref, computed, onMounted } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Shop } from '@element-plus/icons-vue'
import { httpGet } from '@/utils/request'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Shop, OfficeBuilding, MapLocation, User, Coin,
WarningFilled, SuccessFilled, CircleCloseFilled, Clock, Medal, Star
} from '@element-plus/icons-vue'
import { httpGet, httpPost } from '@/utils/request'
import PageHeader from '@/components/PageHeader/PageHeader.vue'
import { useUserStore } from '@/stores/user'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const enterpriseId = computed(() => String(route.params.id || ''))
const loading = ref(false)
@ -24,15 +37,23 @@ const detail = ref<any>({})
const otherList = ref<any[]>([])
const performanceList = ref<any[]>([])
const IDENTITY_MAP: Record<number, string> = { 1: '供应商', 2: '生产厂商', 3: '渠道商', 4: '服务商' }
const STATUS_MAP: Record<number, { label: string; type: 'success' | 'warning' | 'danger' | 'info' }> = {
1: { label: '待审核', type: 'warning' },
2: { label: '已认证', type: 'success' },
3: { label: '已驳回', type: 'danger' }
// ================= 状态/映射常量 (与列表页统一 5 级) =================
const STATUS_META: Record<number, { label: string; type: 'warning' | 'success' | 'danger'; icon: any }> = {
1: { label: '待审核', type: 'warning', icon: WarningFilled },
2: { label: '已认证', type: 'success', icon: SuccessFilled },
3: { label: '已驳回', type: 'danger', icon: CircleCloseFilled }
}
const IDENTITY_MAP: Record<number, string> = { 1: '供应商', 2: '生产厂商', 3: '渠道商', 4: '服务商' }
const LEVEL_MAP: Record<number, string> = {
1: '优质供应商', 2: '钻石供应商', 3: '金牌供应商', 4: '银牌供应商', 5: '铜牌供应商'
}
const LEVEL_MAP: Record<number, string> = { 1: 'A级', 2: 'B级', 3: 'C级', 4: 'D级' }
const isCertified = computed(() => detail.value.status === 2)
const status = computed<number>(() => Number(detail.value.status ?? 0))
const isCertified = computed(() => status.value === 2)
const isPending = computed(() => status.value === 1)
const isRejected = computed(() => status.value === 3)
const statusMeta = computed(() => STATUS_META[status.value])
const logoText = computed(() => (detail.value.enterpriseName || '企').trim().charAt(0))
const registerYear = computed(() => {
const t = detail.value.registerTime
@ -49,7 +70,42 @@ const productList = computed<string[]>(() => {
if (!raw) return []
return String(raw).split(/[,,、;;\s]+/).map((s: string) => s.trim()).filter(Boolean)
})
const addressLine = computed(() => [detail.value.provinceName, detail.value.cityName, detail.value.areaName].filter(Boolean).join(' '))
const tenure = computed(() => {
const y = detail.value.year, m = detail.value.month, d = detail.value.day
if (y === undefined && m === undefined && d === undefined) return ''
const parts: string[] = []
if (y) parts.push(`${y}年`); if (m) parts.push(`${m}个月`); if (d) parts.push(`${d}天`)
return parts.join(' ') || '--'
})
// ================= 本会话审核操作记录 (前端本地留痕, 后端无历史表) =================
const localAuditLogs = ref<{ time: string; operator: string; result: string; reason: string }[]>([])
const auditVisible = ref(false)
const auditBusy = ref(false)
const auditForm = reactive({ result: 2, failReason: '' })
const timelineItems = computed(() => {
const items: { kind: string; title: string; time: string; desc: string; operator?: string }[] = []
// ① 提交认证 (真实数据)
items.push({ kind: 'submit', title: '提交企业认证', time: fmtDate(detail.value.createDate), desc: '企业提交了企业认证申请资料' })
// ② 当前审核结果 (真实数据)
if (status.value === 1) items.push({ kind: 'pending', title: '等待审核', time: '进行中', desc: '运营人员正在审核,请耐心等待' })
else if (status.value === 2) items.push({ kind: 'pass', title: '审核通过', time: '已认证', desc: '企业认证通过,可正常开展业务' })
else if (status.value === 3) items.push({ kind: 'fail', title: '审核驳回', time: '未通过', desc: detail.value.failReason || '未填写驳回原因' })
// ③ 本会话内完成的审核操作 (本地追加)
for (const log of localAuditLogs.value) {
items.push({
kind: log.result.includes('通过') ? 'pass' : 'fail',
title: log.result, time: log.time,
desc: log.reason || '—',
operator: log.operator
})
}
return items
})
// ================= 数据加载 =================
function maskMobile(m: string | number | null | undefined): string {
if (m === undefined || m === null || m === '') return '--'
const s = String(m)
@ -63,6 +119,10 @@ function fmtMoney(n: any): string {
const v = Number(n || 0)
return v ? `¥ ${v.toLocaleString()}` : '¥ 0'
}
function fmtNum(n: any): string {
const v = Number(n)
return Number.isFinite(v) ? String(v) : '--'
}
async function load() {
loading.value = true
@ -87,6 +147,72 @@ async function load() {
} catch { performanceList.value = [] }
}
// ================= 审核交互 =================
function openAudit(defaultResult: number = 2) {
auditForm.result = defaultResult
auditForm.failReason = ''
auditVisible.value = true
}
async function submitAudit() {
if (auditForm.result === 3 && !auditForm.failReason.trim()) {
ElMessage.warning('请填写驳回原因')
return
}
auditBusy.value = true
try {
await httpGet('/user/enterprise/checkStatus', {
id: enterpriseId.value,
status: auditForm.result,
failReason: auditForm.result === 3 ? auditForm.failReason.trim() : ''
})
const passed = auditForm.result === 2
ElMessage.success(passed ? '审核通过,企业已认证' : '已驳回,企业认证未通过')
auditVisible.value = false
// 本会话审计留痕
localAuditLogs.value.push({
time: fmtDate(new Date().toISOString()),
operator: userStore.displayName || '管理员',
result: passed ? '审核通过' : '审核驳回',
reason: passed ? '认可企业提交的认证资料' : auditForm.failReason.trim()
})
await load()
} catch (e: any) {
ElMessage.error(e?.msg || e?.message || '审核操作失败')
} finally {
auditBusy.value = false
}
}
// ================= 状态切换 (后端均为 query 参数) =================
async function toggleEnabled() {
const disabling = detail.value.enabled !== 1
try { await ElMessageBox.confirm(`${disabling ? '禁用' : '启用'}后,该企业${disabling ? '将无法登录和使用平台服务' : '恢复正常使用'}。确认${disabling ? '禁用' : '启用'}?`, '操作确认', { type: 'warning' }) } catch { return }
try {
await httpPost(`/user/enterprise/enabled?id=${enterpriseId.value}`)
ElMessage.success(disabling ? '已禁用该企业' : '已启用该企业')
await load()
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '操作失败') }
}
async function toggleShow() {
const hiding = detail.value.showFlag !== 1
try {
await httpPost(`/user/enterprise/showEnterprise?id=${enterpriseId.value}`)
ElMessage.success(hiding ? '已隐藏企业(列表不再展示)' : '已恢复显示企业')
await load()
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '操作失败') }
}
async function toggleHome() {
const recommend = detail.value.homeShow === 1 ? 0 : 1
try {
await httpGet('/user/enterprise/homeShow', { id: enterpriseId.value, homeShow: recommend })
ElMessage.success(recommend === 1 ? '已推荐到首页' : '已取消首页推荐')
await load()
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '操作失败') }
}
function goStore() {
const eid = detail.value.enterpriseId || detail.value.id
if (!eid) return
@ -99,10 +225,56 @@ onMounted(load)
<template>
<div class="ent-detail" v-loading="loading">
<PageHeader title="企业详情" :sub="`入驻企业信息总览`">
<el-button @click="router.back()">返回列表</el-button>
<el-button @click="router.back()">
<el-icon style="margin-right:4px"><Clock /></el-icon>返回列表
</el-button>
</PageHeader>
<!-- ===== Header 区: logo + 企业名 + 认证标签 + meta ===== -->
<!-- ===== ① 状态横幅 (审核操作区) ===== -->
<div class="audit-banner" :class="`banner-${status || 'unknown'}`" v-if="status">
<div class="banner-icon">
<el-icon v-if="statusMeta" :size="26"><component :is="statusMeta.icon" /></el-icon>
</div>
<div class="banner-main">
<div class="banner-title">
{{ isPending ? '企业认证待审核' : isCertified ? '企业认证已通过' : isRejected ? '企业认证未通过' : '状态未知' }}
</div>
<div class="banner-desc" v-if="isPending">
该企业已提交认证申请资料,请核查营业执照、法人证件及资质证书后给出审核结论。
</div>
<div class="banner-desc" v-else-if="isRejected">
驳回原因:<span class="reason-text">{{ detail.failReason || '未填写' }}</span>
</div>
<div class="banner-desc" v-else-if="isCertified">
企业已通过认证,可正常发布商品、参与询报价与交易。
</div>
<div class="banner-badges">
<el-tag v-if="detail.enabled === 1" type="danger" size="small" effect="dark" round>已禁用</el-tag>
<el-tag v-if="detail.showFlag === 1" type="info" size="small" effect="plain" round>已隐藏</el-tag>
<el-tag v-if="detail.homeShow === 1" type="success" size="small" effect="plain" round>首页推荐</el-tag>
</div>
</div>
<div class="banner-actions">
<template v-if="isPending">
<el-button type="primary" @click="openAudit(2)">
<el-icon style="margin-right:4px"><SuccessFilled /></el-icon>审核通过
</el-button>
<el-button type="danger" plain @click="openAudit(3)">驳 回</el-button>
</template>
<template v-else-if="isRejected">
<el-button type="primary" @click="openAudit(2)">
<el-icon style="margin-right:4px"><SuccessFilled /></el-icon>重新审核
</el-button>
</template>
<template v-else-if="isCertified">
<el-button size="small" :type="detail.enabled === 1 ? 'success' : 'danger'" plain @click="toggleEnabled">
{{ detail.enabled === 1 ? '启用企业' : '禁用企业' }}
</el-button>
</template>
</div>
</div>
<!-- ===== ② 头部信息卡 ===== -->
<div class="detail-header">
<div class="header-logo">
<el-image v-if="detail.enterpriseLogo" :src="detail.enterpriseLogo" fit="cover" class="logo-img" />
@ -112,12 +284,12 @@ onMounted(load)
<div class="header-title-row">
<h2 class="ent-name">{{ detail.enterpriseName || '--' }}</h2>
<span v-for="(t, i) in tags" :key="i" class="name-tag">{{ t }}</span>
<el-tag v-if="STATUS_MAP[detail.status]" :type="STATUS_MAP[detail.status].type" effect="light" round>{{ STATUS_MAP[detail.status].label }}</el-tag>
<el-tag v-if="detail.enabled === 1" type="info" effect="plain" round>已禁用</el-tag>
<el-tag v-if="statusMeta" :type="statusMeta.type" effect="light" round>{{ statusMeta.label }}</el-tag>
</div>
<div class="header-meta">
<span v-if="detail.industry" class="meta-item">{{ detail.industry }}</span>
<span v-if="registerYear !== '--'" class="meta-item">{{ registerYear }} 成立</span>
<span v-if="registerYear !== '--'" class="meta-item">成立于 {{ registerYear }}</span>
<span v-if="tenure" class="meta-item">入驻 {{ tenure }}</span>
<span v-if="detail.creditCode" class="meta-item">信用代码 {{ detail.creditCode }}</span>
</div>
</div>
@ -133,15 +305,53 @@ onMounted(load)
</div>
<div class="stat-cell">
<div class="stat-value">{{ detail.enterpriseRate ?? '--' }}</div>
<div class="stat-label">企业评级</div>
<div class="stat-label">好评率</div>
</div>
</div>
<el-button type="primary" class="store-btn" @click="goStore" :disabled="!detail.enterpriseId && !detail.id">
<el-icon style="margin-right:6px"><Shop /></el-icon>进入店铺
</el-button>
<div class="header-btns">
<el-button type="primary" class="store-btn" @click="goStore" :disabled="!detail.enterpriseId && !detail.id">
<el-icon style="margin-right:6px"><Shop /></el-icon>进入店铺
</el-button>
<el-dropdown trigger="click" placement="bottom-end" v-if="isCertified">
<el-button plain>
更多操作<el-icon class="el-icon--right"><Coin /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="1" @click="toggleShow">{{ detail.showFlag === 1 ? '恢复显示' : '隐藏企业' }}</el-dropdown-item>
<el-dropdown-item :command="2" @click="toggleHome">{{ detail.homeShow === 1 ? '取消首页推荐' : '推荐到首页' }}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<!-- ===== ③ 认证流程时间线 ===== -->
<div class="info-card timeline-card">
<div class="card-head">
<el-icon class="card-head-icon"><Medal /></el-icon>
<span class="card-title-text">认证流程</span>
<span class="card-sub">入驻申请 · 审核结果 · 本会话操作记录</span>
</div>
<el-timeline class="audit-timeline">
<el-timeline-item
v-for="(item, i) in timelineItems"
:key="i"
:type="item.kind === 'pass' ? 'success' : item.kind === 'fail' ? 'danger' : item.kind === 'pending' ? 'warning' : 'primary'"
:hollow="item.kind === 'pending'"
:timestamp="item.time"
placement="top"
>
<div class="tl-content">
<span class="tl-title">{{ item.title }}</span>
<span class="tl-desc">{{ item.desc }}</span>
<span v-if="item.operator" class="tl-operator">操作人:{{ item.operator }}</span>
</div>
</el-timeline-item>
</el-timeline>
</div>
<!-- ===== Tab 导航 ===== -->
<div class="tab-nav">
<div class="tab-item" :class="{ active: activeTab === 'basic' }" @click="activeTab = 'basic'">基本信息</div>
@ -154,21 +364,27 @@ onMounted(load)
<div class="info-grid">
<div class="info-col">
<section class="info-card">
<div class="card-title">公司信息</div>
<div class="card-head">
<el-icon class="card-head-icon"><OfficeBuilding /></el-icon>
<span class="card-title-text">公司信息</span>
</div>
<div class="info-grid-2">
<div class="info-item"><span class="label">公司名称</span><span class="value">{{ detail.enterpriseName || '--' }}</span></div>
<div class="info-item"><span class="label">注册时间</span><span class="value">{{ fmtDate(detail.registerTime) }}</span></div>
<div class="info-item"><span class="label">所属行业</span><span class="value">{{ detail.industry || '--' }}</span></div>
<div class="info-item"><span class="label">企业身份</span><span class="value">{{ IDENTITY_MAP[detail.enterpriseIdentity as number] || detail.enterpriseIdentity || '--' }}</span></div>
<div class="info-item"><span class="label">企业类型</span><span class="value">{{ detail.enterpriseType ?? '--' }}</span></div>
<div class="info-item"><span class="label">统一信用代码</span><span class="value">{{ detail.creditCode || detail.orgNo || '--' }}</span></div>
<div class="info-item"><span class="label">统一信用代码</span><span class="value mono">{{ detail.creditCode || detail.orgNo || '--' }}</span></div>
</div>
</section>
<section class="info-card">
<div class="card-title">地址信息</div>
<div class="card-head">
<el-icon class="card-head-icon"><MapLocation /></el-icon>
<span class="card-title-text">地址信息</span>
</div>
<div class="address-box">
<div class="addr-line">{{ [detail.provinceName, detail.cityName, detail.areaName].filter(Boolean).join(' ') || '--' }}</div>
<div class="addr-line">{{ addressLine || '--' }}</div>
<div class="value">{{ detail.enterpriseAddress || '--' }}</div>
</div>
</section>
@ -176,31 +392,29 @@ onMounted(load)
<div class="info-col">
<section class="info-card">
<div class="card-title">联系人信息</div>
<div class="card-head">
<el-icon class="card-head-icon"><User /></el-icon>
<span class="card-title-text">联系人信息</span>
</div>
<div class="contact-profile">
<div class="contact-avatar">{{ contactInitial }}</div>
<div class="contact-name">{{ detail.linkMan || '--' }}</div>
</div>
<div class="contact-methods">
<div class="contact-method">
<span class="label">联系电话</span><span class="value">{{ maskMobile(detail.linkMobile) }}</span>
</div>
<div class="contact-method">
<span class="label">电子邮箱</span><span class="value">{{ detail.email || '--' }}</span>
</div>
<div class="contact-method">
<span class="label">管理员</span><span class="value">{{ detail.administrator || '--' }} ({{ maskMobile(detail.administratorMobile) }})</span>
</div>
<div class="contact-method">
<span class="label">业务员</span><span class="value">{{ detail.salesperson || '暂未分配' }}</span>
</div>
<div class="contact-method"><span class="label">联系电话</span><span class="value">{{ maskMobile(detail.linkMobile) }}</span></div>
<div class="contact-method"><span class="label">电子邮箱</span><span class="value">{{ detail.email || '--' }}</span></div>
<div class="contact-method"><span class="label">管理员</span><span class="value">{{ detail.administrator || '--' }} ({{ maskMobile(detail.administratorMobile) }})</span></div>
<div class="contact-method"><span class="label">业务员</span><span class="value">{{ detail.salesperson || '暂未分配' }}</span></div>
</div>
</section>
</div>
</div>
<section class="info-card full">
<div class="card-title">经营信息</div>
<div class="card-head">
<el-icon class="card-head-icon"><Coin /></el-icon>
<span class="card-title-text">经营信息</span>
</div>
<div class="info-grid-2">
<div class="info-item span2"><span class="label">经营范围</span><span class="value">{{ detail.scope || detail.scopeOfLicence || '--' }}</span></div>
<div class="info-item span2"><span class="label">主营产品</span>
@ -209,10 +423,10 @@ onMounted(load)
<span v-if="!productList.length" class="value">--</span>
</span>
</div>
<div class="info-item"><span class="label">银行</span><span class="value">{{ detail.bankOfDeposit || '--' }}</span></div>
<div class="info-item"><span class="label">开户名称</span><span class="value">{{ detail.accountName || '--' }}</span></div>
<div class="info-item"><span class="label">账号</span><span class="value">{{ detail.accountCard || '--' }}</span></div>
<div class="info-item"><span class="label">开户银行</span><span class="value">{{ detail.bankOfDeposit || '--' }}</span></div>
<div class="info-item"><span class="label">账户余额</span><span class="value">{{ fmtMoney(detail.accountBalance) }}</span></div>
<div class="info-item"><span class="label">开户名称</span><span class="value">{{ detail.accountName || '--' }}</span></div>
<div class="info-item"><span class="label">银行账号</span><span class="value mono">{{ detail.accountCard || '--' }}</span></div>
</div>
</section>
</div>
@ -220,21 +434,29 @@ onMounted(load)
<!-- ===== Tab 2: 资质认证 ===== -->
<div v-show="activeTab === 'qual'" class="tab-body">
<section class="info-card full">
<div class="card-title">营业执照</div>
<div class="card-head">
<el-icon class="card-head-icon"><DocumentChecked /></el-icon>
<span class="card-title-text">营业执照</span>
<span class="card-sub">点击图片可放大预览</span>
</div>
<el-image v-if="detail.licenseImg" :src="detail.licenseImg" fit="contain" class="cert-img" :preview-src-list="[detail.licenseImg]" preview-teleported />
<div v-else class="cert-empty">暂无营业执照</div>
<div v-else class="cert-empty"><span>暂无营业执照</span></div>
</section>
<section class="info-card full">
<div class="card-title">法人身份证</div>
<div class="card-head">
<el-icon class="card-head-icon"><DocumentChecked /></el-icon>
<span class="card-title-text">法人身份证</span>
<span class="card-sub">点击图片可放大预览</span>
</div>
<div class="idcard-row">
<div class="idcard-cell">
<span class="label">正面</span>
<span class="label">正面(人像面)</span>
<el-image v-if="detail.frontImage" :src="detail.frontImage" fit="cover" class="idcard-img" :preview-src-list="[detail.frontImage]" preview-teleported />
<div v-else class="cert-empty small">暂无</div>
</div>
<div class="idcard-cell">
<span class="label">反面</span>
<span class="label">反面(国徽面)</span>
<el-image v-if="detail.backImage" :src="detail.backImage" fit="cover" class="idcard-img" :preview-src-list="[detail.backImage]" preview-teleported />
<div v-else class="cert-empty small">暂无</div>
</div>
@ -243,33 +465,70 @@ onMounted(load)
</section>
<section class="info-card full">
<div class="card-title">资质证书</div>
<div class="card-head">
<el-icon class="card-head-icon"><Medal /></el-icon>
<span class="card-title-text">资质证书</span>
<span class="card-sub" v-if="otherList.length">共 {{ otherList.length }} 项</span>
</div>
<template v-if="otherList.length">
<div v-for="(cert, i) in otherList" :key="i" class="cert-card">
<div class="cert-info">
<div class="cert-row"><span class="label">资质名称</span><span class="value">{{ cert.qualificationName || '--' }}</span></div>
<div class="cert-row"><span class="label">证书编号</span><span class="value">{{ cert.certificateNumber || '--' }}</span></div>
<div class="cert-row"><span class="label">有效期至</span><span class="value">{{ cert.expirationDate || '--' }}</span></div>
</div>
<div v-if="cert.imageList?.length" class="cert-imgs">
<el-image v-for="(img, j) in cert.imageList" :key="j" :src="img" fit="cover" class="cert-thumb" :preview-src-list="cert.imageList" preview-teleported />
<div class="cert-card" v-for="(cert, i) in otherList" :key="i">
<div class="cert-main">
<div class="cert-info">
<div class="cert-row"><span class="label">资质名称</span><span class="value">{{ cert.qualificationName || '--' }}</span></div>
<div class="cert-row"><span class="label">证书编号</span><span class="value mono">{{ cert.certificateNumber || '--' }}</span></div>
<div class="cert-row"><span class="label">有效期至</span>
<span class="value">{{ cert.expirationDate || '--' }}
<el-tag v-if="cert.isExpiration === 1" type="danger" size="small" style="margin-left:6px">已过期</el-tag>
<el-tag v-else-if="cert.expirationDate" type="success" size="small" style="margin-left:6px">有效</el-tag>
</span>
</div>
</div>
<div v-if="cert.imageList?.length" class="cert-imgs">
<el-image v-for="(img, j) in cert.imageList" :key="j" :src="img" fit="cover" class="cert-thumb" :preview-src-list="cert.imageList" preview-teleported />
</div>
</div>
</div>
</template>
<div v-else class="cert-empty">暂无资质证书</div>
<div v-else class="cert-empty"><span>暂无资质证书</span></div>
</section>
<section class="info-card full">
<div class="card-title">企业授权书</div>
<div class="card-head">
<el-icon class="card-head-icon"><DocumentChecked /></el-icon>
<span class="card-title-text">企业授权书</span>
<span class="card-sub">点击图片可放大预览</span>
</div>
<el-image v-if="detail.enterpriseAccredit" :src="detail.enterpriseAccredit" fit="cover" class="cert-img tall" :preview-src-list="[detail.enterpriseAccredit]" preview-teleported />
<div v-else class="cert-empty">暂无授权书</div>
<div v-else class="cert-empty"><span>暂无授权书</span></div>
</section>
</div>
<!-- ===== Tab 3: 履约情况 ===== -->
<div v-show="activeTab === 'perf'" class="tab-body">
<div class="kpi-grid">
<div class="kpi-card">
<div class="kpi-label">企业评分</div>
<div class="kpi-value primary">{{ fmtNum(detail.enterpriseScore) }}</div>
</div>
<div class="kpi-card">
<div class="kpi-label">好评率</div>
<div class="kpi-value">{{ fmtNum(detail.enterpriseRate) }}<span class="kpi-unit">%</span></div>
</div>
<div class="kpi-card">
<div class="kpi-label">评价总数</div>
<div class="kpi-value">{{ fmtNum(detail.enterpriseCommentNum) }}</div>
</div>
<div class="kpi-card">
<div class="kpi-label">在售商品</div>
<div class="kpi-value">{{ fmtNum(detail.physicalGoodsNum) }}</div>
</div>
</div>
<section class="info-card full">
<div class="card-title">履约评分记录</div>
<div class="card-head">
<el-icon class="card-head-icon"><Star /></el-icon>
<span class="card-title-text">履约评分记录</span>
</div>
<template v-if="performanceList.length">
<div v-for="(item, i) in performanceList" :key="i" class="perf-item">
<span class="perf-date">{{ fmtDate(item.date) }}</span>
@ -277,24 +536,91 @@ onMounted(load)
<span class="perf-score">{{ item.score ?? '--' }} 分</span>
</div>
</template>
<div v-else class="cert-empty">暂无履约评分记录</div>
<div v-else class="perf-empty">暂无履约评分记录</div>
</section>
</div>
<!-- ===== 审核弹窗 ===== -->
<el-dialog v-model="auditVisible" title="企业认证审核" width="480px" :close-on-click-modal="false" destroy-on-close>
<div class="audit-dialog-tip">
正在审核:<strong>{{ detail.enterpriseName || '--' }}</strong>
</div>
<el-form label-width="80px" label-position="left">
<el-form-item label="审核结果">
<el-radio-group v-model="auditForm.result">
<el-radio :value="2">审核通过</el-radio>
<el-radio :value="3">审核驳回</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="auditForm.result === 3" label="驳回原因" required>
<el-input
v-model="auditForm.failReason"
type="textarea" :rows="4"
placeholder="请填写驳回原因,将随站内消息与短信通知企业"
maxlength="200" show-word-limit
/>
</el-form-item>
<el-form-item v-else label="通过说明">
<div class="pass-tip">审核通过后将自动:完成企业认证、初始化审批流、创建默认分类与默认支付方式,并通过站内消息与短信通知企业。</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="auditVisible = false">取消</el-button>
<el-button type="primary" :loading="auditBusy" @click="submitAudit">确定提交</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped lang="scss">
.ent-detail { max-width: 1400px; }
/* ===== Header ===== */
/* ===== ① 状态横幅 ===== */
.audit-banner {
display: flex; align-items: center; gap: 18px;
border-radius: var(--radius-lg); padding: 16px 22px; margin-bottom: 16px;
border: 1px solid transparent;
}
.banner-warning {
background: linear-gradient(90deg, rgba(245,158,11,.14), rgba(245,158,11,.04));
border-color: rgba(245,158,11,.35);
.banner-icon { background: rgba(245,158,11,.18); color: #b45309; }
}
.banner-success {
background: linear-gradient(90deg, rgba(34,197,94,.12), rgba(34,197,94,.03));
border-color: rgba(34,197,94,.3);
.banner-icon { background: rgba(34,197,94,.18); color: #15803d; }
}
.banner-danger {
background: linear-gradient(90deg, rgba(239,68,68,.12), rgba(239,68,68,.03));
border-color: rgba(239,68,68,.32);
.banner-icon { background: rgba(239,68,68,.16); color: #b91c1c; }
}
.banner-unknown {
background: var(--bg, #f6f8fb); border-color: var(--border);
.banner-icon { background: var(--border); color: var(--ink-3); }
}
.banner-icon {
flex: 0 0 48px; width: 48px; height: 48px; border-radius: 12px;
display: flex; align-items: center; justify-content: center;
}
.banner-main { flex: 1; min-width: 0; }
.banner-title { font-size: 16px; font-weight: 700; color: var(--ink); }
.banner-desc { font-size: 13px; color: var(--ink-2); margin-top: 4px; line-height: 1.6; }
.reason-text { color: #b91c1c; font-weight: 600; }
.banner-badges { display: flex; gap: 8px; margin-top: 8px; }
.banner-actions { display: flex; gap: 10px; flex-shrink: 0; }
/* ===== ② 头部信息卡 ===== */
.detail-header {
display: flex; gap: 24px; align-items: center;
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius-lg); padding: 24px 28px; margin-bottom: 16px;
}
.header-logo {
flex: 0 0 88px; width: 88px; height: 88px; border-radius: 12px; overflow: hidden;
flex: 0 0 88px; width: 88px; height: 88px; border-radius: 14px; overflow: hidden;
background: linear-gradient(135deg, var(--primary), var(--primary-soft, #93c5fd));
box-shadow: 0 6px 16px -8px var(--primary);
display: flex; align-items: center; justify-content: center;
}
.logo-img { width: 100%; height: 100%; }
@ -306,16 +632,21 @@ onMounted(load)
background: var(--primary-soft, #e0ecff); color: var(--primary);
font-size: 12px; padding: 2px 12px; border-radius: 20px; white-space: nowrap;
}
.header-meta { display: flex; gap: 20px; flex-wrap: wrap; color: var(--ink-2); font-size: 13px; margin-bottom: 10px; }
.meta-item::before { content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--primary); margin-right: 6px; vertical-align: middle; }
.header-meta { display: flex; gap: 20px; flex-wrap: wrap; color: var(--ink-2); font-size: 13px; }
.meta-item::before {
content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%;
background: var(--primary); margin-right: 6px; vertical-align: middle;
}
.header-right { display: flex; flex-direction: column; align-items: flex-end; gap: 14px; }
.header-stats { display: flex; gap: 32px; }
.stat-cell { text-align: center; }
.stat-value { font-size: 20px; font-weight: 700; color: var(--ink); }
.stat-value { font-size: 18px; font-weight: 700; color: var(--ink); }
.stat-label { font-size: 12px; color: var(--ink-3); margin-top: 4px; }
.header-btns { display: flex; gap: 10px; }
.store-btn { font-weight: 600; }
.mono { font-family: 'SF Mono', Menlo, Consolas, monospace; font-size: 13px; }
/* ===== Tabs ===== */
/* ===== Tab 导航 ===== */
.tab-nav {
display: flex; gap: 36px; border-bottom: 1px solid var(--border);
padding: 0 8px; margin-bottom: 20px;
@ -327,7 +658,7 @@ onMounted(load)
.tab-item:hover { color: var(--ink); }
.tab-item.active { color: var(--primary); font-weight: 600; border-bottom-color: var(--primary); }
/* ===== Body / cards ===== */
/* ===== 卡片通用 ===== */
.tab-body { display: flex; flex-direction: column; gap: 16px; }
.info-grid { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 16px; }
.info-col { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
@ -336,10 +667,17 @@ onMounted(load)
border-radius: var(--radius-lg); padding: 22px 24px;
}
.info-card.full { width: 100%; }
.card-title {
font-size: 15px; font-weight: 700; color: var(--ink); margin-bottom: 18px;
padding-left: 10px; border-left: 3px solid var(--primary);
.card-head {
display: flex; align-items: center; gap: 8px; margin-bottom: 18px;
}
.card-head-icon {
width: 26px; height: 26px; border-radius: 8px;
background: var(--primary-soft, #e0ecff); color: var(--primary);
display: flex; align-items: center; justify-content: center;
}
.card-title-text { font-size: 15px; font-weight: 700; color: var(--ink); }
.card-sub { font-size: 12px; color: var(--ink-3); margin-left: auto; }
.info-grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px 48px; }
.info-item { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.info-item.span2 { grid-column: span 2; }
@ -371,30 +709,50 @@ onMounted(load)
font-size: 12px; padding: 3px 14px; border-radius: 6px;
}
/* ===== 时间线 ===== */
.timeline-card { margin-bottom: 16px; }
.audit-timeline { padding-left: 6px; }
.tl-content { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.tl-title { font-size: 14px; font-weight: 600; color: var(--ink); }
.tl-desc { font-size: 13px; color: var(--ink-2); }
.tl-operator { font-size: 12px; color: var(--ink-3); background: var(--bg, #f6f8fb); padding: 2px 10px; border-radius: 12px; }
/* ===== 资质 ===== */
.cert-img { width: 220px; height: 150px; border-radius: 8px; border: 1px solid var(--border); background: var(--bg, #f6f8fb); }
.cert-img.tall { width: 160px; height: 200px; }
.cert-img {
width: 240px; height: 160px; border-radius: 8px; border: 1px solid var(--border);
background: var(--bg, #f6f8fb); cursor: zoom-in;
}
.cert-img.tall { width: 160px; height: 210px; }
.cert-empty {
width: 220px; height: 120px; border: 1px dashed var(--border); border-radius: 8px;
width: 240px; height: 120px; border: 1px dashed var(--border); border-radius: 8px;
display: flex; align-items: center; justify-content: center;
color: var(--ink-3); font-size: 13px; background: var(--bg, #f6f8fb);
}
.cert-empty.small { width: 150px; height: 100px; }
.idcard-row { display: flex; gap: 24px; margin-bottom: 16px; }
.cert-empty.small { width: 160px; height: 110px; }
.idcard-row { display: flex; gap: 32px; margin-bottom: 16px; }
.idcard-cell { display: flex; flex-direction: column; gap: 8px; }
.idcard-img { width: 180px; height: 120px; border-radius: 8px; border: 1px solid var(--border); }
.idcard-img { width: 200px; height: 130px; border-radius: 8px; border: 1px solid var(--border); cursor: zoom-in; }
.legal-row { display: flex; gap: 12px; align-items: baseline; }
.cert-card {
border: 1px solid var(--border); border-radius: 10px; padding: 16px 18px;
margin-bottom: 14px; background: var(--bg, #fafbfd);
}
.cert-row { display: flex; gap: 12px; margin-bottom: 6px; }
.cert-row { display: flex; gap: 12px; margin-bottom: 6px; align-items: baseline; }
.cert-row .label { min-width: 76px; }
.cert-imgs { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 10px; }
.cert-thumb { width: 100px; height: 120px; border-radius: 6px; border: 1px solid var(--border); }
.cert-imgs { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 12px; }
.cert-thumb { width: 96px; height: 120px; border-radius: 6px; border: 1px solid var(--border); cursor: zoom-in; }
/* ===== 履约 ===== */
.kpi-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; }
.kpi-card {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius-lg); padding: 20px 24px;
}
.kpi-label { font-size: 12px; color: var(--ink-3); margin-bottom: 10px; }
.kpi-value { font-size: 28px; font-weight: 700; color: var(--ink); line-height: 1; }
.kpi-value.primary { color: var(--primary); }
.kpi-unit { font-size: 14px; color: var(--ink-3); margin-left: 2px; }
.perf-item {
display: flex; align-items: center; gap: 16px; padding: 12px 0;
border-bottom: 1px solid var(--border-light, var(--border));
@ -403,9 +761,23 @@ onMounted(load)
.perf-date { font-size: 13px; color: var(--ink-3); min-width: 140px; }
.perf-content { flex: 1; font-size: 14px; color: var(--ink); }
.perf-score { font-size: 15px; font-weight: 700; color: var(--primary); }
.perf-empty { color: var(--ink-3); font-size: 13px; padding: 24px 0; text-align: center; }
/* ===== 审核弹窗 ===== */
.audit-dialog-tip {
background: var(--bg, #f6f8fb); border: 1px solid var(--border);
border-radius: 8px; padding: 10px 14px; font-size: 13px; color: var(--ink-2);
margin-bottom: 18px;
}
.pass-tip {
font-size: 12px; color: var(--ink-3); line-height: 1.7;
background: var(--bg, #f6f8fb); border-radius: 8px; padding: 10px 12px; width: 100%;
}
@media (max-width: 1100px) {
.info-grid { grid-template-columns: 1fr; }
.header-stats { display: none; }
.kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.banner-actions { flex-direction: column; }
}
</style>
</style>