第三个模块时间轴自动推进展示每个时间切片的关系图
This commit is contained in:
		
							parent
							
								
									6230c865e3
								
							
						
					
					
						commit
						c9e63e815c
					
				
							
								
								
									
										
											BIN
										
									
								
								src/assets/images/head/hiddenTitle.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/images/head/hiddenTitle.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 169 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/images/head/linkedPredictionStruct.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/images/head/linkedPredictionStruct.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 164 KiB  | 
| 
						 | 
				
			
			@ -11,7 +11,7 @@
 | 
			
		|||
      >
 | 
			
		||||
        <div
 | 
			
		||||
          class="time-section"
 | 
			
		||||
          :style="{ left: getTimeSectionLeft(time) + 'px' }"
 | 
			
		||||
          :style="{ left: nowSize(getTimeSectionLeft(time)) + 'px' }"
 | 
			
		||||
          @pointerdown.stop="handleSectionPointerDown(time)"
 | 
			
		||||
        ></div>
 | 
			
		||||
      </el-tooltip>
 | 
			
		||||
| 
						 | 
				
			
			@ -33,7 +33,7 @@
 | 
			
		|||
          <div
 | 
			
		||||
            class="timeLine-point"
 | 
			
		||||
            @pointerdown.stop="handlePointPointerDown"
 | 
			
		||||
            :style="{ left: `${currentPosition - 9}px` }"
 | 
			
		||||
            :style="{ left: `${nowSize(currentPosition - 9)}px` }"
 | 
			
		||||
          ></div>
 | 
			
		||||
        </el-tooltip>
 | 
			
		||||
      </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -53,12 +53,12 @@ const props = defineProps({
 | 
			
		|||
  },
 | 
			
		||||
  startTime: {
 | 
			
		||||
    //起始时间
 | 
			
		||||
    type: Date,
 | 
			
		||||
 | 
			
		||||
    default: new Date("2024-05-16 16:56:04")
 | 
			
		||||
  },
 | 
			
		||||
  endTime: {
 | 
			
		||||
    //结束时间
 | 
			
		||||
    type: Date,
 | 
			
		||||
 | 
			
		||||
    default: new Date("2024-05-23 10:16:56")
 | 
			
		||||
  },
 | 
			
		||||
  initPosition: {
 | 
			
		||||
| 
						 | 
				
			
			@ -81,7 +81,7 @@ const props = defineProps({
 | 
			
		|||
const startTime = ref(props.startTime) //开始时间
 | 
			
		||||
const endTime = ref(props.endTime) //结束时间
 | 
			
		||||
const timeList = ref(props.timeList) //时间列表
 | 
			
		||||
const currentTime = ref(new Date("2024-05-16 16:56:04")) // 当前选中的时间
 | 
			
		||||
const currentTime = ref(props.startTime) // 当前选中的时间
 | 
			
		||||
const currentPosition = ref(props.initPosition) // 初始位置
 | 
			
		||||
const isPlaying = ref(props.isAutoPlay) // 是否自动播放
 | 
			
		||||
const axisRef = ref(null) // 轴的引用
 | 
			
		||||
| 
						 | 
				
			
			@ -90,7 +90,7 @@ const axisWidth = nowSize(415) // 轴的长度(px)
 | 
			
		|||
const startTimeMs = startTime.value.getTime() // 起始时间的毫秒数
 | 
			
		||||
const endTimeMs = endTime.value.getTime() // 结束时间的毫秒数
 | 
			
		||||
const totalDuration = endTimeMs - startTimeMs // 计算总持续时间
 | 
			
		||||
const step = 4 // 每次移动的像素数(px)
 | 
			
		||||
const step = nowSize(4) // 每次移动的像素数(px)
 | 
			
		||||
let playTimer = null // 自动播放定时器
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(["click:pointerDown", "slide:pointerUp"])
 | 
			
		||||
| 
						 | 
				
			
			@ -138,7 +138,7 @@ watch(
 | 
			
		|||
// 让 active-needle 标定在 timeList 最后一个时间点
 | 
			
		||||
const showHidden = computed(() => {
 | 
			
		||||
  if (!timeList.value || timeList.value.length === 0) return {}
 | 
			
		||||
  const left = getTimeSectionLeft.value(timeList.value[timeList.value.length - 1]) + 5 // +5px 保持和 time-section 对齐
 | 
			
		||||
  const left = getTimeSectionLeft.value(timeList.value[timeList.value.length - 1]) + nowSize(5) // +5px 保持和 time-section 对齐
 | 
			
		||||
  return { left: `${left}px` }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -153,17 +153,41 @@ const pause = () => {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// 自动播放控制
 | 
			
		||||
// 已处理过的时间点索引
 | 
			
		||||
let processedIndices = new Set()
 | 
			
		||||
 | 
			
		||||
// 重置已处理时间点
 | 
			
		||||
const resetProcessedIndices = () => {
 | 
			
		||||
  processedIndices.clear()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const play = () => {
 | 
			
		||||
  if (!isPlaying.value) return
 | 
			
		||||
  if (!isPlaying.value || timeList.value.length === 0) return
 | 
			
		||||
  // 重置已处理索引,确保每次播放都从当前位置重新开始处理
 | 
			
		||||
  playTimer = setInterval(() => {
 | 
			
		||||
    if (currentPosition.value >= axisWidth) {
 | 
			
		||||
      pause()
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    // 持续移动进度条
 | 
			
		||||
    currentPosition.value = Math.min(axisWidth, currentPosition.value + step)
 | 
			
		||||
    currentTime.value = getTimeFromPosition(currentPosition.value)
 | 
			
		||||
    emit("slide:pointerUp", currentTime.value)
 | 
			
		||||
  }, 300) // 每300ms移动一次
 | 
			
		||||
    // 检查当前位置是否到达或超过某个时间切片
 | 
			
		||||
    for (let i = 0; i < timeList.value.length; i++) {
 | 
			
		||||
      if (processedIndices.has(i)) continue
 | 
			
		||||
      const time = timeList.value[i]
 | 
			
		||||
      const left = getTimeSectionLeft.value(time)
 | 
			
		||||
      // 如果当前位置超过了这个时间切片的位置,并且还没有处理过
 | 
			
		||||
      if (
 | 
			
		||||
        currentPosition.value >= left - nowSize(2) &&
 | 
			
		||||
        currentPosition.value <= left + nowSize(2)
 | 
			
		||||
      ) {
 | 
			
		||||
        currentTime.value = time // 使用精确的时间值
 | 
			
		||||
        emit("slide:pointerUp", time) // 只在timeList中的时间点发送请求
 | 
			
		||||
        processedIndices.add(i)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // 如果到达终点,停止播放
 | 
			
		||||
    if (currentPosition.value >= axisWidth) {
 | 
			
		||||
      pause()
 | 
			
		||||
    }
 | 
			
		||||
  }, 500) // 每500ms移动一小步
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 根据位置计算时间
 | 
			
		||||
| 
						 | 
				
			
			@ -177,6 +201,7 @@ const getTimeFromPosition = (position) => {
 | 
			
		|||
const handlePointerDown = (e) => {
 | 
			
		||||
  if (e.target.classList.contains("timeLine-point")) return
 | 
			
		||||
  pause() // 拖动或点击时暂停自动播放
 | 
			
		||||
  resetProcessedIndices() // 重置已处理时间点
 | 
			
		||||
  const rect = axisRef.value.getBoundingClientRect()
 | 
			
		||||
  const position = Math.max(0, Math.min(axisWidth, e.clientX - rect.left))
 | 
			
		||||
  currentPosition.value = position
 | 
			
		||||
| 
						 | 
				
			
			@ -189,6 +214,7 @@ const handlePointPointerDown = (e) => {
 | 
			
		|||
  e.stopPropagation()
 | 
			
		||||
  e.preventDefault()
 | 
			
		||||
  isDragging.value = true
 | 
			
		||||
  resetProcessedIndices() // 重置已处理时间点
 | 
			
		||||
  // 缓存轴的边界矩形,避免重复计算
 | 
			
		||||
  const rect = axisRef.value.getBoundingClientRect()
 | 
			
		||||
  const axisLeft = rect.left
 | 
			
		||||
| 
						 | 
				
			
			@ -216,9 +242,10 @@ const handlePointPointerDown = (e) => {
 | 
			
		|||
  document.addEventListener("pointerup", handlePointerUp)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const timeSectionWidth = 4 // 与样式保持一致
 | 
			
		||||
const timeSectionWidth = nowSize(4) // 与样式保持一致
 | 
			
		||||
const handleSectionPointerDown = (time) => {
 | 
			
		||||
  pause()
 | 
			
		||||
  resetProcessedIndices() // 重置已处理时间点
 | 
			
		||||
  // 计算该时间点的中心位置
 | 
			
		||||
  const left = getTimeSectionLeft.value(time) + timeSectionWidth / 2
 | 
			
		||||
  currentPosition.value = left
 | 
			
		||||
| 
						 | 
				
			
			@ -232,6 +259,7 @@ const reset = () => {
 | 
			
		|||
  currentTime.value = getTimeFromPosition(props.initPosition)
 | 
			
		||||
  // 清理旧定时器
 | 
			
		||||
  pause()
 | 
			
		||||
  resetProcessedIndices() // 重置已处理时间点
 | 
			
		||||
  // 重新开始播放
 | 
			
		||||
  if (props.isAutoPlay) {
 | 
			
		||||
    isPlaying.value = true
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -186,8 +186,6 @@ export const useGroupDiscoveryStore = defineStore("groupDiscovery", {
 | 
			
		|||
    //获取群体列表数据
 | 
			
		||||
    async initializeGroupList(time = "") {
 | 
			
		||||
      const res = await getGroupEvolutionGroupList(time)
 | 
			
		||||
      console.log("群体列表:", res)
 | 
			
		||||
 | 
			
		||||
      if (res.code != 200) return
 | 
			
		||||
      const iconMap = {
 | 
			
		||||
        节点数: nodePrefix,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,7 +30,7 @@
 | 
			
		|||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import { ref, toRaw, watch } from "vue"
 | 
			
		||||
import { onMounted, ref, toRaw, watch } from "vue"
 | 
			
		||||
import { storeToRefs } from "pinia"
 | 
			
		||||
import { convertToUtcIsoString } from "@/utils/transform"
 | 
			
		||||
import { paintNodeFunction, paintLineFunction } from "@/utils/customePaint"
 | 
			
		||||
| 
						 | 
				
			
			@ -59,6 +59,12 @@ const legendsMap = {
 | 
			
		|||
    { icon: abnormalLeg, text: "异常社团" }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
const zoomSize = new Map([
 | 
			
		||||
  ["groupDiscovery", 0.15],
 | 
			
		||||
  ["groupStructure", 0.15],
 | 
			
		||||
  ["groupMember", 0.15],
 | 
			
		||||
  ["anomalousGroup", 0.7]
 | 
			
		||||
])
 | 
			
		||||
const emit = defineEmits(["click:pointerDownAndSlide"])
 | 
			
		||||
const { timeList, graph } = storeToRefs(props.store)
 | 
			
		||||
const graphTitle = props.store.graphTitle
 | 
			
		||||
| 
						 | 
				
			
			@ -166,11 +172,8 @@ const registEvents = () => {
 | 
			
		|||
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) {
 | 
			
		||||
| 
						 | 
				
			
			@ -195,62 +198,41 @@ 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")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const handleAnomalousGroup = () => {
 | 
			
		||||
      // 当前时间从 store 取;没有就用 T0
 | 
			
		||||
      // 当前时间从 store 取
 | 
			
		||||
      const now = props.store.currentUtc
 | 
			
		||||
      const TA = "2024-06-19T08:57:55Z" // A
 | 
			
		||||
      const TB = "2024-06-19T10:58:03Z" // B
 | 
			
		||||
      const TC = "2024-06-19T12:58:04Z" // C
 | 
			
		||||
      // 组色(与你现有 colorMap 一致)
 | 
			
		||||
      const GROUP_ALPHA = 0.3
 | 
			
		||||
      const RED = "220,50,60"
 | 
			
		||||
      const greenRed = "85, 125, 15"
 | 
			
		||||
      const blueRed = "15, 106, 125"
 | 
			
		||||
      const yellowRed = "125, 114, 15"
 | 
			
		||||
 | 
			
		||||
      // 时间门控:达到阈值就把各组异常节点染红
 | 
			
		||||
      const shouldA = now >= TA
 | 
			
		||||
      const shouldB = now >= TB
 | 
			
		||||
      const shouldC = now >= TC
 | 
			
		||||
      if (shouldA) {
 | 
			
		||||
        graphVis.nodes = graphVis.nodes.map((n) => {
 | 
			
		||||
          if (props.store.graphAbnormalData.groupA.includes(n.id)) {
 | 
			
		||||
            // n.fillColor = RED
 | 
			
		||||
            n.fillColor = greenRed
 | 
			
		||||
      const abnormalData = props.store.graphAbnormalData
 | 
			
		||||
      const abnormalGroupConfigs = [
 | 
			
		||||
        { timeThreshold: "2024-06-19T08:57:55Z", groupKey: "groupA", color: "85, 125, 15" }, // 绿色
 | 
			
		||||
        { timeThreshold: "2024-06-19T10:58:03Z", groupKey: "groupB", color: "125, 114, 15" }, // 黄色
 | 
			
		||||
        { timeThreshold: "2024-06-19T12:58:04Z", groupKey: "groupC", color: "15, 106, 125" } // 蓝色
 | 
			
		||||
      ]
 | 
			
		||||
      // 计算当前应激活的异常组配置
 | 
			
		||||
      const activeConfigs = abnormalGroupConfigs.filter((config) => now >= config.timeThreshold)
 | 
			
		||||
      // 单次遍历处理所有异常节点,避免多次map操作
 | 
			
		||||
      graphVis.nodes = graphVis.nodes.map((node) => {
 | 
			
		||||
        // 检查节点是否属于任何激活的异常组
 | 
			
		||||
        for (const config of activeConfigs) {
 | 
			
		||||
          const abnormalIds = abnormalData[config.groupKey]
 | 
			
		||||
          // 如果节点在异常组中,则设置对应的颜色
 | 
			
		||||
          if (abnormalIds && abnormalIds.includes(node.id)) {
 | 
			
		||||
            node.fillColor = config.color
 | 
			
		||||
            break // 一旦找到匹配的异常组,就不再检查其他组
 | 
			
		||||
          }
 | 
			
		||||
          return n
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      if (shouldB) {
 | 
			
		||||
        graphVis.nodes = graphVis.nodes.map((n) => {
 | 
			
		||||
          if (props.store.graphAbnormalData.groupB.includes(n.id)) {
 | 
			
		||||
            n.fillColor = yellowRed
 | 
			
		||||
          }
 | 
			
		||||
          return n
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      if (shouldC) {
 | 
			
		||||
        graphVis.nodes = graphVis.nodes.map((n) => {
 | 
			
		||||
          if (props.store.graphAbnormalData.groupC.includes(n.id)) {
 | 
			
		||||
            n.fillColor = blueRed
 | 
			
		||||
          }
 | 
			
		||||
          return n
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
        }
 | 
			
		||||
        return node
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    new Map([
 | 
			
		||||
| 
						 | 
				
			
			@ -259,11 +241,10 @@ const runDiffForceLayout = (layoutConfig, layoutType, isAsync) => {
 | 
			
		|||
      ["groupMember", () => handleGroupMemberDiff()],
 | 
			
		||||
      ["anomalousGroup", () => handleAnomalousGroup()]
 | 
			
		||||
    ]).get(storeId)?.()
 | 
			
		||||
    // isPlay.value = true
 | 
			
		||||
    graphVis.zoomFit() //场景视图大小自适应缩放
 | 
			
		||||
    isPlay.value = true
 | 
			
		||||
    graphVis.moveCenter(zoomSize.get(storeId)) //组件布局完毕后自动让整体关系图大小缩放到指定比例
 | 
			
		||||
  }
 | 
			
		||||
  graphVis.excuteLocalLayout(layoutType, layoutConfig, isAsync, () => handleLayoutSuccess())
 | 
			
		||||
  // graphVis.autoGroupLayout(graphVis.getGraphData())
 | 
			
		||||
  graphVis.excuteLocalLayout(layoutType, layoutConfig, isAsync, handleLayoutSuccess())
 | 
			
		||||
}
 | 
			
		||||
// 根据节点的cluster属性进行分组
 | 
			
		||||
const clusterAnalyze = () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -324,7 +305,7 @@ const createGraph = () => {
 | 
			
		|||
  }
 | 
			
		||||
  graphVis.setDragHideLine(false) //拖拽时隐藏连线
 | 
			
		||||
  graphVis.setShowDetailScale(0.1) //展示细节的比例
 | 
			
		||||
  graphVis.setZoomRange(0.1, 5) //缩放区间
 | 
			
		||||
  graphVis.setZoomRange(0.1, 10) //缩放区间
 | 
			
		||||
  registCustomePaintFunc(storeId) //注册自定义绘图方法
 | 
			
		||||
  registEvents()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -333,9 +314,21 @@ const initChart = () => {
 | 
			
		|||
  createGraph()
 | 
			
		||||
  graphVis.addGraph({ ...toRaw(graph.value) })
 | 
			
		||||
  clusterAnalyze()
 | 
			
		||||
  runDiffForceLayout({ strength: -500, ajustCluster: true }, "simulation", true)
 | 
			
		||||
  runDiffForceLayout({ strength: -300, ajustCluster: true }, "simulation", false)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  // 初始化时,如果有时间列表且当前没有图表数据,请求第一个时间切片的数据
 | 
			
		||||
  if (
 | 
			
		||||
    timeList.value.length > 0 &&
 | 
			
		||||
    (!graph.value || !graph.value.nodes || graph.value.nodes.length === 0)
 | 
			
		||||
  ) {
 | 
			
		||||
    const firstTime = timeList.value[0]
 | 
			
		||||
    const utcTime = convertToUtcIsoString(firstTime)
 | 
			
		||||
    emit("click:pointerDownAndSlide", utcTime)
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 添加更新图表的函数
 | 
			
		||||
const updateChart = (newGraphData) => {
 | 
			
		||||
  if (!graphVis) {
 | 
			
		||||
| 
						 | 
				
			
			@ -346,7 +339,7 @@ const updateChart = (newGraphData) => {
 | 
			
		|||
  graphVis.addGraph({ ...toRaw(newGraphData) })
 | 
			
		||||
  // 重新运行力导向布局
 | 
			
		||||
  clusterAnalyze()
 | 
			
		||||
  runDiffForceLayout({ strength: -500, ajustCluster: true }, "simulation", true)
 | 
			
		||||
  runDiffForceLayout({ strength: -300, ajustCluster: true }, "simulation", false)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -301,7 +301,7 @@ provide("statisticsList", characterHiddenStore.statisticsList)
 | 
			
		|||
  .top-container {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: vh(110);
 | 
			
		||||
    background-image: url(@/assets/images/linkPrediction/title/page-title.png);
 | 
			
		||||
    background-image: url("@/assets/images/head/hiddenTitle.png");
 | 
			
		||||
    background-repeat: no-repeat;
 | 
			
		||||
    background-size: 100% 100%;
 | 
			
		||||
    fill: linear-gradient(270deg, rgba(6, 61, 113, 0.1) 0%, rgba(8, 30, 56, 0.38) 100%);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -332,7 +332,7 @@ provide("statisticsList", socialGroupsStore.statisticsList)
 | 
			
		|||
  .top-container {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: vh(110);
 | 
			
		||||
    background-image: url(@/assets/images/linkPrediction/title/page-title.png);
 | 
			
		||||
    background-image: url("@/assets/images/head/linkedPredictionStruct.png");
 | 
			
		||||
    background-repeat: no-repeat;
 | 
			
		||||
    background-size: 100% 100%;
 | 
			
		||||
    fill: linear-gradient(270deg, rgba(6, 61, 113, 0.1) 0%, rgba(8, 30, 56, 0.38) 100%);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user