SocialNetworks_duan/src/views/LinkPrediction/socialGroups/components/communityNode.vue
qumeng039@126.com 2c9da741cf 解决第二个模块关系图第一次点击不渲染问题
使用watch监听communityNodeList的变化,只要发生改变,就调用initChart函数,并且移除在onMounted中调用initChart
2025-08-15 16:59:01 +08:00

367 lines
11 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 class="communityNode-component">
<div class="graph-container" id="container"></div>
<div class="statistic-container">
<div class="statistics-item" v-for="item in statisticsList" :key="item.id">
<img :src="item.icon" class="icon" />
<div class="name">{{ item.name }}:&nbsp;</div>
<div class="count">{{ item.count }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, inject, watch, ref } from "vue" // 添加onUnmounted
import * as echarts from "echarts"
import nodeHoverImg from "@/assets/images/nodeHover.png"
import predictionNodeImg from "@/assets/images/linkPrediction/icon/prediction-node.png"
import hiddenNodeImg from "@/assets/images/linkPrediction/icon/hidden-node.png"
import normalGroupNodeImg from "@/assets/images/linkPrediction/icon/normal-group-node.png"
let chart = null
let linkList = null
let nodeList = null
const emit = defineEmits(["click:node", "click:edge"])
const statisticsList = inject("statisticsList")
const communityNodeList = ref(inject("communityNodeList"))
watch(
communityNodeList,
() => {
initChart()
},
{ deep: true }
)
const initChart = () => {
const imageSelect = (node) => {
if (node.isIncludePredictNodes) {
if (node.selfIncludeImplicitRelationship) {
return `image://${new URL(predictionNodeImg, import.meta.url)}`
} else {
return `image://${new URL(hiddenNodeImg, import.meta.url)}`
}
} else {
return `image://${new URL(normalGroupNodeImg, import.meta.url)}`
}
}
//处理社团节点
const nodes = Object.values(communityNodeList.value).map((item) => ({
id: item.id,
nodeName: item.id,
symbol: imageSelect(item),
symbolSize: 30,
isIncludePredictNodes: item.isIncludePredictNodes,
nodesNum: item.nodesNum,
neighbors: item.neighbors.map((nei) => ({ ...nei, nodeName: parseInt(nei.id) })),
category: item.isIncludePredictNodes ? 1 : 0,
selfIncludeImplicitRelationship:
item.neighbors.map((nei) => nei.id).includes(item.id) && //若该社团的id在该社团邻居中可以找到说明自己这个社团中有社交紧密关系
item.neighbors.find((nei) => nei.id == item.id).isHidden
}))
//处理连边
const links = []
const edgeSet = new Set() //去除重复边
nodes.forEach((communityNode) => {
communityNode.neighbors.forEach((communityNei) => {
const key = [communityNode.nodeName, communityNei.nodeName].sort().join("-")
if (edgeSet.has(key)) return
links.push({
source: communityNode.id,
target: communityNei.id,
edge: communityNei.isHidden ? 1 : 0, //该边存在互动隐关系则权值为1否则为0
lineStyle: {
width: communityNei.isHidden ? 7 : 1, // 无互动=细线,有互动=加粗
color: communityNei.isHidden ? "#FF5E00" : "#37ACD7", // 无互动=灰色,有互动=黄色
type: communityNei.isHidden ? "dashed" : "solid", // 无互动=实线,有互动=虚线
opacity: communityNei.isHidden ? 1 : 0.5
}
})
edgeSet.add(key)
})
})
linkList = links
nodeList = nodes
const data = { nodes, links }
const categories = [
{
name: "普通社团",
category: 0,
icon: `image://${new URL(normalGroupNodeImg, import.meta.url)}`
},
{
name: "含预测节点社团",
category: 1,
icon: `image://${new URL(predictionNodeImg, import.meta.url)}`
},
{
name: "紧密团体关系",
category: 2,
icon: `image://${new URL("@/assets/images/linkPrediction/icon/tight-community-legend-icon.png", import.meta.url)}`
}
]
const option = {
//图例配置
legend: [
{
data: categories.map((c) => ({
name: c.name,
icon: c.icon
})),
right: 15,
bottom: 10,
icon: "circle",
orient: "vertical",
itemWidth: 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",
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.nodesNum}</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.3,
categories: categories,
force: {
edgeLength: 2500,
repulsion: 4000,
gravity: 0.4,
friction: 0.02,
coolingFactor: 0.1
},
animationDurationUpdate: 3500, // 节点移动更平滑
data: data.nodes.map((node) => ({
...node,
symbolSize: node.nodesNum / 10 < 30 ? 50 : 70,
itemStyle: {
color:
node.isIncludePredictNodes === false
? new echarts.graphic.RadialGradient(0.98, 0.38, 0.9, [
{ offset: 1, color: "#1a3860" }, // 最左侧
{ offset: 0.5, color: "#38546b" }, // 中间
{ offset: 0, color: "#5fb3b3" } // 最右侧
])
: new echarts.graphic.RadialGradient(0.98, 0.38, 0.9, [
// anchorCount为1的节点渐变原配色
{ offset: 0, color: "#9eec9c" },
{ offset: 0.37, color: "#aef295" },
{ offset: 1, color: "#c2f989" }
]),
opacity: 1,
borderColor: node.selfIncludeImplicitRelationship ? "#FF5E00" : "#46C6AD",
borderWidth: node.selfIncludeImplicitRelationship ? 3 : 1,
shadowBlur: 4,
borderType: "dashed",
shadowColor: "rgba(19, 27, 114, 0.25)"
},
emphasis: {
itemStyle: {
shadowBlur: 20,
shadowColor: "#c4a651",
borderColor: "#fcd267",
borderWidth: 5,
borderType: "solid"
}
}
})),
links: data.links,
// lineStyle: {
// color: "#37ACD7",
// width: 1
// },
emphasis: {
// 高亮配置
focus: "adjacency", // 高亮相邻节点
lineStyle: {
// 高亮时线条样式
width: 10 // 线条宽度(10)
}
}
}
]
}
chart.setOption(option)
}
const handleClickNode = () => {
chart.on("click", function (params) {
if (params.dataType === "node") {
emit("click:node", params.data)
} else if (params.dataType == "edge") {
const { data } = params
if (data.edge) {
const clickEdgeTarget = data.target
const clickEdgeSource = data.source
console.log("linkList", linkList)
// 找所有的虚线边
const dashedEdgeList = linkList.filter((item) => {
return item.lineStyle.type === "dashed"
})
console.log("dashedEdgeList", dashedEdgeList)
// 从所有的虚线边中找到连接了clickEdgeTarget或者clickEdgeSource的边
const connectEdgeList = dashedEdgeList.filter((item) => {
return (
item.source === clickEdgeSource ||
item.target === clickEdgeTarget ||
item.source === clickEdgeTarget ||
item.target === clickEdgeSource
)
})
console.log("connectEdgeList", connectEdgeList)
let groupIdList = []
// 遍历边的source和target找到所有的groupId
connectEdgeList.forEach((item) => {
if (!groupIdList.includes(item.source)) {
groupIdList.push(item.source)
}
if (!groupIdList.includes(item.target)) {
groupIdList.push(item.target)
}
})
// 只取前三个值
groupIdList = groupIdList.slice(0, 3)
console.log("打印点击边时与目标节点和源节点相连的其他节点id", groupIdList)
emit("click:edge", data, groupIdList)
}
}
})
}
onMounted(() => {
chart = echarts.init(document.getElementById("container"))
handleClickNode()
})
onUnmounted(() => {
if (chart) {
chart.dispose()
chart = null
}
})
</script>
<style scoped lang="less">
.communityNode-component {
width: 100%;
height: 100%;
position: relative;
.graph-container {
width: 100%;
height: 93%;
}
.statistic-container {
width: 420px;
height: 42px;
flex-shrink: 0;
border-radius: 4px;
border: 1px solid #3aa1f8;
background: linear-gradient(270deg, rgba(0, 82, 125, 0.48) 0%, rgba(0, 200, 255, 0.23) 100%);
backdrop-filter: blur(3px);
position: absolute;
bottom: 48px;
left: 15px;
display: flex;
justify-content: space-between;
.statistics-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
.icon {
width: 14px;
height: 14px;
}
.name {
color: rgba(255, 255, 255, 0.76);
text-align: center;
font-family: OPPOSans;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
margin-left: 3px;
}
.count {
color: #fff;
font-family: D-DIN;
font-size: 15px;
font-style: normal;
font-weight: 700;
line-height: normal;
margin-bottom: 2px;
}
}
}
}
</style>