2025-07-25 15:07:08 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="communityNode-component">
|
|
|
|
|
|
<div class="graph-container" id="container"></div>
|
2025-07-28 15:59:40 +08:00
|
|
|
|
<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 }}: </div>
|
|
|
|
|
|
<div class="count">{{ item.count }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-07-25 15:07:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2025-08-15 16:59:01 +08:00
|
|
|
|
import { onMounted, inject, watch, ref } from "vue"
|
2025-07-25 15:07:08 +08:00
|
|
|
|
import * as echarts from "echarts"
|
|
|
|
|
|
import nodeHoverImg from "@/assets/images/nodeHover.png"
|
2025-08-12 15:21:48 +08:00
|
|
|
|
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"
|
2025-07-25 15:07:08 +08:00
|
|
|
|
let chart = null
|
2025-07-25 17:24:14 +08:00
|
|
|
|
const emit = defineEmits(["click:node", "click:edge"])
|
2025-07-28 15:59:40 +08:00
|
|
|
|
const statisticsList = inject("statisticsList")
|
2025-08-15 16:59:01 +08:00
|
|
|
|
const communityNodeList = ref(inject("communityNodeList"))
|
2025-07-31 16:13:39 +08:00
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
interactionStore: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
required: true
|
2025-08-12 15:21:48 +08:00
|
|
|
|
},
|
|
|
|
|
|
storeId: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ""
|
2025-07-31 16:13:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
2025-07-25 15:07:08 +08:00
|
|
|
|
|
2025-08-15 16:59:01 +08:00
|
|
|
|
// 监听communityNodeList变化 解决第一次进入页面不渲染问题
|
|
|
|
|
|
watch(
|
|
|
|
|
|
communityNodeList,
|
|
|
|
|
|
() => {
|
|
|
|
|
|
initChart()
|
|
|
|
|
|
},
|
|
|
|
|
|
{ deep: true }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const initChart = () => {
|
2025-08-12 15:21:48 +08:00
|
|
|
|
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)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-07-25 15:07:08 +08:00
|
|
|
|
//处理社团节点
|
2025-08-15 16:59:01 +08:00
|
|
|
|
// 确保item是有效的对象
|
|
|
|
|
|
const nodes = Object.values(communityNodeList.value).map((item) => ({
|
2025-07-31 16:13:39 +08:00
|
|
|
|
id: item.id,
|
2025-08-12 15:54:05 +08:00
|
|
|
|
nodeName: item.id,
|
2025-07-25 15:07:08 +08:00
|
|
|
|
isIncludePredictNodes: item.isIncludePredictNodes,
|
2025-07-25 17:24:14 +08:00
|
|
|
|
nodesNum: item.nodesNum,
|
2025-07-31 16:13:39 +08:00
|
|
|
|
neighbors: item.neighbors,
|
2025-07-28 15:59:40 +08:00
|
|
|
|
category: item.isIncludePredictNodes ? 1 : 0,
|
2025-08-12 15:21:48 +08:00
|
|
|
|
symbol: imageSelect(item),
|
|
|
|
|
|
symbolSize: 30,
|
2025-07-28 15:59:40 +08:00
|
|
|
|
selfIncludeImplicitRelationship:
|
|
|
|
|
|
item.neighbors.map((nei) => nei.id).includes(item.id) && //若该社团的id在该社团邻居中可以找到,说明自己这个社团中有互动隐关系
|
|
|
|
|
|
item.neighbors.find((nei) => nei.id == item.id).isHidden
|
2025-07-25 15:07:08 +08:00
|
|
|
|
}))
|
|
|
|
|
|
|
2025-07-25 17:24:14 +08:00
|
|
|
|
//处理连边
|
|
|
|
|
|
const links = []
|
2025-07-31 17:06:43 +08:00
|
|
|
|
const newSet = new Set()
|
2025-07-25 17:24:14 +08:00
|
|
|
|
nodes.forEach((communityNode) => {
|
|
|
|
|
|
communityNode.neighbors.forEach((communityNei) => {
|
2025-07-31 17:06:43 +08:00
|
|
|
|
if (newSet.has(communityNei.id)) return
|
2025-07-25 17:24:14 +08:00
|
|
|
|
links.push({
|
2025-07-31 16:13:39 +08:00
|
|
|
|
source: communityNode.id,
|
|
|
|
|
|
target: communityNei.id,
|
2025-07-25 17:24:14 +08:00
|
|
|
|
edge: communityNei.isHidden ? 1 : 0, //该边存在互动隐关系则权值为1,否则为0
|
|
|
|
|
|
lineStyle: {
|
|
|
|
|
|
width: communityNei.isHidden ? 4 : 1, // 无互动=细线,有互动=加粗
|
2025-07-31 16:13:39 +08:00
|
|
|
|
color: communityNei.isHidden ? props.interactionStore.predictionLineColor : "#37ACD7", // 无互动=灰色,有互动=黄色
|
2025-07-31 17:06:43 +08:00
|
|
|
|
opacity: communityNei.isHidden ? 1 : 0.8, // 可选:调整透明度增强模糊感
|
2025-07-31 16:13:39 +08:00
|
|
|
|
type: communityNei.isHidden ? "dashed" : "solid" // 无互动=实线,有互动=虚线
|
2025-07-25 17:24:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
2025-07-31 17:06:43 +08:00
|
|
|
|
newSet.add(communityNode.id) // 添加当前社团节点id到集合中,避免重复添加
|
2025-07-25 17:24:14 +08:00
|
|
|
|
})
|
2025-07-25 15:07:08 +08:00
|
|
|
|
|
|
|
|
|
|
const data = { nodes, links }
|
|
|
|
|
|
const categories = [
|
2025-08-15 16:59:01 +08:00
|
|
|
|
{
|
|
|
|
|
|
name: "普通社团",
|
|
|
|
|
|
category: 0,
|
|
|
|
|
|
icon: `image://${new URL(normalGroupNodeImg, import.meta.url)}`
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "含预测节点社团",
|
|
|
|
|
|
category: 1,
|
|
|
|
|
|
icon: `image://${new URL(predictionNodeImg, import.meta.url)}`
|
|
|
|
|
|
},
|
2025-07-29 16:23:19 +08:00
|
|
|
|
{
|
2025-07-31 16:13:39 +08:00
|
|
|
|
name: props.interactionStore.predictionLegendContent,
|
2025-07-29 16:23:19 +08:00
|
|
|
|
category: 2,
|
2025-07-31 16:13:39 +08:00
|
|
|
|
icon: props.interactionStore.predictionLegendIcon
|
2025-07-29 16:23:19 +08:00
|
|
|
|
}
|
2025-07-25 15:07:08 +08:00
|
|
|
|
]
|
|
|
|
|
|
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" }
|
|
|
|
|
|
])
|
2025-07-29 16:23:19 +08:00
|
|
|
|
: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
|
|
|
|
|
|
{ offset: 0, color: "#9eec9c" }, // 绿色渐变
|
|
|
|
|
|
{ offset: 0.37, color: "#aef295" },
|
|
|
|
|
|
{ offset: 1, color: "#c2f989" }
|
|
|
|
|
|
])
|
|
|
|
|
|
},
|
|
|
|
|
|
icon: c.icon
|
2025-07-25 15:07:08 +08:00
|
|
|
|
})),
|
2025-07-28 15:59:40 +08:00
|
|
|
|
right: 15,
|
|
|
|
|
|
bottom: 10,
|
2025-07-25 15:07:08 +08:00
|
|
|
|
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 ""
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-07-25 17:24:14 +08:00
|
|
|
|
edgeLabel: {
|
|
|
|
|
|
show: false,
|
|
|
|
|
|
position: "middle",
|
|
|
|
|
|
formatter: function (params) {
|
|
|
|
|
|
return params.data.edge
|
|
|
|
|
|
},
|
|
|
|
|
|
fontSize: 14
|
|
|
|
|
|
},
|
2025-07-25 15:07:08 +08:00
|
|
|
|
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,
|
2025-07-25 17:24:14 +08:00
|
|
|
|
zoom: 0.3,
|
2025-07-25 15:07:08 +08:00
|
|
|
|
categories: categories,
|
|
|
|
|
|
force: {
|
2025-07-25 17:24:14 +08:00
|
|
|
|
edgeLength: 2500,
|
2025-07-25 15:07:08 +08:00
|
|
|
|
repulsion: 4000,
|
|
|
|
|
|
gravity: 0.4,
|
|
|
|
|
|
friction: 0.02,
|
|
|
|
|
|
coolingFactor: 0.1
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
animationDurationUpdate: 3500, // 节点移动更平滑
|
|
|
|
|
|
data: data.nodes.map((node) => ({
|
|
|
|
|
|
...node,
|
2025-08-12 15:21:48 +08:00
|
|
|
|
symbolSize: node.nodesNum / 10 < 30 ? 50 : 70,
|
2025-07-25 15:07:08 +08:00
|
|
|
|
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,
|
2025-07-31 17:06:43 +08:00
|
|
|
|
borderColor: node.selfIncludeImplicitRelationship
|
|
|
|
|
|
? props.interactionStore.predictionLineColor
|
|
|
|
|
|
: "#46C6AD",
|
2025-07-28 15:59:40 +08:00
|
|
|
|
borderWidth: node.selfIncludeImplicitRelationship ? 3 : 1,
|
2025-07-25 15:07:08 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
2025-07-25 17:24:14 +08:00
|
|
|
|
|
2025-07-25 15:07:08 +08:00
|
|
|
|
const handleClickNode = () => {
|
|
|
|
|
|
chart.on("click", function (params) {
|
|
|
|
|
|
if (params.dataType === "node") {
|
|
|
|
|
|
emit("click:node", params.data)
|
2025-07-25 17:24:14 +08:00
|
|
|
|
} else if (params.dataType == "edge") {
|
|
|
|
|
|
const { data } = params
|
|
|
|
|
|
if (data.edge) {
|
|
|
|
|
|
emit("click:edge", data)
|
|
|
|
|
|
}
|
2025-07-25 15:07:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-07-25 17:24:14 +08:00
|
|
|
|
|
2025-08-15 16:59:01 +08:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
// 确保容器存在
|
|
|
|
|
|
const container = document.getElementById("container")
|
|
|
|
|
|
if (!container) return
|
|
|
|
|
|
chart = echarts.init(container)
|
2025-08-15 17:28:10 +08:00
|
|
|
|
if (!chart.getOption()) {
|
|
|
|
|
|
initChart()
|
|
|
|
|
|
}
|
2025-07-25 15:07:08 +08:00
|
|
|
|
handleClickNode()
|
|
|
|
|
|
})
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
2025-08-19 11:23:46 +08:00
|
|
|
|
<style scoped lang="scss">
|
2025-07-25 15:07:08 +08:00
|
|
|
|
.communityNode-component {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
.graph-container {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 93%;
|
|
|
|
|
|
}
|
2025-07-28 15:59:40 +08:00
|
|
|
|
.statistic-container {
|
|
|
|
|
|
width: 378px;
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-07-25 15:07:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|