aiccs/src/views/comprehensive/components/CreditArchive.vue

332 lines
9.3 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.

<template>
<div v-loading="loading" class="credit-archive">
<div class="toolbar">
<span class="toolbar-label">年报年度:</span>
<el-select
v-model="reportYear"
size="small"
clearable
placeholder="默认最近一年"
style="width: 160px;"
@change="loadData"
>
<el-option
v-for="y in yearOptions"
:key="y"
:label="`${y} 年`"
:value="y"
/>
</el-select>
<el-button
type="text"
icon="el-icon-refresh"
style="margin-left: 12px;"
@click="resetGraph"
>重置图谱</el-button>
<span class="toolbar-tip">提示:点击一级分支可展开/收起明细,单分支最多展示 {{ maxNodes }} 个节点</span>
</div>
<div ref="graph" class="graph-container" />
<!-- 节点详情弹窗 -->
<el-dialog
:visible.sync="detailVisible"
:title="detailTitle"
width="560px"
append-to-body
>
<el-descriptions v-if="detailData" :column="1" border size="small">
<el-descriptions-item
v-for="(value, key) in detailData"
:key="key"
:label="fieldLabel(key)"
>{{ value || '—' }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script>
import { getCreditArchive } from '@/api/comprehensive'
const MAX_NODES = 20
const CURRENT_YEAR = new Date().getFullYear()
const FIELD_LABEL_MAP = {
name: '姓名',
entName: '企业名称',
uniscid: '统一社会信用代码',
certType: '证件类型',
subConAm: '认缴出资额',
subConProp: '认缴出资比例(%',
respForm: '责任形式',
position: '职务',
lerepsign: '法定代表人标志',
tel: '联系电话',
email: '电子邮箱',
invid: '股东ID',
outinvid: '对外投资ID',
lmid: '联络员ID'
}
const BRANCH_DEFS = [
{ key: 'shareholders', label: '股东信息', color: '#5470c6' },
{ key: 'legalPersons', label: '法定代表人', color: '#91cc75' },
{ key: 'investments', label: '对外投资', color: '#fac858' },
{ key: 'contacts', label: '联络人', color: '#ee6666' }
]
export default {
name: 'CreditArchive',
props: {
pripid: {
type: String,
required: true
},
entName: {
type: String,
default: ''
}
},
data() {
return {
loading: false,
chart: null,
reportYear: null,
yearOptions: this.buildYearOptions(),
maxNodes: MAX_NODES,
archiveData: {
shareholders: [],
legalPersons: [],
investments: [],
contacts: []
},
truncated: {
shareholders: false,
investments: false,
contacts: false
},
branches: BRANCH_DEFS,
detailVisible: false,
detailTitle: '',
detailData: null
}
},
mounted() {
this.loadData()
window.addEventListener('resize', this.resize)
},
beforeDestroy() {
window.removeEventListener('resize', this.resize)
if (this.chart) {
this.chart.dispose()
this.chart = null
}
},
methods: {
buildYearOptions() {
const list = []
for (let y = CURRENT_YEAR; y >= CURRENT_YEAR - 9; y--) {
list.push(y)
}
return list
},
fieldLabel(key) {
return FIELD_LABEL_MAP[key] || key
},
async loadData() {
if (!this.pripid) return
this.loading = true
try {
const res = await getCreditArchive({
pripid: this.pripid,
reportYear: this.reportYear || undefined
})
const data = (res && res.data) || res || {}
this.archiveData = {
shareholders: data.shareholders || [],
legalPersons: data.legalPersons || [],
investments: data.investments || [],
contacts: data.contacts || []
}
this.truncated = data.truncated || { shareholders: false, investments: false, contacts: false }
// Q5首次加载用后端返回的 reportYear 作为默认选中
if (!this.reportYear && data.reportYear) {
this.reportYear = data.reportYear
}
this.$nextTick(() => this.renderGraph())
} catch (e) {
this.$message.error('信用档案数据加载失败')
} finally {
this.loading = false
}
},
renderGraph() {
if (!this.$refs.graph) return
if (!this.chart) {
this.chart = this.$echarts.init(this.$refs.graph)
this.chart.on('click', params => this.handleNodeClick(params))
}
const centerName = this.entName || this.archiveData.entName || '主体'
const centerNode = {
id: 'center',
name: centerName,
symbolSize: 80,
category: 0,
fixed: true,
x: 0,
y: 0,
itemStyle: { color: '#409EFF' },
label: { fontWeight: 'bold' }
}
const branchNodes = this.branches.map((b, i) => ({
id: b.key,
name: `${b.label} (${(this.archiveData[b.key] || []).length})`,
symbolSize: 50,
category: i + 1,
itemStyle: { color: b.color }
}))
const branchLinks = this.branches.map(b => ({
source: 'center',
target: b.key,
lineStyle: { color: '#aaa', width: 2 }
}))
this.chart.setOption({
tooltip: {
trigger: 'item',
formatter: params => {
if (params.dataType === 'edge') return ''
return params.name
}
},
legend: [{
data: this.branches.map(b => b.label),
bottom: 0
}],
series: [{
type: 'graph',
layout: 'force',
roam: true,
draggable: true,
label: { show: true, position: 'right', formatter: '{b}' },
force: { repulsion: 400, edgeLength: 120 },
categories: [
{ name: '主体' },
...this.branches.map(b => ({ name: b.label }))
],
data: [centerNode, ...branchNodes],
links: branchLinks,
lineStyle: { color: 'source', curveness: 0.1 }
}]
}, true)
},
handleNodeClick(params) {
// 点击中心节点 / 边 → 忽略
if (!params.data || params.dataType === 'edge') return
const branch = this.branches.find(b => b.key === params.data.id)
if (branch) {
this.toggleBranch(branch)
return
}
// 二级节点点击 → 弹出明细
if (params.data.rawData) {
const parentKey = String(params.data.id).split('-')[0]
const parentBranch = this.branches.find(b => b.key === parentKey)
this.detailTitle = parentBranch ? `${parentBranch.label}详情` : '详情'
this.detailData = params.data.rawData
this.detailVisible = true
}
},
toggleBranch(branch) {
const list = this.archiveData[branch.key] || []
if (!list.length) {
this.$message.info(`暂无${branch.label}`)
return
}
const option = this.chart.getOption()
const series = option.series[0]
const prefix = branch.key + '-'
const hasChild = series.data.some(n => String(n.id).startsWith(prefix))
if (hasChild) {
// 收起
series.data = series.data.filter(n => !String(n.id).startsWith(prefix))
series.links = series.links.filter(l => !String(l.target).startsWith(prefix))
} else {
// Q11单分支最多展开 MAX_NODES 个节点,超出部分显示"更多..."
const displayList = list.slice(0, MAX_NODES)
const categoryIndex = this.branches.indexOf(branch) + 1
displayList.forEach((item, i) => {
const nodeId = `${branch.key}-${i}`
series.data.push({
id: nodeId,
name: this.getNodeName(branch.key, item),
symbolSize: 30,
category: categoryIndex,
itemStyle: { color: branch.color, opacity: 0.7 },
rawData: item
})
series.links.push({ source: branch.key, target: nodeId })
})
// 后端返回的截断标志或前端二次判断
const isTruncated = this.truncated[branch.key] || list.length > MAX_NODES
if (isTruncated) {
const moreId = `${branch.key}-more`
series.data.push({
id: moreId,
name: `更多... (共 ${list.length}${list.length > MAX_NODES ? '+' : ''} 项)`,
symbolSize: 26,
category: categoryIndex,
itemStyle: { color: branch.color, opacity: 0.4 }
})
series.links.push({ source: branch.key, target: moreId })
}
}
this.chart.setOption(option, true)
},
getNodeName(key, item) {
if (key === 'investments') return item.entName || '—'
if (key === 'shareholders') return item.name || '—'
return item.name || '—'
},
resetGraph() {
this.renderGraph()
},
resize() {
if (this.chart) this.chart.resize()
}
}
}
</script>
<style lang="scss" scoped>
.credit-archive {
.toolbar {
display: flex;
align-items: center;
padding: 10px 12px;
background: #fafafa;
border: 1px solid #ebeef5;
border-bottom: none;
.toolbar-label {
font-size: 13px;
color: #606266;
margin-right: 6px;
}
.toolbar-tip {
margin-left: auto;
font-size: 12px;
color: #909399;
}
}
.graph-container {
width: 100%;
height: 600px;
border: 1px solid #ebeef5;
background: #fff;
}
}
</style>