2025-07-23 15:50:49 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div id="bridgeCommunityChart" style="width: 100%; height: 100%;"></div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { onMounted, onUnmounted } from 'vue';
|
|
|
|
|
|
import * as echarts from 'echarts';
|
|
|
|
|
|
import bridgeData from '@/assets/json/bridge_neighbors_communities.json';
|
|
|
|
|
|
import bridgeNodeHoverBgImg from '@/assets/images/bridge_node_hoverbgimg.png'
|
|
|
|
|
|
|
|
|
|
|
|
let chartInstance = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 处理数据生成ECharts所需格式
|
|
|
|
|
|
const processData = () => {
|
|
|
|
|
|
const nodes = [];
|
|
|
|
|
|
const links = [];
|
|
|
|
|
|
const addedCommunities = new Set();
|
|
|
|
|
|
|
|
|
|
|
|
// 找出usersNum的最小值和最大值,用于归一化
|
|
|
|
|
|
let minUsersNum = Infinity;
|
|
|
|
|
|
let maxUsersNum = -Infinity;
|
|
|
|
|
|
|
|
|
|
|
|
bridgeData.forEach(item => {
|
|
|
|
|
|
item.bridgeCommunities.forEach(community => {
|
|
|
|
|
|
minUsersNum = Math.min(minUsersNum, community.usersNum);
|
|
|
|
|
|
maxUsersNum = Math.max(maxUsersNum, community.usersNum);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 节点大小的范围
|
|
|
|
|
|
const minNodeSize = 30;
|
|
|
|
|
|
const maxNodeSize = 45;
|
|
|
|
|
|
|
|
|
|
|
|
// 添加桥梁节点和社团节点
|
|
|
|
|
|
bridgeData.forEach(item => {
|
|
|
|
|
|
const bridgeId = item.bridgeId;
|
|
|
|
|
|
|
|
|
|
|
|
// 添加桥梁节点 - 可以调整这个值来改变桥梁节点大小
|
|
|
|
|
|
nodes.push({
|
|
|
|
|
|
id: `bridge_${bridgeId}`,
|
|
|
|
|
|
name: `桥梁节点 ${bridgeId.substring(0, 5)}...`,
|
|
|
|
|
|
category: 0,
|
|
|
|
|
|
value: 20 // 桥梁节点大小(可修改)
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 添加社团节点和边
|
|
|
|
|
|
item.bridgeCommunities.forEach(community => {
|
|
|
|
|
|
const communityId = community.communityId;
|
|
|
|
|
|
const communityKey = `community_${communityId}`;
|
|
|
|
|
|
const usersNum = community.usersNum;
|
|
|
|
|
|
|
|
|
|
|
|
// 归一化usersNum到节点大小范围
|
|
|
|
|
|
const size = minNodeSize +
|
|
|
|
|
|
((usersNum - minUsersNum) / (maxUsersNum - minUsersNum)) *
|
|
|
|
|
|
(maxNodeSize - minNodeSize);
|
|
|
|
|
|
|
|
|
|
|
|
if (!addedCommunities.has(communityKey)) {
|
|
|
|
|
|
nodes.push({
|
|
|
|
|
|
id: communityKey,
|
|
|
|
|
|
name: `社团 ${communityId} (${usersNum})`,
|
|
|
|
|
|
category: 1,
|
|
|
|
|
|
value: size // 根据usersNum动态设置大小
|
|
|
|
|
|
});
|
|
|
|
|
|
addedCommunities.add(communityKey);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
links.push({
|
|
|
|
|
|
source: `bridge_${bridgeId}`,
|
|
|
|
|
|
target: communityKey,
|
|
|
|
|
|
value: 1
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
nodes,
|
|
|
|
|
|
links
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化图表
|
|
|
|
|
|
const initChart = () => {
|
|
|
|
|
|
if (chartInstance) {
|
|
|
|
|
|
chartInstance.dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const chartDom = document.getElementById('bridgeCommunityChart');
|
|
|
|
|
|
if (!chartDom) return;
|
|
|
|
|
|
|
|
|
|
|
|
chartInstance = echarts.init(chartDom);
|
|
|
|
|
|
|
|
|
|
|
|
const { nodes, links } = processData();
|
|
|
|
|
|
|
|
|
|
|
|
const categories = [
|
|
|
|
|
|
{name: "桥梁节点", category: 0},
|
|
|
|
|
|
{name: "社团节点", category: 1}
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const option = {
|
|
|
|
|
|
//hover上去的窗口
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
trigger: "item",
|
|
|
|
|
|
trigger: "item",
|
|
|
|
|
|
backgroundColor: "rgba(0,0,0,0)", // 透明背景
|
|
|
|
|
|
borderColor: "rgba(0,0,0,0)", // 透明边框
|
|
|
|
|
|
borderWidth: 0,
|
|
|
|
|
|
extraCssText: "box-shadow:none;padding:0;",
|
|
|
|
|
|
textStyle: {
|
|
|
|
|
|
color: '#fff', // 设置字体颜色为白色
|
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
|
},
|
|
|
|
|
|
formatter: function(params) {
|
|
|
|
|
|
// 判断节点类型
|
|
|
|
|
|
if(params.data.category === 1) {
|
|
|
|
|
|
// 社团节点
|
|
|
|
|
|
const usersNum = params.data.name.match(/\((\d+)\)/)?.[1] || 0;
|
|
|
|
|
|
return `<div
|
|
|
|
|
|
style = "
|
|
|
|
|
|
width: 126px;
|
|
|
|
|
|
height: 44px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
background: url('${bridgeNodeHoverBgImg}');
|
|
|
|
|
|
background-size: cover;
|
|
|
|
|
|
background-position: center;
|
|
|
|
|
|
background-repeat: no-repeat;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
">
|
|
|
|
|
|
<div style="color:#fff;letter-spacing: 0.14px;">
|
|
|
|
|
|
社团用户数:${usersNum}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
}else {
|
|
|
|
|
|
// 桥梁节点: 查找对应的bridgeCommunities长度
|
|
|
|
|
|
const bridgeItem = bridgeData.find(item => item.bridgeId === params.data.id.replace('bridge_', ''));
|
|
|
|
|
|
const communityCount = bridgeItem ? bridgeItem.bridgeCommunities.length : 0;
|
|
|
|
|
|
return `<div
|
|
|
|
|
|
style = "
|
|
|
|
|
|
width: 126px;
|
|
|
|
|
|
height: 44px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
background: url('${bridgeNodeHoverBgImg}');
|
|
|
|
|
|
background-size: cover;
|
|
|
|
|
|
background-position: center;
|
|
|
|
|
|
background-repeat: no-repeat;
|
|
|
|
|
|
padding:20px 20px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
">
|
|
|
|
|
|
链接社团数:${communityCount}
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
//图例配置
|
|
|
|
|
|
legend: [
|
|
|
|
|
|
{
|
|
|
|
|
|
data: categories.map((c) => ({
|
|
|
|
|
|
name: c.name,
|
|
|
|
|
|
// 锚点账号用圆形头像,普通账号保持默认
|
|
|
|
|
|
// 添加自定义图标
|
2025-07-23 16:01:19 +08:00
|
|
|
|
icon: c.category === 0
|
|
|
|
|
|
? `image://${new URL('@/assets/images/icon/bridge_node_legend.png', import.meta.url)}`
|
|
|
|
|
|
: `image://${new URL('@/assets/images/icon/community_node_legend.png', import.meta.url)}`
|
2025-07-23 15:50:49 +08:00
|
|
|
|
})),
|
|
|
|
|
|
right: 15,
|
|
|
|
|
|
bottom: 13,
|
|
|
|
|
|
icon: "circle",
|
|
|
|
|
|
orient: "vertical",
|
|
|
|
|
|
itemWidth: 16,
|
|
|
|
|
|
itemHeight: 16,
|
|
|
|
|
|
itemGap: 12,
|
|
|
|
|
|
backgroundColor: "rgba(0,67,125,0.56)", // 半透明深蓝
|
|
|
|
|
|
borderRadius: 8, // 圆角
|
|
|
|
|
|
borderColor: "#c2f2ff", // 淡蓝色边框
|
|
|
|
|
|
borderWidth: 0.3,
|
|
|
|
|
|
padding: [12, 20, 12, 20], // 上右下左
|
|
|
|
|
|
textStyle: {
|
|
|
|
|
|
color: "#fff",
|
|
|
|
|
|
fontSize: 16,
|
|
|
|
|
|
fontWeight: "normal"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
series: [
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'graph',
|
|
|
|
|
|
zoom: 0.8,
|
|
|
|
|
|
layout: 'force',
|
|
|
|
|
|
animation: false,
|
|
|
|
|
|
draggable: true,
|
|
|
|
|
|
data: nodes,
|
|
|
|
|
|
links: links,
|
|
|
|
|
|
categories: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '桥梁节点',
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
color: '#ff7300'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '社团节点',
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
color: '#1890ff'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
roam: true,
|
|
|
|
|
|
lineStyle: {
|
|
|
|
|
|
color: 'source',
|
|
|
|
|
|
curveness: 0.3
|
|
|
|
|
|
},
|
|
|
|
|
|
force: {
|
|
|
|
|
|
repulsion: 1000,
|
|
|
|
|
|
edgeLength: 100
|
|
|
|
|
|
},
|
|
|
|
|
|
animationDurationUpdate: 3500, // 节点移动更平滑
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
chartInstance.setOption(option);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
initChart();
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener('resize', () => {
|
|
|
|
|
|
if (chartInstance) {
|
|
|
|
|
|
chartInstance.resize();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
if (chartInstance) {
|
|
|
|
|
|
chartInstance.dispose();
|
|
|
|
|
|
chartInstance = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
window.removeEventListener('resize', () => {});
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
#bridgeCommunityChart {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 600px;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|