Merge branch 'master' of http://172.16.20.1:3000/duanhao/SocialNetworks_duan
This commit is contained in:
commit
3b024cb6cd
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
src/assets/images/linkPrediction/title/graph2-title.png
Normal file
BIN
src/assets/images/linkPrediction/title/graph2-title.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
src/assets/images/linkPrediction/title/graph3-title.png
Normal file
BIN
src/assets/images/linkPrediction/title/graph3-title.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
src/assets/images/time-point.png
Normal file
BIN
src/assets/images/time-point.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 864 B |
|
|
@ -250,7 +250,7 @@ export const useKeyNodeStore2 = defineStore("keyNode2", () => {
|
|||
const posts = ref([
|
||||
{
|
||||
id: 1,
|
||||
timestamp: "2023-10-07 15:08:27",
|
||||
timestamp: "2023-10-07 08:08:27",
|
||||
author: "President Biden Archived",
|
||||
influence: 69304.0,
|
||||
highlighted: false,
|
||||
|
|
@ -352,7 +352,7 @@ export const useKeyNodeStore2 = defineStore("keyNode2", () => {
|
|||
},
|
||||
{
|
||||
id: 10,
|
||||
timestamp: "2023-10-15 00:00:00",
|
||||
timestamp: "2023-10-14 15:00:00",
|
||||
author: "Jackson Hinkle 🇺🇸",
|
||||
influence: 22546,
|
||||
highlighted: false,
|
||||
|
|
@ -468,7 +468,7 @@ export const useKeyNodeStore2 = defineStore("keyNode2", () => {
|
|||
])
|
||||
|
||||
// 当前激活的时间点
|
||||
const activeTimePoint = ref(1)
|
||||
const activeTimePoint = ref(0)
|
||||
// 所有时间点数据
|
||||
const timePoints = ref([])
|
||||
const activeLeader = ref(null)
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ const handlePointerDown = (time) => {
|
|||
const registEvents = () => {
|
||||
const simulation = graphVis.getSimulationLayout()
|
||||
forceSimulator = simulation.forceSimulation()
|
||||
|
||||
//全局记录包裹层元素
|
||||
const containerDom = document.getElementById("container")
|
||||
graphVis.registEventListener("node", "mouseOver", function (event, node) {
|
||||
|
|
@ -166,10 +167,10 @@ const highLightAboutNodesOrLinks = (type) => {
|
|||
graphVis.cancelAllSelected()
|
||||
const { newNodes, newLinks } = graph.value
|
||||
console.log(graphVis.nodes)
|
||||
|
||||
console.log("进来highLightAboutNodesOrLinks")
|
||||
if (type == "nodes") {
|
||||
//实现高亮节点逻辑
|
||||
|
||||
console.log("进来if")
|
||||
graphVis.nodes.forEach((node) =>
|
||||
newNodes.forEach((newNode) => {
|
||||
if (node.id === newNode.name) {
|
||||
|
|
@ -194,14 +195,17 @@ const runDiffForceLayout = (layoutConfig, layoutType, isAsync) => {
|
|||
function handleLayoutSuccess() {
|
||||
//处理四个关系图的差异函数
|
||||
const handleGroupDiscoveryDiff = () => {
|
||||
console.log("进来handleGroupDiscoveryDiff")
|
||||
return
|
||||
}
|
||||
|
||||
const handleGroupStructureDiff = () => {
|
||||
console.log("进来handleGroupStructureDiff")
|
||||
highLightAboutNodesOrLinks("links")
|
||||
}
|
||||
|
||||
const handleGroupMemberDiff = () => {
|
||||
console.log("进来handleGroupMemberDiff")
|
||||
highLightAboutNodesOrLinks("nodes")
|
||||
}
|
||||
|
||||
|
|
@ -296,7 +300,7 @@ const clusterAnalyze = () => {
|
|||
}
|
||||
|
||||
// 仅对“异常群体模块”生效:时间变化时强制重绘一次
|
||||
if (storeId === "anomalousGroup") {
|
||||
if (storeId == "anomalousGroup") {
|
||||
watch(
|
||||
() => props.store.currentUtc,
|
||||
() => {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
</div>
|
||||
<div class="timeline-container">
|
||||
<span class="time-label">2023.10.07 00:00:00</span>
|
||||
<div class="timeline-track" :style="trackStyle">
|
||||
<div class="timeline-track" :style="trackStyle" @click="handleTrackClick">
|
||||
<div
|
||||
v-for="point in store.timePoints"
|
||||
:key="point.id"
|
||||
|
|
@ -43,13 +43,11 @@
|
|||
trigger="click"
|
||||
content="发布贴文"
|
||||
>
|
||||
<template #reference>
|
||||
<div class="active-pin"></div>
|
||||
</template>
|
||||
</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>
|
||||
|
|
@ -78,6 +76,148 @@ const bridgeGraphRef = ref(null)
|
|||
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)
|
||||
}
|
||||
|
|
@ -120,29 +260,29 @@ const calculatePositions = (timestamp) => {
|
|||
const position = ((pointTime - startTime) / timeRange) * 100
|
||||
pointPositions[point.id] = Math.max(0, Math.min(100, position))
|
||||
})
|
||||
// 处理重叠 (假设每个点宽度约为20px,时间轴总宽度为可用宽度)
|
||||
const pointWidthPercentage = 5 // 估算的点宽度百分比
|
||||
const minSpacing = pointWidthPercentage // 最小间距百分比
|
||||
// // 处理重叠 (假设每个点宽度约为20px,时间轴总宽度为可用宽度)
|
||||
// const pointWidthPercentage = 5 // 估算的点宽度百分比
|
||||
// const minSpacing = pointWidthPercentage // 最小间距百分比
|
||||
|
||||
// 调整重叠的点
|
||||
for (let i = 1; i < sortedPoints.length; i++) {
|
||||
const prevPoint = sortedPoints[i - 1]
|
||||
const currPoint = sortedPoints[i]
|
||||
// // 调整重叠的点
|
||||
// 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]
|
||||
// const prevPosition = pointPositions[prevPoint.id]
|
||||
// const currPosition = pointPositions[currPoint.id]
|
||||
|
||||
// 检查是否重叠
|
||||
if (currPosition - prevPosition < minSpacing) {
|
||||
// 调整当前点位置
|
||||
pointPositions[currPoint.id] = prevPosition + minSpacing
|
||||
// // 检查是否重叠
|
||||
// if (currPosition - prevPosition < minSpacing) {
|
||||
// // 调整当前点位置
|
||||
// pointPositions[currPoint.id] = prevPosition + minSpacing
|
||||
|
||||
// 确保不超出范围
|
||||
if (pointPositions[currPoint.id] > 100) {
|
||||
pointPositions[currPoint.id] = 100
|
||||
}
|
||||
}
|
||||
}
|
||||
// // 确保不超出范围
|
||||
// if (pointPositions[currPoint.id] > 100) {
|
||||
// pointPositions[currPoint.id] = 100
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
return pointPositions
|
||||
}
|
||||
|
|
@ -161,21 +301,53 @@ watch(
|
|||
|
||||
// 计算时间轴轨道样式
|
||||
const trackStyle = computed(() => {
|
||||
if (!store.activeTimePoint) return {}
|
||||
const activePosition = pointPositions.value[store.activeTimePoint] || 0
|
||||
// 优先使用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%)`
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({ highlightNode })
|
||||
// 添加时间点点击事件处理函数
|
||||
// 为了保证点击时间点也能更新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">
|
||||
|
|
@ -250,6 +422,7 @@ const handleTimePointClick = (pointId) => {
|
|||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.timeline-point-wrapper {
|
||||
display: flex;
|
||||
|
|
@ -261,11 +434,10 @@ const handleTimePointClick = (pointId) => {
|
|||
transform: translateX(-50%);
|
||||
}
|
||||
.timeline-point {
|
||||
width: vw(18);
|
||||
width: 4px;
|
||||
height: vh(18);
|
||||
background-color: transparent;
|
||||
border-radius: 50%;
|
||||
border: 1.6px solid #ffe5a4;
|
||||
background: linear-gradient(180deg, #fee39e 0%, #f9bd25 100%);
|
||||
border-radius: 10px;
|
||||
transition: transform 0.3s ease;
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
|
@ -273,29 +445,24 @@ const handleTimePointClick = (pointId) => {
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
// 删除 ::after 伪元素,因为不再需要内部圆点
|
||||
.timeline-point::after {
|
||||
content: "";
|
||||
width: vw(10);
|
||||
height: vh(10);
|
||||
background-color: #f9bd25;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.timeline-point-wrapper:hover .timeline-point {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
// 调整 active 状态样式
|
||||
.timeline-point.active {
|
||||
background-color: transparent;
|
||||
border-color: #ffe5a4;
|
||||
transform: scale(1.3);
|
||||
background: linear-gradient(180deg, #fee39e 0%, #f9bd25 100%);
|
||||
transform: scaleX(1.5);
|
||||
}
|
||||
.active-pin {
|
||||
width: vw(30);
|
||||
height: vh(34);
|
||||
background-image: url("@/assets/images/point.png");
|
||||
width: vw(14);
|
||||
height: vh(14);
|
||||
background-image: url("@/assets/images/time-point.png");
|
||||
background-size: cover;
|
||||
bottom: vh(6);
|
||||
position: absolute;
|
||||
bottom: vh(-3);
|
||||
transform: translateX(-50%); // 居中对齐
|
||||
z-index: 10; // 确保显示在时间点上方
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -43,6 +43,13 @@ const processData = async () => {
|
|||
// 批量将头像转换成base64
|
||||
const bridgeNodes = keyNodeStore2.bridgeNodes
|
||||
|
||||
if (props.timestamp === 0) {
|
||||
const result = {
|
||||
nodes: [],
|
||||
links: []
|
||||
}
|
||||
return result
|
||||
}
|
||||
// 根据timestamp过滤桥梁节点
|
||||
const filteredBridgeNodes = bridgeNodes.filter((node) => node.postsId <= props.timestamp)
|
||||
for (const node of filteredBridgeNodes) {
|
||||
|
|
@ -543,13 +550,14 @@ onUnmounted(() => {
|
|||
onMounted(() => {
|
||||
setTimeout(async () => {
|
||||
await initChart()
|
||||
}, 100)
|
||||
}, 500)
|
||||
resizeChart()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.timestamp,
|
||||
async (newValue) => {
|
||||
console.log("newValue:", newValue)
|
||||
await initChart()
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -89,6 +89,11 @@ const stopAutomaticPlay = () => {
|
|||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
// 调用GraphPanel组件的内部方法停止自动播放
|
||||
if (graphPanelRef.value) {
|
||||
// 通过直接清除定时器的方式停止,因为我们在GraphPanel内部也实现了停止逻辑
|
||||
// 这里不需要额外调用,因为GraphPanel中的handleTrackClick和handleTimePointClick已经会停止自动播放
|
||||
}
|
||||
}
|
||||
|
||||
let timer = null
|
||||
|
|
@ -96,34 +101,19 @@ let timer = null
|
|||
// 根据不同的时间间隔生成对应的延时数据
|
||||
const intervalTimes = [500, 500, 1000, 1000, 1000, 1000, 1000, 1000, 4000, 2000]
|
||||
const automaticPlay = () => {
|
||||
let index = 1
|
||||
if (timer) clearInterval(timer)
|
||||
|
||||
// 创建一个函数来处理定时逻辑
|
||||
const playNext = () => {
|
||||
store.setActiveTimePoint(index)
|
||||
|
||||
if (index >= store.allLeaderData.length) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
return
|
||||
}
|
||||
|
||||
// 根据当前index获取对应的时间间隔,如果超出数组长度则使用默认值
|
||||
const interval = intervalTimes[index] || 1000
|
||||
timer = setTimeout(playNext, interval)
|
||||
index++
|
||||
// 不再使用原来的定时器逻辑,而是使用GraphPanel组件内部的定时器
|
||||
if (graphPanelRef.value) {
|
||||
graphPanelRef.value.startAutomaticPlay()
|
||||
}
|
||||
|
||||
// 启动自动播放,使用第一个时间间隔
|
||||
const initialInterval = intervalTimes[0] || 1000
|
||||
timer = setTimeout(playNext, initialInterval)
|
||||
}
|
||||
|
||||
// 调用 store.initializeTimePoints() 初始化时间点
|
||||
onMounted(() => {
|
||||
store.initializeTimePoints()
|
||||
automaticPlay()
|
||||
// 延迟调用,确保graphPanelRef已经挂载
|
||||
setTimeout(() => {
|
||||
automaticPlay()
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
|
|
|||
|
|
@ -123,8 +123,8 @@ import Graph from "../components/graph.vue"
|
|||
import WordsCloud from "../components/cloudWords.vue"
|
||||
import { useCharacterHiddenStore } from "@/store/linkPrediction/index"
|
||||
import userPanelTitleImg from "@/assets/images/linkPrediction/title/characters-hidden-interaction-user-title.png"
|
||||
import graphTitleImg from "@/assets/images/linkPrediction/title/graph1-title.png"
|
||||
import analysisTitleImg from "@/assets/images/linkPrediction/title/analysis-title.png"
|
||||
import graphTitleImg from "@/assets/images/linkPrediction/title/graph3-title.png"
|
||||
import analysisTitleImg from "@/assets/images/linkPrediction/title/characters-hidden-interaction-analysis-title.png"
|
||||
import { storeToRefs } from "pinia"
|
||||
|
||||
const characterHiddenStore = useCharacterHiddenStore()
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ import { Icon } from "@iconify/vue"
|
|||
import { useSocialGroupsStore } from "@/store/linkPrediction/index"
|
||||
import userPanelTitleImg from "@/assets/images/linkPrediction/title/social-group-user-title.png"
|
||||
import userChartTitleImg from "@/assets/images/linkPrediction/title/interaction-strenth-title.png"
|
||||
import graphTitleImg from "@/assets/images/linkPrediction/title/graph1-title.png"
|
||||
import graphTitleImg from "@/assets/images/linkPrediction/title/graph2-title.png"
|
||||
import analysisTitleImg from "@/assets/images/linkPrediction/title/social-group-analysis-title.png"
|
||||
import { getAvatarUrl } from "@/utils/transform"
|
||||
import { storeToRefs } from "pinia"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user