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) {
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) {
return http.get(`/linkPrediction/triangle/community_detail?groupIds=${ids}`)
export function getSocialCommunityDetailNodes(ids, time = "2024-05-16 16:56:04") {
return http.get(`/linkPrediction/triangle/community_detail?groupIds=${ids}&dateTime=${time}`)
}
//人物社交隐关系预测用户组列表

View File

@ -10,7 +10,9 @@ import {
getInteractionCommunityDetailNodes,
getInteractionCommunityStatistics,
getSocialCommunityList,
getSocialCommunityDetailNodes
getSocialCommunityStatistics,
getSocialCommunityDetailNodes,
getSocialPostListByRelationId
} from "@/service/api/linkPrediction"
import defaultAvatar from "@/assets/images/avatar/default.png"
@ -197,10 +199,18 @@ export const useSocialGroupsStore = defineStore("socialGroups", {
state: () => ({
userList: [],
communityNodeList: [],
curComponent: "CommunityNode",
curSelecedGroupIds: [],
communityDetailNodeList: [],
statisticsList: [
{ id: 1, icon: nodePrefix, name: "节点数", key: "nodesCount" },
{ 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: [
{
@ -394,24 +404,47 @@ export const useSocialGroupsStore = defineStore("socialGroups", {
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() {
const res = await getSocialCommunityList()
if (res.code != 200) return
this.communityNodeList = res.data
},
// 初始化statisticsList
initStatisticsList() {
/* initStatisticsList() {
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: 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) {
const res = await getSocialCommunityDetailNodes(ids)
async initGraphCommunityDetailNode(ids, time = "2024-05-16 16:56:04") {
this.curSelecedGroupIds = ids
const res = await getSocialCommunityDetailNodes(ids, time)
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 // 开启持久化

View File

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

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 socialGroupsStore.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 tightCommunityPrefix from "@/assets/images/linkPrediction/icon/tightCommunityPrefix.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: tightCommunityPrefix, name: "紧密团体数", key: "tightCommunityCount", count: 1000 }
]
const props = defineProps({
detailNode: {
type: Array,
default: () => []
}
})
const curDetailNode = ref(props.detailNode)
import { storeToRefs } from "pinia"
import { useSocialGroupsStore } from "@/store/llinkPrediction/index"
const socialGroupsStore = useSocialGroupsStore()
const { communityDetailNodeList } = storeToRefs(socialGroupsStore)
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")
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 () => {
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(socialGroupsStore.communityDetailNodeList).length) return
Object.entries(socialGroupsStore.communityDetailNodeList).forEach(([parentId, children]) => {
nodes.push({
id: `parent_${parentId}`,
name: parentId
@ -67,10 +162,10 @@ const initChart = async () => {
const data = { links, nodes }
const categories = [
{ name: "事件活跃者", category: 0 },
{ name: "信息发布者", category: 1 },
{ name: "互动关系", category: 2 },
{ name: "互动隐关系", category: 3 }
{ name: "事件活跃者", category: 0, icon: `image://${new URL('@/assets/images/linkPrediction/icon/event-activist-legend-icon.png', import.meta.url)}` },
{ name: "信息发布者", category: 1, icon: `image://${new URL('@/assets/images/linkPrediction/icon/information-publisher-legend-icon.png', import.meta.url)}` },
{ name: "互动关系", category: 2, icon: `image://${new URL('@/assets/images/linkPrediction/icon/interactive-relationship-legend-icon.png', import.meta.url)}` },
{ name: "紧密团体关系", category: 3, icon: `image://${new URL('@/assets/images/linkPrediction/icon/tight-community-legend-icon.png', import.meta.url)}` }
]
const option = {
//
@ -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
},
@ -226,6 +320,7 @@ const initChart = async () => {
}
onMounted(() => {
console.log("statisticsDetailList", socialGroupsStore.statisticsDetailList)
initChart()
})
</script>
@ -246,7 +341,7 @@ onMounted(() => {
height: 93%;
}
.statistic-container {
width: 400px;
width: 378px;
height: 42px;
flex-shrink: 0;
border-radius: 4px;
@ -299,6 +394,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,23 @@
<img :src="title" alt="" class="title" />
<CommunityNode
v-if="curComponent == 'CommunityNode'"
v-if="socialGroupsStore.curComponent == 'CommunityNode'"
@click:node="handleClickNode"
@click:edge="handleClickEdge"
></CommunityNode>
<DetailNode
v-else
@click:goback="handleClickGoBack"
:detailNode="curSelectedGroup"
></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 { useSocialGroupsStore } from "@/store/llinkPrediction/index"
const socialGroupsStore = useSocialGroupsStore()
const curComponent = ref("CommunityNode")
const curSelectedGroup = ref(null)
const props = defineProps({
title: {
type: String,
@ -31,24 +28,18 @@ const props = defineProps({
})
const handleClickNode = async (nodeInfo) => {
const data = await socialGroupsStore.initGraphCommunityDetailNode([nodeInfo.id])
console.log(data);
curSelectedGroup.value = data
curComponent.value = "detailNode"
//
socialGroupsStore.initGraphCommunityDetailNode([nodeInfo.id])
socialGroupsStore.curComponent = "detailNode"
}
const handleClickEdge = async (edgeInfo) => {
curComponent.value = "detailNode"
const data = await socialGroupsStore.initGraphCommunityDetailNode([
edgeInfo.source,
edgeInfo.target
])
curSelectedGroup.value = data
socialGroupsStore.curComponent = "detailNode"
socialGroupsStore.initGraphCommunityDetailNode([edgeInfo.source, edgeInfo.target])
}
const handleClickGoBack = (currentComponentName) => {
curComponent.value = currentComponentName
console.log(currentComponentName)
socialGroupsStore.curComponent = currentComponentName
}
</script>

View File

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

View File

@ -84,7 +84,10 @@ const postDialog = ref(false);
const currentPostPost = ref(null);
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) => {
@ -98,9 +101,9 @@ const handleTouchButtom = (outoIncrement) => {
onMounted(() => {
socialGroupsStore.initGroupList();
socialGroupsStore.initPostList(0);
socialGroupsStore.initPostList(1);
socialGroupsStore.initGraphCommunityNode();
socialGroupsStore.initStatisticsList();
socialGroupsStore.initGraphStatistics();
});
provide("communityNodeList", socialGroupsStore.communityNodeList);