SocialNetworks_duan/src/views/LinkPrediction/components/detailNode.vue
2025-08-12 15:54:05 +08:00

456 lines
13 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="detailNode-component">
<img src="@/assets/images/icon/goback.png" alt="" class="goback" @click="handleGoback" />
<div class="graph-container" id="container"></div>
<div class="statistic-container">
<div
class="statistics-item"
v-for="item in interactionStore.statisticsDetailList"
: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 class="time-axis">
<TimeAxis
ref="timeAxisRef"
:cursor="new Date(timeList[timeList.length - 1])"
:start-time="new Date('2024-05-16 16:56:04')"
:end-time="new Date('2024-05-23 10:16:56')"
:init-position="0"
:is-auto-play="true"
:time-list="timeList"
@click:pointerDown="sendTimeChangeRequest"
@slide:pointerUp="sendTimeChangeRequest"
></TimeAxis>
</div>
</div>
</template>
<script setup>
import { onMounted, ref, watch, nextTick } from "vue"
import { TansTimestamp, getAvatarUrl } from "@/utils/transform"
import nodeHoverImg from "@/assets/images/nodeHover.png"
import * as echarts from "echarts"
import { storeToRefs } from "pinia"
import TimeAxis from "@/components/timeAxis.vue"
const props = defineProps({
interactionStore: {
type: Object,
required: true
}
})
const timeAxisRef = ref(null)
const { communityDetailNodeRelation, timeList, predictionUserIds, curSelecedGroupIds } =
storeToRefs(props.interactionStore)
const emit = defineEmits(["click:goback", "click:openDialog"])
const chartsData = ref({})
const handleGoback = () => {
timeAxisRef.value?.reset()
emit("click:goback", "CommunityNode")
}
const handleClickNode = () => {
chart.on("click", function (params) {
if (params.dataType == "node" && predictionUserIds.value.includes(params.data.id)) {
emit("click:openDialog", params.data)
}
})
}
// 添加对curSelecedGroupIds的watch确保切换列表项时重置时间轴
watch(
curSelecedGroupIds,
(newIds) => {
if (newIds && newIds.length > 0) {
timeAxisRef.value?.reset()
}
},
{ deep: true }
)
//当点击时间轴的时候communityDetailNodeList改变重新更新关系图
watch(
communityDetailNodeRelation,
() => {
initChart()
},
{ deep: true }
)
//监听predictionUserIds的变化从而筛选需要高亮的预测节点
watch(
predictionUserIds,
(newIds) => {
if (newIds.length != 0) {
nextTick(() => {
highLightUserNodes(newIds)
})
}
},
{ deep: true, immediate: true }
)
// 时间轴变化发送请求
const sendTimeChangeRequest = (currentTime) => {
const currentTimes = TansTimestamp(currentTime, "YYYY-MM-DD HH:mm:ss")
if (props.interactionStore.curRelationId == "") {
props.interactionStore.initGraphCommunityDetailNode(
props.interactionStore.curSelecedGroupIds,
currentTimes
)
} else {
props.interactionStore.initGraphCommunityDetailNode(
props.interactionStore.curSelecedGroupIds,
currentTimes,
props.interactionStore.curRelationId
)
}
}
let chart = null
const initChart = async () => {
if (chart == null) {
chart = echarts.init(document.getElementById("container"))
}
const links = []
let nodes = []
const edgeWidth = (interactionTime) => {
if (interactionTime <= 3) return 3
else if (interactionTime <= 10) return 4
else if (interactionTime <= 20) return 6
else if (interactionTime <= 30) return 8
else return 1
}
if (!Object.keys(props.interactionStore.communityDetailNodeRelation).length) return
//先处理节点
nodes = props.interactionStore.communityDetailNodeList.map((item) => ({
id: item.userId,
nodeName: item.userName,
// 头像
avatarData: item.avatarData,
//默认圆形头像
defaultAvatar: getAvatarUrl(item.defaultAvatar),
//激活状态的头像
activeAvatar: getAvatarUrl(item.activeAvatar),
symbolSize: 40,
postNum: item.postNum,
fans: item.fans,
// 发帖频率
postFreqPerDay: item.postFreqPerDay,
// 参与互动次数
interactionNum: item.interactionNum,
// 参与互动频率
interactionFreqPerDay: item.interactionFreqPerDay,
// 帖文被互动次数
interactedNum: item.interactedNum,
// 最近活跃时间
recentActiveTime: item.recentActiveTime
}))
Object.entries(props.interactionStore.communityDetailNodeRelation).forEach(
([parentId, children]) => {
children.forEach((child) => {
links.push({
source: parentId,
target: child.id,
edge: child.isHidden ? 1 : 0,
interactionTimes: child.interactionTime,
lineStyle: {
width: child.isHidden ? 7 : edgeWidth(child.interactionTime),
color: child.isHidden ? props.interactionStore.predictionLineColor : "#37ACD7", // 无互动=灰色,有互动=黄色
opacity: child.isHidden ? 1 : 0.5, // 可选:调整透明度增强模糊感
type: child.isHidden ? "dashed" : "solid" // 无互动=实线,有互动=虚线
}
})
})
}
)
chartsData.value = { links, nodes }
const categories = [
{ name: "事件活跃者", category: 0, icon: "circle" },
{
name: "互动关系",
category: 1,
icon: `image://${new URL("@/assets/images/linkPrediction/icon/interaction-icon2.png", 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" }
])
: 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.icon
})),
right: 21,
symbolKeepAspect: false,
bottom: 70,
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="
padding:10px 15px;
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 >用户ID${params.data.id}</div>
<div >用户名:${params.data.nodeName}</div>
</div>
</div>`
}
return ""
}
},
edgeLabel: {
show: false,
position: "middle",
formatter: function (params) {
return `${params.data.interactionTimes}次互动`
},
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.08,
categories: categories,
force: {
edgeLength: 4000,
repulsion: 4000,
gravity: 0.1,
friction: 0.02,
coolingFactor: 0.1
},
animationDurationUpdate: 3500, // 节点移动更平滑
data: chartsData.value.nodes.map((node) => ({
...node,
symbol: node.defaultAvatar ? `image://${node.defaultAvatar}` : "circle",
symbolSize: node.defaultAvatar ? 140 : 40,
itemStyle: {
color: new echarts.graphic.RadialGradient(0.98, 0.38, 0.9, [
{ offset: 1, color: "#1a3860" }, // 最左侧
{ offset: 0.5, color: "#38546b" }, // 中间
{ offset: 0, color: "#5fb3b3" } // 最右侧
]),
opacity: 1,
borderColor: "#46C6AD",
borderWidth: 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: chartsData.value.links,
lineStyle: {
color: "#37ACD7",
width: 1
}
}
]
}
chart.setOption(option)
}
const highLightUserNodes = (userIds) => {
if (!userIds) return
// 只让高亮节点显示 activeAvatar其他节点恢复默认头像或圆形
chartsData.value.nodes.forEach((node) => {
if (userIds.includes(node.id) && node.activeAvatar) {
node.symbol = `image://${node.activeAvatar}`
node.symbolSize = 140
} else {
node.symbol = "circle"
node.symbolSize = 40
node.itemStyle = {
color: new echarts.graphic.RadialGradient(0.98, 0.38, 0.9, [
{ offset: 1, color: "#1a3860" }, // 最左侧
{ offset: 0.5, color: "#38546b" }, // 中间
{ offset: 0, color: "#5fb3b3" } // 最右侧
]),
opacity: 1,
borderColor: "#46C6AD",
borderWidth: 1,
shadowBlur: 4,
borderType: "dashed",
shadowColor: "rgba(19, 27, 114, 0.25)"
}
}
})
chart.setOption(
{
series: [
{
data: chartsData.value.nodes
}
]
},
false
)
}
onMounted(() => {
initChart()
highLightUserNodes()
handleClickNode()
})
</script>
<style scoped lang="less">
.detailNode-component {
width: 100%;
height: 100%;
position: relative;
.goback {
position: absolute;
top: -25px;
left: 20px;
cursor: pointer;
}
.graph-container {
width: 100%;
height: 93%;
}
.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: 105px;
left: 21px;
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;
}
}
}
.time-axis {
width: 95%;
height: 42px;
position: absolute;
left: 20px;
bottom: 50px;
z-index: 1;
}
}
</style>