Signed-off-by: changyunju <2743319061@qq.com>

This commit is contained in:
changyunju 2025-06-24 09:31:34 +08:00
parent 9f39908deb
commit 288a17892c

View File

@ -1,8 +1,12 @@
<template>
<div class="main-container">
<!-- ========================================================== -->
<!-- 左侧区域: 意见领袖列表 (OpinionLeaders) -->
<!-- ========================================================== -->
<div class="opinion-leader-layout">
<!-- 顶部描述 -->
<header class="main-header">
评估高影响节点识别特色在于评估节点重要性时有机结合了节点微观影响力与全局影响力提升了意...
</header>
<div class="main-content">
<!-- ==================== 左侧面板: 意见领袖列表 ==================== -->
<div class="left-panel">
<h2 class="panel-title">意见领袖抽展示</h2>
<div class="tabs">
@ -16,7 +20,6 @@
</button>
</div>
<div class="leader-list">
<!-- v-for 循环的是根据 activeTab 筛选后的可见领袖 -->
<div v-for="leader in filteredVisibleLeaders" :key="leader.id" class="leader-item">
<img :src="leader.avatar" :alt="leader.name" class="avatar">
<div class="info">
@ -33,26 +36,26 @@
</div>
</div>
<!-- ========================================================== -->
<!-- 右侧区域: 佩洛西图谱 (PelosiGraph) -->
<!-- ========================================================== -->
<!-- ==================== 右侧面板: 网络图与时间轴 ==================== -->
<div class="right-panel">
<!-- 这个组件现在被整合到右侧面板中 -->
<div class="key-node-recognition">
<div class="background-svg-wrapper">
<!-- 背景SVG与之前相同 -->
<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>
<!-- ECharts 图表容器 -->
<h1 class="main-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 v-for="point in timePoints" :key="point.id" class="timeline-point-wrapper" @click="activeTimePoint = point.id">
<div class="timeline-point" :class="{ active: activeTimePoint === point.id }">
<div v-if="activeTimePoint === point.id" class="active-pin"></div>
</div>
@ -64,10 +67,11 @@
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
import { ref, onMounted, onBeforeUnmount, watch, computed } from 'vue';
import * as echarts from 'echarts/core';
import { GraphChart } from 'echarts/charts';
import { TitleComponent, TooltipComponent } from 'echarts/components';
@ -75,32 +79,36 @@ import { CanvasRenderer } from 'echarts/renderers';
echarts.use([TitleComponent, TooltipComponent, GraphChart, CanvasRenderer]);
// ===================================================================
// ()
// ===================================================================
// --- ---
const chartContainer = ref(null);
let myChart = null;
// 1.
const activeTimePoint = ref(1);
const timePoints = ref(Array.from({ length: 10 }, (_, i) => ({ id: i + 1 })));
// 2.
const allLeaderData = ref([
{ id: '5', name: 'Hu Xijin', chineseName: '胡锡进', followers: '53.8万', posts: '54', avatar: 'https://i.imgur.com/Y3vH2oP.png', category: '自媒体' },
{ id: 'bidishalolo', name: 'bidishalolo', followers: '2387', posts: '8380', avatar: 'https://i.imgur.com/6a5A466.png', category: '自媒体' },
{ id: 'indo-pacific', name: 'Indo-Pacific News', followers: '11.5万', posts: '11.3万', avatar: 'https://i.imgur.com/PkooCvB.png', category: '新闻媒体' },
{ id: '1', name: 'The Spectator Index', followers: '233.5万', posts: '56', avatar: 'https://i.imgur.com/rS2aP3s.png', category: '新闻媒体' },
{ id: 'mickwallace', name: 'Mick Wallace', followers: '24.8万', posts: '10259', avatar: 'https://i.imgur.com/gKk9p3j.png', category: '自媒体' },
//
]);
// 3.
const visibleLeaders = computed(() => {
return allLeaderData.value.slice(0, activeTimePoint.value);
});
// 4.
const tabs = ref(['全部', '新闻媒体', '自媒体', '政府官号']);
const activeTab = ref('全部');
const timePoints = ref(Array.from({ length: 10 }, (_, i) => ({ id: i + 1 })));
const activeTimePoint = ref(1); //
// --- ---
// 1.
const allLeaderData = [
{ id: '5', name: 'Hu Xijin', chineseName: '胡锡进', avatar: 'https://i.imgur.com/Y3vH2oP.png', followers: '53.8万', posts: '54', category: '自媒体' },
{ id: 'bidishalolo', name: 'bidishalolo', avatar: 'https://i.imgur.com/6a5A466.png', followers: '2387', posts: '8380', category: '自媒体' },
{ id: 'indo-pacific', name: 'Indo-Pacific News', avatar: 'https://i.imgur.com/PkooCvB.png', followers: '11.5万', posts: '11.3万', category: '新闻媒体' },
{ id: '1', name: 'The Spectator Index', avatar: 'https://i.imgur.com/rS2aP3s.png', followers: '233.5万', posts: '56', category: '新闻媒体' },
{ id: 'mickwallace', name: 'Mick Wallace', avatar: 'https://i.imgur.com/gKk9p3j.png', followers: '24.8万', posts: '10259', category: '自媒体' },
{ id: '0', name: 'China Coast Guard', chineseName: '中国海警', avatar: 'https://i.imgur.com/rN5V6fD.png', followers: '120万', posts: '88', category: '政府官号' },
{ id: '2', name: 'Nancy Pelosi', avatar: 'https://i.imgur.com/g0t6GqB.png', followers: '1780万', posts: '320', category: '政府官号' },
{ id: '6', name: 'Cat Avatar User', avatar: 'https://i.imgur.com/QhT8k5q.png', followers: '1.2万', posts: '950', category: '自媒体' },
{ id: '7', name: 'User C', avatar: 'https://i.imgur.com/7bO2A5a.png', followers: '8000', posts: '120', category: '自媒体' },
{ id: '8', name: 'Default User', avatar: 'https://i.imgur.com/tP2x2Jg.png', followers: '500', posts: '30', category: '自媒体' },
];
// 2.
const visibleLeaders = ref([]);
// 3. Tab
const filteredVisibleLeaders = computed(() => {
if (activeTab.value === '全部') {
return visibleLeaders.value;
@ -108,124 +116,289 @@ const filteredVisibleLeaders = computed(() => {
return visibleLeaders.value.filter(leader => leader.category === activeTab.value);
});
// 5.
const chartContainer = ref(null);
let myChart = null;
//
const allGraphNodes = computed(() => [
...allLeaderData.value.map(leader => ({
id: leader.id, name: leader.name, symbol: `image://${leader.avatar}`,
symbolSize: 50, category: leader.category === '新闻媒体' ? 1 : 0
// 4. 线
const allGraphNodes = [
...allLeaderData.map(leader => ({
id: leader.id,
name: leader.name,
symbol: `image://${leader.avatar}`,
symbolSize: 50,
category: leader.category === '政府官号' ? 0 : 1,
label: { show: leader.chineseName, color: '#fff' }
})),
]);
const allGraphLinks = ref([ { source: 'bidishalolo', target: '5' }, { source: 'indo-pacific', target: '1' } ]);
//
...Array.from({ length: 30 }, (_, i) => ({ id: `n${i}`, name: `User ${i}`, symbolSize: 15, category: 2 })),
];
// ===================================================================
//
// ===================================================================
const allGraphLinks = [
//
{ source: '5', target: '1' }, { source: '1', target: '2' }, { source: 'indo-pacific', target: '1' },
{ source: '0', target: '2' }, { source: 'mickwallace', target: '5' },
//
{ source: '5', target: 'n0' }, { source: '5', target: 'n1' },
{ source: 'bidishalolo', target: 'n2' },
{ source: 'indo-pacific', target: 'n3' }, { source: 'indo-pacific', target: 'n4' },
{ source: '1', target: 'n5' }, { source: '1', target: 'n6' },
{ source: 'mickwallace', target: 'n7' },
{ source: '0', target: 'n8' }, { source: '0', target: 'n9' }, { source: '0', target: 'n10' },
{ source: '2', target: 'n11' },
{ source: '6', target: 'n12' }, { source: '7', target: 'n13' }, { source: '8', target: 'n14' },
];
// A.
const onTimePointClick = (pointId) => {
activeTimePoint.value = pointId;
// 5. ECharts
const chartOptions = {
tooltip: {},
animationDurationUpdate: 1000,
animationEasingUpdate: 'quinticInOut',
series: [{
type: 'graph', layout: 'force', roam: true, draggable: true,
categories: [
{ name: '政府官号', itemStyle: { color: '#E06300', shadowBlur: 20, shadowColor: '#E06300', borderColor: '#FF8A2B', borderWidth: 2 } },
{ name: '新闻媒体/自媒体', itemStyle: { color: '#0A53B5', shadowBlur: 15, shadowColor: '#3AA1F8', borderColor: '#3AA1F8', borderWidth: 2 } },
{ name: '普通用户', itemStyle: { color: '#0B4B69', borderColor: '#1A8BFF', borderWidth: 1 } },
],
label: { position: 'right', formatter: '{b}', show: false },
lineStyle: { color: 'source', curveness: 0.1, opacity: 0.5 },
force: { repulsion: 120, edgeLength: 80, gravity: 0.1 },
emphasis: { focus: 'adjacency', lineStyle: { width: 5 } },
data: [], links: [],
}]
};
// B.
const updateGraphData = () => {
if (!myChart) return;
// `visibleLeaders` 线
const leadersSet = new Set(visibleLeaders.value.map(l => l.id));
const currentVisibleLinks = allGraphLinks.value.filter(link => leadersSet.has(link.source) && leadersSet.has(link.target));
const currentVisibleNodes = allGraphNodes.value.filter(node => leadersSet.has(node.id));
// --- ---
const updateViewForTimePoint = (timePoint) => {
// 1.
visibleLeaders.value = allLeaderData.slice(0, timePoint);
// 2.
if (!myChart) return;
const leadersToShowIds = visibleLeaders.value.map(l => l.id);
const leadersSet = new Set(leadersToShowIds);
const visibleLinks = allGraphLinks.filter(link =>
leadersSet.has(link.source) && leadersSet.has(link.target) // 线
);
const visibleNodeIds = new Set(leadersToShowIds);
//
// visibleLinks.forEach(link => {
// visibleNodeIds.add(link.source);
// visibleNodeIds.add(link.target);
// });
const visibleNodes = allGraphNodes.filter(node => visibleNodeIds.has(node.id));
// 使
myChart.setOption({
series: [{
data: currentVisibleNodes,
links: currentVisibleLinks
}]
series: [{ data: visibleNodes, links: visibleLinks }]
});
};
// C. ECharts
// --- ---
onMounted(() => {
if (chartContainer.value) {
myChart = echarts.init(chartContainer.value);
myChart.setOption({
tooltip: {}, animationDurationUpdate: 1000,
series: [{
type: 'graph', layout: 'force', roam: true,
categories: [
{ name: '自媒体/政府', itemStyle: { color: '#E06300', shadowBlur: 20, shadowColor: '#E06300', borderWidth: 2, borderColor: '#FF8A2B' } },
{ name: '新闻媒体', itemStyle: { color: '#0A53B5', shadowBlur: 15, shadowColor: '#3AA1F8', borderWidth: 2, borderColor: '#3AA1F8' } },
],
label: { show: false }, lineStyle: { color: 'source', curveness: 0.1, opacity: 0.5 },
force: { repulsion: 150, edgeLength: 100, gravity: 0.1 },
data: [], links: [],
}]
});
updateGraphData(); //
window.addEventListener('resize', myChart.resize);
myChart.setOption(chartOptions);
updateViewForTimePoint(activeTimePoint.value); //
window.addEventListener('resize', resizeChart);
}
});
// D.
watch(activeTimePoint, () => {
if (myChart) {
updateGraphData();
}
onBeforeUnmount(() => {
window.removeEventListener('resize', resizeChart);
if (myChart) myChart.dispose();
});
onUnmounted(() => {
if (myChart) {
window.removeEventListener('resize', myChart.resize);
myChart.dispose();
}
watch(activeTimePoint, (newTimePoint) => {
updateViewForTimePoint(newTimePoint);
});
const resizeChart = () => {
if (myChart) myChart.resize();
};
</script>
<style scoped>
.main-container {
display: flex;
flex-direction: row;
gap: 10px;
padding: 10px;
/* 全局布局 */
.opinion-leader-layout {
width: 1200px;
height: 700px;
background-color: #031024;
color: #cce7ff;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
color: #fff;
display: flex;
flex-direction: column;
border: 1px solid #1a8bff;
}
/* 左侧面板样式 (从 OpinionLeaders.vue 合并) */
.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; border-radius: 4px; }
.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;}
.main-header {
padding: 10px 20px;
font-size: 14px;
background-color: rgba(10, 35, 68, 0.5);
border-bottom: 1px solid #1a8bff;
}
.main-content {
display: flex;
flex-grow: 1;
overflow: hidden;
}
/* 左侧面板 */
.left-panel {
width: 350px;
flex-shrink: 0;
background-color: rgba(6, 45, 90, 0.3);
border-right: 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;
}
.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;
}
/* 滚动条样式 */
.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; }
/* 右侧面板样式 (从 PelosiGraph.vue 合并) */
.right-panel { flex-grow: 1; height: 540px; 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; }
.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;
}
.main-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, .time-label, .timeline-track, .timeline-point-wrapper, .timeline-point, .active-pin {
/* 时间轴样式与上一版完全相同 */
box-sizing: border-box;
}
.timeline-container { width: 100%; height: 50px; display: flex; align-items: center; justify-content: space-between; padding: 0 10px;}
.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-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%); }
.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%);}
</style>