SocialNetworks_duan/src/views/LinkPrediction/components/communityNode.vue
qumeng039@126.com 52fb765d74 样式调整
2025-08-19 17:41:03 +08:00

389 lines
12 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, inject, watch, ref, onUnmounted } from "vue"
import * as echarts from "echarts"
import nodeHoverImg from "@/assets/images/nodeHover.png"
import predictionNodeImg from "@/assets/images/linkPrediction/icon/prediction-node.png"
import charHiddenNodeImg from "@/assets/images/linkPrediction/icon/character-hidden-node.png"
import charInteractionHiddenNodeImg from "@/assets/images/linkPrediction/icon/character-Interaction-hidden-node.png"
import normalGroupNodeImg from "@/assets/images/linkPrediction/icon/normal-group-node.png"
let chart = null
const emit = defineEmits(["click:node", "click:edge"])
const statisticsList = inject("statisticsList")
const communityNodeList = ref(inject("communityNodeList"))
const props = defineProps({
interactionStore: {
type: Object,
required: true
},
storeId: {
type: String,
default: ""
}
})
// 监听communityNodeList变化 解决第一次进入页面不渲染问题
watch(
communityNodeList,
() => {
initChart()
},
{ deep: true }
)
const initChart = () => {
const imageSelect = (node) => {
if (props.storeId == "characterInteraction") {
if (node.isIncludePredictNodes) {
if (node.selfIncludeImplicitRelationship) {
return `image://${new URL(predictionNodeImg, import.meta.url)}`
} else {
return `image://${new URL(charInteractionHiddenNodeImg, import.meta.url)}`
}
} else {
return `image://${new URL(normalGroupNodeImg, import.meta.url)}`
}
} else if (props.storeId == "characterHidden") {
if (node.isIncludePredictNodes) {
if (node.selfIncludeImplicitRelationship) {
return `image://${new URL(predictionNodeImg, import.meta.url)}`
} else {
return `image://${new URL(charHiddenNodeImg, import.meta.url)}`
}
} else {
return `image://${new URL(normalGroupNodeImg, import.meta.url)}`
}
}
}
//处理社团节点
// 确保item是有效的对象
const nodes = Object.values(communityNodeList.value).map((item) => ({
id: item.id,
nodeName: item.id,
isIncludePredictNodes: item.isIncludePredictNodes,
nodesNum: item.nodesNum,
neighbors: item.neighbors,
category: item.isIncludePredictNodes ? 1 : 0,
symbol: imageSelect(item),
symbolSize: 30,
selfIncludeImplicitRelationship:
item.neighbors.map((nei) => nei.id).includes(item.id) && //若该社团的id在该社团邻居中可以找到说明自己这个社团中有互动隐关系
item.neighbors.find((nei) => nei.id == item.id).isHidden
}))
//处理连边
const links = []
const newSet = new Set()
nodes.forEach((communityNode) => {
communityNode.neighbors.forEach((communityNei) => {
if (newSet.has(communityNei.id)) 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 ? props.interactionStore.predictionLineColor : "#37ACD7", // 无互动=灰色,有互动=黄色
opacity: communityNei.isHidden ? 1 : 0.8, // 可选:调整透明度增强模糊感
type: communityNei.isHidden ? "dashed" : "solid" // 无互动=实线,有互动=虚线
}
})
})
newSet.add(communityNode.id) // 添加当前社团节点id到集合中避免重复添加
})
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: props.interactionStore.predictionLegendContent,
category: 2,
icon: props.interactionStore.predictionLegendIcon
}
]
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" }
])
: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ offset: 0, color: "#9eec9c" }, // 绿色渐变
{ offset: 0.37, color: "#aef295" },
{ offset: 1, color: "#c2f989" }
])
},
icon: c.icon
})),
right: 15,
bottom: 10,
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",
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
? props.interactionStore.predictionLineColor
: "#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)
}
}
})
}
// 监听窗口大小变化,重置图表
const handleResize = () => {
if (chart) {
chart.resize()
}
}
//echarts自适应方法
const resizeChart = () => {
window.removeEventListener("resize", handleResize) //先移除旧的监听
window.addEventListener("resize", handleResize)
}
onUnmounted(() => {
window.removeEventListener("resize", handleResize)
if (chart) {
chart.dispose()
}
})
onMounted(() => {
// 确保容器存在
const container = document.getElementById("container")
if (!container) return
chart = echarts.init(container)
if (!chart.getOption()) {
initChart()
}
handleClickNode()
resizeChart()
})
</script>
<style scoped lang="scss">
.communityNode-component {
width: 100%;
height: 100%;
position: relative;
.graph-container {
width: 100%;
height: 93%;
}
.statistic-container {
width: vw(378);
height: vh(42);
flex-shrink: 0;
border-radius: vw(4);
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(vw(3));
position: absolute;
bottom: vh(48);
left: vw(15);
display: flex;
justify-content: space-between;
.statistics-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
.icon {
width: vw(14);
height: vh(14);
}
.name {
color: rgba(255, 255, 255, 0.76);
text-align: center;
font-family: OPPOSans;
font-size: vw(14);
font-style: normal;
font-weight: 400;
line-height: normal;
margin-left: vw(3);
}
.count {
color: #fff;
font-family: D-DIN;
font-size: vw(15);
font-style: normal;
font-weight: 700;
line-height: normal;
margin-bottom: vh(2);
}
}
}
}
</style>