修改第一模块

1. 传播领袖识别页面修改
2. 传播桥梁节点识别添加
This commit is contained in:
qumeng039@126.com 2025-06-24 17:53:58 +08:00
parent 7ba5174538
commit 2381e0241c
18 changed files with 1451 additions and 436 deletions

9
.prettierignore Normal file
View File

@ -0,0 +1,9 @@
/dist/*
.local
.output.js
/node_modules/**
**/*.svg
**/*.sh
/public/*

9
.prettierrc Normal file
View File

@ -0,0 +1,9 @@
{
"semi": true,
"singleQuote": false,
"printWidth":100,
"trailingComma": "none",
"tabWidth": 2,
"endOfLine": "auto",
"arrowParens": "always"
}

View File

@ -1,3 +1,4 @@
{
"recommendations": ["Vue.volar"]
"recommendations": ["Vue.volar"],
}

17
package-lock.json generated
View File

@ -15,6 +15,7 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",
"prettier": "^3.6.0",
"vite": "^6.3.5"
}
},
@ -1323,6 +1324,22 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/prettier": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.0.tgz",
"integrity": "sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/rollup": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz",

View File

@ -16,6 +16,7 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",
"prettier": "^3.6.0",
"vite": "^6.3.5"
}
}

BIN
src/assets/images/Andy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

BIN
src/assets/images/Biden.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

BIN
src/assets/images/Matt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

23
src/utils/transform.js Normal file
View File

@ -0,0 +1,23 @@
// 将图片裁剪为圆形并返回 base64
export function cropImageToCircle(url, size = 100) {
return new Promise((resolve) => {
const img = new window.Image();
img.crossOrigin = "anonymous";
img.onload = function () {
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, size, size);
ctx.save();
ctx.beginPath();
ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2, false);
ctx.closePath();
ctx.clip();
ctx.drawImage(img, 0, 0, size, size);
ctx.restore();
resolve(canvas.toDataURL("image/png"));
};
img.src = url;
});
}

View File

@ -1,105 +1,144 @@
<template>
<div>
<!-- 1. 顶部介绍图片 -->
<div>
<img src="@/assets/images/instruction.png" alt="系统介绍" class="intruduction">
</div>
<div>
<!-- 1. 顶部介绍图片 -->
<div>
<img src="@/assets/images/instruction.png" alt="系统介绍" class="intruduction" />
</div>
<!-- 2. 第一行布局容器 (布局不变) -->
<div class="leader-containner1">
<!-- 2. 第一行布局容器 (布局不变) -->
<div class="leader-containner1">
<!-- 区域1: 意见领袖列表 -->
<div class="left-panel">
<h2 class="panel-title">意见领袖抽展示</h2>
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab"
:class="{ active: activeTab === tab }"
@click="activeTab = tab"
>
{{ tab }}
</button>
</div>
<div class="leader-list">
<div v-for="leader in filteredVisibleLeaders" :key="leader.id" class="leader-item">
<img :src="leader.avatar" :alt="leader.name" class="avatar" />
<div class="info">
<div class="name">
<span class="en-name">{{ leader.name }}</span>
<span v-if="leader.chineseName" class="cn-name">{{ leader.chineseName }}</span>
</div>
<div class="stats">
<span>粉丝数量: {{ leader.followers }}</span>
<span>发帖总数: {{ leader.posts }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 区域1: 意见领袖列表 -->
<div class="left-panel">
<h2 class="panel-title">意见领袖抽展示</h2>
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab"
:class="{ active: activeTab === tab }"
@click="activeTab = tab"
>
{{ tab }}
</button>
</div>
<div class="leader-list">
<div v-for="leader in filteredVisibleLeaders" :key="leader.id" class="leader-item">
<img :src="leader.avatar" :alt="leader.name" class="avatar">
<div class="info">
<div class="name">
<span class="en-name">{{ leader.name }}</span>
<span v-if="leader.chineseName" class="cn-name">{{ leader.chineseName }}</span>
</div>
<div class="stats">
<span>粉丝数量: {{ leader.followers }}</span>
<span>发帖总数: {{ leader.posts }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 区域2: 佩洛西图谱 (视觉效果已更新) -->
<div class="right-panel">
<div class="key-node-recognition">
<div class="background-svg-wrapper">
<svg
width="100%"
height="100%"
viewBox="0 0 800 540"
preserveAspectRatio="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient
id="paint0_linear_bg"
x1="0"
y1="167.1"
x2="800"
y2="167.1"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#063D71" stop-opacity="0.2" />
<stop offset="1" stop-color="#081E38" stop-opacity="0.8" />
</linearGradient>
<linearGradient
id="paint1_linear_border"
x1="400"
y1="0"
x2="400"
y2="540"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#3AA1F8" />
<stop offset="1" stop-color="#3AA1F8" stop-opacity="0.2" />
</linearGradient>
</defs>
<path
d="M798 0.5H2C1.17159 0.5 0.500003 1.17158 0.5 2V538C0.5 538.828 1.17159 539.5 2 539.5H798C798.828 539.5 799.5 538.828 799.5 538V2C799.5 1.17157 798.828 0.5 798 0.5Z"
fill="url(#paint0_linear_bg)"
fill-opacity="0.48"
stroke="url(#paint1_linear_border)"
/>
</svg>
</div>
<div class="content-wrapper">
<h1 class="graph-title">佩洛西系列事件</h1>
<div ref="chartContainer" class="chart-container"></div>
<div class="timeline-container">
<span class="time-label">2022.07.31 00:00:00</span>
<div class="timeline-track">
<div
v-for="point in timePoints"
:key="point.id"
class="timeline-point-wrapper"
@click="onTimePointClick(point.id)"
>
<div class="timeline-point" :class="{ active: activeTimePoint === point.id }">
<div v-if="activeTimePoint === point.id" class="active-pin"></div>
</div>
</div>
</div>
<span class="time-label">2022.08.01 00:00:00</span>
</div>
</div>
</div>
</div>
<!-- 区域2: 佩洛西图谱 (视觉效果已更新) -->
<div class="right-panel">
<div class="key-node-recognition">
<div class="background-svg-wrapper">
<svg width="100%" height="100%" viewBox="0 0 800 540" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
<defs><linearGradient id="paint0_linear_bg" x1="0" y1="167.1" x2="800" y2="167.1" gradientUnits="userSpaceOnUse"><stop stop-color="#063D71" stop-opacity="0.2" /><stop offset="1" stop-color="#081E38" stop-opacity="0.8" /></linearGradient><linearGradient id="paint1_linear_border" x1="400" y1="0" x2="400" y2="540" gradientUnits="userSpaceOnUse"><stop stop-color="#3AA1F8" /><stop offset="1" stop-color="#3AA1F8" stop-opacity="0.2" /></linearGradient></defs>
<path d="M798 0.5H2C1.17159 0.5 0.500003 1.17158 0.5 2V538C0.5 538.828 1.17159 539.5 2 539.5H798C798.828 539.5 799.5 538.828 799.5 538V2C799.5 1.17157 798.828 0.5 798 0.5Z" fill="url(#paint0_linear_bg)" fill-opacity="0.48" stroke="url(#paint1_linear_border)" />
</svg>
</div>
<div class="content-wrapper">
<h1 class="graph-title">佩洛西系列事件</h1>
<div ref="chartContainer" class="chart-container"></div>
<div class="timeline-container">
<span class="time-label">2022.07.31 00:00:00</span>
<div class="timeline-track">
<div v-for="point in timePoints" :key="point.id" class="timeline-point-wrapper" @click="onTimePointClick(point.id)">
<div class="timeline-point" :class="{ active: activeTimePoint === point.id }">
<div v-if="activeTimePoint === point.id" class="active-pin"></div>
</div>
</div>
</div>
<span class="time-label">2022.08.01 00:00:00</span>
</div>
</div>
</div>
</div>
<!-- 区域3: 领袖分析 -->
<LeaderAnalysis :chart-data="analysisChartData" />
</div>
<!-- 区域3: 领袖分析 -->
<LeaderAnalysis :chart-data="analysisChartData" />
</div>
<!-- 第二行布局容器 (布局不变) -->
<div class="leader-containner2">
<EventHeatmap @show-details="openDetailsModal" />
<PostDynamics />
<WordCloud :word-data="wordCloudData" />
</div>
<!-- 第二行布局容器 (布局不变) -->
<div class="leader-containner2">
<EventHeatmap @show-details="openDetailsModal" />
<PostDynamics />
<WordCloud :word-data="wordCloudData" />
</div>
<!-- 详情弹窗 -->
<div v-if="showDetailsModal" class="modal-overlay" @click="closeDetailsModal">
<div class="modal-content" @click.stop>
<button class="close-btn" @click="closeDetailsModal">×</button>
<div ref="detailsChart" class="details-chart-container"></div>
</div>
</div>
</div>
<!-- 详情弹窗 -->
<div v-if="showDetailsModal" class="modal-overlay" @click="closeDetailsModal">
<div class="modal-content" @click.stop>
<button class="close-btn" @click="closeDetailsModal">×</button>
<div ref="detailsChart" class="details-chart-container"></div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick, computed, watch } from 'vue';
import * as echarts from 'echarts/core';
import { GraphChart } from 'echarts/charts';
import { TitleComponent, TooltipComponent } from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
echarts.use([TitleComponent, TooltipComponent, GraphChart, CanvasRenderer]);
import { ref, onMounted, onUnmounted, nextTick, computed, watch } from "vue";
import * as echarts from "echarts/core";
import { GraphChart } from "echarts/charts";
import { TitleComponent, TooltipComponent } from "echarts/components";
import { CanvasRenderer } from "echarts/renderers";
import { SVGRenderer } from "echarts/renderers";
import { cropImageToCircle } from "@/utils/transform"; //
echarts.use([TitleComponent, TooltipComponent, GraphChart, SVGRenderer]);
const allGraphNodes = ref([]);
//
import LeaderAnalysis from './KeyNodeRecognition1/LeaderAnalysis.vue';
import EventHeatmap from './KeyNodeRecognition1/EventHeatmap.vue';
import PostDynamics from './KeyNodeRecognition1/PostDynamics.vue';
import WordCloud from './KeyNodeRecognition1/WordCloud.vue';
import LeaderAnalysis from "./KeyNodeRecognition1/LeaderAnalysis.vue";
import EventHeatmap from "./KeyNodeRecognition1/EventHeatmap.vue";
import PostDynamics from "./KeyNodeRecognition1/PostDynamics.vue";
import WordCloud from "./KeyNodeRecognition1/WordCloud.vue";
// ===================================================================
//
@ -109,193 +148,603 @@ const timePoints = ref(Array.from({ length: 10 }, (_, i) => ({ id: i + 1 })));
// [] 10
const allLeaderData = ref([
{ id: 'huxijin', name: 'Hu Xijin', chineseName: '胡锡进', followers: '53.8万', posts: '54', avatar: new URL('@/assets/images/huxijin.png', import.meta.url).href, category: '自媒体' },
{ id: 'bidishalolo', name: 'bidishalolo', chineseName: null, followers: '2387', posts: '8380', avatar: new URL('@/assets/images/bidishalolo.png', import.meta.url).href, category: '自媒体' },
{ id: 'indopacific', name: 'Indo-Pacific News', chineseName: null, followers: '11.5万', posts: '11.3万', avatar: new URL('@/assets/images/indo.png', import.meta.url).href, category: '新闻媒体' },
{ id: 'spectator', name: 'The Spectator Index', chineseName: null, followers: '233.5万', posts: '56', avatar: new URL('@/assets/images/spectator.png', import.meta.url).href, category: '新闻媒体' },
{ id: 'mickwallace', name: 'Mick Wallace', chineseName: null, followers: '24.8万', posts: '10259', avatar: new URL('@/assets/images/mick.png', import.meta.url).href, category: '自媒体' },
{ id: 'reuters', name: 'Reuters', chineseName: null, followers: '2575.7万', posts: '98', avatar: new URL('@/assets/images/reuters.png', import.meta.url).href, category: '新闻媒体' },
{ id: 'jiushiniya', name: 'Jiushiniya', chineseName: null, followers: '557', posts: '16020', avatar: new URL('@/assets/images/jiushiniya.png', import.meta.url).href, category: '自媒体' },
{ id: 'levi_godman', name: 'Levi_godman', chineseName: null, followers: '4.6万', posts: '12847', avatar: new URL('@/assets/images/levi.png', import.meta.url).href, category: '自媒体' },
{ id: 'liuxiaoming', name: 'Liu Xiaoming', chineseName: '刘晓明', followers: '33.8万', posts: '8757', avatar: new URL('@/assets/images/liuxiaoming.png', import.meta.url).href, category: '自媒体' },
{ id: 'indobosss', name: 'indobosss', chineseName: null, followers: '32', posts: '1403', avatar: new URL('@/assets/images/indobosss.png', import.meta.url).href, category: '自媒体' },
{
id: "huxijin",
name: "Hu Xijin",
chineseName: "胡锡进",
followers: "53.8万",
posts: "54",
avatar: new URL("@/assets/images/huxijin.png", import.meta.url).toString(),
category: "自媒体"
},
{
id: "bidishalolo",
name: "bidishalolo",
chineseName: null,
followers: "2387",
posts: "8380",
avatar: new URL("@/assets/images/bidishalolo.png", import.meta.url).toString(),
category: "自媒体"
},
{
id: "indopacific",
name: "Indo-Pacific News",
chineseName: null,
followers: "11.5万",
posts: "11.3万",
avatar: new URL("@/assets/images/indo.png", import.meta.url).toString(),
category: "新闻媒体"
},
{
id: "spectator",
name: "The Spectator Index",
chineseName: null,
followers: "233.5万",
posts: "56",
avatar: new URL("@/assets/images/spectator.png", import.meta.url).toString(),
category: "新闻媒体"
},
{
id: "mickwallace",
name: "Mick Wallace",
chineseName: null,
followers: "24.8万",
posts: "10259",
avatar: new URL("@/assets/images/mick.png", import.meta.url).toString(),
category: "自媒体"
},
{
id: "reuters",
name: "Reuters",
chineseName: null,
followers: "2575.7万",
posts: "98",
avatar: new URL("@/assets/images/reuters.png", import.meta.url).toString(),
category: "新闻媒体"
},
{
id: "jiushiniya",
name: "Jiushiniya",
chineseName: null,
followers: "557",
posts: "16020",
avatar: new URL("@/assets/images/jiushiniya.png", import.meta.url).toString(),
category: "自媒体"
},
{
id: "levi_godman",
name: "Levi_godman",
chineseName: null,
followers: "4.6万",
posts: "12847",
avatar: new URL("@/assets/images/levi.png", import.meta.url).toString(),
category: "自媒体"
},
{
id: "liuxiaoming",
name: "Liu Xiaoming",
chineseName: "刘晓明",
followers: "33.8万",
posts: "8757",
avatar: new URL("@/assets/images/liuxiaoming.png", import.meta.url).toString(),
category: "自媒体"
},
{
id: "indobosss",
name: "indobosss",
chineseName: null,
followers: "32",
posts: "1403",
avatar: new URL("@/assets/images/indobosss.png", import.meta.url).toString(),
category: "自媒体"
}
]);
// [] 10
const nodeCoordinates = {
'reuters': { x: 150, y: 150 },
'spectator': { x: 350, y: 350 },
'indopacific': { x: 600, y: 450 },
'liuxiaoming': { x: 400, y: 120 },
'huxijin': { x: 200, y: 500 },
'mickwallace': { x: 480, y: 550 },
'jiushiniya': { x: 650, y: 180 },
'levi_godman': { x: 850, y: 250 },
'bidishalolo': { x: 800, y: 500 },
'indobosss': { x: 600, y: 600 },
reuters: { x: 150, y: 150 },
spectator: { x: 350, y: 350 },
indopacific: { x: 600, y: 450 },
liuxiaoming: { x: 400, y: 120 },
huxijin: { x: 200, y: 500 },
mickwallace: { x: 480, y: 550 },
jiushiniya: { x: 650, y: 180 },
levi_godman: { x: 850, y: 250 },
bidishalolo: { x: 800, y: 500 },
indobosss: { x: 600, y: 600 }
};
const visibleLeaders = computed(() => allLeaderData.value.slice(0, activeTimePoint.value));
const tabs = ref(['全部', '新闻媒体', '自媒体', '政府官号']);
const activeTab = ref('全部');
const tabs = ref(["全部", "新闻媒体", "自媒体", "政府官号"]);
const activeTab = ref("全部");
const filteredVisibleLeaders = computed(() => {
if (activeTab.value === '全部') return visibleLeaders.value;
return visibleLeaders.value.filter(leader => leader.category === activeTab.value);
if (activeTab.value === "全部") return visibleLeaders.value;
return visibleLeaders.value.filter((leader) => leader.category === activeTab.value);
});
//
const chartContainer = ref(null);
let myChart = null;
const chartOptions = {
tooltip: {}, animationDurationUpdate: 1500, animationEasingUpdate: 'quinticInOut',
series: [{
type: 'graph', layout: 'none', roam: true, symbolSize: 50,
label: { show: false }, edgeSymbol: ['none', 'none'], edgeSymbolSize: [4, 10],
lineStyle: { width: 1, color: 'rgba(0, 191, 255, 0.4)', opacity: 0.9, },
categories: [
{ name: 'Leader', symbolSize: 70, itemStyle: { borderColor: 'rgba(0, 191, 255, 0.8)', borderWidth: 3, shadowBlur: 15, shadowColor: 'rgba(0, 191, 255, 0.7)' } },
{ name: 'User', symbolSize: 25, itemStyle: { color: '#006A92', borderColor: '#00BFFF', borderWidth: 2, shadowBlur: 5, shadowColor: 'rgba(0, 191, 255, 0.5)' } }
],
data: [], links: []
}]
};
const allGraphNodes = computed(() => {
const leaderNodes = allLeaderData.value.map(leader => {
const coords = nodeCoordinates[leader.id] || { x: Math.random() * 1000, y: Math.random() * 600 };
const node = { id: leader.id, name: leader.name, symbol: `image://${leader.avatar}`, category: 0, ...coords };
if (leader.id === 'mickwallace') {
node.itemStyle = { borderColor: 'rgba(255, 220, 0, 0.9)', shadowColor: 'rgba(255, 220, 0, 0.8)', shadowBlur: 20, };
tooltip: {},
animationDurationUpdate: 1500,
animationEasingUpdate: "quinticInOut",
series: [
{
type: "graph",
layout: "none",
roam: true,
symbolSize: 50,
label: { show: false },
edgeSymbol: ["none", "none"],
symbolClip: true, //
symbol: "circle",
symbolKeepAspect: true, //
edgeSymbolSize: [4, 10],
lineStyle: { width: 1, color: "rgba(0, 191, 255, 0.4)", opacity: 0.9 },
categories: [
{
name: "Leader",
symbolSize: 70,
itemStyle: {
borderColor: "rgba(0, 191, 255, 0.8)",
borderWidth: 3,
shadowBlur: 15,
shadowColor: "rgba(0, 191, 255, 0.7)"
}
},
{
name: "User",
symbolSize: 25,
itemStyle: {
color: "#006A92",
borderColor: "#00BFFF",
borderWidth: 2,
shadowBlur: 5,
shadowColor: "rgba(0, 191, 255, 0.5)"
}
}
return node;
});
const userNodes = Array.from({ length: 40 }, (_, i) => ({ id: `user_${i}`, name: `User ${i}`, x: Math.random() * 1000, y: Math.random() * 600, category: 1 }));
return [...leaderNodes, ...userNodes];
});
],
data: [],
links: []
}
]
};
const formatAllGraphNodes = async () => {
const tempResLeaderData = await Promise.all(
allLeaderData.value.map(async (leader) => {
const coords = nodeCoordinates[leader.id] || {
x: Math.random() * 1000,
y: Math.random() * 600
};
const base64_res = await cropImageToCircle(leader.avatar, 50);
const node = {
id: leader.id,
name: leader.name,
symbol: `image://${base64_res}`,
category: 0,
...coords
};
if (leader.id === "mickwallace") {
node.itemStyle = {
borderColor: "rgba(255, 220, 0, 0.9)",
shadowColor: "rgba(255, 220, 0, 0.8)",
shadowBlur: 20
};
}
return node;
})
);
const userNodes = Array.from({ length: 40 }, (_, i) => ({
id: `user_${i}`,
name: `User ${i}`,
x: Math.random() * 1000,
y: Math.random() * 600,
category: 1
}));
allGraphNodes.value = [...tempResLeaderData, ...userNodes];
};
// []
const allGraphLinks = computed(() => [
{ source: 'reuters', target: 'spectator' }, { source: 'reuters', target: 'indopacific' },
{ source: 'liuxiaoming', target: 'huxijin' }, { source: 'huxijin', target: 'mickwallace' },
{ source: 'spectator', target: 'liuxiaoming' }, { source: 'jiushiniya', target: 'levi_godman' },
{ source: 'bidishalolo', target: 'indobosss' },
//...
{ source: "reuters", target: "spectator" },
{ source: "reuters", target: "indopacific" },
{ source: "liuxiaoming", target: "huxijin" },
{ source: "huxijin", target: "mickwallace" },
{ source: "spectator", target: "liuxiaoming" },
{ source: "jiushiniya", target: "levi_godman" },
{ source: "bidishalolo", target: "indobosss" }
//...
]);
// ===================================================================
//
// ===================================================================
const onTimePointClick = (pointId) => { activeTimePoint.value = pointId; };
const updateGraphData = (currentVisibleLeaders) => {
if (!myChart) return;
const leadersSet = new Set(currentVisibleLeaders.map(l => l.id));
const visibleNodes = allGraphNodes.value.filter(node => node.category === 1 || leadersSet.has(node.id));
const nodeIds = new Set(visibleNodes.map(n => n.id));
const visibleLinks = allGraphLinks.value.filter(link => nodeIds.has(link.source) && nodeIds.has(link.target));
myChart.setOption({ series: [{ data: visibleNodes, links: visibleLinks }] });
const onTimePointClick = (pointId) => {
activeTimePoint.value = pointId;
};
watch(visibleLeaders, (newVisibleLeaders) => {
const updateGraphData = (currentVisibleLeaders) => {
if (!myChart) return;
const leadersSet = new Set(currentVisibleLeaders.map((l) => l.id));
const visibleNodes = allGraphNodes.value.filter(
(node) => node.category === 1 || leadersSet.has(node.id)
);
const nodeIds = new Set(visibleNodes.map((n) => n.id));
const visibleLinks = allGraphLinks.value.filter(
(link) => nodeIds.has(link.source) && nodeIds.has(link.target)
);
myChart.setOption({ series: [{ data: visibleNodes, links: visibleLinks }] });
};
watch(
visibleLeaders,
(newVisibleLeaders) => {
if (myChart) {
updateGraphData(newVisibleLeaders);
}
}, { deep: true, immediate: true });
},
{ deep: true, immediate: true }
);
// ===================================================================
//
// ===================================================================
const analysisChartData = ref([
{ title: '平均发帖数', unit: '数量', max: 10, series: [{ name: '领袖', value: 6.4 }, { name: '所有用户', value: 0.46 }] },
{ title: '帖子平均生存周期', unit: '天数', max: 10, series: [{ name: '领袖', value: 2.19 }, { name: '所有用户', value: 0.46 }] },
{ title: '平均粉丝数', unit: '天数', max: 10, series: [{ name: '领袖', value: 2.19 }, { name: '所有用户', value: 0.46 }] }
{
title: "平均发帖数",
unit: "数量",
max: 10,
series: [
{ name: "领袖", value: 6.4 },
{ name: "所有用户", value: 0.46 }
]
},
{
title: "帖子平均生存周期",
unit: "天数",
max: 10,
series: [
{ name: "领袖", value: 2.19 },
{ name: "所有用户", value: 0.46 }
]
},
{
title: "平均粉丝数",
unit: "天数",
max: 10,
series: [
{ name: "领袖", value: 2.19 },
{ name: "所有用户", value: 0.46 }
]
}
]);
const wordCloudData = ref([
{ text: '佩洛西', size: 'large', color: '#56a9de', top: '28%', left: '70%' },
{ text: '中国', size: 'large', color: '#56a9de', top: '70%', left: '22%' },
{ text: '中国人民解放军', size: 'medium', color: '#cdeeff', top: '15%', left: '40%' },
{ text: '中美关系', size: 'medium', color: '#cdeeff', top: '50%', left: '60%' },
{ text: '台海和平', size: 'medium', color: '#27c1a8', top: '80%', left: '65%' },
{ text: "佩洛西", size: "large", color: "#56a9de", top: "28%", left: "70%" },
{ text: "中国", size: "large", color: "#56a9de", top: "70%", left: "22%" },
{ text: "中国人民解放军", size: "medium", color: "#cdeeff", top: "15%", left: "40%" },
{ text: "中美关系", size: "medium", color: "#cdeeff", top: "50%", left: "60%" },
{ text: "台海和平", size: "medium", color: "#27c1a8", top: "80%", left: "65%" }
]);
const showDetailsModal = ref(false);
const detailsChart = ref(null);
let myDetailsChart = null;
const openDetailsModal = (chartConfig) => {
showDetailsModal.value = true;
nextTick(() => {
if (detailsChart.value) {
myDetailsChart = echarts.init(detailsChart.value);
myDetailsChart.setOption(chartConfig.option);
}
});
showDetailsModal.value = true;
nextTick(() => {
if (detailsChart.value) {
myDetailsChart = echarts.init(detailsChart.value);
myDetailsChart.setOption(chartConfig.option);
}
});
};
const closeDetailsModal = () => {
showDetailsModal.value = false;
if (myDetailsChart) {
myDetailsChart.dispose();
myDetailsChart = null;
}
showDetailsModal.value = false;
if (myDetailsChart) {
myDetailsChart.dispose();
myDetailsChart = null;
}
};
// ===================================================================
//
// ===================================================================
onMounted(() => {
onMounted(async () => {
await formatAllGraphNodes();
if (chartContainer.value) {
myChart = echarts.init(chartContainer.value);
myChart.setOption(chartOptions);
window.addEventListener('resize', myChart.resize);
myChart = echarts.init(chartContainer.value, null, { renderer: "svg" });
// "Hu Xijin"
const firstData = allGraphNodes.value.filter(
(item) => (item.name === "Hu Xijin" && item.category === 0) || item.category === 1
);
myChart.setOption({
...chartOptions,
series: [
{
...chartOptions.series[0],
data: firstData,
links: allGraphLinks.value
}
]
});
window.addEventListener("resize", myChart.resize);
}
});
onUnmounted(() => {
if (myChart) {
window.removeEventListener('resize', myChart.resize);
myChart.dispose();
}
if (myDetailsChart) {
myDetailsChart.dispose();
}
if (myChart) {
window.removeEventListener("resize", myChart.resize);
myChart.dispose();
}
if (myDetailsChart) {
myDetailsChart.dispose();
}
});
</script>
<style scoped>
.intruduction { width: 100%; margin-top: 0px; border-radius: 2px; }
.leader-containner1, .leader-containner2 { display: flex; flex-direction: row; gap: 10px; }
.leader-containner2 { margin-top: 10px; }
.left-panel { width: 350px; flex-shrink: 0; background-color: rgba(6, 45, 90, 0.3); border: 1px solid #1a8bff; display: flex; flex-direction: column; padding: 15px; }
.panel-title { font-size: 18px; font-weight: bold; text-align: center; padding: 10px; margin: 0 0 15px 0; background: linear-gradient(to right, rgba(58, 161, 248, 0), rgba(58, 161, 248, 0.3), rgba(58, 161, 248, 0)); border-top: 1px solid #3aa1f8; border-bottom: 1px solid #3aa1f8; color: #fff; }
.tabs { display: flex; margin-bottom: 15px; border-bottom: 2px solid #1a5a9c; }
.tabs button { background: none; border: none; color: #a9c2e0; padding: 8px 16px; font-size: 14px; cursor: pointer; transition: all 0.3s ease; position: relative; }
.tabs button.active { color: #fff; font-weight: bold; }
.tabs button.active::after { content: ''; position: absolute; bottom: -2px; left: 0; width: 100%; height: 2px; background-color: #3aa1f8; }
.leader-list { flex-grow: 1; overflow-y: auto; color: #fff;}
.leader-list::-webkit-scrollbar { width: 4px; }
.leader-list::-webkit-scrollbar-track { background: transparent; }
.leader-list::-webkit-scrollbar-thumb { background: #3aa1f8; border-radius: 2px; }
.leader-item { display: flex; align-items: center; padding: 10px 5px; border-bottom: 1px solid rgba(58, 161, 248, 0.2); }
.avatar { width: 50px; height: 50px; border-radius: 50%; margin-right: 15px; flex-shrink: 0; }
.info { display: flex; flex-direction: column; gap: 5px; }
.name { display: flex; align-items: baseline; gap: 8px; }
.en-name { font-size: 16px; font-weight: bold; }
.cn-name { font-size: 14px; color: #a9c2e0; }
.stats { font-size: 12px; color: #a9c2e0; display: flex; gap: 20px; }
.right-panel { flex-grow: 1; position: relative; }
.key-node-recognition { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
.background-svg-wrapper { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
.content-wrapper { position: relative; z-index: 2; display: flex; flex-direction: column; height: 100%; padding: 15px 20px; box-sizing: border-box; }
.graph-title { text-align: center; font-size: 20px; font-weight: bold; color: #cce7ff; letter-spacing: 2px; margin: 0 0 5px 0; text-shadow: 0 0 5px rgba(58, 161, 248, 0.5); }
.chart-container { flex-grow: 1; width: 100%; height: calc(100% - 100px); }
.timeline-container { width: 100%; height: 50px; display: flex; align-items: center; justify-content: space-between; padding: 0 10px; box-sizing: border-box; }
.time-label { font-size: 12px; color: #a9c2e0; }
.timeline-track { flex-grow: 1; height: 4px; background: linear-gradient(90deg, #1b62a9, #3aa1f8, #1b62a9); margin: 0 15px; position: relative; display: flex; justify-content: space-between; align-items: center; }
.timeline-point-wrapper { display: flex; align-items: center; justify-content: center; height: 20px; cursor: pointer; }
.timeline-point { width: 10px; height: 10px; background-color: #8dc5ff; border-radius: 50%; border: 1px solid #cce7ff; transition: transform 0.3s ease; position: relative; }
.timeline-point-wrapper:hover .timeline-point { transform: scale(1.5); }
.timeline-point.active { background-color: #ffc94d; border-color: #fff; transform: scale(1.3); }
.active-pin { width: 20px; height: 24px; background-image: url('data:image/svg+xml;utf8,<svg width="20" height="24" viewBox="0 0 20 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 24L15 14H5L10 24Z" fill="%23FFC94D"/><path d="M1.5 8C1.5 4.41015 4.41015 1.5 8 1.5H12C15.5899 1.5 18.5 4.41015 18.5 8V11C18.5 12.6569 17.1569 14 15.5 14H4.5C2.84315 14 1.5 12.6569 1.5 11V8Z" fill="%23FFC94D" stroke="white"/></svg>'); position: absolute; bottom: 5px; left: 50%; transform: translateX(-50%); }
.modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); display: flex; justify-content: center; align-items: center; z-index: 1000; }
.modal-content { position: relative; background-color: #0d1b38; padding: 20px; border-radius: 8px; border: 1px solid #3a95ff; box-shadow: 0 0 25px rgba(58, 149, 255, 0.5); width: 80vw; height: 75vh; display: flex; flex-direction: column; }
.close-btn { position: absolute; top: 10px; right: 15px; background: none; border: none; color: #a7c5d4; font-size: 24px; cursor: pointer; }
.details-chart-container { width: 100%; flex-grow: 1; }
</style>
.intruduction {
width: 100%;
margin-top: 0px;
border-radius: 2px;
}
.leader-containner1,
.leader-containner2 {
display: flex;
flex-direction: row;
gap: 10px;
}
.leader-containner2 {
margin-top: 10px;
}
.left-panel {
width: 350px;
flex-shrink: 0;
background-color: rgba(6, 45, 90, 0.3);
border: 1px solid #1a8bff;
display: flex;
flex-direction: column;
padding: 15px;
}
.panel-title {
font-size: 18px;
font-weight: bold;
text-align: center;
padding: 10px;
margin: 0 0 15px 0;
background: linear-gradient(
to right,
rgba(58, 161, 248, 0),
rgba(58, 161, 248, 0.3),
rgba(58, 161, 248, 0)
);
border-top: 1px solid #3aa1f8;
border-bottom: 1px solid #3aa1f8;
color: #fff;
}
.tabs {
display: flex;
margin-bottom: 15px;
border-bottom: 2px solid #1a5a9c;
}
.tabs button {
background: none;
border: none;
color: #a9c2e0;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.tabs button.active {
color: #fff;
font-weight: bold;
}
.tabs button.active::after {
content: "";
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background-color: #3aa1f8;
}
.leader-list {
width: 100%;
height: 410px;
flex-grow: 1;
overflow: auto;
scrollbar-width: none;
color: #fff;
}
.leader-list::-webkit-scrollbar {
width: 4px;
}
.leader-list::-webkit-scrollbar-track {
background: transparent;
}
.leader-list::-webkit-scrollbar-thumb {
background: #3aa1f8;
border-radius: 2px;
}
.leader-item {
display: flex;
align-items: center;
padding: 10px 5px;
border-bottom: 1px solid rgba(58, 161, 248, 0.2);
}
.avatar {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 15px;
flex-shrink: 0;
}
.info {
display: flex;
flex-direction: column;
gap: 5px;
}
.name {
display: flex;
align-items: baseline;
gap: 8px;
}
.en-name {
font-size: 16px;
font-weight: bold;
}
.cn-name {
font-size: 14px;
color: #a9c2e0;
}
.stats {
font-size: 12px;
color: #a9c2e0;
display: flex;
gap: 20px;
}
.right-panel {
flex-grow: 1;
position: relative;
}
.key-node-recognition {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.background-svg-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.content-wrapper {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
height: 100%;
padding: 15px 20px;
box-sizing: border-box;
}
.graph-title {
text-align: center;
font-size: 20px;
font-weight: bold;
color: #cce7ff;
letter-spacing: 2px;
margin: 0 0 5px 0;
text-shadow: 0 0 5px rgba(58, 161, 248, 0.5);
}
.chart-container {
flex-grow: 1;
width: 100%;
height: calc(100% - 100px);
}
.timeline-container {
width: 100%;
height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
box-sizing: border-box;
}
.time-label {
font-size: 12px;
color: #a9c2e0;
}
.timeline-track {
flex-grow: 1;
height: 4px;
background: linear-gradient(90deg, #1b62a9, #3aa1f8, #1b62a9);
margin: 0 15px;
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
}
.timeline-point-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 20px;
cursor: pointer;
}
.timeline-point {
width: 10px;
height: 10px;
background-color: #8dc5ff;
border-radius: 50%;
border: 1px solid #cce7ff;
transition: transform 0.3s ease;
position: relative;
}
.timeline-point-wrapper:hover .timeline-point {
transform: scale(1.5);
}
.timeline-point.active {
background-color: #ffc94d;
border-color: #fff;
transform: scale(1.3);
}
.active-pin {
width: 20px;
height: 24px;
background-image: url('data:image/svg+xml;utf8,<svg width="20" height="24" viewBox="0 0 20 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 24L15 14H5L10 24Z" fill="%23FFC94D"/><path d="M1.5 8C1.5 4.41015 4.41015 1.5 8 1.5H12C15.5899 1.5 18.5 4.41015 18.5 8V11C18.5 12.6569 17.1569 14 15.5 14H4.5C2.84315 14 1.5 12.6569 1.5 11V8Z" fill="%23FFC94D" stroke="white"/></svg>');
position: absolute;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
position: relative;
background-color: #0d1b38;
padding: 20px;
border-radius: 8px;
border: 1px solid #3a95ff;
box-shadow: 0 0 25px rgba(58, 149, 255, 0.5);
width: 50vw;
height: 45vh;
display: flex;
flex-direction: column;
}
.close-btn {
position: absolute;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
top: 10px;
right: 15px;
border: none;
color: #a7c5d4;
background: none;
font-size: 24px;
cursor: pointer;
}
.details-chart-container {
width: 100%;
flex-grow: 1;
}
</style>

View File

@ -1,90 +1,486 @@
<template>
<div>
<!-- 1. 顶部介绍图片 -->
<div>
<img src="../assets/images/instruction.png" alt="" class="intruduction">
<img src="@/assets/images/instruction.png" alt="系统介绍" class="intruduction" />
</div>
<!-- 2. 第一行布局容器 (布局不变) -->
<div class="leader-containner1">
<div class="leader-show">
<img src="../assets/images/leader-show.png" alt="" style="margin-top: -6px;">
<div class="all-leader">
<!-- 添加四个选择按钮 -->
<button v-for="(item, index) in buttonList" :key="index" :class="{ active: activeButton === index }"
@click="changeData(index)">
{{ item }}
</button>
<!-- 意见领袖列表容器 -->
<div class="leader-data">
<!-- v-if="timePointDataIndex === 0" 暂时保留如果后续有时间点切换逻辑 -->
<div v-if="timePointDataIndex === 0" class="leader-list-wrapper">
<!-- 使用 v-for 循环渲染筛选后的意见领袖 -->
<div v-for="leader in filteredLeaders" :key="leader.name" class="leader-item">
<img :src="leader.avatar" alt="avatar" class="leader-avatar" />
<div class="leader-info">
<p class="leader-name">{{ leader.name }}</p>
<span class="leader-stat">粉丝数量{{ leader.followers }}</span>
<span class="leader-stat">发帖总数{{ leader.posts }}</span>
</div>
</div>
</div>
<!-- 其他时间点占位符 -->
<div v-if="timePointDataIndex === 1">时间点 2 的数据信息</div>
<div v-if="timePointDataIndex === 2">时间点 3 的数据信息</div>
<div v-if="timePointDataIndex === 3">时间点 4 的数据信息</div>
<div v-if="timePointDataIndex === 4">时间点 5 的数据信息</div>
</div>
</div>
</div>
<!-- 时间节点图 -->
<div class="leader-radio">
<!-- 标题 -->
<div>
<img src="../assets/images/peiluoxi.png" alt="" style="margin-left: 125px;">
<!-- 区域1: 意见领袖列表 -->
<div class="left-panel">
<h2 class="panel-title">意见领袖抽展示</h2>
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab"
:class="{ active: activeTab === tab }"
@click="activeTab = tab"
>
{{ tab }}
</button>
</div>
<div>
</div>
<div class="leader-time">
<div class="leader-list">
<div v-for="leader in filteredVisibleLeaders" :key="leader.id" class="leader-item">
<img :src="leader.avatar" :alt="leader.name" class="avatar" />
<div class="info">
<div class="name">
<span class="en-name">{{ leader.name }}</span>
<span v-if="leader.chineseName" class="cn-name">{{ leader.chineseName }}</span>
</div>
<div class="stats">
<span>粉丝数量: {{ leader.followers }}</span>
<span>发帖总数: {{ leader.posts }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 领袖分析 -->
<div class="leader-ansys">
<div><img src="../assets/images/leader-ansys.png" alt="" style="margin-top: -6px;"></div>
<!-- 区域2: 佩洛西图谱 (视觉效果已更新) -->
<div class="right-panel">
<div class="key-node-recognition">
<div class="background-svg-wrapper">
<svg
width="100%"
height="100%"
viewBox="0 0 800 540"
preserveAspectRatio="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient
id="paint0_linear_bg"
x1="0"
y1="167.1"
x2="800"
y2="167.1"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#063D71" stop-opacity="0.2" />
<stop offset="1" stop-color="#081E38" stop-opacity="0.8" />
</linearGradient>
<linearGradient
id="paint1_linear_border"
x1="400"
y1="0"
x2="400"
y2="540"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#3AA1F8" />
<stop offset="1" stop-color="#3AA1F8" stop-opacity="0.2" />
</linearGradient>
</defs>
<path
d="M798 0.5H2C1.17159 0.5 0.500003 1.17158 0.5 2V538C0.5 538.828 1.17159 539.5 2 539.5H798C798.828 539.5 799.5 538.828 799.5 538V2C799.5 1.17157 798.828 0.5 798 0.5Z"
fill="url(#paint0_linear_bg)"
fill-opacity="0.48"
stroke="url(#paint1_linear_border)"
/>
</svg>
</div>
<div class="content-wrapper">
<h1 class="graph-title">佩洛西系列事件</h1>
<div ref="chartContainer" class="chart-container"></div>
<div class="timeline-container">
<span class="time-label">2022.07.31 00:00:00</span>
<div class="timeline-track">
<div
v-for="point in timePoints"
:key="point.id"
class="timeline-point-wrapper"
@click="onTimePointClick(point.id)"
>
<div class="timeline-point" :class="{ active: activeTimePoint === point.id }">
<div v-if="activeTimePoint === point.id" class="active-pin"></div>
</div>
</div>
</div>
<span class="time-label">2022.08.01 00:00:00</span>
</div>
</div>
</div>
</div>
<!-- 区域3: 领袖分析 -->
<LeaderAnalysis :chart-data="analysisChartData" />
</div>
<!-- 第二行布局容器 (布局不变) -->
<div class="leader-containner2">
<div class="event-hot">
<img src="../assets/images/evenhot.png" alt="" style="margin-top: -6px;">
<EventHeatmap @show-details="openDetailsModal" />
<PostDynamics />
<WordCloud :word-data="wordCloudData" />
</div>
<!-- 详情弹窗 -->
<div v-if="showDetailsModal" class="modal-overlay" @click="closeDetailsModal">
<div class="modal-content" @click.stop>
<button class="close-btn" @click="closeDetailsModal">×</button>
<div ref="detailsChart" class="details-chart-container"></div>
</div>
<div class="leader-post">
</div>
<div class="words">
<img src="../assets/images/words.png" alt="" style="width: 100%;height: 308px;margin-top: -6px;">
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { ref, onMounted, onUnmounted, nextTick, computed, watch } from "vue";
import * as echarts from "echarts/core";
import { GraphChart } from "echarts/charts";
import { TitleComponent, TooltipComponent } from "echarts/components";
import { CanvasRenderer } from "echarts/renderers";
import { SVGRenderer } from "echarts/renderers";
import { cropImageToCircle } from "@/utils/transform";
echarts.use([TitleComponent, TooltipComponent, GraphChart, SVGRenderer]);
const allGraphNodes = ref([]);
//
import LeaderAnalysis from "./KeyNodeRecognition1/LeaderAnalysis.vue";
import EventHeatmap from "./KeyNodeRecognition1/EventHeatmap.vue";
import PostDynamics from "./KeyNodeRecognition1/PostDynamics.vue";
import WordCloud from "./KeyNodeRecognition1/WordCloud.vue";
//
const buttonList = ['全部', '新闻媒体', '自媒体', '政府官号'];
//
const activeButton = ref(0);
// ===================================================================
//
// ===================================================================
const activeTimePoint = ref(1);
const timePoints = ref(Array.from({ length: 10 }, (_, i) => ({ id: i + 1 })));
//
const changeData = (index) => {
activeButton.value = index;
// [] 10
const allLeaderData = ref([
{
id: "Israel Defense Forces",
name: "Israel Defense Forces",
chineseName: null,
followers: "299.8万",
posts: "2.4万",
avatar: new URL("@/assets/images/Israel.png", import.meta.url).toString(),
category: "政府官号"
},
{
id: "Emmanuel Macron",
name: "Emmanuel Macron",
chineseName: null,
followers: "1018.6万",
posts: "1.4万",
avatar: new URL("@/assets/images/Emmanuel.png", import.meta.url).toString(),
category: "政府官号"
},
{
id: "President Biden Archived",
name: "President Biden Archived",
chineseName: null,
followers: "3670.7万",
posts: "1万",
avatar: new URL("@/assets/images/Biden.png", import.meta.url).toString(),
category: "政府官号"
},
{
id: "Secretary Antony Blinken",
name: "Secretary Antony Blinken",
chineseName: null,
followers: "223万",
posts: "6082",
avatar: new URL("@/assets/images/Antony.png", import.meta.url).toString(),
category: "政府官号"
},
{
id: "Paul Golding",
name: "Paul Golding",
chineseName: null,
followers: "27万",
posts: "4.1万",
avatar: new URL("@/assets/images/Golding.png", import.meta.url).toString(),
category: "政府官号"
},
{
id: "Andy Ngo",
name: "Andy Ngo",
chineseName: null,
followers: "167.8万",
posts: "5.5万",
avatar: new URL("@/assets/images/Andy.png", import.meta.url).toString(),
category: "自媒体"
},
{
id: "Joe Truzman",
name: "Joe Truzman",
chineseName: null,
followers: "11.4万",
posts: "3.8万",
avatar: new URL("@/assets/images/Truzman.png", import.meta.url).toString(),
category: "自媒体"
},
{
id: "Jackson Hinkle 🇺🇸",
name: "Jackson Hinkle 🇺🇸",
chineseName: null,
followers: "304.9万",
posts: "2.9万",
avatar: new URL("@/assets/images/Jackson.png", import.meta.url).toString(),
category: "自媒体"
},
{
id: "Rep. Matt Gaetz",
name: "Rep. Matt Gaetz",
chineseName: null,
followers: "316.4万",
posts: "1.4万",
avatar: new URL("@/assets/images/Matt.png", import.meta.url).toString(),
category: "自媒体"
},
{
id: "OSINTdefender",
name: "OSINTdefender",
chineseName: null,
followers: "133.5万",
posts: "4.8万",
avatar: new URL("@/assets/images/OSINTdefender.png", import.meta.url).toString(),
category: "新闻媒体"
}
]);
// [] 10
const nodeCoordinates = {
reuters: { x: 150, y: 150 },
spectator: { x: 350, y: 350 },
indopacific: { x: 600, y: 450 },
liuxiaoming: { x: 400, y: 120 },
huxijin: { x: 200, y: 500 },
mickwallace: { x: 480, y: 550 },
jiushiniya: { x: 650, y: 180 },
levi_godman: { x: 850, y: 250 },
bidishalolo: { x: 800, y: 500 },
indobosss: { x: 600, y: 600 }
};
const visibleLeaders = computed(() => allLeaderData.value.slice(0, activeTimePoint.value));
const tabs = ref(["全部", "新闻媒体", "自媒体", "政府官号"]);
const activeTab = ref("全部");
const filteredVisibleLeaders = computed(() => {
if (activeTab.value === "全部") return visibleLeaders.value;
return visibleLeaders.value.filter((leader) => leader.category === activeTab.value);
});
//
const chartContainer = ref(null);
let myChart = null;
const chartOptions = {
tooltip: {},
animationDurationUpdate: 1500,
animationEasingUpdate: "quinticInOut",
series: [
{
type: "graph",
layout: "none",
roam: true,
symbolSize: 50,
label: { show: false },
edgeSymbol: ["none", "none"],
symbolClip: true, //
symbolKeepAspect: true, //
edgeSymbolSize: [4, 10],
lineStyle: { width: 1, color: "rgba(0, 191, 255, 0.4)", opacity: 0.9 },
categories: [
{
name: "Leader",
symbolSize: 70,
itemStyle: {
borderColor: "rgba(0, 191, 255, 0.8)",
borderWidth: 3,
shadowBlur: 15,
shadowColor: "rgba(0, 191, 255, 0.7)"
}
},
{
name: "User",
symbolSize: 25,
itemStyle: {
color: "#006A92",
borderColor: "#00BFFF",
borderWidth: 2,
shadowBlur: 5,
shadowColor: "rgba(0, 191, 255, 0.5)"
}
}
],
data: [],
links: []
}
]
};
const formatAllGraphNodes = async () => {
const tempResLeaderData = await Promise.all(
allLeaderData.value.map(async (leader) => {
const coords = nodeCoordinates[leader.id] || {
x: Math.random() * 1000,
y: Math.random() * 600
};
const base64_res = await cropImageToCircle(leader.avatar, 50);
const node = {
id: leader.id,
name: leader.name,
symbol: `image://${base64_res}`,
category: 0,
...coords
};
if (leader.id === "mickwallace") {
node.itemStyle = {
borderColor: "rgba(255, 220, 0, 0.9)",
shadowColor: "rgba(255, 220, 0, 0.8)",
shadowBlur: 20
};
}
return node;
})
);
const userNodes = Array.from({ length: 40 }, (_, i) => ({
id: `user_${i}`,
name: `User ${i}`,
x: Math.random() * 1000,
y: Math.random() * 600,
category: 1
}));
allGraphNodes.value = [...tempResLeaderData, ...userNodes];
};
// []
const allGraphLinks = computed(() => [
{ source: "reuters", target: "spectator" },
{ source: "reuters", target: "indopacific" },
{ source: "liuxiaoming", target: "huxijin" },
{ source: "huxijin", target: "mickwallace" },
{ source: "spectator", target: "liuxiaoming" },
{ source: "jiushiniya", target: "levi_godman" },
{ source: "bidishalolo", target: "indobosss" }
//...
]);
// ===================================================================
//
// ===================================================================
const onTimePointClick = (pointId) => {
activeTimePoint.value = pointId;
};
const updateGraphData = (currentVisibleLeaders) => {
if (!myChart) return;
const leadersSet = new Set(currentVisibleLeaders.map((l) => l.id));
const visibleNodes = allGraphNodes.value.filter(
(node) => node.category === 1 || leadersSet.has(node.id)
);
const nodeIds = new Set(visibleNodes.map((n) => n.id));
const visibleLinks = allGraphLinks.value.filter(
(link) => nodeIds.has(link.source) && nodeIds.has(link.target)
);
myChart.setOption({ series: [{ data: visibleNodes, links: visibleLinks }] });
};
watch(
visibleLeaders,
(newVisibleLeaders) => {
if (myChart) {
updateGraphData(newVisibleLeaders);
}
},
{ deep: true, immediate: true }
);
// ===================================================================
//
// ===================================================================
const analysisChartData = ref([
{
title: "平均发帖数",
unit: "数量",
max: 10,
series: [
{ name: "领袖", value: 6.4 },
{ name: "所有用户", value: 0.46 }
]
},
{
title: "帖子平均生存周期",
unit: "天数",
max: 10,
series: [
{ name: "领袖", value: 2.19 },
{ name: "所有用户", value: 0.46 }
]
},
{
title: "平均粉丝数",
unit: "天数",
max: 10,
series: [
{ name: "领袖", value: 2.19 },
{ name: "所有用户", value: 0.46 }
]
}
]);
const wordCloudData = ref([
{ text: "佩洛西", size: "large", color: "#56a9de", top: "28%", left: "70%" },
{ text: "中国", size: "large", color: "#56a9de", top: "70%", left: "22%" },
{ text: "中国人民解放军", size: "medium", color: "#cdeeff", top: "15%", left: "40%" },
{ text: "中美关系", size: "medium", color: "#cdeeff", top: "50%", left: "60%" },
{ text: "台海和平", size: "medium", color: "#27c1a8", top: "80%", left: "65%" }
]);
const showDetailsModal = ref(false);
const detailsChart = ref(null);
let myDetailsChart = null;
const openDetailsModal = (chartConfig) => {
showDetailsModal.value = true;
nextTick(() => {
if (detailsChart.value) {
myDetailsChart = echarts.init(detailsChart.value);
myDetailsChart.setOption(chartConfig.option);
}
});
};
const closeDetailsModal = () => {
showDetailsModal.value = false;
if (myDetailsChart) {
myDetailsChart.dispose();
myDetailsChart = null;
}
};
// ===================================================================
//
// ===================================================================
onMounted(async () => {
await formatAllGraphNodes();
if (chartContainer.value) {
myChart = echarts.init(chartContainer.value, null, { renderer: "svg" });
// "Hu Xijin"
const firstData = allGraphNodes.value.filter(
(item) =>
(item.name === "Israel Defense Forces" && item.category === 0) || item.category === 1
);
myChart.setOption({
...chartOptions,
series: [
{
...chartOptions.series[0],
data: firstData,
links: allGraphLinks.value
}
]
});
window.addEventListener("resize", myChart.resize);
}
});
onUnmounted(() => {
if (myChart) {
window.removeEventListener("resize", myChart.resize);
myChart.dispose();
}
if (myDetailsChart) {
myDetailsChart.dispose();
}
});
</script>
<style scoped>
@ -93,153 +489,263 @@ const changeData = (index) => {
margin-top: 0px;
border-radius: 2px;
}
.leader-containner1 {
.leader-containner1,
.leader-containner2 {
display: flex;
flex-direction: row;
gap: 10px;
}
.leader-containner2{
display: flex;
flex-direction: row;
}
.leader-show {
width: 352px;
height: 540px;
background-color: #04142166;
border-style: solid;
border-width: 0px;
border-image: linear-gradient(to bottom, #3AA1F8, #3AA1F833) 1;
border-radius: 2px;
/* 添加内阴影 */
box-shadow: 0px 0px 18px 0px #0A2E55 inset;
/* 添加背景模糊 */
backdrop-filter: blur(4px);
/* 为了兼容 Safari 浏览器 */
-webkit-backdrop-filter: blur(4px);
}
.all-leader {
width: 305px;
height: 450px;
margin-left: 24px;
font-size: 0;
/* 解决 inline-block 元素间的间隙问题 */
}
.all-leader button {
height: 24px;
margin-left: 0px;
font-family: OPPOSans;
font-weight: 300;
font-size: 14px;
line-height: 18px;
letter-spacing: 0%;
text-align: center;
cursor: pointer;
background-color: #04142166;
color: #E1F4FF;
border: 1px solid #1C588F;
border-radius: 0;
/* 确保默认没有圆角 */
display: inline-block;
/* 让按钮横向排列 */
vertical-align: middle;
/* 垂直居中对齐 */
}
/* 第一个按钮添加左圆角 */
.all-leader button:first-child {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
/* 最后一个按钮添加右圆角 */
.all-leader button:nth-child(4) {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
/* 激活按钮样式 */
.all-leader button.active {
background-color: #236291;
}
.leader-radio {
width: 800px;
height: 540px;
margin-left: 10px;
background-color: #04142166;
border-style: solid;
border-width: 0px;
border-image: linear-gradient(to bottom, #3AA1F8, #3AA1F833) 1;
border-radius: 2px;
/* 添加内阴影 */
box-shadow: 0px 0px 18px 0px #0A2E55 inset;
/* 添加背景模糊 */
backdrop-filter: blur(4px);
/* 为了兼容 Safari 浏览器 */
-webkit-backdrop-filter: blur(4px);
}
.leader-ansys {
width: 372px;
height: 542px;
margin-left: 10px;
border-radius: 2px;
background-color: #04142166;
border-style: solid;
border-width: 0px;
border-image: linear-gradient(to bottom, #3AA1F8, #3AA1F833) 1;
border-radius: 2px;
/* 添加内阴影 */
box-shadow: 0px 0px 18px 0px #0A2E55 inset;
/* 添加背景模糊 */
backdrop-filter: blur(4px);
/* 为了兼容 Safari 浏览器 */
-webkit-backdrop-filter: blur(4px);
}
.event-hot{
width: 352px;
height:296px;
margin-top: 10px;
border-radius: 2px;
background-color: #04142166;
border-style: solid;
border-width: 0px;
border-image: linear-gradient(to bottom, #3AA1F8, #3AA1F833) 1;
border-radius: 2px;
/* 添加内阴影 */
box-shadow: 0px 0px 18px 0px #0A2E55 inset;
/* 添加背景模糊 */
backdrop-filter: blur(4px);
/* 为了兼容 Safari 浏览器 */
-webkit-backdrop-filter: blur(4px);
}
.leader-post{
width: 800px;
height: 296px;
margin-top: 10px;
margin-left: 10px;
border-radius: 2px;
background-color: #04142166;
border-style: solid;
border-width: 0px;
border-image: linear-gradient(to bottom, #3AA1F8, #3AA1F833) 1;
border-radius: 2px;
/* 添加内阴影 */
box-shadow: 0px 0px 18px 0px #0A2E55 inset;
/* 添加背景模糊 */
backdrop-filter: blur(4px);
/* 为了兼容 Safari 浏览器 */
-webkit-backdrop-filter: blur(4px);
}
.words{
width: 372px;
height: 296px;
.leader-containner2 {
margin-top: 10px;
margin-left: 10px;
}
.left-panel {
width: 350px;
flex-shrink: 0;
background-color: rgba(6, 45, 90, 0.3);
border: 1px solid #1a8bff;
display: flex;
flex-direction: column;
padding: 15px;
}
.panel-title {
font-size: 18px;
font-weight: bold;
text-align: center;
padding: 10px;
margin: 0 0 15px 0;
background: linear-gradient(
to right,
rgba(58, 161, 248, 0),
rgba(58, 161, 248, 0.3),
rgba(58, 161, 248, 0)
);
border-top: 1px solid #3aa1f8;
border-bottom: 1px solid #3aa1f8;
color: #fff;
}
.tabs {
display: flex;
margin-bottom: 15px;
border-bottom: 2px solid #1a5a9c;
}
.tabs button {
background: none;
border: none;
color: #a9c2e0;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.tabs button.active {
color: #fff;
font-weight: bold;
}
.tabs button.active::after {
content: "";
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background-color: #3aa1f8;
}
.leader-list {
width: 100%;
height: 410px;
flex-grow: 1;
overflow: auto;
scrollbar-width: none;
color: #fff;
}
.leader-list::-webkit-scrollbar {
width: 4px;
}
.leader-list::-webkit-scrollbar-track {
background: transparent;
}
.leader-list::-webkit-scrollbar-thumb {
background: #3aa1f8;
border-radius: 2px;
}
.leader-item {
display: flex;
align-items: center;
padding: 10px 5px;
border-bottom: 1px solid rgba(58, 161, 248, 0.2);
}
.avatar {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 15px;
flex-shrink: 0;
}
.info {
display: flex;
flex-direction: column;
gap: 5px;
}
.name {
display: flex;
align-items: baseline;
gap: 8px;
}
.en-name {
font-size: 16px;
font-weight: bold;
}
.cn-name {
font-size: 14px;
color: #a9c2e0;
}
.stats {
font-size: 12px;
color: #a9c2e0;
display: flex;
gap: 20px;
}
.right-panel {
flex-grow: 1;
position: relative;
}
.key-node-recognition {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.background-svg-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.content-wrapper {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
height: 100%;
padding: 15px 20px;
box-sizing: border-box;
}
.graph-title {
text-align: center;
font-size: 20px;
font-weight: bold;
color: #cce7ff;
letter-spacing: 2px;
margin: 0 0 5px 0;
text-shadow: 0 0 5px rgba(58, 161, 248, 0.5);
}
.chart-container {
flex-grow: 1;
width: 100%;
height: calc(100% - 100px);
}
.timeline-container {
width: 100%;
height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
box-sizing: border-box;
}
.time-label {
font-size: 12px;
color: #a9c2e0;
}
.timeline-track {
flex-grow: 1;
height: 4px;
background: linear-gradient(90deg, #1b62a9, #3aa1f8, #1b62a9);
margin: 0 15px;
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
}
.timeline-point-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 20px;
cursor: pointer;
}
.timeline-point {
width: 10px;
height: 10px;
background-color: #8dc5ff;
border-radius: 50%;
border: 1px solid #cce7ff;
transition: transform 0.3s ease;
position: relative;
}
.timeline-point-wrapper:hover .timeline-point {
transform: scale(1.5);
}
.timeline-point.active {
background-color: #ffc94d;
border-color: #fff;
transform: scale(1.3);
}
.active-pin {
width: 20px;
height: 24px;
background-image: url('data:image/svg+xml;utf8,<svg width="20" height="24" viewBox="0 0 20 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 24L15 14H5L10 24Z" fill="%23FFC94D"/><path d="M1.5 8C1.5 4.41015 4.41015 1.5 8 1.5H12C15.5899 1.5 18.5 4.41015 18.5 8V11C18.5 12.6569 17.1569 14 15.5 14H4.5C2.84315 14 1.5 12.6569 1.5 11V8Z" fill="%23FFC94D" stroke="white"/></svg>');
position: absolute;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
position: relative;
background-color: #0d1b38;
padding: 20px;
border-radius: 8px;
border: 1px solid #3a95ff;
box-shadow: 0 0 25px rgba(58, 149, 255, 0.5);
width: 50vw;
height: 45vh;
display: flex;
flex-direction: column;
}
.close-btn {
position: absolute;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
top: 10px;
right: 15px;
border: none;
color: #a7c5d4;
background: none;
font-size: 24px;
cursor: pointer;
}
.details-chart-container {
width: 100%;
flex-grow: 1;
}
</style>