This commit is contained in:
duanhao 2025-07-29 12:14:05 +08:00
commit 4ae6698aae
8 changed files with 247 additions and 54 deletions

9
package-lock.json generated
View File

@ -14,6 +14,8 @@
"dayjs": "^1.11.13",
"echarts": "^5.6.0",
"element-plus": "^2.10.1",
"loadsh": "^0.0.4",
"lodash-es": "^4.17.21",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.3.0",
"vue": "^3.5.13",
@ -3131,6 +3133,13 @@
"node": ">=6.11.5"
}
},
"node_modules/loadsh": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/loadsh/-/loadsh-0.0.4.tgz",
"integrity": "sha512-U+wLL8InpfRalWrr+0SuhWgGt10M4OyAk6G8xCYo2rwpiHtxZkWiFpjei0vO463ghW8LPCdhqQxXlMy2qicAEw==",
"deprecated": "This is a typosquat on the popular Lodash package. This is not maintained nor is the original Lodash package.",
"license": "MIT"
},
"node_modules/local-pkg": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz",

View File

@ -15,6 +15,8 @@
"dayjs": "^1.11.13",
"echarts": "^5.6.0",
"element-plus": "^2.10.1",
"loadsh": "^0.0.4",
"lodash-es": "^4.17.21",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.3.0",
"vue": "^3.5.13",

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B

View File

@ -11,8 +11,8 @@ export function getInteractionCommunityNodes() {
}
//人物互动隐关系预测的社团内部节点
export function getInteractionCommunityDetailNodes(ids) {
return http.get(`linkPrediction/interaction/community_detail?groupIds=${ids}`)
export function getInteractionCommunityDetailNodes(ids, time = "2024.05.16 16:56:04") {
return http.get(`linkPrediction/interaction/community_detail?groupIds=${ids}&dateTime=${time}`)
}
//人物互动隐关系预测的社团统计

View File

@ -26,9 +26,10 @@ export const useCharacterInteractionStore = defineStore("characterInteraction",
userChartList: [], //组相关性
posts: [],
communityNodeList: [],
curComponent: "CommunityNode",
curSelecedGroupIds: [],
communityDetailNodeList: [],
curSelectedGroup: [],
anlysisList: [
{
id: 1,
@ -142,6 +143,11 @@ export const useCharacterInteractionStore = defineStore("characterInteraction",
{ id: 1, icon: nodePrefix, name: "节点数", key: "nodesCount" },
{ id: 2, icon: communityPrefix, name: "社团数", key: "groupCount" },
{ id: 3, icon: hiddenPrefix, name: "隐关系数", key: "hiddenInteractionCount" }
],
statisticsDetailList: [
{ id: 1, icon: nodePrefix, name: "节点数", key: "nodesCount" },
{ id: 2, icon: communityPrefix, name: "社团数", key: "groupCount" },
{ id: 3, icon: hiddenPrefix, name: "隐关系数", key: "hiddenInteractionCount" }
]
}),
actions: {
@ -170,10 +176,15 @@ export const useCharacterInteractionStore = defineStore("characterInteraction",
if (res.code != 200) return
this.communityNodeList = res.data
},
async initGraphCommunityDetailNode(ids) {
const res = await getInteractionCommunityDetailNodes(ids)
async initGraphCommunityDetailNode(ids, time = "2024.05.16 16:56:04") {
this.curSelecedGroupIds = ids
const res = await getInteractionCommunityDetailNodes(ids, time)
if (res.code != 200) return
return res.data
this.statisticsDetailList = this.statisticsDetailList.map((item) => ({
...item,
count: res.data.communityStatistics[item.key]
}))
this.communityDetailNodeList = res.data.userRelation
},
async initGraphStatistics() {
const res = await getInteractionCommunityStatistics()

View File

@ -83,6 +83,7 @@ const postDialog = ref(false)
const currentPostPost = ref(null)
const handleSelectedUserGroup = (group) => {
interactionStore.curComponent = "detailNode"
console.log(group)
}

View File

@ -3,51 +3,146 @@
<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 statisticsList" :key="item.id">
<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"></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>
<div class="active-sign" :style="{ left: `${currentPosition}px` }">
<div class="active-needle"></div>
<div class="timeLine-point" @pointerdown.stop="handlePointPointerDown"></div>
</div>
</div>
<div class="time">{{ TansTimestamp(endTime, "YYYY.MM.DD HH:mm:ss") }}</div>
</div>
<div class="current-time-display">
<span>当前时间: {{ TansTimestamp(currentTime, "YYYY.MM.DD HH:mm:ss") }}</span>
</div>
</div>
</template>
<script setup>
import nodePrefix from "@/assets/images/linkPrediction/icon/node-count-prefix.png"
import communityPrefix from "@/assets/images/linkPrediction/icon/community-count-prefix.png"
import hiddenPrefix from "@/assets/images/linkPrediction/icon/hidden-count-prefix.png"
import { defineProps, defineEmits, onMounted, ref } from "vue"
import { defineEmits, onMounted, ref, onUnmounted, computed, watch } from "vue"
import { TansTimestamp } from "@/utils/transform"
import nodeHoverImg from "@/assets/images/nodeHover.png"
import * as echarts from "echarts"
const statisticsList = [
{ id: 1, icon: nodePrefix, name: "节点数", key: "nodesCount", count: 3000 },
{ id: 2, icon: communityPrefix, name: "社团数", key: "groupCount", count: 1000 },
{ id: 3, icon: hiddenPrefix, name: "隐关系数", key: "hiddenInteractionCount", count: 1000 }
]
const props = defineProps({
detailNode: {
type: Array,
default: () => []
}
})
const curDetailNode = ref(props.detailNode)
import { storeToRefs } from "pinia"
import { useCharacterInteractionStore } from "@/store/llinkPrediction/index"
const interactionStore = useCharacterInteractionStore()
const { communityDetailNodeList } = storeToRefs(interactionStore)
const emit = defineEmits(["click:goback"])
const handleGoback = () => {
emit("click:goback", "CommunityNode")
}
let chart = null
//communityDetailNodeList
watch(
communityDetailNodeList,
(newValue) => {
initChart()
},
{ deep: 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 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 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
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")
interactionStore.initGraphCommunityDetailNode(interactionStore.curSelecedGroupIds, currentTimes)
}
//
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")
console.log("拖动结束,当前时间:", currentTimes)
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", () => {})
})
let chart = null
const initChart = async () => {
chart = echarts.init(document.getElementById("container"))
const links = []
const nodes = []
if (!Object.keys(curDetailNode.value).length) return
Object.entries(curDetailNode.value).forEach(([parentId, children]) => {
if (!Object.keys(interactionStore.communityDetailNodeList).length) return
Object.entries(interactionStore.communityDetailNodeList).forEach(([parentId, children]) => {
nodes.push({
id: `parent_${parentId}`,
name: parentId
@ -177,11 +272,10 @@ const initChart = async () => {
roam: true,
zoom: 0.3,
categories: categories,
force: {
edgeLength: 2500,
repulsion: 4000,
gravity: 0.4,
gravity: 0.1,
friction: 0.02,
coolingFactor: 0.1
},
@ -299,6 +393,95 @@ onMounted(() => {
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;
}
.active-sign {
position: relative;
z-index: 2;
.active-needle {
width: 30px;
height: 34px;
background-image: url("@/assets/images/point.png");
background-size: cover;
bottom: 1px;
left: -6px;
position: absolute;
}
.timeLine-point {
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;
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>

View File

@ -3,26 +3,20 @@
<img :src="title" alt="" class="title" />
<CommunityNode
v-if="curComponent == 'CommunityNode'"
v-if="interactionStore.curComponent == 'CommunityNode'"
@click:node="handleClickNode"
@click:edge="handleClickEdge"
></CommunityNode>
<DetailNode
v-else
@click:goback="handleClickGoBack"
:detailNode="curSelectedGroup"
></DetailNode>
<DetailNode v-else @click:goback="handleClickGoBack"></DetailNode>
</div>
</template>
<script setup>
import { defineProps, ref } from "vue"
import { defineProps } from "vue"
import CommunityNode from "./communityNode.vue"
import DetailNode from "./detailNode.vue"
import { useCharacterInteractionStore } from "@/store/llinkPrediction/index"
const interactionStore = useCharacterInteractionStore()
const curComponent = ref("CommunityNode")
const curSelectedGroup = ref(null)
const props = defineProps({
title: {
type: String,
@ -31,23 +25,17 @@ const props = defineProps({
})
const handleClickNode = async (nodeInfo) => {
const data = await interactionStore.initGraphCommunityDetailNode([nodeInfo.id])
curSelectedGroup.value = data
curComponent.value = "detailNode"
interactionStore.initGraphCommunityDetailNode([nodeInfo.id])
interactionStore.curComponent = "detailNode"
}
const handleClickEdge = async (edgeInfo) => {
curComponent.value = "detailNode"
const data = await interactionStore.initGraphCommunityDetailNode([
edgeInfo.source,
edgeInfo.target
])
curSelectedGroup.value = data
const handleClickEdge = (edgeInfo) => {
interactionStore.curComponent = "detailNode"
interactionStore.initGraphCommunityDetailNode([edgeInfo.source, edgeInfo.target])
}
const handleClickGoBack = (currentComponentName) => {
curComponent.value = currentComponentName
console.log(currentComponentName)
interactionStore.curComponent = currentComponentName
}
</script>