2025-07-15 11:07:24 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="communityNode-component">
|
|
|
|
|
|
<div id="container" class="container" ref="teamContainer"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { defineProps, onMounted, ref, defineEmits, defineExpose, watch } from "vue";
|
|
|
|
|
|
import * as echarts from "echarts";
|
|
|
|
|
|
import anchor from "@/assets/json/anchor_community.json";
|
|
|
|
|
|
import community from "@/assets/json/community_nodes.json";
|
|
|
|
|
|
import nodeHoverImg from "@/assets/images/nodeHover.png";
|
|
|
|
|
|
import { storeToRefs } from "pinia";
|
|
|
|
|
|
import { useKeyNodeRecognitionStore } from "@/store/keyNodeRecognition/index";
|
|
|
|
|
|
import csvUrl from "@/assets/json/community_edges.csv?url";
|
|
|
|
|
|
const keyNodeStore = useKeyNodeRecognitionStore();
|
|
|
|
|
|
const { currentUser } = storeToRefs(keyNodeStore);
|
|
|
|
|
|
const props = defineProps({});
|
|
|
|
|
|
const emit = defineEmits(["click:anchorNode"]);
|
|
|
|
|
|
const teamContainer = ref(null);
|
|
|
|
|
|
|
|
|
|
|
|
//当前在userPanel中选择的某个user
|
|
|
|
|
|
const currentSelectedUser = ref(null);
|
|
|
|
|
|
let chart = null;
|
|
|
|
|
|
//当选中userPanel中的某个用户的时候,更新当前组件的用户
|
|
|
|
|
|
watch(currentUser, (val) => {
|
|
|
|
|
|
if (val) {
|
|
|
|
|
|
currentSelectedUser.value = val;
|
|
|
|
|
|
const option = chart.getOption();
|
|
|
|
|
|
|
|
|
|
|
|
//实现选中只高亮一个
|
|
|
|
|
|
chart.dispatchAction({
|
|
|
|
|
|
//先让所有的先取消高亮
|
|
|
|
|
|
type: "downplay",
|
|
|
|
|
|
seriesIndex: 0
|
|
|
|
|
|
});
|
|
|
|
|
|
const nodeIndex = option.series[0].data.findIndex((item) =>
|
|
|
|
|
|
item.anchorList.includes(currentSelectedUser.value.name)
|
|
|
|
|
|
);
|
|
|
|
|
|
chart.dispatchAction({
|
|
|
|
|
|
//再让单独一个高亮
|
|
|
|
|
|
type: "highlight",
|
|
|
|
|
|
dataIndex: nodeIndex
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 平移到节点
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const initChart = async () => {
|
|
|
|
|
|
// 初始化图表
|
|
|
|
|
|
chart = echarts.init(document.getElementById("container"));
|
|
|
|
|
|
|
|
|
|
|
|
//处理data
|
|
|
|
|
|
//统计每一个锚点在某些社团,这个社团包含锚点的个数(key: 社团id, value: 这个社团含有几个锚点)
|
|
|
|
|
|
//统计每个社团包含的锚点id列表
|
|
|
|
|
|
const anchorCommunityInfo = {};
|
|
|
|
|
|
Object.entries(anchor).forEach(([anchorId, communityId]) => {
|
|
|
|
|
|
if (!anchorCommunityInfo[communityId]) {
|
|
|
|
|
|
//如果该社团不包含锚点
|
|
|
|
|
|
anchorCommunityInfo[communityId] = []; //就初始化该社团的锚点数组
|
|
|
|
|
|
}
|
|
|
|
|
|
anchorCommunityInfo[communityId].push(anchorId); //把这个社团中的所有锚点id存到列表中(处理一对多的关系:一个社团对应多个锚点)
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
//包含锚点的社团 (绿色闪烁的节点)
|
|
|
|
|
|
const includeAnchorCommunity = Object.entries(anchorCommunityInfo).map(
|
|
|
|
|
|
([communityId, anchorList]) => ({
|
|
|
|
|
|
name: parseInt(communityId), // 社团id
|
|
|
|
|
|
anchorCount: anchorList.length, // 该社团包含锚点的个数
|
|
|
|
|
|
anchorList: anchorList // 该社团包含的锚点id数组
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 创建包含锚点的社团名称集合(提高查找效率)
|
|
|
|
|
|
const includedCommunityId = includeAnchorCommunity.map((node) => node.name.toString());
|
|
|
|
|
|
|
|
|
|
|
|
//筛选出不包含锚点的社团(不闪烁的蓝色节点)
|
|
|
|
|
|
const uncontainedAnchorCommunity = Object.keys(community)
|
|
|
|
|
|
.filter((node) => !includedCommunityId.includes(node))
|
|
|
|
|
|
.map((node) => ({ name: parseInt(node), anchorCount: 0 }));
|
|
|
|
|
|
|
|
|
|
|
|
//合并成功后,统一设置id,避免重复id出现导致echarts渲染失败,并且取出每个社团中有多少个节点
|
|
|
|
|
|
const nodes = includeAnchorCommunity.concat(uncontainedAnchorCommunity).map((node, index) => ({
|
|
|
|
|
|
id: index,
|
|
|
|
|
|
total: Object.values(community)[node.name].length,
|
|
|
|
|
|
category: node.anchorCount > 0 ? 1 : 0, //(1:含有锚点的社团分类,0:不含锚点的社团)
|
|
|
|
|
|
...node
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
//处理links
|
|
|
|
|
|
// 先把csv文件里的数据进行格式化
|
|
|
|
|
|
let links = [];
|
|
|
|
|
|
const linksCsv = await fetch(csvUrl);
|
|
|
|
|
|
const csvText = await linksCsv.text();
|
|
|
|
|
|
const lines = csvText.split("\n");
|
|
|
|
|
|
links = lines
|
|
|
|
|
|
.slice(1)
|
|
|
|
|
|
.map((line) => {
|
|
|
|
|
|
if (!line) return null;
|
|
|
|
|
|
const values = line.split(",");
|
|
|
|
|
|
return {
|
|
|
|
|
|
source: parseInt(values[0]),
|
|
|
|
|
|
target: parseInt(values[1]),
|
|
|
|
|
|
edge: parseInt(values[2])
|
|
|
|
|
|
};
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
|
|
|
|
|
const data = { nodes, links };
|
2025-07-15 16:01:27 +08:00
|
|
|
|
console.log(data);
|
2025-07-15 11:07:24 +08:00
|
|
|
|
|
|
|
|
|
|
const categories = [
|
2025-07-15 16:01:27 +08:00
|
|
|
|
{ name: "普通社团", category: 0 },
|
|
|
|
|
|
{ name: "含锚点社团", category: 1 }
|
2025-07-15 11:07:24 +08:00
|
|
|
|
];
|
|
|
|
|
|
// // 初始选项
|
|
|
|
|
|
const option = {
|
|
|
|
|
|
//图例配置
|
|
|
|
|
|
legend: [
|
|
|
|
|
|
{
|
2025-07-15 16:01:27 +08:00
|
|
|
|
data: categories.map((c) => ({
|
|
|
|
|
|
name: c.name,
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
color:
|
|
|
|
|
|
c.category === 0
|
|
|
|
|
|
? new echarts.graphic.LinearGradient(
|
|
|
|
|
|
1,
|
|
|
|
|
|
0,
|
|
|
|
|
|
0,
|
|
|
|
|
|
0, // 从左到右渐变(x0=1, x1=0)
|
|
|
|
|
|
[
|
|
|
|
|
|
{ offset: 0, color: "#49c3ed" }, // 起始色(最右)
|
|
|
|
|
|
{ offset: 0.5, color: "#5fa3e0" }, // 中间色
|
|
|
|
|
|
{ offset: 1, color: "#7286d4" } // 结束色(最左)
|
|
|
|
|
|
]
|
|
|
|
|
|
) // 分类0的渐变
|
|
|
|
|
|
: new echarts.graphic.LinearGradient(
|
|
|
|
|
|
1,
|
|
|
|
|
|
0,
|
|
|
|
|
|
0,
|
|
|
|
|
|
0, // 从左到右渐变 (x0=1, x1=0)
|
|
|
|
|
|
[
|
|
|
|
|
|
{ offset: 0, color: "#7ff2c1" },
|
|
|
|
|
|
{ offset: 0.37, color: "#85e7d2" },
|
|
|
|
|
|
{ offset: 1, color: "#8bdbe4" }
|
|
|
|
|
|
]
|
|
|
|
|
|
) // 分类1的渐变
|
|
|
|
|
|
}
|
|
|
|
|
|
})),
|
2025-07-15 11:07:24 +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"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
//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;",
|
|
|
|
|
|
formatter: function (params) {
|
|
|
|
|
|
if (params.dataType === "node") {
|
|
|
|
|
|
return `<div
|
|
|
|
|
|
style="
|
|
|
|
|
|
width: 107px;
|
|
|
|
|
|
height: 68px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
background: url('${nodeHoverImg}');
|
|
|
|
|
|
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;">
|
|
|
|
|
|
<div >节点数:${params.data.total}</div>
|
|
|
|
|
|
<div>锚点数:${params.data.anchorCount}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
//权值设置样式
|
|
|
|
|
|
edgeLabel: {
|
|
|
|
|
|
show: false,
|
|
|
|
|
|
position: "middle",
|
|
|
|
|
|
formatter: function (params) {
|
|
|
|
|
|
return params.data.edge;
|
|
|
|
|
|
},
|
|
|
|
|
|
fontSize: 14
|
|
|
|
|
|
},
|
|
|
|
|
|
emphasis: {
|
|
|
|
|
|
edgeLabel: {
|
|
|
|
|
|
show: true,
|
|
|
|
|
|
color: "#fff",
|
|
|
|
|
|
fontSize: 18,
|
|
|
|
|
|
textShadowColor: "#fff",
|
|
|
|
|
|
textShadowBlur: 0,
|
|
|
|
|
|
textShadowOffsetX: 0,
|
|
|
|
|
|
textShadowOffsetY: 0
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
series: [
|
|
|
|
|
|
{
|
|
|
|
|
|
type: "graph",
|
|
|
|
|
|
layout: "force",
|
|
|
|
|
|
animation: false,
|
|
|
|
|
|
draggable: true,
|
|
|
|
|
|
roam: true,
|
|
|
|
|
|
zoom: 0.4,
|
|
|
|
|
|
categories: categories,
|
|
|
|
|
|
force: {
|
|
|
|
|
|
initLayout: null,
|
|
|
|
|
|
edgeLength: 1000,
|
|
|
|
|
|
repulsion: 1000,
|
|
|
|
|
|
gravity: 0.4,
|
|
|
|
|
|
friction: 0.6,
|
|
|
|
|
|
layoutAnimation: true,
|
|
|
|
|
|
coolingFactor: 0.1
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
animationDurationUpdate: 3500, // 节点移动更平滑
|
|
|
|
|
|
data: data.nodes.map((node) => ({
|
|
|
|
|
|
...node,
|
|
|
|
|
|
symbolSize: node.total / 20 < 30 ? 30 : node.total / 20,
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
color: {
|
|
|
|
|
|
type: "radial",
|
|
|
|
|
|
x: 0.97,
|
|
|
|
|
|
y: 0.38,
|
|
|
|
|
|
r: 0.86,
|
|
|
|
|
|
colorStops:
|
|
|
|
|
|
node.anchorCount === 0
|
|
|
|
|
|
? [
|
|
|
|
|
|
// anchorCount为0的节点渐变(深蓝到青)
|
|
|
|
|
|
{ offset: 0, color: "#49c3ed" }, // 最右侧
|
|
|
|
|
|
{ offset: 0.5, color: "#5fa3e0" }, // 中间
|
|
|
|
|
|
{ offset: 1, color: "#7286d4" } // 最左侧
|
|
|
|
|
|
]
|
|
|
|
|
|
: [
|
|
|
|
|
|
// anchorCount为1的节点渐变(原配色)
|
|
|
|
|
|
{ offset: 0, color: "#7ff2c1" },
|
|
|
|
|
|
{ offset: 0.37, color: "#85e7d2" },
|
|
|
|
|
|
{ offset: 1, color: "#8bdbe4" }
|
|
|
|
|
|
]
|
|
|
|
|
|
},
|
|
|
|
|
|
opacity: 1,
|
|
|
|
|
|
borderColor: "#46C6AD",
|
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
|
shadowBlur: 4,
|
|
|
|
|
|
borderType: "dashed",
|
|
|
|
|
|
shadowColor: "rgba(19, 27, 114, 0.25)"
|
|
|
|
|
|
},
|
|
|
|
|
|
emphasis: {
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
shadowBlur: 20,
|
|
|
|
|
|
shadowColor: "#c4a651",
|
|
|
|
|
|
borderColor: "#fcd267",
|
|
|
|
|
|
borderWidth: node.category == 0 ? 1 : 5,
|
|
|
|
|
|
borderType: "solid"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})),
|
|
|
|
|
|
links: data.links,
|
|
|
|
|
|
lineStyle: {
|
|
|
|
|
|
color: "#37ACD7",
|
|
|
|
|
|
width: 1
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
chart.setOption(option);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleClickNode = () => {
|
|
|
|
|
|
chart.on("click", function (params) {
|
|
|
|
|
|
//params.data.name才是社团id
|
|
|
|
|
|
if (params.dataType === "node" && params.data.category == 1) {
|
|
|
|
|
|
emit("click:anchorNode", params.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
initChart();
|
|
|
|
|
|
handleClickNode();
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="less">
|
|
|
|
|
|
.communityNode-component {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
.container {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 450px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|