修改第一模块
1. 传播领袖识别页面修改 2. 传播桥梁节点识别添加
							
								
								
									
										9
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
/dist/*
 | 
			
		||||
.local
 | 
			
		||||
.output.js
 | 
			
		||||
/node_modules/**
 | 
			
		||||
 | 
			
		||||
**/*.svg
 | 
			
		||||
**/*.sh
 | 
			
		||||
 | 
			
		||||
/public/*
 | 
			
		||||
							
								
								
									
										9
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
{
 | 
			
		||||
  "semi": true,
 | 
			
		||||
  "singleQuote": false,
 | 
			
		||||
  "printWidth":100,
 | 
			
		||||
  "trailingComma": "none",
 | 
			
		||||
  "tabWidth": 2,
 | 
			
		||||
  "endOfLine": "auto",
 | 
			
		||||
  "arrowParens": "always"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
{
 | 
			
		||||
  "recommendations": ["Vue.volar"]
 | 
			
		||||
  "recommendations": ["Vue.volar"],
 | 
			
		||||
  
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										17
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						| 
						 | 
				
			
			@ -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",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 28 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/images/Antony.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 286 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/images/Biden.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 247 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/images/Emmanuel.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 331 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/images/Golding.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 351 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/images/Israel.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 153 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/images/Jackson.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 257 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/images/Matt.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 281 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/images/OSINTdefender.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 157 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/images/Truzman.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 340 KiB  | 
							
								
								
									
										23
									
								
								src/utils/transform.js
									
									
									
									
									
										Normal 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;
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||