admin_new/src/modules/user/enterprise/EnterpriseDetailView.vue

412 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
/**
* F006 企业详情页 - 独立页面
*
* 参考老 vue user/enterprise/enterprise-detail.vue 设计 + 新系统主题规范
* 数据源:
* - GET /user/enterprise/getInfo?enterpriseId= 企业全量信息(80+字段)
* - GET /user/performancescorerecord/page?enterpriseId= 履约评分记录
* 3 Tab: 基本信息 / 资质认证 / 履约情况
*/
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Shop } from '@element-plus/icons-vue'
import { httpGet } from '@/utils/request'
import PageHeader from '@/components/PageHeader/PageHeader.vue'
const route = useRoute()
const router = useRouter()
const enterpriseId = computed(() => String(route.params.id || ''))
const loading = ref(false)
const activeTab = ref('basic')
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' }
}
const LEVEL_MAP: Record<number, string> = { 1: 'A级', 2: 'B级', 3: 'C级', 4: 'D级' }
const isCertified = computed(() => detail.value.status === 2)
const logoText = computed(() => (detail.value.enterpriseName || '企').trim().charAt(0))
const registerYear = computed(() => {
const t = detail.value.registerTime
return t ? String(t).slice(0, 10) : '--'
})
const contactInitial = computed(() => (detail.value.linkMan || '联').trim().charAt(0))
const tags = computed<string[]>(() => {
const raw = detail.value.supplierLabelList || detail.value.supplierLabel || ''
if (Array.isArray(raw)) return raw
return String(raw).split(/[,,、;;\s]+/).map((s: string) => s.trim()).filter(Boolean)
})
const productList = computed<string[]>(() => {
const raw = detail.value.mainProducts
if (!raw) return []
return String(raw).split(/[,,、;;\s]+/).map((s: string) => s.trim()).filter(Boolean)
})
function maskMobile(m: string | number | null | undefined): string {
if (m === undefined || m === null || m === '') return '--'
const s = String(m)
return s.length === 11 ? `${s.slice(0, 3)}****${s.slice(7)}` : s
}
function fmtDate(d: string | null | undefined): string {
if (!d) return '--'
return String(d).slice(0, 19).replace('T', ' ')
}
function fmtMoney(n: any): string {
const v = Number(n || 0)
return v ? `¥ ${v.toLocaleString()}` : '¥ 0'
}
async function load() {
loading.value = true
try {
const resp: any = await httpGet('/user/enterprise/getInfo', { enterpriseId: enterpriseId.value })
if (resp) {
detail.value = resp
otherList.value = (resp.enterpriseOtherList || []).map((it: any) => ({
...it,
imageList: it.images ? String(it.images).split(',').map((s: string) => s.trim()).filter(Boolean) : []
}))
}
} catch { detail.value = {} } finally { loading.value = false }
try {
const r: any = await httpGet('/user/performancescorerecord/page', { enterpriseId: enterpriseId.value, page: 1, limit: 20 })
performanceList.value = (r?.list || []).map((it: any) => ({
date: it.createDate,
content: `评分员「${it.userName || '--'}」发起履约评分`,
score: it.averageScore ?? it.score
}))
} catch { performanceList.value = [] }
}
function goStore() {
const eid = detail.value.enterpriseId || detail.value.id
if (!eid) return
window.open(`/v2/shop/${eid}`, '_blank')
}
onMounted(load)
</script>
<template>
<div class="ent-detail" v-loading="loading">
<PageHeader title="企业详情" :sub="`入驻企业信息总览`">
<el-button @click="router.back()">返回列表</el-button>
</PageHeader>
<!-- ===== Header 区: logo + 企业名 + 认证标签 + meta ===== -->
<div class="detail-header">
<div class="header-logo">
<el-image v-if="detail.enterpriseLogo" :src="detail.enterpriseLogo" fit="cover" class="logo-img" />
<span v-else class="logo-fallback">{{ logoText }}</span>
</div>
<div class="header-main">
<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>
</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="detail.creditCode" class="meta-item">信用代码 {{ detail.creditCode }}</span>
</div>
</div>
<div class="header-right">
<div class="header-stats">
<div class="stat-cell">
<div class="stat-value">{{ LEVEL_MAP[detail.supplierLevel as number] || '--' }}</div>
<div class="stat-label">供应商等级</div>
</div>
<div class="stat-cell">
<div class="stat-value">{{ detail.currentScore ?? '--' }}</div>
<div class="stat-label">当前评分</div>
</div>
<div class="stat-cell">
<div class="stat-value">{{ detail.enterpriseRate ?? '--' }}</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>
</div>
<!-- ===== Tab 导航 ===== -->
<div class="tab-nav">
<div class="tab-item" :class="{ active: activeTab === 'basic' }" @click="activeTab = 'basic'">基本信息</div>
<div class="tab-item" :class="{ active: activeTab === 'qual' }" @click="activeTab = 'qual'">资质认证</div>
<div class="tab-item" :class="{ active: activeTab === 'perf' }" @click="activeTab = 'perf'">履约情况</div>
</div>
<!-- ===== Tab 1: 基本信息 ===== -->
<div v-show="activeTab === 'basic'" class="tab-body">
<div class="info-grid">
<div class="info-col">
<section class="info-card">
<div class="card-title">公司信息</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>
</section>
<section class="info-card">
<div class="card-title">地址信息</div>
<div class="address-box">
<div class="addr-line">{{ [detail.provinceName, detail.cityName, detail.areaName].filter(Boolean).join(' ') || '--' }}</div>
<div class="value">{{ detail.enterpriseAddress || '--' }}</div>
</div>
</section>
</div>
<div class="info-col">
<section class="info-card">
<div class="card-title">联系人信息</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>
</section>
</div>
</div>
<section class="info-card full">
<div class="card-title">经营信息</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>
<span class="product-tags">
<span v-for="(p, i) in productList" :key="i" class="product-tag">{{ p }}</span>
<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">{{ fmtMoney(detail.accountBalance) }}</span></div>
</div>
</section>
</div>
<!-- ===== Tab 2: 资质认证 ===== -->
<div v-show="activeTab === 'qual'" class="tab-body">
<section class="info-card full">
<div class="card-title">营业执照</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>
</section>
<section class="info-card full">
<div class="card-title">法人身份证</div>
<div class="idcard-row">
<div class="idcard-cell">
<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>
<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>
</div>
<div v-if="detail.legalMan" class="legal-row"><span class="label">法人代表</span><span class="value">{{ detail.legalMan }}</span></div>
</section>
<section class="info-card full">
<div class="card-title">资质证书</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>
</div>
</template>
<div v-else class="cert-empty">暂无资质证书</div>
</section>
<section class="info-card full">
<div class="card-title">企业授权书</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>
</section>
</div>
<!-- ===== Tab 3: 履约情况 ===== -->
<div v-show="activeTab === 'perf'" class="tab-body">
<section class="info-card full">
<div class="card-title">履约评分记录</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>
<span class="perf-content">{{ item.content }}</span>
<span class="perf-score">{{ item.score ?? '--' }} 分</span>
</div>
</template>
<div v-else class="cert-empty">暂无履约评分记录</div>
</section>
</div>
</div>
</template>
<style scoped lang="scss">
.ent-detail { max-width: 1400px; }
/* ===== Header ===== */
.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;
background: linear-gradient(135deg, var(--primary), var(--primary-soft, #93c5fd));
display: flex; align-items: center; justify-content: center;
}
.logo-img { width: 100%; height: 100%; }
.logo-fallback { color: #fff; font-size: 32px; font-weight: 700; }
.header-main { flex: 1; min-width: 0; }
.header-title-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
.ent-name { font-size: 22px; font-weight: 700; margin: 0; color: var(--ink); }
.name-tag {
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-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-label { font-size: 12px; color: var(--ink-3); margin-top: 4px; }
.store-btn { font-weight: 600; }
/* ===== Tabs ===== */
.tab-nav {
display: flex; gap: 36px; border-bottom: 1px solid var(--border);
padding: 0 8px; margin-bottom: 20px;
}
.tab-item {
font-size: 15px; color: var(--ink-3); padding: 10px 4px; cursor: pointer;
border-bottom: 2px solid transparent; transition: all .2s;
}
.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; }
.info-card {
background: var(--surface); border: 1px solid var(--border);
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);
}
.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; }
.label { font-size: 12px; color: var(--ink-3); }
.value { font-size: 14px; color: var(--ink); font-weight: 500; word-break: break-word; }
/* 地址 */
.address-box {
background: var(--bg, #f6f8fb); border: 1px dashed var(--border);
border-radius: 10px; padding: 16px 18px; display: flex; flex-direction: column; gap: 8px;
}
.addr-line { font-size: 13px; color: var(--primary); font-weight: 600; }
/* 联系人 */
.contact-profile { display: flex; flex-direction: column; align-items: center; margin-bottom: 20px; }
.contact-avatar {
width: 72px; height: 72px; border-radius: 50%;
background: var(--primary-soft, #e0ecff); color: var(--primary);
font-size: 26px; font-weight: 700; display: flex; align-items: center; justify-content: center;
}
.contact-name { margin-top: 12px; font-size: 16px; font-weight: 700; color: var(--ink); }
.contact-methods { display: flex; flex-direction: column; gap: 14px; }
.contact-method { display: flex; flex-direction: column; gap: 4px; }
/* 产品标签 */
.product-tags { display: flex; flex-wrap: wrap; gap: 8px; }
.product-tag {
background: var(--primary-soft, #e0ecff); color: var(--primary);
font-size: 12px; padding: 3px 14px; border-radius: 6px;
}
/* ===== 资质 ===== */
.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-empty {
width: 220px; 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; }
.idcard-cell { display: flex; flex-direction: column; gap: 8px; }
.idcard-img { width: 180px; height: 120px; border-radius: 8px; border: 1px solid var(--border); }
.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 .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); }
/* ===== 履约 ===== */
.perf-item {
display: flex; align-items: center; gap: 16px; padding: 12px 0;
border-bottom: 1px solid var(--border-light, var(--border));
}
.perf-item:last-child { border-bottom: none; }
.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); }
@media (max-width: 1100px) {
.info-grid { grid-template-columns: 1fr; }
.header-stats { display: none; }
}
</style>