桥梁二级网络

This commit is contained in:
duanhao 2025-07-25 15:26:19 +08:00
parent 402337707a
commit 556d774e87
8 changed files with 12709 additions and 3 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

12208
src/assets/json/group.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -46,12 +46,16 @@
<div class="chart-container">
<DynamicGraph
ref="leaderGraphRef"
v-if="false"
v-show="false"
:timestamp="store.activeTimePoint"
:allLeaderData="store.allLeaderData"
@handle:openDialog="handleGraphNodeClick"
/>
<BridgeCommunityGraph v-if="true" :timestamp="store.activeTimePoint" ref="bridgeGraphRef"/>
<BridgeCommunityGraph ref="bridgeGraphRef" v-if="currentComponent == 'BridgeCommunityNode'" :timestamp="store.activeTimePoint" @click:navigateToCommunityDetail="handleClickCommunity"/>
<DetailCommunityGraph v-else ref="detailCommunityGraphRef"
:communityId="currentSelectedCommunityId"
@click:goback="handleClickGoBack"
/>
</div>
<div class="timeline-container">
<span class="time-label">2023.10.07 00:00:00</span>
@ -99,16 +103,31 @@ import { ref, defineExpose, watch, computed } from 'vue';
import { useKeyNodeStore2 } from '@/store/keyNodeStore2';
import DynamicGraph from "./graph/dynamicGraph.vue";
import BridgeCommunityGraph from './graph/bridgeCommunityGraph.vue';
import DetailCommunityGraph from './graph/detailCommunityGraph.vue';
const store = useKeyNodeStore2();
const leaderGraphRef = ref(null);
// BridgeCommunityGraph
const bridgeGraphRef = ref(null);
// --BridgeCommunityGraph
const currentComponent = ref("BridgeCommunityNode");
const currentSelectedCommunityId = ref(null);
const handleGraphNodeClick = (leaderData) => {
store.openLeaderDetail(leaderData);
};
const handleClickCommunity = (communityId) => {
if(communityId) {
currentComponent.value = "DetailCommunityNode";
currentSelectedCommunityId.value = communityId;
}
}
const handleClickGoBack = (currentComponentName) => {
currentComponent.value = currentComponentName;
};
const highlightNode = (leaderId) => {
if (leaderGraphRef.value) {
leaderGraphRef.value.highlightNode(leaderId);

View File

@ -3,13 +3,14 @@
</template>
<script setup>
import { onMounted, onUnmounted, defineProps, watch, ref } from 'vue';
import { onMounted, onUnmounted, defineProps, watch, ref, defineEmits } from 'vue';
import * as echarts from 'echarts';
import bridgeData from '@/assets/json/bridge_neighbors_communities.json';
import bridgeNodeHoverBgImg from '@/assets/images/bridge_node_hoverbgimg.png'
import { useKeyNodeStore2 } from '@/store/keyNodeStore2'; // store
import { cropToCircleAsync } from "@/utils/transform"; //
// timestamp
const props = defineProps({
timestamp: {
type: Number,
@ -17,6 +18,9 @@ const props = defineProps({
}
})
// --
const emit = defineEmits(['click:navigateToCommunityDetail']);
let chartInstance = null;
// ID
@ -180,6 +184,13 @@ const initChart = async() => {
id: leader.id
});
}
} else if(params.data && params.data.category === 1) {
//
// ID
const communityId = params.data.id.split('_')[1];
console.log("点击社团节点:", communityId);
//
emit('click:navigateToCommunityDetail', communityId);
}
});

View File

@ -0,0 +1,468 @@
<template>
<div class="detailCommunityGraph-component">
<img src="@/assets/images/icon/goback.png" alt="" class="goback" @click="handleGoback" />
<!-- 左下方信息框 -->
<div class="info-box">
<div class="info-item">
<img src="@/assets/images/icon/node_nums.png" alt="">
<span class="label">节点数:</span>
<span class="value">{{ totalNodes }}</span>
</div>
<div class="info-item">
<img src="@/assets/images/icon/bridge_node_nums.png" alt="">
<span class="label">桥梁节点数:</span>
<span class="value">{{ bridgeNodeCount }}</span>
</div>
</div>
<div class="container" ref="detailContainer" id="container"></div>
</div>
</template>
<script setup>
import { defineEmits, defineProps, ref, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';
import { useKeyNodeStore2 } from '@/store/keyNodeStore2';
import groupData from '@/assets/json/group.json';
import nodeHoverImg from '@/assets/images/bridge_node_hoverbgimg.png'
import { cropToCircleAsync } from "@/utils/transform"; //
const props = defineProps({
communityId: {
type: String,
default: ""
}
});
const emit = defineEmits(["click:goback"]);
const detailContainer = ref(null);
let chart = null;
const keyNodeStore2 = useKeyNodeStore2();
const activeNodeId = ref('');
//
const nodesData = ref([]);
//
const totalNodes = ref(0);
//
const bridgeNodeCount = ref(0);
//
const initBridgeNodeData = async () => {
//
const bridgeNodes = keyNodeStore2.bridgeNodes;
//
for (const node of bridgeNodes) {
if (node.defImg) {
node.defImg = await cropToCircleAsync(node.defImg);
}
if (node.activeImg) {
node.activeImg = await cropToCircleAsync(node.activeImg);
}
}
return bridgeNodes;
};
//
const initChart = async () => {
if (chart) {
chart.dispose();
}
chart = echarts.init(detailContainer.value);
// ->
chart.on('click', function(params){
//
if(params.data && params.data.category === 0) {
// id
const nodeId = params.data.originalId;
//
const leader = keyNodeStore2.allLeaderData.find(l => l.nodeId === nodeId)
if(leader) {
// leader
keyNodeStore2.openLeaderDetail({
id: leader.id
})
}
}
})
//
let communityData = null;
// communityId
if (groupData.hasOwnProperty(props.communityId)) {
communityData = groupData[props.communityId];
} else {
const numericId = Number(props.communityId);
if (!isNaN(numericId) && groupData.hasOwnProperty(numericId)) {
communityData = groupData[numericId];
}
}
if (!communityData || !communityData.user_ids) {
chart.setOption({
title: {
text: '无数据',
left: 'center',
top: 'center'
}
});
return;
}
//
const bridgeNodes = await initBridgeNodeData();
// ID
const bridgeNodeIds = new Set(
bridgeNodes.map(node => String(node.Node))
);
//
const bridgeNodeImages = new Map(
bridgeNodes.map(node => [
String(node.Node),
{ defImg: node.defImg, activeImg: node.activeImg }
])
);
//
const userNodes = communityData.user_ids.map(userId => {
const userIdStr = String(userId);
const isBridgeNode = bridgeNodeIds.has(userIdStr);
let symbol = 'circle';
let symbolSize = 40;
let userName = userIdStr; // userNameid
let followerCount = '';
let followers = '';
let mediaCategory = ''
if (isBridgeNode) {
const images = bridgeNodeImages.get(userIdStr);
symbol = images.defImg ? `image://${images.defImg}` : 'circle';
symbolSize = 60;
// leader
const leaderData = keyNodeStore2.allLeaderData.find(leader => String(leader.nodeId) === userIdStr);
if (leaderData) {
userName = leaderData.name;
followerCount = leaderData.followerCount;
followers = leaderData.followers;
mediaCategory = leaderData.category
}
}
return {
id: userIdStr,
userName: userName,
// name: ` ${userId}`,
symbolSize: symbolSize,
symbol: symbol,
category: isBridgeNode ? 0 : 1,
originalId: userIdStr, // ID
...(isBridgeNode && {
followerCount: followerCount,
followers: followers,
mediaCategory: mediaCategory
})
};
});
//
totalNodes.value = userNodes.length;
bridgeNodeCount.value = userNodes.filter(node => node.category === 0).length;
//
nodesData.value = userNodes;
const categories = [
{name: "桥梁节点", category: 0},
{name: "普通节点", category: 1}
]
//
const option = {
//
legend: [{
data: categories.map( c => ({
name: c.name,
icon: c.category === 0
? `image://${new URL('@/assets/images/icon/bridge_node_legend.png', import.meta.url)}`
: `image://${new URL('@/assets/images/community_user_node.png', import.meta.url)}`
})),
right: 10,
bottom: 15,
icon: "circle",
orient: "vertical",
itemWidth: 16,
itemHeight: 16,
itemGap: 12,
backgroundColor: "rgba(0,67,125,0.56)", //
borderRadius: 8, //
borderColor: "#c2f2ff", //
borderWidth: 0.3,
padding: [12, 20, 12, 20], //
textStyle: {
color: "#fff",
fontSize: 16,
fontWeight: "normal"
}
}],
//hover
tooltip: {
trigger: "item",
trigger: "item",
backgroundColor: "rgba(0,0,0,0)", //
borderColor: "rgba(0,0,0,0)", //
borderWidth: 0,
extraCssText: "box-shadow:none;padding:0;",
formatter: function (params) {
if (params.dataType === "node" && params.data.category === 0) {
return `<div
style="
height: 76px;
border-radius: 4px;
background: url('${nodeHoverImg}');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
padding:20px 20px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
"
>
<div>
<div style="color: #fff; letter-spacing: 0.14px; display: flex; align-items: center">
<div style="font-size: 16px">${params.data.userName}</div>
<div
style="
margin-left: 10px;
padding: 3px 7px;
background-color: rgba(171, 247, 255, 0.2);
border-radius: 2px;
font-size: 12px;
"
>
${params.data.mediaCategory}
</div>
</div>
<div style="display: flex; font-size: 14px; margin-top: 5px;color:#fff">
<div>关注数:${params.data.followerCount} </div>
<div style="margin-left: 20px">粉丝数:${params.data.followers}</div>
</div>
</div>
</div>`;
} else if (params.data.category === 1) {
return `<div
style="
height: 56px;
border-radius: 4px;
background: url('${nodeHoverImg}');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
padding:10px 10px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
"
>
<div>
<div style="color: #fff; letter-spacing: 0.14px; display: flex; align-items: center">
<div style="font-size: 16px">${params.data.id}</div>
<div
style="
margin-left: 10px;
padding: 3px 7px;
background-color: rgba(171, 247, 255, 0.2);
border-radius: 2px;
font-size: 12px;
"
>
普通自媒体
</div>
</div>
</div>
</div>`;
}
}
},
animationDuration: 1500,
animationEasingUpdate: 'quinticInOut',
series: [
{
type: 'graph',
zoom: 0.5,
layout: 'force',
force: {
repulsion: 100,
edgeLength: 80
},
roam: true,
label: {
show: false
},
data: userNodes,
links: [], //
categories: [
{
name: '桥梁节点'
},
{
name: '普通节点',
itemStyle: {
//
color: new echarts.graphic.RadialGradient(0.98, 0.38, 0.9, [
{ offset: 1, color: "#476996" }, //
{ offset: 0.5, color: "#38546b" }, //
{ offset: 0, color: "#5fb3b3" } //
]),
opacity: 0.8,
//
borderColor: '#2AB9FE',
borderWidth: 1,
borderType: 'dashed',
borderImageSource: 'linear-gradient(90deg, #2AB9FE 12.25%, #52FFF3 100.6%)',
borderImageSlice: 1,
},
}
],
lineStyle: {
opacity: 0.9,
width: 1,
curveness: 0
}
}
]
};
chart.setOption(option);
//
chart.on('mouseover', 'series.graph', function(params) {
if (params.data && bridgeNodeIds.has(params.data.originalId)) {
activeNodeId.value = params.data.originalId;
updateNodeImage(bridgeNodeImages);
}
});
chart.on('mouseout', 'series.graph', function(params) {
if (params.data && bridgeNodeIds.has(params.data.originalId)) {
activeNodeId.value = '';
updateNodeImage(bridgeNodeImages);
}
});
};
//
const updateNodeImage = (bridgeNodeImages) => {
if (!chart || !nodesData.value.length) return;
//
const newNodes = [...nodesData.value];
newNodes.forEach((node) => {
if (bridgeNodeImages.has(node.originalId)) {
const images = bridgeNodeImages.get(node.originalId);
node.symbol = node.originalId === activeNodeId.value
? `image://${images.activeImg}`
: `image://${images.defImg}`;
}
});
// 使setOption
chart.setOption({
series: [{
data: newNodes
}]
});
// nodesData
nodesData.value = newNodes;
};
//
const handleResize = () => {
if (chart) {
chart.resize();
}
};
onMounted(async () => {
window.addEventListener('resize', handleResize);
await initChart();
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
if (chart) {
chart.dispose();
chart = null;
}
});
//
const handleGoback = () => {
emit("click:goback", "BridgeCommunityNode");
};
</script>
<style scoped lang="less">
.detailCommunityGraph-component {
width: 100%;
height: 100%;
.container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.goback {
position: absolute;
top: 10px;
left: 20px;
cursor: pointer;
}
//
.info-box {
position: absolute;
top: 425px;
left: 20px;
width: 250px;
height: 42px;
background-image: url('@/assets/images/detail_community_info.png');
background-size: cover;
background-position: center;
padding: 15px;
display: flex;
flex-direction: row; // row
justify-content: space-around; //
align-items: center; //
z-index: 10; // z-index
.info-item {
display: flex;
align-items: center;
img {
margin-right: 5px; //
width: 14px; //
height: 14px; //
}
.label {
color: #FFFFFFC2;
font-family: OPPOSans;
font-style: Regular;
font-size: 14px;
margin-right: 5px;
}
.value {
color: #fff;
font-family: D-DIN;
font-style: DIN-Bold;
font-size: 16px;
font-weight: bold;
text-align: center;
}
}
}
}
</style>