2025-07-15 11:07:24 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="detailNode-component">
|
|
|
|
|
|
<img src="@/assets/images/icon/goback.png" alt="" class="goback" @click="handleGoback" />
|
|
|
|
|
|
<div class="container" ref="detailContainer" id="container"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { defineProps, onMounted, ref } from "vue";
|
|
|
|
|
|
import * as echarts from "echarts";
|
|
|
|
|
|
import { cropToCircleAsync } from "@/utils/transform";
|
|
|
|
|
|
import anchorNeighbors from "@/assets/json/anchor_neighbors.json";
|
|
|
|
|
|
import nodeHoverImg from "@/assets/images/nodeHover.png";
|
|
|
|
|
|
import { useKeyNodeRecognitionStore } from "@/store/keyNodeRecognition/index";
|
|
|
|
|
|
const keyNodeStore = useKeyNodeRecognitionStore();
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
communityNode: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: 0
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
const emit = defineEmits(["click:openDialog", "click:goback"]);
|
|
|
|
|
|
const detailContainer = ref(null);
|
|
|
|
|
|
const currentSelectedCommunity = ref({});
|
|
|
|
|
|
let chart = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 根据节点数量计算缩放比例
|
|
|
|
|
|
const calculateInitialZoom = (nodes) => {
|
|
|
|
|
|
const NODE_COUNT = nodes.length;
|
|
|
|
|
|
if (NODE_COUNT > 1000) return 0.1;
|
|
|
|
|
|
if (NODE_COUNT > 200) return 0.2;
|
|
|
|
|
|
if (NODE_COUNT > 100) return 0.3;
|
|
|
|
|
|
if (NODE_COUNT > 50) return 0.4;
|
|
|
|
|
|
return 0.8; // 默认值
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const initChart = async () => {
|
|
|
|
|
|
currentSelectedCommunity.value = props.communityNode;
|
|
|
|
|
|
|
|
|
|
|
|
chart = echarts.init(document.getElementById("container"));
|
|
|
|
|
|
//处理节点
|
|
|
|
|
|
//从锚点邻居数据集中查找出该锚点所有的邻居节点
|
|
|
|
|
|
let nodes = [];
|
|
|
|
|
|
|
|
|
|
|
|
//批量将头像转换成base64
|
|
|
|
|
|
const extraInfo = keyNodeStore.anchorExtraInfos;
|
|
|
|
|
|
const keys = Object.keys(extraInfo);
|
|
|
|
|
|
for (const key of keys) {
|
|
|
|
|
|
const userInfo = extraInfo[key];
|
|
|
|
|
|
userInfo.avatar = await cropToCircleAsync(userInfo.avatar);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//筛选出本社团中所有锚点的邻居节点并重新将需要补充的数据映射到该列表中
|
|
|
|
|
|
const filterResult = Object.entries(anchorNeighbors)
|
|
|
|
|
|
.filter(([anchorId]) => currentSelectedCommunity.value.anchorList.includes(anchorId))
|
|
|
|
|
|
.map((filteredList) => ({
|
|
|
|
|
|
...extraInfo[filteredList[0]],
|
|
|
|
|
|
anchor: filteredList[0],
|
|
|
|
|
|
neighbors: filteredList[1].map((neighNode) => ({ name: neighNode, avatar: "" }))
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
//处理连线
|
|
|
|
|
|
let links = [];
|
|
|
|
|
|
filterResult.forEach(({ anchor, neighbors }) => {
|
|
|
|
|
|
(neighbors ?? []).forEach((neigh) => {
|
|
|
|
|
|
links.push({ source: anchor, target: neigh.name });
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 合并所有锚点和邻居节点到一个数组并去重
|
|
|
|
|
|
let nodeSet = new Set();
|
|
|
|
|
|
filterResult.forEach((item) => {
|
|
|
|
|
|
//添加锚点自己
|
|
|
|
|
|
if (!nodeSet.has(item.anchor)) {
|
|
|
|
|
|
nodes.push({ name: item?.anchor, value: item?.anchor, category: 1, ...item });
|
|
|
|
|
|
nodeSet.add(item?.anchor);
|
|
|
|
|
|
}
|
|
|
|
|
|
//添加该锚点的邻居
|
|
|
|
|
|
(item.neighbors || []).forEach((n) => {
|
|
|
|
|
|
if (!nodeSet.has(n?.name)) {
|
|
|
|
|
|
nodes.push({ name: n.name, value: n.name, category: 0, avatar: n.avatar ?? "" });
|
|
|
|
|
|
nodeSet.add(n.name);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = { nodes, links };
|
|
|
|
|
|
console.log(data);
|
|
|
|
|
|
|
2025-07-15 16:01:27 +08:00
|
|
|
|
const categories = [
|
|
|
|
|
|
{ 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, [
|
|
|
|
|
|
{ offset: 0, color: "#49c3ed" },
|
|
|
|
|
|
{ offset: 0.5, color: "#5fa3e0" },
|
|
|
|
|
|
{ offset: 1, color: "#7286d4" }
|
|
|
|
|
|
])
|
|
|
|
|
|
: "#cccccc" // 锚点账号用灰色纯色(默认头像常用色)
|
|
|
|
|
|
},
|
|
|
|
|
|
// 锚点账号用圆形头像,普通账号保持默认
|
|
|
|
|
|
icon:
|
|
|
|
|
|
c.category === 1
|
|
|
|
|
|
? "path://M50 50 m -50 0 a 50 50 0 1 0 100 0 a 50 50 0 1 0 -100 0 Z" // 精确圆形路径
|
|
|
|
|
|
: "circle" // 普通账号用ECharts内置circle
|
|
|
|
|
|
})),
|
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" && params.data.category) {
|
|
|
|
|
|
return `<div
|
|
|
|
|
|
style="
|
|
|
|
|
|
height: 76px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
background: url('${nodeHoverImg}');
|
|
|
|
|
|
background-size: cover;
|
|
|
|
|
|
background-position: center;
|
|
|
|
|
|
background-repeat: no-repeat;
|
|
|
|
|
|
padding:20px 20px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style="color: #fff; letter-spacing: 0.14px; display: flex; align-items: center">
|
|
|
|
|
|
<div style="font-size: 16px">${params.data.name}</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style="
|
|
|
|
|
|
margin-left: 10px;
|
|
|
|
|
|
padding: 3px 7px;
|
|
|
|
|
|
background-color: rgba(171, 247, 255, 0.2);
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
"
|
|
|
|
|
|
>
|
|
|
|
|
|
${params.data.label}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="display: flex; font-size: 14px; margin-top: 5px;color:#fff">
|
|
|
|
|
|
<div>关注数: ${params.data.atten}</div>
|
|
|
|
|
|
<div style="margin-left: 20px">粉丝数: ${params.data.fancy}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>`;
|
2025-07-15 16:01:27 +08:00
|
|
|
|
} else if (params.dataType === "node" && !params.data.category) {
|
|
|
|
|
|
return `<div
|
2025-07-15 11:07:24 +08:00
|
|
|
|
style="
|
|
|
|
|
|
height: 56px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
background: url('${nodeHoverImg}');
|
|
|
|
|
|
background-size: cover;
|
|
|
|
|
|
background-position: center;
|
|
|
|
|
|
background-repeat: no-repeat;
|
|
|
|
|
|
padding:10px 10px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style="color: #fff; letter-spacing: 0.14px; display: flex; align-items: center">
|
|
|
|
|
|
<div style="font-size: 16px">${params.data.name}</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style="
|
|
|
|
|
|
margin-left: 10px;
|
|
|
|
|
|
padding: 3px 7px;
|
|
|
|
|
|
background-color: rgba(171, 247, 255, 0.2);
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
"
|
|
|
|
|
|
>
|
|
|
|
|
|
普通自媒体
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>`;
|
2025-07-15 16:01:27 +08:00
|
|
|
|
}
|
2025-07-15 11:07:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
series: [
|
|
|
|
|
|
{
|
|
|
|
|
|
type: "graph",
|
|
|
|
|
|
zoom: calculateInitialZoom(data.nodes),
|
|
|
|
|
|
layout: "force",
|
|
|
|
|
|
animation: false,
|
|
|
|
|
|
draggable: true,
|
|
|
|
|
|
roam: true,
|
|
|
|
|
|
categories: categories,
|
|
|
|
|
|
force: {
|
|
|
|
|
|
initLayout: "circular",
|
|
|
|
|
|
edgeLength: data.nodes.length > 1000 ? 600 : 300,
|
|
|
|
|
|
repulsion: 5000,
|
|
|
|
|
|
gravity: 0.9,
|
|
|
|
|
|
friction: 0.1,
|
|
|
|
|
|
layoutAnimation: true,
|
|
|
|
|
|
coolingFactor: 0.1
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
animationDurationUpdate: 3500, // 节点移动更平滑
|
|
|
|
|
|
data: data.nodes.map((node) => ({
|
|
|
|
|
|
...node,
|
|
|
|
|
|
symbolSize: node.category == 1 ? 70 : 30,
|
|
|
|
|
|
symbol: node?.avatar ? `image://${node?.avatar}` : "",
|
|
|
|
|
|
z: node.category === 1 ? 10 : 1, // 设置层级
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
color: {
|
|
|
|
|
|
type: "radial",
|
|
|
|
|
|
x: 0.97,
|
|
|
|
|
|
y: 0.38,
|
|
|
|
|
|
r: 0.86,
|
|
|
|
|
|
colorStops:
|
|
|
|
|
|
node.category === 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: 0,
|
|
|
|
|
|
shadowBlur: 4,
|
|
|
|
|
|
shadowColor: "rgba(19, 27, 114, 0.25)",
|
|
|
|
|
|
shadowOffsetY: 4
|
|
|
|
|
|
}
|
|
|
|
|
|
})),
|
|
|
|
|
|
links: data.links,
|
|
|
|
|
|
lineStyle: {
|
|
|
|
|
|
color: "#37ACD7",
|
|
|
|
|
|
width: 1
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
chart.setOption(option);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleClickNode = () => {
|
|
|
|
|
|
chart.on("click", function (params) {
|
|
|
|
|
|
if (params.dataType == "node" && params.data.category == 1) {
|
|
|
|
|
|
emit("click:openDialog", params.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleGoback = () => {
|
|
|
|
|
|
emit("click:goback", "CommunityNode");
|
|
|
|
|
|
};
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
await initChart();
|
|
|
|
|
|
handleClickNode();
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="less">
|
|
|
|
|
|
.detailNode-component {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
.container {
|
|
|
|
|
|
width: 100%;
|
2025-07-15 16:01:27 +08:00
|
|
|
|
height: 500px;
|
2025-07-15 11:07:24 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.goback {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 10px;
|
|
|
|
|
|
left: 20px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|