This commit is contained in:
qumeng039@126.com 2025-07-29 16:47:08 +08:00
commit 2ea11cfd03
11 changed files with 287 additions and 69 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 B

After

Width:  |  Height:  |  Size: 148 B

View File

@ -40,7 +40,16 @@ export function getInteractionPostList(userGroupId) {
//社交紧密团体识别的贴文列表 //社交紧密团体识别的贴文列表
export function getSocialPostList(outoIncrement) { export function getSocialPostList(outoIncrement) {
return http.get(`/linkPrediction/social/post_list?page=${outoIncrement}`) return http.get(`/linkPrediction/triangle/post_list?page=${outoIncrement}`)
}
// 社交紧密团体的对应紧密关系的帖文--根据relationId来查找
export function getSocialPostListByRelationId(relationId){
return http.get(`/linkPrediction/user_posts_list?relationId=${relationId}`)
}
// 社交紧密团体识别的社团统计
export function getSocialCommunityStatistics() {
return http.get(`linkPrediction/triangle/community_statistics`)
} }
// 社交紧密团体的社团列表数据 // 社交紧密团体的社团列表数据
@ -49,8 +58,8 @@ export function getSocialCommunityList() {
} }
// 社交紧密团体的社团内部节点 // 社交紧密团体的社团内部节点
export function getSocialCommunityDetailNodes(ids) { export function getSocialCommunityDetailNodes(ids, time = "2024-05-16 16:56:04") {
return http.get(`/linkPrediction/triangle/community_detail?groupIds=${ids}`) return http.get(`/linkPrediction/triangle/community_detail?groupIds=${ids}&dateTime=${time}`)
} }
//人物社交隐关系预测用户组列表 //人物社交隐关系预测用户组列表

View File

@ -10,7 +10,9 @@ import {
getInteractionCommunityDetailNodes, getInteractionCommunityDetailNodes,
getInteractionCommunityStatistics, getInteractionCommunityStatistics,
getSocialCommunityList, getSocialCommunityList,
getSocialCommunityDetailNodes getSocialCommunityStatistics,
getSocialCommunityDetailNodes,
getSocialPostListByRelationId
} from "@/service/api/linkPrediction" } from "@/service/api/linkPrediction"
import defaultAvatar from "@/assets/images/avatar/default.png" import defaultAvatar from "@/assets/images/avatar/default.png"
@ -197,10 +199,18 @@ export const useSocialGroupsStore = defineStore("socialGroups", {
state: () => ({ state: () => ({
userList: [], userList: [],
communityNodeList: [], communityNodeList: [],
curComponent: "CommunityNode",
curSelecedGroupIds: [],
communityDetailNodeList: [],
statisticsList: [ statisticsList: [
{ id: 1, icon: nodePrefix, name: "节点数", key: "nodesCount" }, { id: 1, icon: nodePrefix, name: "节点数", key: "nodesCount" },
{ id: 2, icon: communityPrefix, name: "社团数", key: "groupCount" }, { id: 2, icon: communityPrefix, name: "社团数", key: "groupCount" },
{ id: 3, icon: tightCommunityPrefix, name: "紧密团体数", key: "tightCommunityCount" } { id: 3, icon: tightCommunityPrefix, name: "紧密团体数", key: "hiddenInteractionCount" }
],
statisticsDetailList: [
{ id: 1, icon: nodePrefix, name: "节点数", key: "nodesCount" },
{ id: 2, icon: communityPrefix, name: "社团数", key: "groupCount" },
{ id: 3, icon: tightCommunityPrefix, name: "紧密团体数", key: "hiddenInteractionCount" }
], ],
userChartList: [ userChartList: [
{ {
@ -394,24 +404,47 @@ export const useSocialGroupsStore = defineStore("socialGroups", {
this.posts = res.data this.posts = res.data
} }
}, },
// 获取对应用户组的postList
async getSocialGroupPostListByRelationId(relationId){
const res = await getSocialPostListByRelationId(relationId)
if (res.code != 200) return
// console.log("打印对应relationId的帖文列表", res.data)
this.posts = res.data
},
async initGraphCommunityNode() { async initGraphCommunityNode() {
const res = await getSocialCommunityList() const res = await getSocialCommunityList()
if (res.code != 200) return if (res.code != 200) return
this.communityNodeList = res.data this.communityNodeList = res.data
}, },
// 初始化statisticsList // 初始化statisticsList
initStatisticsList() { /* initStatisticsList() {
this.statisticsList = [ this.statisticsList = [
{ id: 1, icon: nodePrefix, name: "用户数", key: "userCount", count: 1000 }, { id: 1, icon: nodePrefix, name: "节点数", key: "nodesCount", count: 1000 },
{ id: 2, icon: communityPrefix, name: "社团数", key: "groupCount", count: 1000 }, { id: 2, icon: communityPrefix, name: "社团数", key: "groupCount", count: 1000 },
{ id: 3, icon: tightCommunityPrefix, name: "紧密团体数", key: "tightCommunityCount", count: 1000 } { id: 3, icon: tightCommunityPrefix, name: "紧密团体数", key: "hiddenInteractionCount", count: 1000 }
] ]
}, */
// 社交紧密团体识别的社团统计
async initGraphStatistics() {
const res = await getSocialCommunityStatistics()
this.statisticsList = this.statisticsList.map((item) => ({
...item,
count: res.data[item.key]
}))
}, },
// 传递社交团体的数组,获取其详情 // 传递社交团体的数组,获取其详情
async initGraphCommunityDetailNode(ids) { async initGraphCommunityDetailNode(ids, time = "2024-05-16 16:56:04") {
const res = await getSocialCommunityDetailNodes(ids) this.curSelecedGroupIds = ids
const res = await getSocialCommunityDetailNodes(ids, time)
if (res.code != 200) return if (res.code != 200) return
return res.data this.statisticsDetailList = this.statisticsDetailList.map((item) => ({
...item,
count: res.data.communityStatistics[item.key]
}))
console.log("打印社交团体详情:");
console.log(res.data);
this.communityDetailNodeList = res.data.userRelation
} }
}, },
persist: true // 开启持久化 persist: true // 开启持久化

View File

@ -62,9 +62,9 @@ const initChart = async () => {
const data = { nodes, links } const data = { nodes, links }
const categories = [ const categories = [
{ name: "普通社团", category: 0 }, { name: "普通社团", category: 0, icon: `image://${new URL('@/assets/images/linkPrediction/icon/node-legend-icon.png', import.meta.url)}`},
{ name: "含预测节点社团", category: 1 }, { name: "含预测节点社团", category: 1, icon: `image://${new URL('@/assets/images/linkPrediction/icon/community-legend-icon.png', import.meta.url)}`},
{ name: "紧密团体关系", category: 2 } { name: "紧密团体关系", category: 2, icon: `image://${new URL('@/assets/images/linkPrediction/icon/tight-community-legend-icon.png', import.meta.url)}`}
] ]
const option = { const option = {
// //
@ -92,11 +92,7 @@ const initChart = async () => {
{ offset: 1, color: "#fbc2eb" } { offset: 1, color: "#fbc2eb" }
]) ])
}, },
icon: c.category === 2 icon: c.icon
? `image://${new URL('@/assets/images/linkPrediction/icon/tight-community-legend-icon.png', import.meta.url)}`
: c.category === 0
? `image://${new URL('@/assets/images/linkPrediction/icon/node-legend-icon.png', import.meta.url)}`
: `image://${new URL('@/assets/images/linkPrediction/icon/community-legend-icon.png', import.meta.url)}`,
})), })),
right: 15, right: 15,
bottom: 10, bottom: 10,

View File

@ -3,51 +3,146 @@
<img src="@/assets/images/icon/goback.png" alt="" class="goback" @click="handleGoback" /> <img src="@/assets/images/icon/goback.png" alt="" class="goback" @click="handleGoback" />
<div class="graph-container" id="container"></div> <div class="graph-container" id="container"></div>
<div class="statistic-container"> <div class="statistic-container">
<div class="statistics-item" v-for="item in statisticsList" :key="item.id"> <div
class="statistics-item"
v-for="item in socialGroupsStore.statisticsDetailList"
:key="item.id"
>
<img :src="item.icon" class="icon" /> <img :src="item.icon" class="icon" />
<div class="name">{{ item.name }}:&nbsp;</div> <div class="name">{{ item.name }}:&nbsp;</div>
<div class="count">{{ item.count }}</div> <div class="count">{{ item.count }}</div>
</div> </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> </div>
</template> </template>
<script setup> <script setup>
import nodePrefix from "@/assets/images/linkPrediction/icon/node-count-prefix.png" import { defineEmits, onMounted, ref, onUnmounted, computed, watch } from "vue"
import communityPrefix from "@/assets/images/linkPrediction/icon/community-count-prefix.png" import { TansTimestamp } from "@/utils/transform"
import tightCommunityPrefix from "@/assets/images/linkPrediction/icon/tightCommunityPrefix.png"
import { defineProps, defineEmits, onMounted, ref } from "vue"
import nodeHoverImg from "@/assets/images/nodeHover.png" import nodeHoverImg from "@/assets/images/nodeHover.png"
import * as echarts from "echarts" import * as echarts from "echarts"
const statisticsList = [ import { storeToRefs } from "pinia"
{ id: 1, icon: nodePrefix, name: "节点数", key: "nodesCount", count: 3000 }, import { useSocialGroupsStore } from "@/store/llinkPrediction/index"
{ id: 2, icon: communityPrefix, name: "社团数", key: "groupCount", count: 1000 }, const socialGroupsStore = useSocialGroupsStore()
{ id: 3, icon: tightCommunityPrefix, name: "紧密团体数", key: "tightCommunityCount", count: 1000 } const { communityDetailNodeList } = storeToRefs(socialGroupsStore)
]
const props = defineProps({
detailNode: {
type: Array,
default: () => []
}
})
const curDetailNode = ref(props.detailNode)
const emit = defineEmits(["click:goback"]) const emit = defineEmits(["click:goback"])
const handleGoback = () => { const handleGoback = () => {
emit("click:goback", "CommunityNode") 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")
socialGroupsStore.initGraphCommunityDetailNode(socialGroupsStore.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 () => { const initChart = async () => {
chart = echarts.init(document.getElementById("container")) chart = echarts.init(document.getElementById("container"))
const links = [] const links = []
const nodes = [] const nodes = []
if (!Object.keys(curDetailNode.value).length) return if (!Object.keys(socialGroupsStore.communityDetailNodeList).length) return
Object.entries(curDetailNode.value).forEach(([parentId, children]) => { Object.entries(socialGroupsStore.communityDetailNodeList).forEach(([parentId, children]) => {
nodes.push({ nodes.push({
id: `parent_${parentId}`, id: `parent_${parentId}`,
name: parentId name: parentId
@ -67,10 +162,10 @@ const initChart = async () => {
const data = { links, nodes } const data = { links, nodes }
const categories = [ const categories = [
{ name: "事件活跃者", category: 0 }, { name: "事件活跃者", category: 0, icon: `image://${new URL('@/assets/images/linkPrediction/icon/event-activist-legend-icon.png', import.meta.url)}` },
{ name: "信息发布者", category: 1 }, { name: "信息发布者", category: 1, icon: `image://${new URL('@/assets/images/linkPrediction/icon/information-publisher-legend-icon.png', import.meta.url)}` },
{ name: "互动关系", category: 2 }, { name: "互动关系", category: 2, icon: `image://${new URL('@/assets/images/linkPrediction/icon/interactive-relationship-legend-icon.png', import.meta.url)}` },
{ name: "互动隐关系", category: 3 } { name: "紧密团体关系", category: 3, icon: `image://${new URL('@/assets/images/linkPrediction/icon/tight-community-legend-icon.png', import.meta.url)}` }
] ]
const option = { const option = {
// //
@ -177,11 +272,10 @@ const initChart = async () => {
roam: true, roam: true,
zoom: 0.3, zoom: 0.3,
categories: categories, categories: categories,
force: { force: {
edgeLength: 2500, edgeLength: 2500,
repulsion: 4000, repulsion: 4000,
gravity: 0.4, gravity: 0.1,
friction: 0.02, friction: 0.02,
coolingFactor: 0.1 coolingFactor: 0.1
}, },
@ -226,6 +320,7 @@ const initChart = async () => {
} }
onMounted(() => { onMounted(() => {
console.log("statisticsDetailList", socialGroupsStore.statisticsDetailList)
initChart() initChart()
}) })
</script> </script>
@ -246,7 +341,7 @@ onMounted(() => {
height: 93%; height: 93%;
} }
.statistic-container { .statistic-container {
width: 400px; width: 378px;
height: 42px; height: 42px;
flex-shrink: 0; flex-shrink: 0;
border-radius: 4px; border-radius: 4px;
@ -299,6 +394,95 @@ onMounted(() => {
bottom: 50px; bottom: 50px;
border-radius: 4px; border-radius: 4px;
z-index: 1; 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> </style>

View File

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

View File

@ -59,6 +59,8 @@ const props = defineProps({
const handleUserItem = (index, group = {}) => { const handleUserItem = (index, group = {}) => {
curUserGroupIndex.value = index; curUserGroupIndex.value = index;
// console.log("item",index);
// console.log("item",group);
emit("click:selectedGroup", group); emit("click:selectedGroup", group);
}; };
</script> </script>

View File

@ -84,7 +84,10 @@ const postDialog = ref(false);
const currentPostPost = ref(null); const currentPostPost = ref(null);
const handleSelectedUserGroup = (group) => { const handleSelectedUserGroup = (group) => {
console.log(group); socialGroupsStore.curComponent = "detailkNode"
const groupIds = group?.list.map((item)=>item.groupId)
socialGroupsStore.initGraphCommunityDetailNode(groupIds)
socialGroupsStore.getSocialGroupPostListByRelationId(group?.relationId)
}; };
const handleOpenPostDialog = (post) => { const handleOpenPostDialog = (post) => {
@ -98,9 +101,9 @@ const handleTouchButtom = (outoIncrement) => {
onMounted(() => { onMounted(() => {
socialGroupsStore.initGroupList(); socialGroupsStore.initGroupList();
socialGroupsStore.initPostList(0); socialGroupsStore.initPostList(1);
socialGroupsStore.initGraphCommunityNode(); socialGroupsStore.initGraphCommunityNode();
socialGroupsStore.initStatisticsList(); socialGroupsStore.initGraphStatistics();
}); });
provide("communityNodeList", socialGroupsStore.communityNodeList); provide("communityNodeList", socialGroupsStore.communityNodeList);