332 lines
9.3 KiB
Vue
332 lines
9.3 KiB
Vue
|
|
<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>
|