SocialNetworks/src/views/keyNodeRecognition3/components/detailNode.vue

320 lines
9.4 KiB
Vue
Raw Normal View History

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>