SocialNetworks_duan/src/views/KeyNodeRecognition2/components/graph/bridgeCommunityGraph.vue

508 lines
15 KiB
Vue
Raw Normal View History

2025-07-23 15:50:49 +08:00
<template>
<div id="bridgeCommunityChart" style="width: 100%; height: 100%;"></div>
</template>
<script setup>
2025-07-25 15:26:19 +08:00
import { onMounted, onUnmounted, defineProps, watch, ref, defineEmits } from 'vue';
2025-07-23 15:50:49 +08:00
import * as echarts from 'echarts';
import bridgeData from '@/assets/json/bridge_neighbors_communities.json';
import bridgeNodeHoverBgImg from '@/assets/images/bridge_node_hoverbgimg.png'
2025-07-23 19:40:41 +08:00
import { useKeyNodeStore2 } from '@/store/keyNodeStore2'; // 引入store
2025-07-24 17:02:04 +08:00
import { cropToCircleAsync } from "@/utils/transform"; // 引入头像处理
2025-07-25 15:26:19 +08:00
// 接收父组件传递的timestamp
2025-07-24 17:02:04 +08:00
const props = defineProps({
timestamp: {
type: Number,
default: 1
}
})
2025-07-23 15:50:49 +08:00
2025-07-25 15:26:19 +08:00
// 新增事件发射器--用于点击社团节点时,通知父组件跳转到社团的详情页
const emit = defineEmits(['click:navigateToCommunityDetail']);
2025-07-23 15:50:49 +08:00
let chartInstance = null;
2025-07-24 17:02:04 +08:00
// 跟踪当前激活的节点ID
const activeNodeId = ref(null);
// 用于后续鼠标悬浮桥梁节点高亮
const nodesData = ref([]);
const linksData = ref([]);
2025-07-23 15:50:49 +08:00
// 处理数据生成ECharts所需格式
2025-07-24 17:02:04 +08:00
const processData = async() => {
2025-07-23 15:50:49 +08:00
const nodes = [];
const links = [];
const addedCommunities = new Set();
2025-07-25 17:45:05 +08:00
// 新增存储已处理的桥梁节点ID
const addedBridgeNodes = new Set();
2025-07-24 17:02:04 +08:00
// 获取store实例
const keyNodeStore2 = useKeyNodeStore2();
// 批量将头像转换成base64
const bridgeNodes = keyNodeStore2.bridgeNodes;
// 根据timestamp过滤桥梁节点
const filteredBridgeNodes = bridgeNodes.filter(node => node.postsId <= props.timestamp);
for (const node of filteredBridgeNodes) {
if (node.defImg) {
node.defImg = await cropToCircleAsync(node.defImg);
}
if (node.activeImg) {
node.activeImg = await cropToCircleAsync(node.activeImg);
}
}
2025-07-23 15:50:49 +08:00
// 找出usersNum的最小值和最大值用于归一化
let minUsersNum = Infinity;
let maxUsersNum = -Infinity;
2025-07-24 17:02:04 +08:00
// 只处理过滤后的桥梁节点相关的社团
2025-07-23 15:50:49 +08:00
bridgeData.forEach(item => {
2025-07-24 17:02:04 +08:00
// 检查当前桥梁节点是否应该被包含
const isIncluded = filteredBridgeNodes.some(node => node.Node === item.bridgeId);
if (isIncluded) {
item.bridgeCommunities.forEach(community => {
minUsersNum = Math.min(minUsersNum, community.usersNum);
maxUsersNum = Math.max(maxUsersNum, community.usersNum);
});
}
2025-07-23 15:50:49 +08:00
});
2025-07-24 17:02:04 +08:00
2025-07-23 15:50:49 +08:00
// 节点大小的范围
const minNodeSize = 30;
2025-07-24 17:02:04 +08:00
const maxNodeSize = 50;
2025-07-23 15:50:49 +08:00
bridgeData.forEach(item => {
2025-07-24 17:02:04 +08:00
// 检查当前桥梁节点是否应该被包含
const isIncluded = filteredBridgeNodes.some(node => node.Node === item.bridgeId);
if (!isIncluded) {
return;
}
2025-07-23 15:50:49 +08:00
const bridgeId = item.bridgeId;
2025-07-23 19:40:41 +08:00
// 查找对应的桥梁节点图片
2025-07-24 17:02:04 +08:00
const bridgeNodeInfo = filteredBridgeNodes.find(node => node.Node === bridgeId);
const isActiveNode = activeNodeId.value === bridgeId;
const bridgeNodeImg = bridgeNodeInfo ?
(isActiveNode ? bridgeNodeInfo.activeImg : bridgeNodeInfo.defImg) : '';
2025-07-23 15:50:49 +08:00
2025-07-24 17:02:04 +08:00
// 添加桥梁节点
2025-07-25 17:45:05 +08:00
const bridgeNodeId = `bridge_${bridgeId}`;
2025-07-23 15:50:49 +08:00
nodes.push({
2025-07-25 17:45:05 +08:00
id: bridgeNodeId,
2025-07-23 15:50:49 +08:00
category: 0,
2025-07-24 17:02:04 +08:00
symbol: bridgeNodeImg ? `image://${bridgeNodeImg}` : 'circle',
symbolSize: 100,
originalId: bridgeId // 存储原始ID用于识别
2025-07-23 15:50:49 +08:00
});
2025-07-25 17:45:05 +08:00
addedBridgeNodes.add(bridgeId);
2025-07-24 17:02:04 +08:00
2025-07-23 15:50:49 +08:00
// 添加社团节点和边
item.bridgeCommunities.forEach(community => {
const communityId = community.communityId;
2025-07-23 19:40:41 +08:00
const userNumofCommunity = community.usersNum
const communityKey = `community_${communityId}_${userNumofCommunity}`;
2025-07-23 15:50:49 +08:00
const usersNum = community.usersNum;
// 归一化usersNum到节点大小范围
const size = minNodeSize +
((usersNum - minUsersNum) / (maxUsersNum - minUsersNum)) *
(maxNodeSize - minNodeSize);
2025-07-24 17:02:04 +08:00
// 添加社团节点(对于已有的社团不重复添加)
2025-07-23 15:50:49 +08:00
if (!addedCommunities.has(communityKey)) {
nodes.push({
id: communityKey,
category: 1,
2025-07-23 19:40:41 +08:00
symbolSize: size
2025-07-23 15:50:49 +08:00
});
addedCommunities.add(communityKey);
}
2025-07-24 17:02:04 +08:00
// 添加社团与桥梁的边
2025-07-23 15:50:49 +08:00
links.push({
2025-07-25 17:45:05 +08:00
source: bridgeNodeId,
2025-07-23 15:50:49 +08:00
target: communityKey,
value: 1
});
2025-07-24 17:02:04 +08:00
})
2025-07-28 16:02:19 +08:00
// 添加桥梁节点之间的边
2025-07-25 17:45:05 +08:00
if (item.bridgeNeighbours && item.bridgeNeighbours.length > 0) {
item.bridgeNeighbours.forEach(neighborBridgeId => {
// 检查邻居桥梁节点是否已处理
if (addedBridgeNodes.has(neighborBridgeId)) {
const neighborBridgeNodeId = `bridge_${neighborBridgeId}`;
// 添加桥梁到桥梁的边
links.push({
source: bridgeNodeId,
target: neighborBridgeNodeId,
value: 2, // 用不同的value区分桥梁间的边和桥梁-社团的边
lineStyle: {
color: '#ff7300', // 桥梁间的边使用不同颜色
width: 2
}
});
}
});
}
2025-07-24 17:02:04 +08:00
})
2025-07-23 15:50:49 +08:00
2025-07-24 17:02:04 +08:00
const result = { nodes, links };
nodesData.value = nodes;
linksData.value = links;
return result;
2025-07-23 15:50:49 +08:00
};
// 初始化图表
2025-07-24 17:02:04 +08:00
const initChart = async() => {
2025-07-23 15:50:49 +08:00
if (chartInstance) {
chartInstance.dispose();
}
2025-07-24 17:02:04 +08:00
2025-07-23 15:50:49 +08:00
const chartDom = document.getElementById('bridgeCommunityChart');
if (!chartDom) return;
chartInstance = echarts.init(chartDom);
2025-07-24 17:02:04 +08:00
// 添加鼠标悬浮事件监听
chartInstance.on('mouseover', function(params) {
2025-08-04 17:40:17 +08:00
if (!params.data) return;
const isBridgeNode = params.data.category === 0;
const isCommunityNode = params.data.category === 1;
if (isBridgeNode) {
2025-07-24 17:02:04 +08:00
// 如果是桥梁节点
const nodeId = params.data.originalId;
// 无论当前是否有激活节点,都设置为当前节点
activeNodeId.value = nodeId;
updateNodeImage(nodeId, true);
}
2025-08-04 17:40:17 +08:00
2025-07-24 17:02:04 +08:00
});
// 添加鼠标离开事件监听
chartInstance.on('mouseout', function(params) {
2025-08-04 17:40:17 +08:00
if (!params.data) return;
const isBridgeNode = params.data.category === 0;
const isCommunityNode = params.data.category === 1;
if (isBridgeNode) {
2025-07-24 17:02:04 +08:00
// 如果是桥梁节点
const nodeId = params.data.originalId;
if (activeNodeId.value === nodeId) {
activeNodeId.value = null;
updateNodeImage(nodeId, false);
}
}
});
// 添加点击事件监听
chartInstance.on('click', function(params) {
if (params.data && params.data.category === 0) {
// 如果是桥梁节点
const nodeId = params.data.originalId;
// 获取store实例
const keyNodeStore2 = useKeyNodeStore2();
// 查找对应的leader数据
const leader = keyNodeStore2.allLeaderData.find(l => l.nodeId === nodeId);
if (leader) {
// 打开leader详情弹窗
keyNodeStore2.openLeaderDetail({
id: leader.id
});
}
2025-07-25 15:26:19 +08:00
} else if(params.data && params.data.category === 1) {
// 如果是社团节点
// 提取社团ID
const communityId = params.data.id.split('_')[1];
console.log("点击社团节点:", communityId);
2025-07-28 16:02:19 +08:00
2025-07-25 15:26:19 +08:00
// 触发跳转事件
emit('click:navigateToCommunityDetail', communityId);
2025-07-24 17:02:04 +08:00
}
});
2025-07-23 15:50:49 +08:00
2025-07-24 17:02:04 +08:00
const { nodes, links } = await processData();
2025-07-23 15:50:49 +08:00
const categories = [
{name: "桥梁节点", category: 0},
2025-07-24 17:02:04 +08:00
{name: "普通社团", category: 1}
2025-07-23 15:50:49 +08:00
]
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) {
2025-07-24 17:02:04 +08:00
// 如果是边,则不显示提示框
if (params.dataType === 'edge') {
return '';
}
2025-07-23 15:50:49 +08:00
// 判断节点类型
if(params.data.category === 1) {
// 社团节点
2025-07-23 19:40:41 +08:00
const parts = params.data.id.split('_');
const extractedUserNum = parseInt(parts[parts.length - 1], 10);
2025-07-23 15:50:49 +08:00
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;">
2025-07-23 19:40:41 +08:00
社团用户数${extractedUserNum}
2025-07-23 15:50:49 +08:00
</div>
</div>`;
}else {
2025-07-24 17:02:04 +08:00
// 桥梁节点
// 查找对应的bridgeCommunities长度
const bridgeItem = bridgeData.find(item => item.bridgeId === (params.data.id || '').replace('bridge_', ''));
2025-07-23 15:50:49 +08:00
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',
2025-07-24 17:02:04 +08:00
zoom: 0.1,
2025-07-23 15:50:49 +08:00
layout: 'force',
2025-07-24 17:02:04 +08:00
force: {
// 节点间的排斥力,越大越疏散
repulsion: 50000,
// 连线长度,数值越大越分散
edgeLength: [800,6000],
// 全局吸引力,越小越松散
gravity: 0.8,
// 节点阻尼(摩擦),越小越灵活
friction: 0.2,
},
2025-07-23 15:50:49 +08:00
animation: false,
draggable: true,
data: nodes,
links: links,
categories: [
{
name: '桥梁节点',
itemStyle: {
2025-07-23 19:40:41 +08:00
// 节点颜色
color: '#ff7300',
2025-07-24 17:02:04 +08:00
},
2025-07-23 15:50:49 +08:00
},
{
2025-07-24 17:02:04 +08:00
name: '普通社团',
2025-07-23 15:50:49 +08:00
itemStyle: {
2025-07-23 19:40:41 +08:00
// 节点颜色
2025-07-24 17:02:04 +08:00
color: new echarts.graphic.RadialGradient(0.98, 0.38, 0.9, [
{ offset: 1, color: "#1a3860" }, // 最左侧
{ offset: 0.5, color: "#38546b" }, // 中间
{ offset: 0, color: "#5fb3b3" } // 最右侧
2025-07-23 19:40:41 +08:00
]),
2025-07-24 17:02:04 +08:00
opacity: 0.8,
2025-07-23 19:40:41 +08:00
// 边框样式
borderColor: '#2AB9FE',
borderWidth: 1,
borderType: 'dashed',
borderImageSource: 'linear-gradient(90deg, #2AB9FE 12.25%, #52FFF3 100.6%)',
borderImageSlice: 1,
},
2025-07-23 15:50:49 +08:00
}
],
roam: true,
lineStyle: {
2025-07-24 17:02:04 +08:00
// color: '#50AAD6',
color: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ offset: 0, color: '#233144' },
{ offset: 0.9, color: '#0578b0' }
]),
curveness: 0,
with: 2,
type: 'solid',
opacity: 0.5,
2025-07-23 15:50:49 +08:00
},
animationDurationUpdate: 3500, // 节点移动更平滑
2025-07-24 17:02:04 +08:00
animationEasingUpdate: 'quinticInOut',
emphasis: { // 高亮配置
focus: 'adjacency', // 高亮相邻节点
2025-08-04 17:40:17 +08:00
blurScope: 'coordinateSystem', // 失去焦点时取消所有高亮
2025-07-24 17:02:04 +08:00
lineStyle: { // 高亮时线条样式
width: 10 // 线条宽度(10)
}
},
2025-07-23 15:50:49 +08:00
}
]
};
chartInstance.setOption(option);
};
2025-07-24 17:02:04 +08:00
// 更新节点图片的方法
const updateNodeImage = (bridgeId, isActive) => {
// 查找对应的桥梁节点信息
const keyNodeStore2 = useKeyNodeStore2();
const filteredBridgeNodes = keyNodeStore2.bridgeNodes.filter(node => node.postsId <= props.timestamp);
// 创建新的节点数据数组(避免直接修改响应式数据)
const newNodes = [...nodesData.value];
// 先重置所有节点为默认图片
if (isActive) {
newNodes.forEach((node, index) => {
if (node.category === 0) { // 只处理桥梁节点
const nodeInfo = filteredBridgeNodes.find(n => n.Node === node.originalId);
if (nodeInfo && node.originalId !== bridgeId) {
newNodes[index].symbol = nodeInfo.defImg ? `image://${nodeInfo.defImg}` : 'circle';
}
}
});
}
// 更新当前节点
const bridgeNodeInfo = filteredBridgeNodes.find(node => node.Node === bridgeId);
if (bridgeNodeInfo) {
// 找到要更新的节点索引
const nodeIndex = nodesData.value.findIndex(node => node.originalId === bridgeId);
if (nodeIndex !== -1) {
// 创建新的节点数据数组(避免直接修改响应式数据)
const newNodes = [...nodesData.value];
// 更新节点图片
const imgUrl = isActive ? bridgeNodeInfo.activeImg : bridgeNodeInfo.defImg;
newNodes[nodeIndex].symbol = imgUrl ? `image://${imgUrl}` : 'circle';
2025-07-24 17:48:09 +08:00
chartInstance.dispatchAction({
//再让单独一个高亮
type: "highlight",
dataIndex: nodeIndex
});
2025-07-24 17:02:04 +08:00
// 使用setOption进行增量更新
chartInstance.setOption({
series: [{
data: newNodes
}]
});
2025-07-24 17:48:09 +08:00
2025-07-24 17:02:04 +08:00
// 更新nodesData保持数据一致性
nodesData.value = newNodes;
}
}
};
// 添加highlightNode方法
const highlightNode = (leaderId) => {
// 查找对应的桥梁节点ID
const keyNodeStore2 = useKeyNodeStore2();
const leader = keyNodeStore2.allLeaderData.find(l => l.id === leaderId);
if (leader) {
// 先重置之前的激活节点
if (activeNodeId.value) {
updateNodeImage(activeNodeId.value, false);
}
// 设置新的激活节点
activeNodeId.value = leader.nodeId;
updateNodeImage(leader.nodeId, true);
}
};
2025-07-23 15:50:49 +08:00
onMounted(() => {
2025-07-24 17:02:04 +08:00
setTimeout(async () => {
await initChart();
2025-07-23 15:50:49 +08:00
}, 100);
window.addEventListener('resize', () => {
if (chartInstance) {
chartInstance.resize();
}
});
});
2025-07-24 17:02:04 +08:00
watch(
() => props.timestamp,
async (newValue) => {
await initChart()
}
)
2025-07-23 15:50:49 +08:00
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
window.removeEventListener('resize', () => {});
});
2025-07-24 17:02:04 +08:00
// 暴露highlightNode方法
defineExpose({ highlightNode });
2025-07-23 15:50:49 +08:00
</script>
<style scoped>
#bridgeCommunityChart {
width: 100%;
height: 600px;
}
</style>