提交 f04b1b70 authored 作者: 刘宇琪's avatar 刘宇琪

refactor -刘宇琪 实体清单-深度挖掘-echarts绘图重构为g6绘图

上级 80718eb3
...@@ -598,7 +598,7 @@ ...@@ -598,7 +598,7 @@
</template> </template>
<script setup> <script setup>
import NewsList from "@/components/NewsList/NewsList.vue"; import NewsList from "@/components/base/newsList/index.vue";
import RiskSignal from "@/components/RiskSignal/RiskSignal.vue"; import RiskSignal from "@/components/RiskSignal/RiskSignal.vue";
import { onMounted, ref, computed, reactive, shallowRef, watch, nextTick } from "vue"; import { onMounted, ref, computed, reactive, shallowRef, watch, nextTick } from "vue";
import { useContainerScroll } from "@/hooks/useScrollShow"; import { useContainerScroll } from "@/hooks/useScrollShow";
......
<template>
<div class="relation-graph-wrapper">
<div class="graph-controls">
<div
v-for="item in controlBtns"
:key="item.type"
:class="['control-btn', { 'control-btn-active': currentLayoutType === item.type }]"
@click="handleClickControlBtn(item.type)"
>
<img :src="item.icon" alt="" />
</div>
</div>
<div ref="containerRef" class="graph-container"></div>
<div v-if="selectedNode" class="node-popup">
<div class="popup-header">
<img :src="selectedNode.image || defaultIcon" alt="" class="popup-icon" />
<div class="popup-title">{{ selectedNode.name }}</div>
<el-icon class="close-icon" @click="selectedNode = null">
<Close />
</el-icon>
</div>
<div class="popup-body">
<div v-if="selectedNode.isSanctioned" class="tag-row">
<span class="red-dot"></span>
<span class="red-text">被制裁实体</span>
</div>
<p class="desc">{{ selectedNode.description || '暂无描述' }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
import G6 from '@antv/g6'
import { Close } from '@element-plus/icons-vue'
import echartsIcon01 from '../assets/echartsicon01.png'
import echartsIcon02 from '../assets/echartsicon02.png'
import echartsIcon03 from '../assets/echartsicon03.png'
import defaultIcon from '../assets/echartsicon03.png'
const props = defineProps({
graphData: {
type: Object,
default: () => ({ nodes: [], links: [] })
},
treeData: {
type: Object,
default: () => null
},
controlActive: {
type: Number,
default: 1
}
})
const emit = defineEmits(['nodeClick', 'layoutChange'])
const containerRef = ref(null)
const graphInstance = ref(null)
const currentLayoutType = ref(1)
const selectedNode = ref(null)
const controlBtns = [
{ type: 1, icon: echartsIcon01, name: '力导向布局' },
{ type: 2, icon: echartsIcon02, name: '树布局' },
{ type: 3, icon: echartsIcon03, name: '环状布局' }
]
const initGraph = (layoutType = 1) => {
if (!containerRef.value) return
destroyGraph()
nextTick(() => {
const container = containerRef.value
const width = container.offsetWidth || 800
const height = container.offsetHeight || 600
if (layoutType === 2) {
initTreeGraph(width, height)
} else if (layoutType === 3) {
initCircularGraph(width, height)
} else {
initNormalGraph(layoutType, width, height)
}
})
}
const initNormalGraph = (layoutType, width, height) => {
const data = processGraphData(props.graphData)
if (!data.nodes || data.nodes.length === 0) return
const layout = {
type: 'force',
center: [width / 2, height / 2],
preventOverlap: true,
nodeSpacing: 80,
linkDistance: 250,
nodeStrength: -800,
edgeStrength: 0.1,
collideStrength: 0.8,
alphaDecay: 0.01,
alphaMin: 0.001
}
graphInstance.value = new G6.Graph({
container: containerRef.value,
width,
height,
fitView: true,
fitViewPadding: 100,
fitCenter: true,
animate: true,
animateCfg: {
duration: 300,
easing: 'easeLinear'
},
minZoom: 0.1,
maxZoom: 10,
modes: {
default: [
'drag-canvas',
'zoom-canvas',
'drag-node',
{
type: 'activate-relations',
trigger: 'mouseenter',
resetSelected: true
}
]
},
layout,
defaultNode: {
type: 'image',
size: 40,
clipCfg: {
show: true,
type: 'circle',
r: 20
},
labelCfg: {
position: 'bottom',
offset: 10,
style: {
fill: '#333',
fontSize: 11,
fontFamily: 'Microsoft YaHei',
textAlign: 'center',
background: {
fill: 'rgba(255, 255, 255, 0.95)',
padding: [4, 6, 4, 6],
radius: 4
}
}
}
},
defaultEdge: {
type: 'quadratic',
style: {
stroke: '#5B8FF9',
lineWidth: 3,
opacity: 0.9,
endArrow: {
path: 'M 0,0 L 12,6 L 12,-6 Z',
fill: '#5B8FF9'
}
},
labelCfg: {
autoRotate: true,
style: {
fill: '#333',
fontSize: 10,
fontFamily: 'Microsoft YaHei',
background: {
fill: '#fff',
padding: [2, 4, 2, 4],
radius: 2
}
}
}
},
nodeStateStyles: {
active: {
shadowColor: '#1459BB',
shadowBlur: 15,
stroke: '#1459BB',
lineWidth: 3
},
inactive: {
opacity: 0.3
}
},
edgeStateStyles: {
active: {
stroke: '#1459BB',
lineWidth: 4
},
inactive: {
opacity: 0.15
}
}
})
graphInstance.value.data(data)
graphInstance.value.render()
bindGraphEvents()
}
const initCircularGraph = (width, height) => {
const data = processGraphData(props.graphData)
if (!data.nodes || data.nodes.length === 0) return
const centerX = width / 2
const centerY = height / 2
const radius = Math.min(width, height) / 2 - 120
const otherNodes = data.nodes.filter(n => !n.isCenter)
const nodeCount = otherNodes.length
otherNodes.forEach((node, index) => {
const angle = (2 * Math.PI * index) / nodeCount - Math.PI / 2
node.x = centerX + radius * Math.cos(angle)
node.y = centerY + radius * Math.sin(angle)
})
const centerNode = data.nodes.find(n => n.isCenter)
if (centerNode) {
centerNode.x = centerX
centerNode.y = centerY
centerNode.fx = centerX
centerNode.fy = centerY
}
graphInstance.value = new G6.Graph({
container: containerRef.value,
width,
height,
fitView: false,
fitCenter: false,
animate: true,
animateCfg: {
duration: 300,
easing: 'easeLinear'
},
minZoom: 0.1,
maxZoom: 10,
modes: {
default: [
'drag-canvas',
'zoom-canvas',
'drag-node',
{
type: 'activate-relations',
trigger: 'mouseenter',
resetSelected: true
}
]
},
defaultNode: {
type: 'image',
size: 40,
clipCfg: {
show: true,
type: 'circle',
r: 20
},
labelCfg: {
position: 'bottom',
offset: 10,
style: {
fill: '#333',
fontSize: 11,
fontFamily: 'Microsoft YaHei',
textAlign: 'center',
background: {
fill: 'rgba(255, 255, 255, 0.95)',
padding: [4, 6, 4, 6],
radius: 4
}
}
}
},
defaultEdge: {
type: 'quadratic',
style: {
stroke: '#5B8FF9',
lineWidth: 3,
opacity: 0.9,
endArrow: {
path: 'M 0,0 L 12,6 L 12,-6 Z',
fill: '#5B8FF9'
}
},
labelCfg: {
autoRotate: true,
style: {
fill: '#333',
fontSize: 10,
fontFamily: 'Microsoft YaHei',
background: {
fill: '#fff',
padding: [2, 4, 2, 4],
radius: 2
}
}
}
},
nodeStateStyles: {
active: {
shadowColor: '#1459BB',
shadowBlur: 15,
stroke: '#1459BB',
lineWidth: 3
},
inactive: {
opacity: 0.3
}
},
edgeStateStyles: {
active: {
stroke: '#1459BB',
lineWidth: 4
},
inactive: {
opacity: 0.15
}
}
})
graphInstance.value.data(data)
graphInstance.value.render()
bindGraphEvents()
}
const initTreeGraph = (width, height) => {
const treeDataSource = convertGraphToTree(props.graphData)
if (!treeDataSource) return
graphInstance.value = new G6.TreeGraph({
container: containerRef.value,
width,
height,
fitView: true,
fitViewPadding: 80,
animate: true,
animateCfg: {
duration: 300,
easing: 'easeLinear'
},
minZoom: 0.1,
maxZoom: 10,
modes: {
default: [
'drag-canvas',
'zoom-canvas',
'drag-node',
{
type: 'collapse-expand',
onChange: function onChange(item, collapsed) {
const data = item.getModel()
data.collapsed = collapsed
return true
}
}
]
},
layout: {
type: 'compactBox',
direction: 'LR',
getId: function getId(d) {
return d.id
},
getHeight: function getHeight() {
return 16
},
getWidth: function getWidth() {
return 16
},
getVGap: function getVGap() {
return 30
},
getHGap: function getHGap() {
return 120
}
},
defaultNode: {
type: 'image',
size: 40,
clipCfg: {
show: true,
type: 'circle',
r: 20
},
labelCfg: {
position: 'right',
offset: 10,
style: {
fill: '#333',
fontSize: 11,
fontFamily: 'Microsoft YaHei',
background: {
fill: 'rgba(255, 255, 255, 0.95)',
padding: [4, 6, 4, 6],
radius: 4
}
}
}
},
defaultEdge: {
type: 'cubic-horizontal',
style: {
stroke: '#5B8FF9',
lineWidth: 3
}
},
nodeStateStyles: {
active: {
shadowColor: '#1459BB',
shadowBlur: 15,
stroke: '#1459BB',
lineWidth: 3
}
}
})
graphInstance.value.data(treeDataSource)
graphInstance.value.render()
graphInstance.value.fitView()
bindGraphEvents()
}
const convertGraphToTree = (graphData) => {
if (!graphData || !graphData.nodes || graphData.nodes.length === 0) {
return null
}
const nodes = graphData.nodes
const links = graphData.links || graphData.edges || []
const centerNode = nodes[0]
const centerId = String(centerNode.id || '0')
const childIdSet = new Set()
const childrenNodes = []
links.forEach((link) => {
const source = String(link.source)
const target = String(link.target)
if (source === centerId && !childIdSet.has(target)) {
const node = nodes.find(n => String(n.id) === target)
if (node) {
childIdSet.add(target)
childrenNodes.push({
id: target,
label: node.name || '',
img: node.image || defaultIcon,
size: node.symbolSize || 40,
name: node.name,
image: node.image,
isSanctioned: node.isSanctioned
})
}
} else if (target === centerId && !childIdSet.has(source)) {
const node = nodes.find(n => String(n.id) === source)
if (node) {
childIdSet.add(source)
childrenNodes.push({
id: source,
label: node.name || '',
img: node.image || defaultIcon,
size: node.symbolSize || 40,
name: node.name,
image: node.image,
isSanctioned: node.isSanctioned
})
}
}
})
if (childrenNodes.length === 0) {
nodes.slice(1).forEach((node) => {
const nodeId = String(node.id)
if (!childIdSet.has(nodeId)) {
childIdSet.add(nodeId)
childrenNodes.push({
id: nodeId,
label: node.name || '',
img: node.image || defaultIcon,
size: node.symbolSize || 40,
name: node.name,
image: node.image,
isSanctioned: node.isSanctioned
})
}
})
}
return {
id: centerId,
label: centerNode.name || '',
img: centerNode.image || defaultIcon,
size: centerNode.symbolSize || 60,
name: centerNode.name,
image: centerNode.image,
isSanctioned: centerNode.isSanctioned,
children: childrenNodes
}
}
const processGraphData = (rawData) => {
if (!rawData || !rawData.nodes || rawData.nodes.length === 0) {
return { nodes: [], edges: [] }
}
const nodeMap = new Map()
const nodes = []
rawData.nodes.forEach((node, index) => {
const nodeId = String(node.id || index)
if (nodeMap.has(nodeId)) {
return
}
nodeMap.set(nodeId, true)
const isCenter = index === 0
const size = node.symbolSize || (isCenter ? 60 : 40)
nodes.push({
id: nodeId,
label: node.name || '',
img: node.image || defaultIcon,
size,
isCenter,
clipCfg: {
show: true,
type: 'circle',
r: size / 2
},
style: {
cursor: 'pointer'
},
labelCfg: {
position: 'bottom',
offset: 12,
style: {
fill: isCenter ? '#1459BB' : '#333',
fontSize: isCenter ? 13 : 11,
fontWeight: isCenter ? 'bold' : 'normal',
fontFamily: 'Microsoft YaHei',
textAlign: 'center'
}
},
...node,
id: nodeId
})
})
const edgeMap = new Map()
const edges = []
const rawEdges = rawData.links || rawData.edges || []
rawEdges.forEach((edge, index) => {
const source = String(edge.source)
const target = String(edge.target)
const edgeKey = `${source}-${target}`
if (edgeMap.has(edgeKey)) {
return
}
if (!nodeMap.has(source) || !nodeMap.has(target)) {
return
}
edgeMap.set(edgeKey, true)
edges.push({
id: `edge-${index}`,
source,
target,
label: edge.name || ''
})
})
return { nodes, edges }
}
const bindGraphEvents = () => {
if (!graphInstance.value) return
graphInstance.value.on('node:click', (evt) => {
const node = evt.item
const model = node.getModel()
selectedNode.value = model
emit('nodeClick', model)
})
graphInstance.value.on('canvas:click', () => {
selectedNode.value = null
})
}
const handleClickControlBtn = (btn) => {
currentLayoutType.value = btn
emit('layoutChange', btn)
initGraph(btn)
}
const destroyGraph = () => {
if (graphInstance.value) {
graphInstance.value.destroy()
graphInstance.value = null
}
}
const handleResize = () => {
if (graphInstance.value && containerRef.value) {
const width = containerRef.value.offsetWidth
const height = containerRef.value.offsetHeight
graphInstance.value.changeSize(width, height)
graphInstance.value.fitView()
}
}
watch(
() => props.graphData,
() => {
initGraph(currentLayoutType.value)
},
{ deep: true }
)
watch(
() => props.treeData,
() => {
if (currentLayoutType.value === 2) {
initGraph(2)
}
},
{ deep: true }
)
watch(
() => props.controlActive,
(newVal) => {
if (newVal !== currentLayoutType.value) {
handleClickControlBtn(newVal)
}
}
)
onMounted(() => {
initGraph(1)
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
destroyGraph()
})
defineExpose({
refresh: () => initGraph(currentLayoutType.value),
changeLayout: (type) => handleClickControlBtn(type),
getGraph: () => graphInstance.value
})
</script>
<style lang="scss" scoped>
.relation-graph-wrapper {
position: relative;
width: 100%;
height: 100%;
}
.graph-container {
width: 100%;
height: 100%;
}
.graph-controls {
position: absolute;
top: 16px;
right: 16px;
display: flex;
gap: 8px;
z-index: 10;
.control-btn {
width: 32px;
height: 32px;
border-radius: 4px;
border: 1px solid rgba(234, 236, 238, 1);
background: rgba(255, 255, 255, 1);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
img {
width: 16px;
height: 16px;
}
&:hover {
border-color: rgba(5, 95, 194, 0.5);
}
}
.control-btn-active {
border-color: rgba(5, 95, 194, 1);
background: rgba(231, 243, 255, 1);
}
}
.node-popup {
position: absolute;
bottom: 16px;
left: 16px;
width: 320px;
background: rgba(255, 255, 255, 1);
border-radius: 8px;
box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(234, 236, 238, 1);
z-index: 20;
.popup-header {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid rgba(234, 236, 238, 1);
.popup-icon {
width: 32px;
height: 32px;
margin-right: 8px;
border-radius: 50%;
object-fit: cover;
}
.popup-title {
flex: 1;
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
color: rgba(59, 65, 75, 1);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.close-icon {
cursor: pointer;
color: rgba(132, 136, 142, 1);
font-size: 16px;
&:hover {
color: rgba(5, 95, 194, 1);
}
}
}
.popup-body {
padding: 12px 16px;
.tag-row {
display: flex;
align-items: center;
margin-bottom: 8px;
.red-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: rgba(245, 63, 63, 1);
margin-right: 8px;
}
.red-text {
font-size: 14px;
font-family: "Microsoft YaHei";
color: rgba(245, 63, 63, 1);
}
}
.desc {
font-size: 14px;
font-family: "Microsoft YaHei";
line-height: 22px;
color: rgba(95, 101, 108, 1);
margin: 0;
}
}
}
</style>
\ No newline at end of file
...@@ -9,63 +9,6 @@ ...@@ -9,63 +9,6 @@
</div> </div>
<div class="main" v-if="activeIndex === 0"> <div class="main" v-if="activeIndex === 0">
<div class="left"> <div class="left">
<!-- <div class="title-com">
<div class="box"></div>
<div class="text">本次制裁实体清单列表</div>
<div class="right-group">
<div class="btn">
<img src="../../assets/数据库按钮.png" alt="" />
<img src="../../assets/下载按钮.png" alt="" />
<img src="../../assets/收藏按钮.png" alt="" />
</div>
</div>
</div>
<div class="left-main">
<div class="filter-bar">
<el-select v-model="searchDomain" placeholder="全部领域" class="domain-select">
<el-option label="全部领域" value="" />
<el-option label="人工智能" value="1" />
<el-option label="生物科技" value="2" />
<el-option label="新一代信息技术" value="3" />
<el-option label="量子科技" value="4" />
<el-option label="新能源" value="5" />
<el-option label="集成电路" value="6" />
<el-option label="海洋" value="7" />
<el-option label="先进制造" value="8" />
<el-option label="新材料" value="9" />
<el-option label="航空航天" value="10" />
<el-option label="深海" value="11" />
<el-option label="极地" value="12" />
<el-option label="太空" value="13" />
<el-option label="核" value="14" />
</el-select>
<el-input v-model="searchText" placeholder="搜索实体" class="search-input">
<template #suffix>
<el-icon class="el-input__icon"><Search /></el-icon>
</template>
</el-input>
</div>
<div class="entity-tree custom-scrollbar">
<div class="tree-group" v-for="group in entityList" :key="group.id">
<div class="group-header" @click="toggleGroup(group)">
<el-icon class="arrow-icon" :class="{ expanded: group.expanded }">
<CaretRight />
</el-icon>
<span class="group-name">{{ group.name }}</span>
<span class="group-count">{{ group.count }}家</span>
</div>
<div class="group-children custom-scrollbar" v-show="group.expanded">
<div class="entity-item" v-for="item in group.children" :key="item.id"
:class="{ active: activeEntityId === item.id }" @click="selectEntity(item)">
<div class="item-icon">
<img :src="defaultTitle" alt="" class="item-img" />
</div>
<span class="item-name">{{ item.name }}</span>
</div>
</div>
</div>
</div>
</div> -->
<AnalysisBox title="本次制裁实体清单列表"> <AnalysisBox title="本次制裁实体清单列表">
<div class="left-main"> <div class="left-main">
<div class="filter-bar"> <div class="filter-bar">
...@@ -134,26 +77,6 @@ ...@@ -134,26 +77,6 @@
<div class="rule-checkbox" v-if="rightActiveTab === 'equity'"> <div class="rule-checkbox" v-if="rightActiveTab === 'equity'">
<el-checkbox v-model="is50PercentRule" size="large">50%规则涉及实体</el-checkbox> <el-checkbox v-model="is50PercentRule" size="large">50%规则涉及实体</el-checkbox>
</div> </div>
<!-- <el-select v-model="filterType" placeholder="全部类型" class="header-select">
<el-option label="全部类型" value="" />
</el-select>
<el-select v-model="filterDomain" placeholder="全部领域" class="header-select last-select">
<el-option label="全部领域" value="" />
<el-option label="人工智能" value="1" />
<el-option label="生物科技" value="2" />
<el-option label="新一代信息技术" value="3" />
<el-option label="量子科技" value="4" />
<el-option label="新能源" value="5" />
<el-option label="集成电路" value="6" />
<el-option label="海洋" value="7" />
<el-option label="先进制造" value="8" />
<el-option label="新材料" value="9" />
<el-option label="航空航天" value="10" />
<el-option label="深海" value="11" />
<el-option label="极地" value="12" />
<el-option label="太空" value="13" />
<el-option label="核" value="14" />
</el-select> -->
<div class="btn"> <div class="btn">
<img src="../../assets/数据库按钮.png" alt="" /> <img src="../../assets/数据库按钮.png" alt="" />
<img src="../../assets/下载按钮.png" alt="" /> <img src="../../assets/下载按钮.png" alt="" />
...@@ -162,50 +85,14 @@ ...@@ -162,50 +85,14 @@
</div> </div>
</div> </div>
<div class="right-echarts"> <div class="right-echarts">
<div class="chart-wrapper"> <RelationGraph
<div class="chart-controls"> ref="relationGraphRef"
<div class="control-btn" :class="{ controlBtnActive: controlActive === 1 }" :graph-data="graphData"
@click="handleClickControlBtn(1)"> :tree-data="treeData"
<img :src="echartsIcon01" /> :control-active="controlActive"
</div> @node-click="handleNodeClick"
<div class="control-btn" :class="{ controlBtnActive: controlActive === 2 }" @layout-change="handleLayoutChange"
@click="handleClickControlBtn(2)"> />
<img :src="echartsIcon02" />
</div>
<div class="control-btn" :class="{ controlBtnActive: controlActive === 3 }"
@click="handleClickControlBtn(3)">
<img :src="echartsIcon03" />
</div>
</div>
<div class="chart-legend">
<div class="legend-item"><span class="dot blue"></span>已被制裁实体</div>
<div class="legend-item"><span class="dot grey"></span>未被制裁实体</div>
</div>
<div ref="chartRef" id="chartbox" class="chart-container"></div>
<div class="node-popup" v-if="selectedNode">
<div class="popup-header">
<img :src="defaultTitle" class="popup-icon" />
<span class="popup-title">{{ selectedNode.name.replace(/\n/g, "") }}</span>
<el-icon class="close-icon" @click="selectedNode = null">
<Close />
</el-icon>
</div>
<div class="popup-body">
<div class="tag-row">
<span class="red-dot"></span>
<span class="red-text">
<!-- 2025年7月15日 《实体清单》 -->
暂无数据
</span>
</div>
<div class="desc">
<!-- 因获取和试图获取美国原产物品以支持中国军事和国防相关空间领域活动以及中国量子技术能力而被列入。 -->
暂无数据
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -215,16 +102,12 @@ ...@@ -215,16 +102,12 @@
<script setup> <script setup>
import { ref, onMounted, nextTick, watch, onUnmounted } from "vue"; import { ref, onMounted, nextTick, watch, onUnmounted } from "vue";
import { debounce } from "lodash"; import { debounce } from "lodash";
import * as echarts from "echarts";
import { Search, CaretRight, Close } from "@element-plus/icons-vue"; import { Search, CaretRight, Close } from "@element-plus/icons-vue";
import defaultTitle from "../../assets/default-icon2.png"; import defaultTitle from "../../assets/default-icon2.png";
import icon01 from "./assets/icon01.png"; import icon01 from "./assets/icon01.png";
import icon02 from "./assets/icon02.png"; import icon02 from "./assets/icon02.png";
import icon01Active from "./assets/icon01-active.png"; import icon01Active from "./assets/icon01-active.png";
import icon02Active from "./assets/icon02-active.png"; import icon02Active from "./assets/icon02-active.png";
import echartsIcon01 from "./assets/echartsIcon01.png";
import echartsIcon02 from "./assets/echartsIcon02.png";
import echartsIcon03 from "./assets/echartsIcon03.png";
import company from "./assets/company.png"; import company from "./assets/company.png";
import companyActive from "./assets/company-active.png"; import companyActive from "./assets/company-active.png";
import { import {
...@@ -232,137 +115,10 @@ import { ...@@ -232,137 +115,10 @@ import {
getSingleSanctionEntitySupplyChain, getSingleSanctionEntitySupplyChain,
getSingleSanctionEntityEquity getSingleSanctionEntityEquity
} from "@/api/exportControlV2.0"; } from "@/api/exportControlV2.0";
import getTreeChart from "./utils/treeChart"; import RelationGraph from './components/RelationGraph.vue';
import setChart from "@/utils/setChart"; import AnalysisBox from '@/components/base/boxBackground/analysisBox.vue';
const controlActive = ref(1);
const treeData = ref([{}]);
const handleClickControlBtn = btn => {
controlActive.value = btn;
if (btn === 1 || btn === 3) {
isInChart.value = true;
initChart();
} else if (btn === 2) {
isInChart.value = false;
if (chartInstance.value) {
chartInstance.value.dispose();
}
console.log("treeData", treeData.value);
let treeChart = getTreeChart(treeData.value);
setChart(treeChart, "chartbox");
}
};
const isInChart = ref(false);
const handleMouseEnter = () => {
if (controlActive.value !== 2) {
isInChart.value = true;
} else {
isInChart.value = false;
}
};
const handleMouseLeave = () => {
isInChart.value = false;
};
// 单次制裁-深度挖掘-制裁实体股权信息-列表
const singleSanctionEntityEquityData = ref(null);
// 单次制裁-深度挖掘-制裁实体股权信息-请求
const getSingleSanctionEntityEquityRequest = async () => {
try {
const res = await getSingleSanctionEntityEquity({
orgId: activeEntityId.value,
rule: is50PercentRule.value
});
if (res.code === 200) {
singleSanctionEntityEquityData.value = res.data || null;
initChart();
}
} catch (error) {
console.log(error);
}
};
// 单次制裁-深度挖掘-制裁实体供应链信息-列表
const singleSanctionEntitySupplyChainData = ref(null);
// 单次制裁-深度挖掘-制裁实体供应链信息-请求
const getSingleSanctionEntitySupplyChainRequest = async () => {
try {
const res = await getSingleSanctionEntitySupplyChain({
orgId: activeEntityId.value
});
if (res.code === 200) {
singleSanctionEntitySupplyChainData.value = res.data || null;
initChart();
treeData.value[0].id = res.data.orgId;
treeData.value[0].name = res.data.orgName;
treeData.value[0].symbol = "image://" + companyActive;
treeData.value[0].symbolSize = 50;
treeData.value[0].value = 10;
treeData.value[0].children = res.data.parentOrgList.map(item => {
return {
id: item.id,
name: item.name,
symbolSize: 30,
value: 10,
symbol: `image://${company}`
};
});
console.log("treeData0", treeData.value);
}
} catch (error) {
console.log(error);
}
};
// 单次制裁-深度挖掘-本次制裁实体清单列表
const singleSanctionEntityList = ref([]);
// 单次制裁-深度挖掘-本次制裁实体清单列表-请求
const getSingleSanctionEntityListRequest = async () => {
try {
const res = await getSingleSanctionEntityList({
sanRecordId: sanRecordId.value,
isOnlyCn: false,
domainId: searchDomain.value || undefined,
searchText: searchText.value || undefined
});
if (res.code === 200) {
entityList.value = (res.data || []).map((group, index) => ({
id: `group-${index}`,
name: group.orgType,
count: group.orgInfoList ? group.orgInfoList.length : 0,
expanded: index === 0, // 默认展开第一个分组
children: (group.orgInfoList || []).map(org => ({
id: org.id,
name: org.orgNameZh
}))
}));
// 如果有数据,且当前没有选中的实体,默认选中第一个分组的第一个实体
if (entityList.value.length > 0 && entityList.value[0].children && entityList.value[0].children.length > 0) {
const firstEntity = entityList.value[0].children[0];
if (!activeEntityId.value) {
activeEntityId.value = firstEntity.id;
currentEntityName.value = firstEntity.name;
}
}
}
} catch (error) {
console.log(error);
}
};
const sanRecordId = ref(""); const sanRecordId = ref("");
const getUrlParams = () => {
const urlParams = new URLSearchParams(window.location.search);
sanRecordId.value = urlParams.get("id") || "";
};
// const activeTab = ref(["实体穿透分析", "重点实体识别"]);
const activeTab = ref(["实体穿透分析"]); const activeTab = ref(["实体穿透分析"]);
const activeIndex = ref(0); const activeIndex = ref(0);
const rightActiveTab = ref("supplyChain"); const rightActiveTab = ref("supplyChain");
...@@ -376,6 +132,22 @@ const currentEntityName = ref(""); ...@@ -376,6 +132,22 @@ const currentEntityName = ref("");
const is50PercentRule = ref(false); const is50PercentRule = ref(false);
const entityList = ref([]); const entityList = ref([]);
const controlActive = ref(1);
const isInChart = ref(false);
const relationGraphRef = ref(null);
const graphData = ref({ nodes: [], links: [] });
const treeData = ref(null);
const selectedNode = ref(null);
const singleSanctionEntityEquityData = ref(null);
const singleSanctionEntitySupplyChainData = ref(null);
const singleSanctionEntityList = ref([]);
const getUrlParams = () => {
const urlParams = new URLSearchParams(window.location.search);
sanRecordId.value = urlParams.get("id") || "";
};
const toggleGroup = group => { const toggleGroup = group => {
group.expanded = !group.expanded; group.expanded = !group.expanded;
...@@ -386,512 +158,194 @@ const selectEntity = item => { ...@@ -386,512 +158,194 @@ const selectEntity = item => {
currentEntityName.value = item.name; currentEntityName.value = item.name;
}; };
const chartRef = ref(null); const handleMouseEnter = () => {
const chartInstance = ref(null); if (controlActive.value !== 2) {
const selectedNode = ref(null); isInChart.value = true;
} else {
const initChart = () => { isInChart.value = false;
if (!chartRef.value) return;
if (chartInstance.value) {
chartInstance.value.dispose();
} }
};
chartInstance.value = echarts.init(chartRef.value); const handleMouseLeave = () => {
isInChart.value = false;
};
let option = {}; const handleNodeClick = (node) => {
selectedNode.value = node;
};
if (rightActiveTab.value === "supplyChain") { const handleLayoutChange = (type) => {
option = getSupplyChainOption(); controlActive.value = type;
if (type !== 2) {
isInChart.value = true;
} else { } else {
option = getEquityOption(); isInChart.value = false;
} }
chartInstance.value.setOption(option);
chartInstance.value.on("click", params => {
if (params.dataType === "node") {
selectedNode.value = params.data;
} else {
selectedNode.value = null;
}
});
chartInstance.value.getZr().on("click", params => {
if (!params.target) {
selectedNode.value = null;
}
});
}; };
const getSupplyChainOption = () => { const updateGraphData = () => {
if (!singleSanctionEntitySupplyChainData.value) return {}; const data = rightActiveTab.value === 'supplyChain'
const data = singleSanctionEntitySupplyChainData.value; ? singleSanctionEntitySupplyChainData.value
: singleSanctionEntityEquityData.value;
if (!data) return;
const nodes = []; const nodes = [];
const links = []; const links = [];
const centerX = 550;
const centerY = 400;
// 中心节点
nodes.push({ nodes.push({
id: "0", id: "0",
name: data.orgName, name: data.orgName,
category: 0, // 强制为制裁中 image: companyActive,
symbol: "image://" + companyActive, // 强制使用制裁中图标 symbolSize: 60,
x: centerX, isSanctioned: true
y: centerY,
symbolSize: 50,
label: {
fontSize: 16,
fontWeight: "bold",
color: "#055FC2", // 使用制裁蓝,在图标下方更清晰
position: "bottom",
distance: 10,
width: 150,
overflow: "break"
}
}); });
// 父级节点 (供应商)
const parentList = data.parentOrgList || []; const parentList = data.parentOrgList || [];
parentList.forEach((item, index) => { parentList.forEach((item, index) => {
// 使用 (index + 0.5) 使节点在半圆弧内居中分布
const angle = -Math.PI + ((index + 0.5) / parentList.length) * Math.PI;
// 交错半径:偶数索引使用 340,奇数索引使用 440,拉开垂直距离
const radius = index % 2 === 0 ? 340 : 440;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
// 动态计算标签位置:根据余弦值判断左右,根据正弦值判断上下
let position = "right";
let align = "left";
const cosA = Math.cos(angle);
const sinA = Math.sin(angle);
if (Math.abs(cosA) < 0.3) {
// 顶部区域
position = "top";
align = "center";
} else if (cosA < 0) {
// 左侧区域
position = "left";
align = "right";
}
nodes.push({ nodes.push({
id: `p-${item.id}`, id: `p-${item.id || index}`,
name: item.name, name: item.name,
category: item.isSanctioned ? 0 : 1, image: item.isSanctioned ? companyActive : company,
symbol: "image://" + (item.isSanctioned ? companyActive : company), symbolSize: 40,
x: x, isSanctioned: item.isSanctioned
y: y,
isSanctioned: item.isSanctioned,
label: {
position: position,
align: align,
distance: 8,
width: 110,
overflow: "break",
lineHeight: 14,
fontSize: 11
}
}); });
links.push({ links.push({
source: `p-${item.id}`, source: `p-${item.id || index}`,
target: "0", target: "0",
value: "供应商", name: rightActiveTab.value === 'supplyChain' ? "供应商" : (item.type || "持股")
isSanctioned: item.isSanctioned && data.isSanctioned
}); });
}); });
// 子级节点 (客户)
const childList = data.childrenOrgList || []; const childList = data.childrenOrgList || [];
childList.forEach((item, index) => { childList.forEach((item, index) => {
const angle = ((index + 0.5) / childList.length) * Math.PI;
// 交错半径
const radius = index % 2 === 0 ? 340 : 440;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
// 动态计算标签位置
let position = "right";
let align = "left";
const cosA = Math.cos(angle);
const sinA = Math.sin(angle);
if (Math.abs(cosA) < 0.3) {
// 底部区域
position = "bottom";
align = "center";
} else if (cosA < 0) {
// 左侧区域
position = "left";
align = "right";
}
nodes.push({ nodes.push({
id: `c-${item.id}`, id: `c-${item.id || index}`,
name: item.name, name: item.name,
category: item.isSanctioned ? 0 : 1, image: item.isSanctioned ? companyActive : company,
symbol: "image://" + (item.isSanctioned ? companyActive : company), symbolSize: 40,
x: x, isSanctioned: item.isSanctioned
y: y,
isSanctioned: item.isSanctioned,
label: {
position: position,
align: align,
distance: 8,
width: 110,
overflow: "break",
lineHeight: 14,
fontSize: 11
}
}); });
links.push({ links.push({
source: "0", source: "0",
target: `c-${item.id}`, target: `c-${item.id || index}`,
value: "客户", name: rightActiveTab.value === 'supplyChain' ? "客户" : (item.type || "投资")
isSanctioned: item.isSanctioned && data.isSanctioned
}); });
}); });
return { graphData.value = { nodes, links };
tooltip: {
show: true,
formatter: params => {
if (params.dataType === "node") {
return `<div style="padding: 8px; max-width: 300px; white-space: normal; word-break: break-all;">${params.data.name}</div>`;
}
return "";
}
},
series: [
{
type: "graph",
layout: "none",
symbolSize: 36,
roam: true,
label: {
show: true,
formatter: "{b}",
fontSize: 12,
hideOverlap: true
},
edgeSymbol: ["none", "arrow"],
edgeSymbolSize: [4, 8],
edgeLabel: {
position: "middle",
offset: [0, 13],
fontSize: 12,
fontWeight: 400,
fontFamily: "Microsoft YaHei",
lineHeight: 16,
show: true,
formatter: "{c}",
color: "rgba(170, 173, 177, 1)",
backgroundColor: "rgba(234, 236, 238, 1)",
padding: [4, 8],
borderRadius: 20
},
data: nodes.map(node => ({
...node,
label: {
color: node.category === 0 ? "#055FC2" : "#5F656C",
...node.label
}
})),
links: links.map(link => {
return {
...link,
lineStyle: {
color: link.isSanctioned ? "rgba(100, 180, 255, 1)" : "rgb(180, 181, 182)",
width: 1,
curveness: 0
},
label: link.isSanctioned
? {
show: true,
formatter: "{c}",
backgroundColor: "rgba(231, 243, 255, 1)",
color: "rgba(50, 150, 250, 1)",
borderRadius: 20,
padding: [4, 8],
fontSize: 12,
fontWeight: 400,
fontFamily: "Microsoft YaHei",
lineHeight: 16
}
: undefined
};
})
}
]
};
}; };
const getEquityOption = () => { const updateTreeData = (data) => {
if (!singleSanctionEntityEquityData.value) return {}; if (!data) return;
const data = singleSanctionEntityEquityData.value;
const nodes = [];
const links = [];
const centerX = 550;
const centerY = 400;
// 中心节点 treeData.value = {
nodes.push({ id: data.orgId,
id: "0",
name: data.orgName, name: data.orgName,
category: 0, // 强制为制裁中 image: companyActive,
symbol: "image://" + companyActive, // 强制使用制裁中图标
x: centerX,
y: centerY,
symbolSize: 50, symbolSize: 50,
label: { children: (data.parentOrgList || []).map((item, index) => ({
fontSize: 16, id: item.id || `p-${index}`,
fontWeight: "bold",
color: "#055FC2", // 使用制裁蓝,在图标下方更清晰
position: "bottom",
distance: 10,
width: 150,
overflow: "break"
}
});
// 父级节点 (股东)
const parentList = data.parentOrgList || [];
parentList.forEach((item, index) => {
// 在顶部水平排列
const total = parentList.length;
const gap = 200;
const startX = centerX - ((total - 1) * gap) / 2;
const x = startX + index * gap;
const y = centerY - 250; // 向上偏移
nodes.push({
id: `p-${index}`,
name: item.name, name: item.name,
category: item.isSanctioned ? 0 : 1, image: item.isSanctioned ? companyActive : company,
symbol: "image://" + (item.isSanctioned ? companyActive : company), symbolSize: 30
x: x, }))
y: y, };
isSanctioned: item.isSanctioned, };
label: {
position: "top",
distance: 8,
width: 110,
overflow: "break",
lineHeight: 14,
fontSize: 11
}
});
links.push({ const getSingleSanctionEntityEquityRequest = async () => {
source: `p-${index}`, try {
target: "0", const res = await getSingleSanctionEntityEquity({
value: item.type || "持股", // 使用 type 字段 orgId: activeEntityId.value,
isSanctioned: item.isSanctioned // 中心节点强制制裁,高亮取决于对方 rule: is50PercentRule.value
}); });
}); if (res.code === 200) {
singleSanctionEntityEquityData.value = res.data || null;
// 子级节点 (对外投资) updateGraphData();
const childList = data.childrenOrgList || []; }
childList.forEach((item, index) => { } catch (error) {
// 在底部水平排列 console.log(error);
const total = childList.length; }
const gap = 240; // 稍微拉开一点间距 };
const startX = centerX - ((total - 1) * gap) / 2;
const x = startX + index * gap;
const y = centerY + 250; // 向下偏移
nodes.push({ const getSingleSanctionEntitySupplyChainRequest = async () => {
id: `c-${index}`, try {
name: item.name, const res = await getSingleSanctionEntitySupplyChain({
category: item.isSanctioned ? 0 : 1, orgId: activeEntityId.value
symbol: "image://" + (item.isSanctioned ? companyActive : company),
x: x,
y: y,
isSanctioned: item.isSanctioned,
label: {
position: "bottom",
distance: 8,
width: 110,
overflow: "break",
lineHeight: 14,
fontSize: 11
}
}); });
if (res.code === 200) {
singleSanctionEntitySupplyChainData.value = res.data || null;
updateGraphData();
updateTreeData(res.data);
}
} catch (error) {
console.log(error);
}
};
links.push({ const getSingleSanctionEntityListRequest = async () => {
source: "0", try {
target: `c-${index}`, const res = await getSingleSanctionEntityList({
value: item.type || "持股", // 使用 type 字段 sanRecordId: sanRecordId.value,
isSanctioned: item.isSanctioned // 中心节点强制制裁,高亮取决于对方 isOnlyCn: false,
domainId: searchDomain.value || undefined,
searchText: searchText.value || undefined
}); });
}); if (res.code === 200) {
entityList.value = (res.data || []).map((group, index) => ({
id: `group-${index}`,
name: group.orgType,
count: group.orgInfoList ? group.orgInfoList.length : 0,
expanded: index === 0,
children: (group.orgInfoList || []).map(org => ({
id: org.id,
name: org.orgNameZh
}))
}));
return { if (entityList.value.length > 0 && entityList.value[0].children && entityList.value[0].children.length > 0) {
tooltip: { const firstEntity = entityList.value[0].children[0];
show: true, if (!activeEntityId.value) {
formatter: params => { activeEntityId.value = firstEntity.id;
if (params.dataType === "node") { currentEntityName.value = firstEntity.name;
return `<div style="padding: 8px; max-width: 300px; white-space: normal; word-break: break-all;">${params.data.name}</div>`;
} }
return "";
} }
}, }
series: [ } catch (error) {
{ console.log(error);
type: "graph", }
layout: "none",
symbolSize: 36,
roam: true,
label: {
show: true,
formatter: "{b}",
fontSize: 12,
hideOverlap: true
},
edgeSymbol: ["none", "arrow"],
edgeSymbolSize: [4, 8],
edgeLabel: {
position: "middle",
offset: [0, 13],
fontSize: 12,
fontWeight: 400,
fontFamily: "Microsoft YaHei",
lineHeight: 16,
show: true,
formatter: "{c}",
color: "rgba(170, 173, 177, 1)",
backgroundColor: "rgba(234, 236, 238, 1)",
padding: [4, 8],
borderRadius: 20
},
data: nodes.map(node => ({
...node,
label: {
color: node.category === 0 ? "#055FC2" : "#5F656C",
...node.label
}
})),
links: links.map(link => {
return {
...link,
lineStyle: {
color: link.isSanctioned ? "rgba(100, 180, 255, 1)" : "rgb(180, 181, 182)",
width: 1,
curveness: 0
},
label: link.isSanctioned
? {
show: true,
formatter: "{c}",
backgroundColor: "rgba(231, 243, 255, 1)",
color: "rgba(50, 150, 250, 1)",
borderRadius: 20,
padding: [4, 8],
fontSize: 12,
fontWeight: 400,
fontFamily: "Microsoft YaHei",
lineHeight: 16
}
: undefined
};
})
}
]
};
}; };
const debouncedGetList = debounce(() => { watch(rightActiveTab, async (newTab) => {
getSingleSanctionEntityListRequest(); if (newTab === 'supplyChain') {
}, 1000); await getSingleSanctionEntitySupplyChainRequest();
} else {
watch(searchText, () => { await getSingleSanctionEntityEquityRequest();
debouncedGetList();
});
watch(searchDomain, () => {
getSingleSanctionEntityListRequest();
});
watch(activeIndex, val => {
if (val === 0) {
nextTick(() => {
if (activeEntityId.value) {
if (rightActiveTab.value === "supplyChain") {
getSingleSanctionEntitySupplyChainRequest();
} else {
getSingleSanctionEntityEquityRequest();
}
}
initChart();
});
} }
}); });
watch(activeEntityId, val => { watch(activeEntityId, async (newId) => {
if (val) { if (newId) {
if (rightActiveTab.value === "supplyChain") { if (rightActiveTab.value === 'supplyChain') {
getSingleSanctionEntitySupplyChainRequest(); await getSingleSanctionEntitySupplyChainRequest();
} else { } else {
getSingleSanctionEntityEquityRequest(); await getSingleSanctionEntityEquityRequest();
} }
initChart();
} }
}); });
watch(is50PercentRule, () => { watch(is50PercentRule, async () => {
if (rightActiveTab.value === "equity" && activeEntityId.value) { if (rightActiveTab.value === 'equity') {
getSingleSanctionEntityEquityRequest(); await getSingleSanctionEntityEquityRequest();
} }
}); });
watch( onMounted(async () => {
rightActiveTab,
val => {
if (activeEntityId.value) {
if (val === "supplyChain") {
getSingleSanctionEntitySupplyChainRequest();
} else {
getSingleSanctionEntityEquityRequest();
}
}
nextTick(() => {
initChart();
});
},
{ immediate: true }
);
onMounted(() => {
// 获取URL参数
getUrlParams(); getUrlParams();
// 单次制裁-深度挖掘-本次制裁实体清单列表-请求 await getSingleSanctionEntityListRequest();
getSingleSanctionEntityListRequest();
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
if (chartInstance.value) {
chartInstance.value.dispose();
}
if (debouncedGetList && debouncedGetList.cancel) {
debouncedGetList.cancel();
}
window.removeEventListener("resize", handleResize);
}); });
const handleResize = () => {
if (chartInstance.value) {
chartInstance.value.resize();
}
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论