SocialNetworks_duan/src/views/LinkPrediction/socialGroups/components/communityNode.vue

322 lines
9.8 KiB
Vue
Raw Normal View History

2025-07-29 12:13:55 +08:00
<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, defineEmits, inject } from "vue" // 添加onUnmounted
import * as echarts from "echarts"
import nodeHoverImg from "@/assets/images/nodeHover.png"
let chart = null
const emit = defineEmits(["click:node", "click:edge"])
const statisticsList = inject("statisticsList");
const communityNodeList = inject("communityNodeList");
const initChart = async () => {
chart = echarts.init(document.getElementById("container"))
//处理社团节点
const nodes = Object.values(communityNodeList).map((item) => ({
id: (item.id),
name: (item.id),
isIncludePredictNodes: item.isIncludePredictNodes,
nodesNum: item.nodesNum,
neighbors: item.neighbors.map((nei) => ({ ...nei, name: 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.name, communityNei.name].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 ? 4 : 1, // 无互动=细线,有互动=加粗
color: communityNei.isHidden ? "#FF5E00" : "#37ACD7", // 无互动=灰色,有互动=黄色
type: communityNei.isHidden ? "dashed" : "solid", // 无互动=实线,有互动=虚线
dashArray: [2, 1] // 2像素实线1像素空白
}
})
edgeSet.add(key)
})
})
const data = { nodes, links }
const categories = [
{ name: "普通社团", category: 0 },
{ name: "含预测节点社团", category: 1 },
{ name: "紧密团体关系", category: 2 }
]
const option = {
//图例配置
legend: [
{
data: categories.map((c) => ({
name: c.name,
itemStyle: {
color:
c.category === 0
? new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ offset: 1, color: "#1a3860" }, // 深蓝渐变
{ offset: 0.5, color: "#38546b" },
{ offset: 0, color: "#5fb3b3" }
])
: c.category === 1
? new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ offset: 0, color: "#9eec9c" }, // 绿色渐变
{ offset: 0.37, color: "#aef295" },
{ offset: 1, color: "#c2f989" }
])
: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ offset: 0, color: "#ff9a9e" }, // 红色渐变(用于 category === 2
{ offset: 0.5, color: "#fad0c4" },
{ offset: 1, color: "#fbc2eb" }
])
},
icon: c.category === 2
? `image://${new URL('@/assets/images/linkPrediction/icon/tight-community-legend-icon.png', import.meta.url)}`
: c.category === 0
? `image://${new URL('@/assets/images/linkPrediction/icon/node-legend-icon.png', import.meta.url)}`
: `image://${new URL('@/assets/images/linkPrediction/icon/community-legend-icon.png', import.meta.url)}`,
})),
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 / 40 < 30 ? 40 : node.nodesNum / 40,
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: node.isIncludePredictNodes == false ? 1 : 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) {
emit("click:edge", data)
}
}
})
}
onMounted(async () => {
await initChart()
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>