SocialNetworks_duan/src/views/KeyNodeDiscern/bridgeCommunication/components/GraphPanel.vue
2025-08-26 16:54:54 +08:00

469 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="right-panel">
<img src="@/assets/images/head/bayi.png" alt="" class="head" />
<div class="key-node-recognition">
<div class="content-wrapper">
<div class="chart-container">
<BridgeCommunityGraph
ref="bridgeGraphRef"
v-if="currentComponent == 'BridgeCommunityNode'"
:timestamp="store.activeTimePoint"
@click:navigateToCommunityDetail="handleClickCommunity"
/>
<DetailCommunityGraph
v-else
ref="detailCommunityGraphRef"
:communityId="currentSelectedCommunityId"
@click:goback="handleClickGoBack"
/>
</div>
<div class="timeline-container">
<span class="time-label">2023.10.07 00:00:00</span>
<div class="timeline-track" :style="trackStyle" @click="handleTrackClick">
<div
v-for="point in store.timePoints"
:key="point.id"
class="timeline-point-wrapper"
:style="{ left: `${pointPositions[point.id]}%` }"
@click="handleTimePointClick(point.id)"
>
<el-tooltip
class="timePoint-box-item"
effect="light"
:content="point.timestamp"
placement="bottom"
>
<div class="timeline-point" :class="{ active: store.activeTimePoint === point.id }">
<el-popover
v-if="store.activeTimePoint === point.id"
effect="dark"
placement="top"
:title="point.leaderId"
:width="50"
trigger="click"
content="发布贴文"
>
</el-popover>
</div>
</el-tooltip>
</div>
<div class="active-pin" :style="{ left: `${clickedPosition}%` }"></div>
</div>
<span class="time-label">2023.10.15 00:00:00</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, defineExpose, watch, computed, defineProps } from "vue"
import { useKeyNodeStore2 } from "@/store/keyNodeStore2"
// 接收 stopAutomaticPlay 方法
const props = defineProps({
stopAutomaticPlay: { type: Function, required: true }
})
import BridgeCommunityGraph from "./graph/bridgeCommunityGraph.vue"
import DetailCommunityGraph from "./graph/detailCommunityGraph.vue"
const store = useKeyNodeStore2()
const leaderGraphRef = ref(null)
// 添加对BridgeCommunityGraph的引用
const bridgeGraphRef = ref(null)
// 设置当前激活的组件--默认是BridgeCommunityGraph
const currentComponent = ref("BridgeCommunityNode")
const currentSelectedCommunityId = ref(null)
// 用于存储点击位置
const clickedPosition = ref(0)
// 添加定时器相关变量
let animationTimer = null
const isAutoPlaying = ref(false)
// 开始自动播放方法
const startAutomaticPlay = () => {
// 如果已经在播放,先清除
if (animationTimer) {
clearInterval(animationTimer)
}
isAutoPlaying.value = true
// 每300ms移动2%
animationTimer = setInterval(() => {
// 计算新位置
const newPosition = clickedPosition.value + 2
// 检查是否到达终点
if (newPosition >= 100) {
clickedPosition.value = 100
props.stopAutomaticPlay()
return
}
// 更新位置
clickedPosition.value = newPosition
// 根据当前位置更新activeTimePoint
updateActiveTimePointByPosition(newPosition)
}, 300)
}
// 根据位置更新activeTimePoint
const updateActiveTimePointByPosition = (position) => {
// 获取排序后的时间点
const sortedPoints = [...store.timePoints].sort((a, b) => {
return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
})
// 计算每个时间点的原始位置百分比(不考虑重叠调整)
const startTime = new Date("2023-10-07T00:00:00").getTime()
const endTime = new Date("2023-10-15T00:00:00").getTime()
const timeRange = endTime - startTime
const originalPositions = {}
sortedPoints.forEach((point) => {
const pointTime = new Date(point.timestamp).getTime()
const pointPos = ((pointTime - startTime) / timeRange) * 100
originalPositions[point.id] = pointPos
})
// 找到当前位置对应的时间点
let targetPointId = null
if (sortedPoints.length > 0 && position < originalPositions[sortedPoints[0].id]) {
store.setActiveTimePoint(0)
} else {
for (let i = 0; i < sortedPoints.length; i++) {
const point = sortedPoints[i]
const pointPosition = originalPositions[point.id]
// 如果位置大于等于当前时间点位置,则选中当前时间点
if (position >= pointPosition) {
targetPointId = point.id
} else {
break
}
}
}
// 如果找到了对应的时间点且与当前activeTimePoint不同则更新
if (targetPointId && targetPointId !== store.activeTimePoint) {
store.setActiveTimePoint(targetPointId)
}
}
const handleTrackClick = (event) => {
// 停止自动播放
props.stopAutomaticPlay()
// 清除内部定时器
if (animationTimer) {
clearInterval(animationTimer)
animationTimer = null
isAutoPlaying.value = false
}
// 获取时间轴轨道的DOM元素和尺寸信息
const trackElement = event.currentTarget
const rect = trackElement.getBoundingClientRect()
// 计算点击位置在时间轴上的百分比
const clickPosition = ((event.clientX - rect.left) / rect.width) * 100
// 保存点击位置
clickedPosition.value = clickPosition
// 获取排序后的时间点
const sortedPoints = [...store.timePoints].sort((a, b) => {
return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
})
// 计算每个时间点的原始位置百分比(不考虑重叠调整)
const startTime = new Date("2023-10-07T00:00:00").getTime()
const endTime = new Date("2023-10-15T00:00:00").getTime()
const timeRange = endTime - startTime
const originalPositions = {};
sortedPoints.forEach((point) => {
const pointTime = new Date(point.timestamp).getTime();
const position = ((pointTime - startTime) / timeRange) * 100;
originalPositions[point.id] = position;
});
// 特殊情况如果点击位置小于所有时间点的位置则设置activeTimePoint为0
if (sortedPoints.length > 0 && clickPosition < originalPositions[sortedPoints[0].id]) {
store.setActiveTimePoint(0);
return; // 直接返回,不再执行后续逻辑
}
// 找到点击位置对应的时间点
let targetPointId = null;
for (let i = 0; i < sortedPoints.length; i++) {
const point = sortedPoints[i];
const pointPosition = originalPositions[point.id];
// 如果点击位置大于等于当前时间点位置,且小于下一个时间点位置,则选中当前时间点
if (
clickPosition >= pointPosition &&
(i === sortedPoints.length - 1 || clickPosition < originalPositions[sortedPoints[i + 1].id])
) {
targetPointId = point.id;
break;
}
}
// 如果找到了对应的时间点则更新activeTimePoint
if (targetPointId) {
store.setActiveTimePoint(targetPointId);
}
};
const handleGraphNodeClick = (leaderData) => {
store.openLeaderDetail(leaderData)
}
const handleClickCommunity = (communityId) => {
if (communityId) {
currentComponent.value = "DetailCommunityNode"
currentSelectedCommunityId.value = communityId
}
}
const handleClickGoBack = (currentComponentName) => {
currentComponent.value = currentComponentName
}
const highlightNode = (leaderId) => {
if (leaderGraphRef.value) {
leaderGraphRef.value.highlightNode(leaderId)
}
// 调用BridgeCommunityGraph的highlightNode方法
if (bridgeGraphRef.value) {
bridgeGraphRef.value.highlightNode(leaderId)
}
}
// 计算时间点在时间轴上的位置百分比
const calculatePositions = (timestamp) => {
// 时间范围: 2023-10-07T00:00:00 到 2023-10-15T00:00:00
const startTime = new Date("2023-10-07T00:00:00").getTime()
const endTime = new Date("2023-10-15T00:00:00").getTime()
const timeRange = endTime - startTime
// 复制并排序时间点数据
const sortedPoints = [...store.timePoints].sort((a, b) => {
return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
})
// 计算每个点的原始位置
const pointPositions = {}
sortedPoints.forEach((point) => {
const pointTime = new Date(point.timestamp).getTime()
const position = ((pointTime - startTime) / timeRange) * 100
pointPositions[point.id] = Math.max(0, Math.min(100, position))
})
// // 处理重叠 (假设每个点宽度约为20px时间轴总宽度为可用宽度)
// const pointWidthPercentage = 5 // 估算的点宽度百分比
// const minSpacing = pointWidthPercentage // 最小间距百分比
// // 调整重叠的点
// for (let i = 1; i < sortedPoints.length; i++) {
// const prevPoint = sortedPoints[i - 1]
// const currPoint = sortedPoints[i]
// const prevPosition = pointPositions[prevPoint.id]
// const currPosition = pointPositions[currPoint.id]
// // 检查是否重叠
// if (currPosition - prevPosition < minSpacing) {
// // 调整当前点位置
// pointPositions[currPoint.id] = prevPosition + minSpacing
// // 确保不超出范围
// if (pointPositions[currPoint.id] > 100) {
// pointPositions[currPoint.id] = 100
// }
// }
// }
return pointPositions
}
// 响应式引用存储位置信息
const pointPositions = ref({})
// 监听时间点变化,重新计算位置
watch(
() => store.timePoints,
() => {
pointPositions.value = calculatePositions()
},
{ immediate: true, deep: true }
)
// 计算时间轴轨道样式
const trackStyle = computed(() => {
// 优先使用clickedPosition如果没有则使用activeTimePoint对应的位置
const activePosition =
clickedPosition.value !== null
? clickedPosition.value
: pointPositions.value[store.activeTimePoint] || 0
return {
background: `linear-gradient(90deg, #3B7699 0%, #00F3FF ${activePosition}%, #3B7699 ${activePosition}%, #3B7699 100%)`
}
})
// 为了保证点击时间点也能更新clickedPosition修改handleTimePointClick方法
const handleTimePointClick = (pointId) => {
// 停止自动播放
props.stopAutomaticPlay()
// 清除内部定时器
if (animationTimer) {
clearInterval(animationTimer)
animationTimer = null
isAutoPlaying.value = false
}
// 设置当前激活的时间点
store.setActiveTimePoint(pointId)
// 查找该时间点的原始位置
const point = store.timePoints.find((p) => p.id === pointId)
if (point) {
const startTime = new Date("2023-10-07T00:00:00").getTime()
const endTime = new Date("2023-10-15T00:00:00").getTime()
const timeRange = endTime - startTime
const pointTime = new Date(point.timestamp).getTime()
const position = ((pointTime - startTime) / timeRange) * 100
clickedPosition.value = position
}
}
// 在组件卸载时清除定时器
import { onUnmounted } from "vue"
onUnmounted(() => {
if (animationTimer) {
clearInterval(animationTimer)
}
})
defineExpose({ highlightNode, startAutomaticPlay })
</script>
<style scoped lang="scss">
.right-panel {
width: 100%;
height: 100%;
position: relative;
}
.head {
margin: 0 auto;
}
.key-node-recognition {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.background-svg-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.content-wrapper {
width: 100%;
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
.graph-img-title {
display: absolute;
top: 0;
margin: 0 auto;
}
}
.chart-container {
width: 100%;
height: 100%;
margin-top: vh(35);
}
.timeline-container {
width: 95%;
height: vh(40);
display: flex;
align-items: center;
justify-content: space-between;
padding: vh(0) vw(10);
box-sizing: border-box;
position: absolute;
z-index: 1;
bottom: vh(15);
left: vw(20);
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(3px);
}
.time-label {
font-size: vw(12);
color: #a9c2e0;
}
.timeline-track {
flex-grow: 1;
height: vh(4);
margin: vh(0) vw(15);
position: relative;
display: flex;
align-items: center;
cursor: pointer;
}
.timeline-point-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: vh(20);
cursor: pointer;
position: absolute;
transform: translateX(-50%);
}
.timeline-point {
width: 4px;
height: vh(18);
background: linear-gradient(180deg, #fee39e 0%, #f9bd25 100%);
border-radius: 10px;
transition: transform 0.3s ease;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
// 删除 ::after 伪元素因为不再需要内部圆点
.timeline-point::after {
display: none;
}
// 调整 active 状态样式
.timeline-point.active {
background: linear-gradient(180deg, #fee39e 0%, #f9bd25 100%);
transform: scaleX(1.5);
}
.active-pin {
width: vw(14);
height: vh(14);
background-image: url("@/assets/images/time-point.png");
background-size: cover;
position: absolute;
bottom: vh(-3);
transform: translateX(-50%); // 居中对齐
z-index: 10; // 确保显示在时间点上方
}
</style>