SocialNetworks_duan/src/views/KeyNodeRecognition2/components/graph/bridgeCommunityGraph.vue
2025-08-11 15:35:45 +08:00

539 lines
16 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 id="bridgeCommunityChart" style="width: 100%; height: 100%;"></div>
</template>
<script setup>
import { onMounted, onUnmounted, defineProps, watch, ref, defineEmits } 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'
import { useKeyNodeStore2 } from '@/store/keyNodeStore2'; // 引入store
import { cropToCircleAsync } from "@/utils/transform"; // 引入头像处理
// 接收父组件传递的timestamp
const props = defineProps({
timestamp: {
type: Number,
default: 1
}
})
// 新增事件发射器--用于点击社团节点时,通知父组件跳转到社团的详情页
const emit = defineEmits(['click:navigateToCommunityDetail']);
let chartInstance = null;
// 跟踪当前激活的节点ID
const activeNodeId = ref(null);
// 用于后续鼠标悬浮桥梁节点高亮
const nodesData = ref([]);
const linksData = ref([]);
// 处理数据生成ECharts所需格式
const processData = async() => {
const nodes = [];
const links = [];
const addedCommunities = new Set();
// 新增存储已处理的桥梁节点ID
const addedBridgeNodes = new Set();
// 获取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);
}
}
// 找出usersNum的最小值和最大值用于归一化
let minUsersNum = Infinity;
let maxUsersNum = -Infinity;
// 只处理过滤后的桥梁节点相关的社团
bridgeData.forEach(item => {
// 检查当前桥梁节点是否应该被包含
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);
});
}
});
// 节点大小的范围
const minNodeSize = 30;
const maxNodeSize = 50;
bridgeData.forEach(item => {
// 检查当前桥梁节点是否应该被包含
const isIncluded = filteredBridgeNodes.some(node => node.Node === item.bridgeId);
if (!isIncluded) {
return;
}
const bridgeId = item.bridgeId;
// 查找对应的桥梁节点图片
const bridgeNodeInfo = filteredBridgeNodes.find(node => node.Node === bridgeId);
const isActiveNode = activeNodeId.value === bridgeId;
const bridgeNodeImg = bridgeNodeInfo ?
(isActiveNode ? bridgeNodeInfo.activeImg : bridgeNodeInfo.defImg) : '';
// 添加桥梁节点
const bridgeNodeId = `bridge_${bridgeId}`;
nodes.push({
id: bridgeNodeId,
category: 0,
symbol: bridgeNodeImg ? `image://${bridgeNodeImg}` : 'circle',
symbolSize: 150,
originalId: bridgeId // 存储原始ID用于识别
});
addedBridgeNodes.add(bridgeId);
// 添加社团节点和边
item.bridgeCommunities.forEach(community => {
const communityId = community.communityId;
const userNumofCommunity = community.usersNum
const communityKey = `community_${communityId}_${userNumofCommunity}`;
const usersNum = community.usersNum;
// 归一化usersNum到节点大小范围
const size = minNodeSize +
((usersNum - minUsersNum) / (maxUsersNum - minUsersNum)) *
(maxNodeSize - minNodeSize);
// 添加社团节点(对于已有的社团不重复添加)
if (!addedCommunities.has(communityKey)) {
nodes.push({
id: communityKey,
category: 1,
symbolSize: size
});
addedCommunities.add(communityKey);
}
// 添加社团与桥梁的边
links.push({
source: bridgeNodeId,
target: communityKey,
value: 1
});
})
// 添加桥梁节点之间的边
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
}
});
}
});
}
})
const result = { nodes, links };
nodesData.value = nodes;
linksData.value = links;
return result;
};
// 初始化图表
const initChart = async() => {
if (chartInstance) {
chartInstance.dispose();
}
const chartDom = document.getElementById('bridgeCommunityChart');
if (!chartDom) return;
chartInstance = echarts.init(chartDom);
const SERIES_INDEX = 0;
// 添加鼠标悬浮事件监听
chartInstance.on('mouseover', function(params) {
if (!params.data) return;
if (params.dataType === 'node') {
// 仅节点时触发相邻高亮
chartInstance.dispatchAction({
type: 'focusNodeAdjacency',
seriesIndex: SERIES_INDEX,
dataIndex: params.dataIndex
});
// 已有的桥梁节点头像切换逻辑
const isBridgeNode = params.data.category === 0;
if (isBridgeNode) {
const nodeId = params.data.originalId;
activeNodeId.value = nodeId;
updateNodeImage(nodeId, true);
}
} else if (params.dataType === 'edge') {
// 悬浮在边上:不高亮
chartInstance.dispatchAction({ type: 'unfocusNodeAdjacency', seriesIndex: SERIES_INDEX });
chartInstance.dispatchAction({ type: 'downplay', seriesIndex: SERIES_INDEX });
}
});
// 添加鼠标离开事件监听
chartInstance.on('mouseout', function(params) {
if (!params.data) return;
if (params.dataType === 'node') {
// 仅节点移出时取消相邻高亮
chartInstance.dispatchAction({
type: 'unfocusNodeAdjacency',
seriesIndex: SERIES_INDEX
});
// 你已有的桥梁节点头像还原逻辑
const isBridgeNode = params.data.category === 0;
if (isBridgeNode) {
const nodeId = params.data.originalId;
if (activeNodeId.value === nodeId) {
activeNodeId.value = null;
updateNodeImage(nodeId, false);
}
}
} else if (params.dataType === 'edge') {
// 从边移出也确保没有残留高亮
chartInstance.dispatchAction({
type: 'unfocusNodeAdjacency',
seriesIndex: SERIES_INDEX
});
}
});
// 添加点击事件监听
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
});
}
} else if(params.data && params.data.category === 1) {
// 如果是社团节点
// 提取社团ID
const communityId = params.data.id.split('_')[1];
console.log("点击社团节点:", communityId);
// 触发跳转事件
emit('click:navigateToCommunityDetail', communityId);
}
});
const { nodes, links } = await 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.dataType === 'edge') {
return '';
}
// 判断节点类型
if(params.data.category === 1) {
// 社团节点
const parts = params.data.id.split('_');
const extractedUserNum = parseInt(parts[parts.length - 1], 10);
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;">
社团用户数:${extractedUserNum}
</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,
// 锚点账号用圆形头像,普通账号保持默认
// 添加自定义图标
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)}`
})),
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.1,
layout: 'force',
force: {
// 节点间的排斥力,越大越疏散
repulsion: 50000,
// 连线长度,数值越大越分散
edgeLength: [800,6000],
// 全局吸引力,越小越松散
gravity: 0.8,
// 节点阻尼(摩擦),越小越灵活
friction: 0.2,
},
animation: false,
draggable: true,
data: nodes,
links: links,
categories: [
{
name: '桥梁节点',
itemStyle: {
// 节点颜色
color: '#ff7300',
},
},
{
name: '普通社团',
itemStyle: {
// 节点颜色
color: new echarts.graphic.RadialGradient(0.98, 0.38, 0.9, [
{ offset: 1, color: "#1a3860" }, // 最左侧
{ offset: 0.5, color: "#38546b" }, // 中间
{ offset: 0, color: "#5fb3b3" } // 最右侧
]),
opacity: 0.8,
// 边框样式
borderColor: '#2AB9FE',
borderWidth: 1,
borderType: 'dashed',
borderImageSource: 'linear-gradient(90deg, #2AB9FE 12.25%, #52FFF3 100.6%)',
borderImageSlice: 1,
},
}
],
roam: true,
lineStyle: {
// 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,
},
animationDurationUpdate: 3500, // 节点移动更平滑
animationEasingUpdate: 'quinticInOut',
emphasis: {
focus: 'adjacency', // 有了这个,才会触发“其他元素进入 blur 状态”
blurScope: 'series', // 只在本系列里变暗
scale: false, // 不要把节点放大,避免跳动
lineStyle: { opacity: 1 },
itemStyle: { opacity: 1 },
label: { opacity: 1 }
},
blur: {
// 非相邻元素进入“模糊/暗”状态时的样式(相当于“隐藏”)
lineStyle: { opacity: 0.05 },
itemStyle: { opacity: 0.1 },
label: { opacity: 0 }
}
}
]
};
chartInstance.setOption(option);
};
// 更新节点图片的方法
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';
chartInstance.dispatchAction({
//再让单独一个高亮
type: "highlight",
dataIndex: nodeIndex
});
// 使用setOption进行增量更新
chartInstance.setOption({
series: [{
data: newNodes
}]
});
// 更新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);
}
};
onMounted(() => {
setTimeout(async () => {
await initChart();
}, 100);
window.addEventListener('resize', () => {
if (chartInstance) {
chartInstance.resize();
}
});
});
watch(
() => props.timestamp,
async (newValue) => {
await initChart()
}
)
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
window.removeEventListener('resize', () => {});
});
// 暴露highlightNode方法
defineExpose({ highlightNode });
</script>
<style scoped>
#bridgeCommunityChart {
width: 100%;
height: 600px;
}
</style>