828 lines
24 KiB
Vue
828 lines
24 KiB
Vue
<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 socialGroupsStore.statisticsDetailList"
|
||
:key="item.id"
|
||
>
|
||
<img :src="item.icon" class="icon" />
|
||
<div class="name">{{ item.name }}: </div>
|
||
<div class="count">{{ item.count }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="time-axis">
|
||
<div class="time">{{ TansTimestamp(startTime, "YYYY.MM.DD HH:mm:ss") }}</div>
|
||
<div class="axis" ref="axisRef" @pointerdown="handlePointerDown">
|
||
<div class="progress-bar" :style="trackStyle"></div>
|
||
<el-tooltip
|
||
v-for="(time, index) in timePointsWithPositions"
|
||
:key="index"
|
||
:content="TansTimestamp(time.time, 'YYYY.MM.DD HH:mm:ss')"
|
||
placement="bottom"
|
||
effect="light"
|
||
>
|
||
<div
|
||
class="time-sign"
|
||
:style="{ left: `${time.position}px` }"
|
||
@click="handleTimePointClick(time.timeStr)"
|
||
></div>
|
||
</el-tooltip>
|
||
<el-tooltip
|
||
:content="TansTimestamp(timeList[timeList.length - 1], 'YYYY.MM.DD HH:mm:ss')"
|
||
placement="bottom"
|
||
effect="light"
|
||
>
|
||
<div class="active-needle" :style="{ left: `${lastPosition}px` }"></div>
|
||
</el-tooltip>
|
||
<el-tooltip
|
||
:content="TansTimestamp(currentTime, 'YYYY.MM.DD HH:mm:ss')"
|
||
placement="bottom"
|
||
effect="light"
|
||
>
|
||
<div
|
||
class="timeLine-point"
|
||
:style="{ left: `${currentPosition}px` }"
|
||
@pointerdown.stop="handlePointPointerDown"
|
||
></div>
|
||
</el-tooltip>
|
||
</div>
|
||
<div class="time">{{ TansTimestamp(endTime, "YYYY.MM.DD HH:mm:ss") }}</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { onMounted, ref, onUnmounted, computed, 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 { useSocialGroupsStore } from "@/store/linkPrediction/index"
|
||
const socialGroupsStore = useSocialGroupsStore()
|
||
const { communityDetailNodeList } = storeToRefs(socialGroupsStore)
|
||
const { timeList } = storeToRefs(socialGroupsStore)
|
||
const { curHighlightUserIdList } = storeToRefs(socialGroupsStore)
|
||
// 添加对curSelecedGroupIds的监听,用于检测列表项切换
|
||
const { curSelecedGroupIds } = storeToRefs(socialGroupsStore)
|
||
|
||
// 添加对curSelecedGroupIds的watch,确保切换列表项时重置时间轴
|
||
watch(
|
||
curSelecedGroupIds,
|
||
(newIds) => {
|
||
if (newIds && newIds.length > 0) {
|
||
// 重置时间轴位置到起点
|
||
currentPosition.value = 0
|
||
currentTime.value = new Date("2024-05-16 16:56:04")
|
||
// 重新开始自动播放
|
||
pause() // 先停止可能正在运行的计时器
|
||
play() // 重新开始播放
|
||
}
|
||
},
|
||
{ deep: true }
|
||
)
|
||
// 用于监听是从 列表 点进来的还是从 边 点进来的
|
||
const { clickEvent } = storeToRefs(socialGroupsStore)
|
||
|
||
const chartsData = ref({})
|
||
|
||
const emit = defineEmits(["click:goback", "click:openDialog"])
|
||
const handleGoback = () => {
|
||
pause()
|
||
emit("click:goback", "CommunityNode")
|
||
}
|
||
|
||
//当点击时间轴的时候,communityDetailNodeList改变,重新更新关系图
|
||
watch(
|
||
communityDetailNodeList,
|
||
(newValue) => {
|
||
initChart()
|
||
},
|
||
{ deep: true }
|
||
)
|
||
// 监听需要高亮的用户id
|
||
watch(
|
||
curHighlightUserIdList,
|
||
(newHiglightUserIdList) => {
|
||
if (newHiglightUserIdList.length != 0) {
|
||
nextTick(() => {
|
||
highLightUserNodes(newHiglightUserIdList)
|
||
})
|
||
}
|
||
},
|
||
{
|
||
deep: true,
|
||
immediate: true
|
||
}
|
||
)
|
||
|
||
// 时间轴相关数据
|
||
const startTime = ref(new Date("2024-05-16 16:56:04"))
|
||
const endTime = ref(new Date("2024-05-23 10:16:56"))
|
||
const currentTime = ref(new Date("2024-05-16 16:56:04")) // 当前选中的时间
|
||
const currentPosition = ref(0) // 初始位置(轴长度的一半)
|
||
const lastPosition = ref(0) // 时间列表最后的时间点的位置
|
||
const isPlaying = ref(false) // 是否自动播放
|
||
let playTimer = null
|
||
|
||
// 自动播放控制
|
||
const play = () => {
|
||
if (isPlaying.value) return
|
||
isPlaying.value = true
|
||
playTimer = setInterval(() => {
|
||
// 步进像素
|
||
const step = 4 // 每次移动4px,可根据需要调整速度
|
||
if (currentPosition.value >= axisWidth) {
|
||
pause()
|
||
return
|
||
}
|
||
currentPosition.value = Math.min(axisWidth, currentPosition.value + step)
|
||
currentTime.value = getTimeFromPosition(currentPosition.value)
|
||
sendTimeChangeRequest()
|
||
}, 300) // 每300ms移动一次
|
||
}
|
||
|
||
// 发送请求逻辑封装
|
||
const sendTimeChangeRequest = () => {
|
||
const currentTimes = TansTimestamp(currentTime.value, "YYYY-MM-DD HH:mm:ss")
|
||
if (socialGroupsStore.curRelationId == "") {
|
||
socialGroupsStore.initGraphCommunityDetailNode(
|
||
socialGroupsStore.curSelecedGroupIds,
|
||
currentTimes
|
||
)
|
||
} else {
|
||
socialGroupsStore.initGraphCommunityDetailNode(
|
||
socialGroupsStore.curSelecedGroupIds,
|
||
currentTimes,
|
||
socialGroupsStore.curRelationId
|
||
)
|
||
}
|
||
}
|
||
|
||
const pause = () => {
|
||
isPlaying.value = false
|
||
if (playTimer) {
|
||
clearInterval(playTimer)
|
||
playTimer = null
|
||
}
|
||
}
|
||
|
||
const axisRef = ref(null)
|
||
const isDragging = ref(false)
|
||
|
||
// 缓存时间计算相关的常量
|
||
const axisWidth = 426
|
||
const startTimeMs = startTime.value.getTime()
|
||
const endTimeMs = endTime.value.getTime()
|
||
const totalDuration = endTimeMs - startTimeMs
|
||
|
||
// 计算每个时间点的位置
|
||
const timePointsWithPositions = computed(() => {
|
||
// 确保 timeList 是数组
|
||
const list = Array.isArray(timeList.value) ? timeList.value : []
|
||
if (list.length === 0) return []
|
||
|
||
return list.map((timeStr) => {
|
||
const time = new Date(timeStr)
|
||
const timeMs = time.getTime()
|
||
const ratio = Math.max(0, Math.min(1, (timeMs - startTimeMs) / totalDuration))
|
||
const position = ratio * axisWidth
|
||
return { time, position, timeStr }
|
||
})
|
||
})
|
||
|
||
watch(timeList, (newList) => {
|
||
console.log("🔥 timeList 被更新了:", newList)
|
||
})
|
||
|
||
// watch来监听timeList变化并设置初始值
|
||
watch(
|
||
timePointsWithPositions,
|
||
(newTimePoints) => {
|
||
if (newTimePoints && newTimePoints.length > 0) {
|
||
// 始终将currentPosition设置为0(时间轴起点)
|
||
currentPosition.value = 0
|
||
currentTime.value = "2024-05-16 16:56:04"
|
||
lastPosition.value = newTimePoints[newTimePoints.length - 1].position
|
||
// 触发图更新
|
||
const currentTimes = TansTimestamp(currentTime.value, "YYYY-MM-DD HH:mm:ss")
|
||
socialGroupsStore.initGraphCommunityDetailNode(
|
||
socialGroupsStore.curSelecedGroupIds,
|
||
currentTimes
|
||
)
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
// 添加时间点点击事件处理函数
|
||
const handleTimePointClick = (timeStr) => {
|
||
pause()
|
||
|
||
const time = new Date(timeStr)
|
||
currentTime.value = time
|
||
const ratio = (time.getTime() - startTimeMs) / totalDuration
|
||
currentPosition.value = ratio * axisWidth
|
||
|
||
// 触发图更新
|
||
const currentTimes = TansTimestamp(currentTime.value, "YYYY-MM-DD HH:mm:ss")
|
||
socialGroupsStore.initGraphCommunityDetailNode(socialGroupsStore.curSelecedGroupIds, currentTimes)
|
||
}
|
||
|
||
// 根据位置计算时间
|
||
const getTimeFromPosition = (position) => {
|
||
const ratio = Math.max(0, Math.min(1, position / axisWidth))
|
||
const timeOffset = totalDuration * ratio
|
||
return new Date(startTimeMs + timeOffset)
|
||
}
|
||
|
||
// 指针按下事件
|
||
const handlePointerDown = (e) => {
|
||
if (e.target.classList.contains("timeLine-point")) return
|
||
pause() // 拖动或点击时暂停自动播放
|
||
const rect = axisRef.value.getBoundingClientRect()
|
||
const position = Math.max(0, Math.min(axisWidth, e.clientX - rect.left))
|
||
|
||
// 直接更新位置,不使用节流函数
|
||
currentPosition.value = position
|
||
currentTime.value = getTimeFromPosition(position)
|
||
|
||
// 点击后输出当前时间
|
||
const currentTimes = TansTimestamp(currentTime.value, "YYYY-MM-DD HH:mm:ss")
|
||
if (socialGroupsStore.curRelationId === "") {
|
||
socialGroupsStore.initGraphCommunityDetailNode(
|
||
socialGroupsStore.curSelecedGroupIds,
|
||
currentTimes
|
||
)
|
||
} else {
|
||
socialGroupsStore.initGraphCommunityDetailNode(
|
||
socialGroupsStore.curSelecedGroupIds,
|
||
currentTimes,
|
||
socialGroupsStore.curRelationId
|
||
)
|
||
}
|
||
}
|
||
|
||
// 时间点指针按下事件
|
||
const handlePointPointerDown = (e) => {
|
||
e.stopPropagation()
|
||
e.preventDefault()
|
||
isDragging.value = true
|
||
// 缓存轴的边界矩形,避免重复计算
|
||
const rect = axisRef.value.getBoundingClientRect()
|
||
const axisLeft = rect.left
|
||
|
||
const handlePointerMove = (e) => {
|
||
if (!isDragging.value) return
|
||
const position = Math.max(0, Math.min(axisWidth, e.clientX - axisLeft))
|
||
|
||
// 直接更新位置,不检查阈值,确保实时响应
|
||
currentPosition.value = position
|
||
currentTime.value = getTimeFromPosition(position)
|
||
}
|
||
|
||
const handlePointerUp = () => {
|
||
isDragging.value = false
|
||
// 拖动结束时输出当前时间
|
||
const currentTimes = TansTimestamp(currentTime.value, "YYYY-MM-DD HH:mm:ss")
|
||
if (socialGroupsStore.curRelationId === "") {
|
||
socialGroupsStore.initGraphCommunityDetailNode(
|
||
socialGroupsStore.curSelecedGroupIds,
|
||
currentTimes
|
||
)
|
||
} else {
|
||
socialGroupsStore.initGraphCommunityDetailNode(
|
||
socialGroupsStore.curSelecedGroupIds,
|
||
currentTimes,
|
||
socialGroupsStore.curRelationId
|
||
)
|
||
}
|
||
|
||
document.removeEventListener("pointermove", handlePointerMove)
|
||
document.removeEventListener("pointerup", handlePointerUp)
|
||
}
|
||
|
||
document.addEventListener("pointermove", handlePointerMove, { passive: true })
|
||
document.addEventListener("pointerup", handlePointerUp)
|
||
}
|
||
|
||
const trackStyle = computed(() => {
|
||
const progressPercent = Math.min(100, (currentPosition.value / 426) * 100)
|
||
return {
|
||
background: `linear-gradient(90deg, #00F3FF 0%, #00F3FF ${progressPercent}%, #3B7699 ${progressPercent}%, #3B7699 100%)`,
|
||
width: "100%"
|
||
}
|
||
})
|
||
|
||
// 组件卸载时清理事件监听器
|
||
onUnmounted(() => {
|
||
document.removeEventListener("pointermove", () => {})
|
||
document.removeEventListener("pointerup", () => {})
|
||
// 清理计时器
|
||
if (playTimer) {
|
||
pause()
|
||
clearInterval(playTimer)
|
||
playTimer = null
|
||
}
|
||
})
|
||
|
||
let chart = null
|
||
|
||
const initChart = async () => {
|
||
if (!chart) {
|
||
chart = echarts.init(document.getElementById("container"))
|
||
}
|
||
const links = []
|
||
const nodes = []
|
||
const edgeWidth = (interactionTime) => {
|
||
if (interactionTime === 0) return 1
|
||
if (interactionTime === 1) return 2
|
||
if (interactionTime <= 2) return 3
|
||
if (interactionTime <= 3) return 4
|
||
else if (interactionTime <= 10) return 6
|
||
else if (interactionTime <= 20) return 8
|
||
else if (interactionTime <= 30) return 10
|
||
else return 1
|
||
}
|
||
// 添加边唯一标识集合,用于检测重复边
|
||
const edgeSet = new Set()
|
||
// 使用Map存储边,方便后续查找和更新
|
||
const edgeMap = new Map()
|
||
if (!Object.keys(socialGroupsStore.communityDetailNodeList).length) return
|
||
// 先获取到所有节点
|
||
socialGroupsStore.communityAllNodeList.forEach((item) => {
|
||
nodes.push({
|
||
id: item.userId,
|
||
nodeName: item.userName,
|
||
// 头像
|
||
avatarData: item.avatarData,
|
||
// 节点的默认圆形头像
|
||
defaultAvatar: getAvatarUrl(item.defaultAvatar),
|
||
// 节点的高亮圆形头像
|
||
activeAvatar: getAvatarUrl(item.activeAvatar),
|
||
// 粉丝量
|
||
fans: item.fans,
|
||
symbolSize: 40,
|
||
// 发帖数
|
||
postNum: item.postNum,
|
||
// 发帖频率
|
||
postFreqPerDay: item.postFreqPerDay,
|
||
// 参与互动次数
|
||
interactionNum: item.interactionNum,
|
||
// 参与互动频率
|
||
interactionFreqPerDay: item.interactionFreqPerDay,
|
||
// 帖文被互动次数
|
||
interactedNum: item.interactedNum,
|
||
// 最近活跃时间
|
||
recentActiveTime: item.recentActiveTime
|
||
})
|
||
})
|
||
Object.entries(socialGroupsStore.communityDetailNodeList).forEach(([parentId, children]) => {
|
||
children.forEach((child) => {
|
||
// 生成边的唯一标识符,与方向无关
|
||
const edgeKey = [parentId, child.id].sort().join("-")
|
||
// 检查边是否已存在
|
||
if (!edgeSet.has(edgeKey)) {
|
||
edgeSet.add(edgeKey)
|
||
const newEdge = {
|
||
source: parentId,
|
||
target: child.id,
|
||
edge: child.isHidden ? 1 : 0,
|
||
interactionTimes: child.interactionTime,
|
||
lineStyle: {
|
||
width: child.isHidden ? 7 : edgeWidth(child.interactionTime),
|
||
color: child.isHidden ? "#FF5E00" : "#37ACD7", // 无互动=橙色,有互动=蓝色
|
||
type: child.isHidden ? "dashed" : "solid", // 无互动=虚线,有互动=实线
|
||
opacity: child.isHidden ? 1 : 0.5
|
||
}
|
||
}
|
||
links.push(newEdge)
|
||
edgeMap.set(edgeKey, newEdge)
|
||
} else {
|
||
// 边已存在,检查isHidden状态是否变化
|
||
const existingEdge = edgeMap.get(edgeKey)
|
||
// 只在isHidden从false变为true时更新样式
|
||
if (!existingEdge.edge && child.isHidden) {
|
||
existingEdge.edge = 1
|
||
existingEdge.lineStyle = {
|
||
width: 7,
|
||
color: "#FF5E00",
|
||
type: "dashed",
|
||
opacity: 1
|
||
}
|
||
}
|
||
}
|
||
})
|
||
})
|
||
|
||
chartsData.value = { links, nodes }
|
||
const data = { 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: "紧密团体关系",
|
||
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,
|
||
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.15,
|
||
categories: categories,
|
||
force: {
|
||
edgeLength: 2500,
|
||
repulsion: 20000,
|
||
gravity: 0.1,
|
||
friction: 0.02,
|
||
coolingFactor: 0.1
|
||
},
|
||
|
||
animationDurationUpdate: 3500, // 节点移动更平滑
|
||
data: data.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: data.links,
|
||
lineStyle: {
|
||
color: "#37ACD7"
|
||
}
|
||
}
|
||
]
|
||
}
|
||
chart.setOption(option)
|
||
}
|
||
|
||
const highLightUserNodes = (newHiglightUserIdList) => {
|
||
if (!newHiglightUserIdList || !chartsData.value || !chartsData.value.nodes) return
|
||
// 只让高亮节点显示 activeAvatar,其他节点恢复默认头像或圆形
|
||
chartsData.value.nodes.forEach((node) => {
|
||
if (newHiglightUserIdList.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
|
||
}
|
||
]
|
||
})
|
||
}
|
||
|
||
const handleClickNode = () => {
|
||
chart.on("click", function (params) {
|
||
if (params.dataType == "node" && curHighlightUserIdList.value.includes(params.data.id)) {
|
||
console.log("detail点击点:", params.data)
|
||
emit("click:openDialog", params.data)
|
||
}
|
||
})
|
||
}
|
||
|
||
onMounted(() => {
|
||
initChart()
|
||
chart.on("legendselectchanged", function (params) {
|
||
// 解决变形问题
|
||
setTimeout(() => {
|
||
chart.resize()
|
||
}, 0)
|
||
})
|
||
highLightUserNodes()
|
||
play()
|
||
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;
|
||
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;
|
||
left: 20px;
|
||
bottom: 50px;
|
||
border-radius: 4px;
|
||
z-index: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0px 10px;
|
||
color: #fff;
|
||
touch-action: none; // 防止触摸设备上的默认行为
|
||
.time {
|
||
font-family: "PingFang SC";
|
||
font-size: 16px;
|
||
font-style: normal;
|
||
font-weight: 400;
|
||
line-height: normal;
|
||
}
|
||
.axis {
|
||
width: 426px;
|
||
height: 6px;
|
||
border-radius: 20px;
|
||
background-color: #3b7699;
|
||
cursor: pointer;
|
||
transform: translateZ(0); // 启用硬件加速
|
||
position: relative;
|
||
.progress-bar {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
height: 6px;
|
||
background-color: #00ecf9;
|
||
border-radius: 20px;
|
||
z-index: 1;
|
||
}
|
||
.time-sign {
|
||
width: 4px;
|
||
height: 18px;
|
||
border-radius: 10px;
|
||
background: linear-gradient(180deg, #fee39e 0%, #f9bd25 100%);
|
||
z-index: 1;
|
||
// 新增:添加绝对定位
|
||
position: absolute;
|
||
// 调整垂直位置使其居中于时间轴
|
||
top: -6px;
|
||
&::before {
|
||
content: "";
|
||
position: absolute;
|
||
top: -6px;
|
||
left: -8px;
|
||
width: 20px;
|
||
height: 30px;
|
||
background: transparent;
|
||
}
|
||
}
|
||
|
||
.active-needle {
|
||
width: 30px;
|
||
height: 34px;
|
||
background-image: url("@/assets/images/point.png");
|
||
background-size: cover;
|
||
bottom: 1px;
|
||
left: -11px;
|
||
position: absolute;
|
||
}
|
||
.timeLine-point {
|
||
z-index: 2;
|
||
width: 18px;
|
||
height: 18px;
|
||
background-color: transparent;
|
||
border-radius: 50%;
|
||
border: 1.6px solid #ffe5a4;
|
||
position: absolute;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
top: -6px;
|
||
left: -5px;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
will-change: left;
|
||
transform: translate3d(0, 0, 0); // 强制启用硬件加速
|
||
&:hover {
|
||
transform: translate3d(0, 0, 0) scale(1.1);
|
||
}
|
||
&:active {
|
||
transform: translate3d(0, 0, 0) scale(0.95);
|
||
}
|
||
&::after {
|
||
content: "";
|
||
width: 10px;
|
||
height: 10px;
|
||
background-color: #f9bd25;
|
||
border-radius: 50%;
|
||
position: absolute;
|
||
}
|
||
}
|
||
}
|
||
.current-time-display {
|
||
position: absolute;
|
||
bottom: 20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: rgba(0, 67, 125, 0.8);
|
||
border: 1px solid #3aa1f8;
|
||
border-radius: 4px;
|
||
padding: 8px 16px;
|
||
color: #fff;
|
||
font-family: "PingFang SC";
|
||
font-size: 14px;
|
||
font-weight: 400;
|
||
backdrop-filter: blur(3px);
|
||
}
|
||
}
|
||
}
|
||
</style>
|