桥梁二级网络
This commit is contained in:
parent
402337707a
commit
556d774e87
BIN
src/assets/images/community_user_node.png
Normal file
BIN
src/assets/images/community_user_node.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/assets/images/detail_community_info.png
Normal file
BIN
src/assets/images/detail_community_info.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/images/icon/bridge_node_nums.png
Normal file
BIN
src/assets/images/icon/bridge_node_nums.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 305 B |
BIN
src/assets/images/icon/node_nums.png
Normal file
BIN
src/assets/images/icon/node_nums.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 228 B |
12208
src/assets/json/group.json
Normal file
12208
src/assets/json/group.json
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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; // 默认为普通节点,userName为id
|
||||
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>
|
||||
Loading…
Reference in New Issue
Block a user