Merge branch 'master' of http://172.16.20.1:3000/duanhao/SocialNetworks_duan
This commit is contained in:
commit
906d69402a
|
|
@ -27,6 +27,7 @@ export const useKeyNodeStore2 = defineStore('keyNode2', () => {
|
|||
const allLeaderData = ref([
|
||||
{
|
||||
id: "President Biden Archived",
|
||||
nodeId: "1349149096909668363",
|
||||
postId: 1,
|
||||
name: "President Biden Archived",
|
||||
chineseName: null,
|
||||
|
|
@ -39,6 +40,7 @@ export const useKeyNodeStore2 = defineStore('keyNode2', () => {
|
|||
},
|
||||
{
|
||||
id: "Joe Truzman",
|
||||
nodeId: "3006348240",
|
||||
postId: 2,
|
||||
name: "Joe Truzman",
|
||||
chineseName: null,
|
||||
|
|
@ -51,6 +53,7 @@ export const useKeyNodeStore2 = defineStore('keyNode2', () => {
|
|||
},
|
||||
{
|
||||
id: "OSINTdefender",
|
||||
nodeId: "1457867047334031360",
|
||||
postId: 3,
|
||||
name: "OSINTdefender",
|
||||
chineseName: null,
|
||||
|
|
@ -63,6 +66,7 @@ export const useKeyNodeStore2 = defineStore('keyNode2', () => {
|
|||
},
|
||||
{
|
||||
id: "Paul Golding",
|
||||
nodeId: "455264233",
|
||||
postId: 4,
|
||||
name: "Paul Golding",
|
||||
chineseName: null,
|
||||
|
|
@ -75,6 +79,7 @@ export const useKeyNodeStore2 = defineStore('keyNode2', () => {
|
|||
},
|
||||
{
|
||||
id: "Rep. Matt Gaetz",
|
||||
nodeId: "818948638890217473",
|
||||
postId: 5,
|
||||
name: "Rep. Matt Gaetz",
|
||||
chineseName: null,
|
||||
|
|
@ -87,6 +92,7 @@ export const useKeyNodeStore2 = defineStore('keyNode2', () => {
|
|||
},
|
||||
{
|
||||
id: "Israel Defense Forces",
|
||||
nodeId: "18576537",
|
||||
postId: 6,
|
||||
name: "Israel Defense Forces",
|
||||
chineseName: null,
|
||||
|
|
@ -99,6 +105,7 @@ export const useKeyNodeStore2 = defineStore('keyNode2', () => {
|
|||
},
|
||||
{
|
||||
id: "Andy Ngo",
|
||||
nodeId: "2835451658",
|
||||
postId: 7,
|
||||
name: "Andy Ngo",
|
||||
chineseName: null,
|
||||
|
|
@ -111,6 +118,7 @@ export const useKeyNodeStore2 = defineStore('keyNode2', () => {
|
|||
},
|
||||
{
|
||||
id: "Secretary Antony Blinken",
|
||||
nodeId: "1350150750966603777",
|
||||
postId: 8,
|
||||
name: "Secretary Antony Blinken",
|
||||
chineseName: null,
|
||||
|
|
@ -123,6 +131,8 @@ export const useKeyNodeStore2 = defineStore('keyNode2', () => {
|
|||
},
|
||||
{
|
||||
id: "Emmanuel Macron",
|
||||
nodeId: "1976143068",
|
||||
postId: 9,
|
||||
name: "Emmanuel Macron",
|
||||
chineseName: null,
|
||||
followers: "1018.6万",
|
||||
|
|
@ -134,6 +144,8 @@ export const useKeyNodeStore2 = defineStore('keyNode2', () => {
|
|||
},
|
||||
{
|
||||
id: "Jackson Hinkle 🇺🇸",
|
||||
nodeId: "1151913018936053760",
|
||||
postId: 10,
|
||||
name: "Jackson Hinkle 🇺🇸",
|
||||
chineseName: null,
|
||||
followers: "304.9万",
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
:allLeaderData="store.allLeaderData"
|
||||
@handle:openDialog="handleGraphNodeClick"
|
||||
/>
|
||||
<BridgeCommunityGraph v-if="true" />
|
||||
<BridgeCommunityGraph v-if="true" :timestamp="store.activeTimePoint" ref="bridgeGraphRef"/>
|
||||
</div>
|
||||
<div class="timeline-container">
|
||||
<span class="time-label">2023.10.07 00:00:00</span>
|
||||
|
|
@ -102,6 +102,8 @@ import BridgeCommunityGraph from './graph/bridgeCommunityGraph.vue';
|
|||
|
||||
const store = useKeyNodeStore2();
|
||||
const leaderGraphRef = ref(null);
|
||||
// 添加对BridgeCommunityGraph的引用
|
||||
const bridgeGraphRef = ref(null);
|
||||
|
||||
const handleGraphNodeClick = (leaderData) => {
|
||||
store.openLeaderDetail(leaderData);
|
||||
|
|
@ -111,6 +113,10 @@ const highlightNode = (leaderId) => {
|
|||
if (leaderGraphRef.value) {
|
||||
leaderGraphRef.value.highlightNode(leaderId);
|
||||
}
|
||||
// 调用BridgeCommunityGraph的highlightNode方法
|
||||
if (bridgeGraphRef.value) {
|
||||
bridgeGraphRef.value.highlightNode(leaderId);
|
||||
}
|
||||
};
|
||||
|
||||
// 计算时间点在时间轴上的位置百分比
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@
|
|||
</div>
|
||||
<div class="heat-item">
|
||||
<p class="diamond"></p>
|
||||
贴文被转总数: 1329
|
||||
贴文被转总数: {{ store.activeLeader.leaderOriginInfo.transmit }}
|
||||
</div>
|
||||
<div class="heat-item">
|
||||
<p class="diamond"></p>
|
||||
|
|
|
|||
|
|
@ -3,51 +3,91 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import { onMounted, onUnmounted, defineProps, watch, ref } 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"; // 引入头像处理
|
||||
|
||||
const props = defineProps({
|
||||
timestamp: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
})
|
||||
|
||||
let chartInstance = null;
|
||||
|
||||
// 跟踪当前激活的节点ID
|
||||
const activeNodeId = ref(null);
|
||||
// 用于后续鼠标悬浮桥梁节点高亮
|
||||
const nodesData = ref([]);
|
||||
const linksData = ref([]);
|
||||
|
||||
// 处理数据生成ECharts所需格式
|
||||
const processData = () => {
|
||||
const processData = async() => {
|
||||
const nodes = [];
|
||||
const links = [];
|
||||
const addedCommunities = new Set();
|
||||
const keyNodeStore2 = useKeyNodeStore2(); // 获取store实例
|
||||
// 获取store实例
|
||||
const keyNodeStore2 = useKeyNodeStore2();
|
||||
|
||||
// 批量将头像转换成base64
|
||||
const bridgeNodes = keyNodeStore2.bridgeNodes;
|
||||
|
||||
// 根据timestamp过滤桥梁节点
|
||||
const filteredBridgeNodes = bridgeNodes.filter(node => node.postsId <= props.timestamp);
|
||||
for (const node of filteredBridgeNodes) {
|
||||
if (node.defImg) {
|
||||
node.defImg = await cropToCircleAsync(node.defImg);
|
||||
}
|
||||
if (node.activeImg) {
|
||||
node.activeImg = await cropToCircleAsync(node.activeImg);
|
||||
}
|
||||
}
|
||||
|
||||
// 找出usersNum的最小值和最大值,用于归一化
|
||||
let minUsersNum = Infinity;
|
||||
let maxUsersNum = -Infinity;
|
||||
|
||||
// 只处理过滤后的桥梁节点相关的社团
|
||||
bridgeData.forEach(item => {
|
||||
// 检查当前桥梁节点是否应该被包含
|
||||
const isIncluded = filteredBridgeNodes.some(node => node.Node === item.bridgeId);
|
||||
if (isIncluded) {
|
||||
item.bridgeCommunities.forEach(community => {
|
||||
minUsersNum = Math.min(minUsersNum, community.usersNum);
|
||||
maxUsersNum = Math.max(maxUsersNum, community.usersNum);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 节点大小的范围
|
||||
const minNodeSize = 30;
|
||||
const maxNodeSize = 45;
|
||||
const maxNodeSize = 50;
|
||||
|
||||
// 添加桥梁节点和社团节点
|
||||
bridgeData.forEach(item => {
|
||||
// 检查当前桥梁节点是否应该被包含
|
||||
const isIncluded = filteredBridgeNodes.some(node => node.Node === item.bridgeId);
|
||||
if (!isIncluded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bridgeId = item.bridgeId;
|
||||
// 查找对应的桥梁节点图片
|
||||
const bridgeNodeInfo = keyNodeStore2.bridgeNodes.find(node => node.Node === bridgeId);
|
||||
const bridgeNodeImg = bridgeNodeInfo ? bridgeNodeInfo.defImg : '';
|
||||
const bridgeNodeInfo = filteredBridgeNodes.find(node => node.Node === bridgeId);
|
||||
const isActiveNode = activeNodeId.value === bridgeId;
|
||||
const bridgeNodeImg = bridgeNodeInfo ?
|
||||
(isActiveNode ? bridgeNodeInfo.activeImg : bridgeNodeInfo.defImg) : '';
|
||||
|
||||
// 添加桥梁节点 - 可以调整这个值来改变桥梁节点大小
|
||||
// 添加桥梁节点
|
||||
nodes.push({
|
||||
id: `bridge_${bridgeId}`,
|
||||
// name: `桥梁节点 ${bridgeId.substring(0, 5)}...`,
|
||||
category: 0,
|
||||
value: 20, // 桥梁节点大小(可修改)
|
||||
symbol: bridgeNodeImg ? `image://${bridgeNodeImg}` : 'circle', // 使用图片或默认圆形
|
||||
symbolSize: 40 // 调整图片大小
|
||||
symbol: bridgeNodeImg ? `image://${bridgeNodeImg}` : 'circle',
|
||||
symbolSize: 100,
|
||||
originalId: bridgeId // 存储原始ID用于识别
|
||||
});
|
||||
|
||||
// 添加社团节点和边
|
||||
|
|
@ -56,53 +96,98 @@ const processData = () => {
|
|||
const userNumofCommunity = community.usersNum
|
||||
const communityKey = `community_${communityId}_${userNumofCommunity}`;
|
||||
const usersNum = community.usersNum;
|
||||
|
||||
// 归一化usersNum到节点大小范围
|
||||
const size = minNodeSize +
|
||||
((usersNum - minUsersNum) / (maxUsersNum - minUsersNum)) *
|
||||
(maxNodeSize - minNodeSize);
|
||||
|
||||
// 添加社团节点(对于已有的社团不重复添加)
|
||||
if (!addedCommunities.has(communityKey)) {
|
||||
nodes.push({
|
||||
id: communityKey,
|
||||
// name: `社团 ${communityId} (${usersNum})`,
|
||||
category: 1,
|
||||
value: size, // 根据usersNum动态设置大小
|
||||
symbolSize: size
|
||||
});
|
||||
addedCommunities.add(communityKey);
|
||||
}
|
||||
|
||||
// 添加社团与桥梁的边
|
||||
links.push({
|
||||
source: `bridge_${bridgeId}`,
|
||||
target: communityKey,
|
||||
value: 1
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
nodes,
|
||||
links
|
||||
};
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
const result = { nodes, links };
|
||||
nodesData.value = nodes;
|
||||
linksData.value = links;
|
||||
return result;
|
||||
};
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
const initChart = async() => {
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
}
|
||||
|
||||
|
||||
const chartDom = document.getElementById('bridgeCommunityChart');
|
||||
if (!chartDom) return;
|
||||
|
||||
chartInstance = echarts.init(chartDom);
|
||||
|
||||
const { nodes, links } = processData();
|
||||
// 添加鼠标悬浮事件监听
|
||||
chartInstance.on('mouseover', function(params) {
|
||||
if (params.data && params.data.category === 0) {
|
||||
// 如果是桥梁节点
|
||||
const nodeId = params.data.originalId;
|
||||
// 无论当前是否有激活节点,都设置为当前节点
|
||||
activeNodeId.value = nodeId;
|
||||
updateNodeImage(nodeId, true);
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// 添加鼠标离开事件监听
|
||||
chartInstance.on('mouseout', function(params) {
|
||||
if (params.data && params.data.category === 0) {
|
||||
// 如果是桥梁节点
|
||||
const nodeId = params.data.originalId;
|
||||
if (activeNodeId.value === nodeId) {
|
||||
activeNodeId.value = null;
|
||||
updateNodeImage(nodeId, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 添加点击事件监听
|
||||
chartInstance.on('click', function(params) {
|
||||
if (params.data && params.data.category === 0) {
|
||||
// 如果是桥梁节点
|
||||
const nodeId = params.data.originalId;
|
||||
// 获取store实例
|
||||
const keyNodeStore2 = useKeyNodeStore2();
|
||||
// 查找对应的leader数据
|
||||
const leader = keyNodeStore2.allLeaderData.find(l => l.nodeId === nodeId);
|
||||
if (leader) {
|
||||
// 打开leader详情弹窗
|
||||
keyNodeStore2.openLeaderDetail({
|
||||
id: leader.id
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { nodes, links } = await processData();
|
||||
|
||||
const categories = [
|
||||
{name: "桥梁节点", category: 0},
|
||||
{name: "社团节点", category: 1}
|
||||
{name: "普通社团", category: 1}
|
||||
]
|
||||
|
||||
const option = {
|
||||
|
|
@ -119,10 +204,13 @@ const initChart = () => {
|
|||
fontSize: 14,
|
||||
},
|
||||
formatter: function(params) {
|
||||
// 如果是边,则不显示提示框
|
||||
if (params.dataType === 'edge') {
|
||||
return '';
|
||||
}
|
||||
// 判断节点类型
|
||||
if(params.data.category === 1) {
|
||||
// 社团节点
|
||||
// const usersNum = params.data.name.match(/\((\d+)\)/)?.[1] || 0;
|
||||
const parts = params.data.id.split('_');
|
||||
const extractedUserNum = parseInt(parts[parts.length - 1], 10);
|
||||
return `<div
|
||||
|
|
@ -144,8 +232,9 @@ const initChart = () => {
|
|||
</div>
|
||||
</div>`;
|
||||
}else {
|
||||
// 桥梁节点: 查找对应的bridgeCommunities长度
|
||||
const bridgeItem = bridgeData.find(item => item.bridgeId === params.data.id.replace('bridge_', ''));
|
||||
// 桥梁节点
|
||||
// 查找对应的bridgeCommunities长度
|
||||
const bridgeItem = bridgeData.find(item => item.bridgeId === (params.data.id || '').replace('bridge_', ''));
|
||||
const communityCount = bridgeItem ? bridgeItem.bridgeCommunities.length : 0;
|
||||
return `<div
|
||||
style = "
|
||||
|
|
@ -200,8 +289,18 @@ const initChart = () => {
|
|||
series: [
|
||||
{
|
||||
type: 'graph',
|
||||
zoom: 0.8,
|
||||
zoom: 0.1,
|
||||
layout: 'force',
|
||||
force: {
|
||||
// 节点间的排斥力,越大越疏散
|
||||
repulsion: 50000,
|
||||
// 连线长度,数值越大越分散
|
||||
edgeLength: [800,6000],
|
||||
// 全局吸引力,越小越松散
|
||||
gravity: 0.8,
|
||||
// 节点阻尼(摩擦),越小越灵活
|
||||
friction: 0.2,
|
||||
},
|
||||
animation: false,
|
||||
draggable: true,
|
||||
data: nodes,
|
||||
|
|
@ -212,17 +311,18 @@ const initChart = () => {
|
|||
itemStyle: {
|
||||
// 节点颜色
|
||||
color: '#ff7300',
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '社团节点',
|
||||
name: '普通社团',
|
||||
itemStyle: {
|
||||
// 节点颜色
|
||||
color: new echarts.graphic.RadialGradient(0.97, 0.38, 0.8, [
|
||||
{ offset: 0, color: "#49c3ed" }, // 最右侧
|
||||
{ offset: 0.5, color: "#5fa3e0" }, // 中间
|
||||
{ offset: 1, color: "#7286d4" } // 最左侧
|
||||
color: new echarts.graphic.RadialGradient(0.98, 0.38, 0.9, [
|
||||
{ offset: 1, color: "#1a3860" }, // 最左侧
|
||||
{ offset: 0.5, color: "#38546b" }, // 中间
|
||||
{ offset: 0, color: "#5fb3b3" } // 最右侧
|
||||
]),
|
||||
opacity: 0.8,
|
||||
// 边框样式
|
||||
borderColor: '#2AB9FE',
|
||||
borderWidth: 1,
|
||||
|
|
@ -230,19 +330,28 @@ const initChart = () => {
|
|||
borderImageSource: 'linear-gradient(90deg, #2AB9FE 12.25%, #52FFF3 100.6%)',
|
||||
borderImageSlice: 1,
|
||||
},
|
||||
symbolSize: 18
|
||||
}
|
||||
],
|
||||
roam: true,
|
||||
lineStyle: {
|
||||
color: 'source',
|
||||
curveness: 0.3
|
||||
},
|
||||
force: {
|
||||
repulsion: 1000,
|
||||
edgeLength: 100
|
||||
// color: '#50AAD6',
|
||||
color: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
|
||||
{ offset: 0, color: '#233144' },
|
||||
{ offset: 0.9, color: '#0578b0' }
|
||||
]),
|
||||
curveness: 0,
|
||||
with: 2,
|
||||
type: 'solid',
|
||||
opacity: 0.5,
|
||||
},
|
||||
animationDurationUpdate: 3500, // 节点移动更平滑
|
||||
animationEasingUpdate: 'quinticInOut',
|
||||
emphasis: { // 高亮配置
|
||||
focus: 'adjacency', // 高亮相邻节点
|
||||
lineStyle: { // 高亮时线条样式
|
||||
width: 10 // 线条宽度(10)
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -250,9 +359,68 @@ const initChart = () => {
|
|||
chartInstance.setOption(option);
|
||||
};
|
||||
|
||||
// 更新节点图片的方法
|
||||
const updateNodeImage = (bridgeId, isActive) => {
|
||||
// 查找对应的桥梁节点信息
|
||||
const keyNodeStore2 = useKeyNodeStore2();
|
||||
const filteredBridgeNodes = keyNodeStore2.bridgeNodes.filter(node => node.postsId <= props.timestamp);
|
||||
// 创建新的节点数据数组(避免直接修改响应式数据)
|
||||
const newNodes = [...nodesData.value];
|
||||
// 先重置所有节点为默认图片
|
||||
if (isActive) {
|
||||
newNodes.forEach((node, index) => {
|
||||
if (node.category === 0) { // 只处理桥梁节点
|
||||
const nodeInfo = filteredBridgeNodes.find(n => n.Node === node.originalId);
|
||||
if (nodeInfo && node.originalId !== bridgeId) {
|
||||
newNodes[index].symbol = nodeInfo.defImg ? `image://${nodeInfo.defImg}` : 'circle';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// 更新当前节点
|
||||
const bridgeNodeInfo = filteredBridgeNodes.find(node => node.Node === bridgeId);
|
||||
if (bridgeNodeInfo) {
|
||||
// 找到要更新的节点索引
|
||||
const nodeIndex = nodesData.value.findIndex(node => node.originalId === bridgeId);
|
||||
if (nodeIndex !== -1) {
|
||||
// 创建新的节点数据数组(避免直接修改响应式数据)
|
||||
const newNodes = [...nodesData.value];
|
||||
// 更新节点图片
|
||||
const imgUrl = isActive ? bridgeNodeInfo.activeImg : bridgeNodeInfo.defImg;
|
||||
newNodes[nodeIndex].symbol = imgUrl ? `image://${imgUrl}` : 'circle';
|
||||
|
||||
// 使用setOption进行增量更新
|
||||
chartInstance.setOption({
|
||||
series: [{
|
||||
data: newNodes
|
||||
}]
|
||||
});
|
||||
|
||||
// 更新nodesData(保持数据一致性)
|
||||
nodesData.value = newNodes;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 添加highlightNode方法
|
||||
const highlightNode = (leaderId) => {
|
||||
// 查找对应的桥梁节点ID
|
||||
const keyNodeStore2 = useKeyNodeStore2();
|
||||
const leader = keyNodeStore2.allLeaderData.find(l => l.id === leaderId);
|
||||
if (leader) {
|
||||
// 先重置之前的激活节点
|
||||
if (activeNodeId.value) {
|
||||
updateNodeImage(activeNodeId.value, false);
|
||||
}
|
||||
// 设置新的激活节点
|
||||
activeNodeId.value = leader.nodeId;
|
||||
updateNodeImage(leader.nodeId, true);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
initChart();
|
||||
setTimeout(async () => {
|
||||
await initChart();
|
||||
}, 100);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
|
|
@ -262,6 +430,13 @@ onMounted(() => {
|
|||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.timestamp,
|
||||
async (newValue) => {
|
||||
await initChart()
|
||||
}
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
|
|
@ -269,6 +444,9 @@ onUnmounted(() => {
|
|||
}
|
||||
window.removeEventListener('resize', () => {});
|
||||
});
|
||||
|
||||
// 暴露highlightNode方法
|
||||
defineExpose({ highlightNode });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ const graphPanelRef = ref(null);
|
|||
// 添加 activeChartTab 状态---用于判断在事件热度图弹窗显示的图表类型
|
||||
const activeChartTab = ref('trend'); // 默认值设为 'trend'
|
||||
const handleLeaderSelect = (leader) => {
|
||||
console.log("选中的领袖ID:", leader);
|
||||
if (graphPanelRef.value && leader.id) {
|
||||
graphPanelRef.value.highlightNode(leader.id);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user