Signed-off-by: changyunju <2743319061@qq.com>
This commit is contained in:
		
							parent
							
								
									9f39908deb
								
							
						
					
					
						commit
						288a17892c
					
				| 
						 | 
					@ -1,64 +1,68 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="main-container">
 | 
					  <div class="opinion-leader-layout">
 | 
				
			||||||
    <!-- ========================================================== -->
 | 
					    <!-- 顶部描述 -->
 | 
				
			||||||
    <!-- 左侧区域: 意见领袖列表 (OpinionLeaders) -->
 | 
					    <header class="main-header">
 | 
				
			||||||
    <!-- ========================================================== -->
 | 
					      评估、高影响节点识别。特色在于评估节点重要性时有机结合了节点微观影响力与全局影响力,提升了意...
 | 
				
			||||||
    <div class="left-panel">
 | 
					    </header>
 | 
				
			||||||
      <h2 class="panel-title">意见领袖抽展示</h2>
 | 
					
 | 
				
			||||||
      <div class="tabs">
 | 
					    <div class="main-content">
 | 
				
			||||||
        <button 
 | 
					      <!-- ==================== 左侧面板: 意见领袖列表 ==================== -->
 | 
				
			||||||
          v-for="tab in tabs" 
 | 
					      <div class="left-panel">
 | 
				
			||||||
          :key="tab" 
 | 
					        <h2 class="panel-title">意见领袖抽展示</h2>
 | 
				
			||||||
          :class="{ active: activeTab === tab }" 
 | 
					        <div class="tabs">
 | 
				
			||||||
          @click="activeTab = tab"
 | 
					          <button 
 | 
				
			||||||
        >
 | 
					            v-for="tab in tabs" 
 | 
				
			||||||
          {{ tab }}
 | 
					            :key="tab" 
 | 
				
			||||||
        </button>
 | 
					            :class="{ active: activeTab === tab }" 
 | 
				
			||||||
      </div>
 | 
					            @click="activeTab = tab"
 | 
				
			||||||
      <div class="leader-list">
 | 
					          >
 | 
				
			||||||
        <!-- v-for 循环的是根据 activeTab 筛选后的可见领袖 -->
 | 
					            {{ tab }}
 | 
				
			||||||
        <div v-for="leader in filteredVisibleLeaders" :key="leader.id" class="leader-item">
 | 
					          </button>
 | 
				
			||||||
          <img :src="leader.avatar" :alt="leader.name" class="avatar">
 | 
					        </div>
 | 
				
			||||||
          <div class="info">
 | 
					        <div class="leader-list">
 | 
				
			||||||
            <div class="name">
 | 
					          <div v-for="leader in filteredVisibleLeaders" :key="leader.id" class="leader-item">
 | 
				
			||||||
              <span class="en-name">{{ leader.name }}</span>
 | 
					            <img :src="leader.avatar" :alt="leader.name" class="avatar">
 | 
				
			||||||
              <span v-if="leader.chineseName" class="cn-name">{{ leader.chineseName }}</span>
 | 
					            <div class="info">
 | 
				
			||||||
            </div>
 | 
					              <div class="name">
 | 
				
			||||||
            <div class="stats">
 | 
					                <span class="en-name">{{ leader.name }}</span>
 | 
				
			||||||
              <span>粉丝数量: {{ leader.followers }}</span>
 | 
					                <span v-if="leader.chineseName" class="cn-name">{{ leader.chineseName }}</span>
 | 
				
			||||||
              <span>发帖总数: {{ leader.posts }}</span>
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div class="stats">
 | 
				
			||||||
 | 
					                <span>粉丝数量: {{ leader.followers }}</span>
 | 
				
			||||||
 | 
					                <span>发帖总数: {{ leader.posts }}</span>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <!-- ========================================================== -->
 | 
					      <!-- ==================== 右侧面板: 网络图与时间轴 ==================== -->
 | 
				
			||||||
    <!-- 右侧区域: 佩洛西图谱 (PelosiGraph) -->
 | 
					      <div class="right-panel">
 | 
				
			||||||
    <!-- ========================================================== -->
 | 
					        <!-- 这个组件现在被整合到右侧面板中 -->
 | 
				
			||||||
    <div class="right-panel">
 | 
					        <div class="key-node-recognition">
 | 
				
			||||||
      <div class="key-node-recognition">
 | 
					          <div class="background-svg-wrapper">
 | 
				
			||||||
        <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">
 | 
					            <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>
 | 
					              <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)" />
 | 
					              <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>
 | 
					            </svg>
 | 
				
			||||||
        </div>
 | 
					          </div>
 | 
				
			||||||
        <div class="content-wrapper">
 | 
					          <div class="content-wrapper">
 | 
				
			||||||
          <h1 class="graph-title">佩洛西系列事件</h1>
 | 
					            <h1 class="main-title">佩洛西系列事件</h1>
 | 
				
			||||||
          <!-- ECharts 图表容器 -->
 | 
					            <!-- 图表容器 -->
 | 
				
			||||||
          <div ref="chartContainer" class="chart-container"></div>
 | 
					            <div ref="chartContainer" class="chart-container"></div>
 | 
				
			||||||
          <!-- 时间轴 -->
 | 
					            <!-- 时间轴 -->
 | 
				
			||||||
          <div class="timeline-container">
 | 
					            <div class="timeline-container">
 | 
				
			||||||
            <span class="time-label">2022.07.31 00:00:00</span>
 | 
					              <span class="time-label">2022.07.31 00:00:00</span>
 | 
				
			||||||
            <div class="timeline-track">
 | 
					              <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 class="timeline-point" :class="{ active: activeTimePoint === point.id }">
 | 
				
			||||||
                  <div v-if="activeTimePoint === point.id" class="active-pin"></div>
 | 
					                    <div v-if="activeTimePoint === point.id" class="active-pin"></div>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
 | 
					              <span class="time-label">2022.08.01 00:00:00</span>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <span class="time-label">2022.08.01 00:00:00</span>
 | 
					 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
| 
						 | 
					@ -67,7 +71,7 @@
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script setup>
 | 
					<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 * as echarts from 'echarts/core';
 | 
				
			||||||
import { GraphChart } from 'echarts/charts';
 | 
					import { GraphChart } from 'echarts/charts';
 | 
				
			||||||
import { TitleComponent, TooltipComponent } from 'echarts/components';
 | 
					import { TitleComponent, TooltipComponent } from 'echarts/components';
 | 
				
			||||||
| 
						 | 
					@ -75,32 +79,36 @@ import { CanvasRenderer } from 'echarts/renderers';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
echarts.use([TitleComponent, TooltipComponent, GraphChart, CanvasRenderer]);
 | 
					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 tabs = ref(['全部', '新闻媒体', '自媒体', '政府官号']);
 | 
				
			||||||
const activeTab = 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(() => {
 | 
					const filteredVisibleLeaders = computed(() => {
 | 
				
			||||||
  if (activeTab.value === '全部') {
 | 
					  if (activeTab.value === '全部') {
 | 
				
			||||||
    return visibleLeaders.value;
 | 
					    return visibleLeaders.value;
 | 
				
			||||||
| 
						 | 
					@ -108,124 +116,289 @@ const filteredVisibleLeaders = computed(() => {
 | 
				
			||||||
  return visibleLeaders.value.filter(leader => leader.category === activeTab.value);
 | 
					  return visibleLeaders.value.filter(leader => leader.category === activeTab.value);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 5. 右侧图谱状态与逻辑
 | 
					
 | 
				
			||||||
const chartContainer = ref(null);
 | 
					// 4. 定义图表的全量节点和连线
 | 
				
			||||||
let myChart = null;
 | 
					const allGraphNodes = [
 | 
				
			||||||
// 全量图谱数据
 | 
					  ...allLeaderData.map(leader => ({
 | 
				
			||||||
const allGraphNodes = computed(() => [
 | 
					    id: leader.id,
 | 
				
			||||||
  ...allLeaderData.value.map(leader => ({ 
 | 
					    name: leader.name,
 | 
				
			||||||
      id: leader.id, name: leader.name, symbol: `image://${leader.avatar}`, 
 | 
					    symbol: `image://${leader.avatar}`,
 | 
				
			||||||
      symbolSize: 50, category: leader.category === '新闻媒体' ? 1 : 0 
 | 
					    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. 当时间轴被点击时,更新核心状态
 | 
					// 5. ECharts 基础配置
 | 
				
			||||||
const onTimePointClick = (pointId) => {
 | 
					const chartOptions = {
 | 
				
			||||||
    activeTimePoint.value = pointId;
 | 
					  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 = () => {
 | 
					const updateViewForTimePoint = (timePoint) => {
 | 
				
			||||||
 | 
					  // 1. 更新左侧列表的数据
 | 
				
			||||||
 | 
					  visibleLeaders.value = allLeaderData.slice(0, timePoint);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 2. 更新右侧图表的数据
 | 
				
			||||||
  if (!myChart) return;
 | 
					  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 leadersToShowIds = visibleLeaders.value.map(l => l.id);
 | 
				
			||||||
  myChart.setOption({ 
 | 
					  const leadersSet = new Set(leadersToShowIds);
 | 
				
			||||||
      series: [{ 
 | 
					
 | 
				
			||||||
          data: currentVisibleNodes, 
 | 
					  const visibleLinks = allGraphLinks.filter(link => 
 | 
				
			||||||
          links: currentVisibleLinks 
 | 
					    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: visibleNodes, links: visibleLinks }]
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// C. ECharts 初始化
 | 
					// --- 生命周期与侦听器 ---
 | 
				
			||||||
onMounted(() => {
 | 
					onMounted(() => {
 | 
				
			||||||
  if (chartContainer.value) {
 | 
					  if (chartContainer.value) {
 | 
				
			||||||
    myChart = echarts.init(chartContainer.value);
 | 
					    myChart = echarts.init(chartContainer.value);
 | 
				
			||||||
    myChart.setOption({
 | 
					    myChart.setOption(chartOptions);
 | 
				
			||||||
      tooltip: {}, animationDurationUpdate: 1000,
 | 
					    updateViewForTimePoint(activeTimePoint.value); // 初始加载
 | 
				
			||||||
      series: [{
 | 
					    window.addEventListener('resize', resizeChart);
 | 
				
			||||||
        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);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// D. 侦听核心状态的变化,自动更新图表
 | 
					onBeforeUnmount(() => {
 | 
				
			||||||
watch(activeTimePoint, () => {
 | 
					  window.removeEventListener('resize', resizeChart);
 | 
				
			||||||
    if (myChart) {
 | 
					  if (myChart) myChart.dispose();
 | 
				
			||||||
      updateGraphData();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onUnmounted(() => {
 | 
					watch(activeTimePoint, (newTimePoint) => {
 | 
				
			||||||
    if (myChart) {
 | 
					  updateViewForTimePoint(newTimePoint);
 | 
				
			||||||
      window.removeEventListener('resize', myChart.resize);
 | 
					 | 
				
			||||||
      myChart.dispose();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const resizeChart = () => {
 | 
				
			||||||
 | 
					  if (myChart) myChart.resize();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped>
 | 
					<style scoped>
 | 
				
			||||||
.main-container {
 | 
					/* 全局布局 */
 | 
				
			||||||
    display: flex;
 | 
					.opinion-leader-layout {
 | 
				
			||||||
    flex-direction: row;
 | 
					  width: 1200px;
 | 
				
			||||||
    gap: 10px;
 | 
					  height: 700px;
 | 
				
			||||||
    padding: 10px;
 | 
					  background-color: #031024;
 | 
				
			||||||
    background-color: #031024;
 | 
					  color: #cce7ff;
 | 
				
			||||||
    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
 | 
					  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
 | 
				
			||||||
    color: #fff;
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  border: 1px solid #1a8bff;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* 左侧面板样式 (从 OpinionLeaders.vue 合并) */
 | 
					.main-header {
 | 
				
			||||||
.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; }
 | 
					  padding: 10px 20px;
 | 
				
			||||||
.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; }
 | 
					  font-size: 14px;
 | 
				
			||||||
.tabs { display: flex; margin-bottom: 15px; border-bottom: 2px solid #1a5a9c; }
 | 
					  background-color: rgba(10, 35, 68, 0.5);
 | 
				
			||||||
.tabs button { background: none; border: none; color: #a9c2e0; padding: 8px 16px; font-size: 14px; cursor: pointer; transition: all 0.3s ease; position: relative; }
 | 
					  border-bottom: 1px solid #1a8bff;
 | 
				
			||||||
.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-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 { width: 4px; }
 | 
				
			||||||
.leader-list::-webkit-scrollbar-track { background: transparent; }
 | 
					.leader-list::-webkit-scrollbar-track { background: transparent; }
 | 
				
			||||||
.leader-list::-webkit-scrollbar-thumb { background: #3aa1f8; border-radius: 2px; }
 | 
					.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 合并) */
 | 
					.leader-item {
 | 
				
			||||||
.right-panel { flex-grow: 1; height: 540px; position: relative; }
 | 
					  display: flex;
 | 
				
			||||||
.key-node-recognition { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
 | 
					  align-items: center;
 | 
				
			||||||
.background-svg-wrapper { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
 | 
					  padding: 10px 5px;
 | 
				
			||||||
.content-wrapper { position: relative; z-index: 2; display: flex; flex-direction: column; height: 100%; padding: 15px 20px; box-sizing: border-box; }
 | 
					  border-bottom: 1px solid rgba(58, 161, 248, 0.2);
 | 
				
			||||||
.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; }
 | 
					.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; }
 | 
					.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-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-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 { 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-wrapper:hover .timeline-point { transform: scale(1.5); }
 | 
				
			||||||
.timeline-point.active { background-color: #ffc94d; border-color: #fff; transform: scale(1.3); }
 | 
					.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>
 | 
					</style>
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user