Files
Situation-Awareness-Platfor…/src/views/Jyh/security/index.vue

2135 lines
59 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="index-module__NV_5cW__container">
<!-- 头部区域标题副标题武汉核心数据统计 -->
<div class="index-module__NV_5cW__header">
<div class="index-module__NV_5cW__floatingElements">
<div class="index-module__NV_5cW__floatingElement"></div>
<div class="index-module__NV_5cW__floatingElement"></div>
<div class="index-module__NV_5cW__floatingElement"></div>
</div>
<div class="index-module__NV_5cW__titleSection">
<!-- <div class="index-module__NV_5cW__titleContainer">-->
<!-- <h1 class="index-module__NV_5cW__title">全球开源风险态势感知监控中心</h1>-->
<!-- </div>-->
<!-- <div class="index-module__NV_5cW__subtitle">-->
<!-- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"-->
<!-- stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"-->
<!-- class="lucide lucide-users" aria-hidden="true">-->
<!-- <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>-->
<!-- <path d="M16 3.128a4 4 0 0 1 0 7.744"></path>-->
<!-- <path d="M22 21v-2a4 4 0 0 0-3-3.87"></path>-->
<!-- <circle cx="9" cy="7" r="4"></circle>-->
<!-- </svg>-->
<!-- <span>汇聚武汉开源力量共建城市开源生态</span>-->
<!-- </div>-->
<!-- 武汉4项核心开源数据统计 -->
<div class="stats-glass-bar">
<div class="stat-node">
<span class="stat-value">120+</span>
<span class="stat-desc">开源企业</span>
</div>
<div class="stat-separator"></div>
<div class="stat-node">
<span class="stat-value">30+</span>
<span class="stat-desc">高校参与</span>
</div>
<div class="stat-separator"></div>
<div class="stat-node">
<span class="stat-value">300+</span>
<span class="stat-desc">社区活动</span>
</div>
<div class="stat-separator"></div>
<div class="stat-node">
<span class="stat-value">200+</span>
<span class="stat-desc">开源项目</span>
</div>
</div>
</div>
</div>
<!-- 内容区域ECharts图表模块 + 项目列表 -->
<div class="index-module__NV_5cW__content">
<!-- ECharts图表区域 -->
<div class="index-module__NV_5cW__chartsSection">
<!-- <div class="index-module__NV_5cW__sectionHeader">-->
<!-- <h2 class="index-module__NV_5cW__sectionTitle">-->
<!-- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"-->
<!-- stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"-->
<!-- class="lucide lucide-chart-pie" aria-hidden="true">-->
<!-- <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>-->
<!-- <circle cx="12" cy="10" r="3"></circle>-->
<!-- </svg>-->
<!-- <span>全球开源风险态势感知监控中心数据可视化</span>-->
<!-- </h2>-->
<!-- </div>-->
<!-- 图表网格布局新增多个模块 -->
<div class="index-module__NV_5cW__chartsGrid">
<!-- 全球威胁情报分析地图 - 移到最上面 -->
<div class="index-module__NV_5cW__chartCard chartCard__wide">
<h3 class="chartCard__title">🛡 全球威胁情报分析与监测</h3>
<div class="chartCard__container map-container">
<ThreatIntelMap />
</div>
</div>
<div class="index-module__NV_5cW__chartCard">
<h3 class="chartCard__title">行业分布</h3>
<div class="chartCard__container">
<div class="num-empty" v-if="isEmpty.university"></div>
<div v-else id="cweAttackSurfaceRadar" class="chart"></div>
</div>
</div>
<div class="index-module__NV_5cW__chartCard">
<h3 class="chartCard__title">高危组件 Top 10</h3>
<div class="chartCard__container">
<div class="num-empty" v-if="isEmpty.community"></div>
<div v-else id="highRiskComponentRank" class="chart"></div>
</div>
</div>
<div class="index-module__NV_5cW__chartCard list-card">
<h3 class="chartCard__title">情报流</h3>
<!-- 新增ref + 鼠标事件 -->
<div class="chartCard__container list-container"
ref="scrollContainer1"
@mouseenter="pauseScroll(1)"
@mouseleave="resumeScroll(1)">
<div class="project-list">
<div class="project-item" v-for="(item, index) in feedList" :key="index">
<span class="project-rank" :class="item.type">{{ item.type }}</span>
<div class="project-info">
<span class="project-name">{{ item.content }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="index-module__NV_5cW__chartCard chartCard__wide">
<div class="chartCard__header">
<h3 class="chartCard__title">CVE 月度趋势</h3>
<!-- 视图切换按钮用于切换全球和区域视图 -->
<div class="chartCard__actions">
<button
class="chartCard__actionBtn"
:class="{ 'chartCard__actionBtn--active': cveView === 'global' }"
@click="switchCveView('global')"
>
全球
</button>
<button
class="chartCard__actionBtn"
:class="{ 'chartCard__actionBtn--active': cveView === 'regional' }"
@click="switchCveView('regional')"
>
区域
</button>
</div>
</div>
<div class="chartCard__container">
<div class="num-empty" v-if="isEmpty.cveData"></div>
<div v-else id="CVEChart" class="chart"></div>
</div>
</div>
</div>
</div>
<!-- 编程语言使用趋势堆叠面积图 -->
<div class="index-module__NV_5cW__chartCard chartCard__wide mb-[1.5rem]">
<h3 class="chartCard__title">漏洞语言分布</h3>
<div class="chartCard__container">
<div class="num-empty" v-if="isEmpty.languageTrend"></div>
<div v-else id="languageTrendChart" class="chart"></div>
</div>
</div>
<!-- 高危漏洞占比图表 -->
<div class="index-module__NV_5cW__chartCard mb-[1.5rem]">
<h3 class="chartCard__title">漏洞等级分布</h3>
<div class="chartCard__container">
<div class="num-empty" v-if="isEmpty.vulnerabilitySeverity"></div>
<div v-else id="vulnerabilitySeverityChart" class="chart"></div>
</div>
</div>
<!-- 气泡图 -->
<div class="index-module__NV_5cW__chartCard mb-[1.5rem]">
<h3 class="chartCard__title">编程语言安全态势</h3>
<div class="chartCard__container">
<div class="num-empty" v-if="isEmpty.scatterData"></div>
<div v-else id="scatterChart" class="chart"></div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, nextTick, watch, onUnmounted } from 'vue';
import * as echarts from 'echarts';
import { usePageResize } from '@/utils/hooks/usePageResize';
import ThreatIntelMap from './components/ThreatIntelMap.vue';
// 页面尺寸响应式
const { widthType } = usePageResize();
// 1. 武汉优秀开源项目数据
const excellentProjects = ref([
{ name: '调问DWSurvey', desc: '问卷表单', org: '极推科技(武汉)有限公司' },
{ name: 'Eova', desc: '低代码开发平台', org: '武汉简码科技有限公司' },
{ name: 'ClickVisual', desc: '轻量级日志查询、分析、报警可视化平台', org: '武汉初心科技有限公司' },
{ name: 'meta2d.js', desc: '2D图元组成的可视化引擎', org: '武汉乐吾乐科技有限责任公司' },
{ name: 'Juno', desc: '分布式微服务管理系统', org: '武汉斗鱼鱼乐网络科技有限公司' },
{ name: 'Opentalent 开源北极星', desc: '开源人才评价系统', org: '湖北大学' },
{ name: 'fountain', desc: '仓颉编程语言实现的JAVA Spring框架的国产替代', org: '江汉大学' },
{ name: 'deepin', desc: '深度操作系统', org: '武汉深之度科技有限公司' },
{ name: 'RustSBI', desc: '安全引导程序', org: '华中科技大学' },
{ name: 'NextDenovo', desc: '高效且精确的长读长纠错与组装工具', org: '武汉希望组生物科技有限公司' }
]);
// 情报流数据
const feedList = ref([
{
type: 'CISA',
content: 'CISA KEV新增: WordPress 的 OneClick Chat to Order 插件,存在不安全的直接对象引用漏洞 (CVE-2025-13526) '
},
{
type: '投毒',
content: '投毒监控: npm 基建级包大规模投毒事件影响chalk, debug, ansi-styles 等 18 个包'
},
{
type: 'CNVD',
content: 'CNVD预警: Google Chrome信息泄露漏洞CNVD-2025-3038304'
},
{
type: 'GitHub',
content: 'GitHub安全通告: MovableType 软件的 ContentType 页面 "编辑分类集" 功能存在存储型跨站脚本漏洞'
},
{
type: '修复',
content: '漏洞修复: Node.js 发布 v20.11.0 版本,修复 3 个高危漏洞'
},
{
type: '供应链',
content: '供应链预警: Maven 中央仓库发现 12 个伪造开源组件,包含后门程序'
}
]);
// 2. 2025武汉市优秀开源软件项目
const excellentProjects2025 = ref([
{ name: '调问DWSurvey', desc: '问卷表单', org: '极推科技(武汉)有限公司' },
{ name: 'Eova', desc: '低代码开发平台', org: '武汉简码科技有限公司' },
{ name: 'ClickVisual', desc: '轻量级日志查询、分析、报警可视化平台', org: '武汉初心科技有限公司' },
{ name: 'meta2d.js', desc: '2D图元组成的可视化引擎', org: '武汉乐吾乐科技有限责任公司' },
{ name: 'Juno', desc: '分布式微服务管理系统', org: '武汉斗鱼鱼乐网络科技有限公司' },
{ name: 'Opentalent 开源北极星', desc: '开源人才评价系统', org: '湖北大学' },
{ name: 'fountain', desc: '仓颉编程语言实现的JAVA Spring框架的国产替代', org: '江汉大学' }
]);
// 3. 图表数据定义
const chartData = ref({
// 高校开源参与分布(包含华中科大、武大、湖北大学、江汉大学等)
university: [
{ value: 30, name: '华中科技大学' },
{ value: 25, name: '武汉大学' },
{ value: 15, name: '湖北大学' },
{ value: 10, name: '江汉大学' },
{ value: 8, name: '武汉理工大学' },
{ value: 7, name: '华中师范大学' },
{ value: 5, name: '其他高校' }
],
// 开源社区区域分布
community: {
xAxis: ['武昌区', '洪山区', '东湖高新区', '江汉区', '汉阳区', '青山区'],
data: [28, 35, 42, 18, 15, 12]
},
// OSS Compass开源项目排行提取OpenRank总分左边数值
ossCompass: {
yAxis: [
'openharmony/docs',
'mindspore/mindspore',
'openharmony/arkui_ace_engine',
'apache/doris',
'openharmony/interface_sdk-js',
'anolis-challenge/activity-school',
'PaddlePaddle/Paddle',
'StarRocks/starrocks',
'openharmony/xts_acts',
'ant-design/ant-design'
],
data: [91.55, 67.35, 62.07, 48.82, 44.06, 39.77, 39.55, 34.2, 30.39, 30.37],
// 完整分数数据用于tooltip
fullData: [
{ openRank: '91.55 | 30.86', quality: '92.06 | 8.905', wxb: '70.69 | -14.8', score: '84.77 | 8.32' },
{ openRank: '67.35 | 27.41', quality: '96.92 | 7.17', wxb: '98.915 | 7.1', score: '87.73 | 13.89' },
{ openRank: '62.07 | 17.98', quality: '71.69 | 2.885', wxb: '76.06 | 10.9', score: '69.94 | 10.59' },
{ openRank: '48.82 | 10.14', quality: '67.26 | 0.573', wxb: '30.5 | 7.3', score: '48.86 | 6.00' },
{ openRank: '44.06 | 11.49', quality: '32.5 | 3.14', wxb: '67.55 | 16.7', score: '48.04 | 10.44' },
{ openRank: '39.77 | 37.64', quality: '31.05 | 0.215', wxb: '24.798 | -18.9', score: '31.87 | 6.32' },
{ openRank: '39.55 | 7.25', quality: '68.55 | -2.937', wxb: '68.26 | -17.3', score: '58.79 | -4.33' },
{ openRank: '34.2 | 2.08', quality: '49.07 | -0.877', wxb: '47.07 | 9.9', score: '43.45 | 3.70' },
{ openRank: '30.39 | 10.97', quality: '29.8 | 5.633', wxb: '27.045 | 4.3', score: '29.08 | 6.97' },
{ openRank: '30.37 | 8.57', quality: '37.1 | 4.295', wxb: '35.097 | 6.8', score: '34.19 | 6.55' }
]
},
// 武汉开源开发者排行榜模拟数据前10名按贡献值排序
developer: {
yAxis: ['张XX', '李XX', '王XX', '刘XX', '陈XX', '杨XX', '黄XX', '赵XX', '吴XX', '周XX'],
data: [980, 850, 720, 650, 580, 520, 480, 420, 380, 350]
},
// 武汉市语言与技术竞争趋势2021-2025多系列
languageTech: {
xAxis: ['2021', '2022', '2023', '2024', '2025'],
series: [
{ name: 'Java', data: [85, 82, 78, 75, 72] },
{ name: 'Python', data: [78, 80, 85, 88, 90] },
{ name: 'Rust', data: [20, 35, 45, 60, 75] },
{ name: '仓颉', data: [0, 5, 15, 30, 45] },
{ name: 'Go', data: [60, 65, 70, 72, 75] }
]
},
// 编程语言使用趋势数据
languageTrend: {
years: ['2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024'],
series: [
{
name: 'JavaScript',
type: 'established',
color: '#FAC858',
data: [65, 64, 69, 70, 69, 65, 61, 61]
},
{
name: 'Python',
type: 'established',
color: '#5470C6',
data: [32, 41, 49, 55, 52, 53, 54, 60]
},
{
name: 'HTML/CSS',
type: 'established',
color: '#91CC75',
data: [60, 55, 61, 61, 60, 54, 52, 52]
},
{
name: 'SQL',
type: 'established',
color: '#9254DE',
data: [42, 47, 56, 56, 54, 49, 52, 50]
},
{
name: 'Java',
type: 'established',
color: '#EE6666',
data: [47, 51, 50, 54, 49, 48, 49, 49]
},
{
name: 'TypeScript',
type: 'emerging',
color: '#73C0DE',
data: [12, 17, 25, 28, 29, 34, 34, 50]
},
{
name: 'Shell',
type: 'established',
color: '#52C41A',
data: [29, 29, 40, 39, 37, 34, 34, 41]
},
{
name: 'C++',
type: 'established',
color: '#FF6E00',
data: [17, 18, 20, 27, 23, 25, 25, 21]
},
{
name: 'C#',
type: 'established',
color: '#722ED1',
data: [20, 22, 24, 22, 21, 23, 21, 19]
},
{
name: 'Go',
type: 'emerging',
color: '#00DDFF',
data: [8, 12, 18, 19, 17, 19, 17, 14]
},
{
name: 'PHP',
type: 'declining',
color: '#F5222D',
data: [30, 26, 29, 27, 32, 20, 18, 15]
},
{
name: 'Rust',
type: 'emerging',
color: '#FC8452',
data: [2, 2, 5, 7, 6, 9, 10, 10]
}
]
},
// 高危组件数据(基于真实开源项目使用情况和漏洞统计)
highRiskComponent: [
{ name: 'Log4j2', cvss: 9.8, vulnCount: 15, usage: '1200万+', desc: 'CVE-2021-44228等严重RCE漏洞' },
{ name: 'Apache Struts2', cvss: 9.7, vulnCount: 12, usage: '850万+', desc: 'S2系列远程代码执行漏洞' },
{ name: 'OpenSSL', cvss: 9.6, vulnCount: 18, usage: '2500万+', desc: '心脏出血、幽灵猫等高危漏洞' },
{ name: 'Spring Framework', cvss: 9.5, vulnCount: 9, usage: '1800万+', desc: 'Spring4Shell等RCE漏洞' },
{ name: 'Jackson', cvss: 9.4, vulnCount: 11, usage: '1000万+', desc: '反序列化漏洞影响Java生态' },
{ name: 'Fastjson', cvss: 9.3, vulnCount: 14, usage: '600万+', desc: '多个反序列化RCE漏洞' },
{ name: 'Apache Commons', cvss: 9.2, vulnCount: 8, usage: '1500万+', desc: 'Collections反序列化漏洞' },
{ name: 'jQuery', cvss: 9.1, vulnCount: 6, usage: '3000万+', desc: 'XSS和原型污染漏洞' },
{ name: 'Lodash', cvss: 9.0, vulnCount: 5, usage: '2200万+', desc: '原型污染漏洞影响Node.js生态' },
{ name: 'Moment.js', cvss: 9.0, vulnCount: 4, usage: '1100万+', desc: 'ReDoS和原型污染漏洞' }
],
// 漏洞占比数据
vulnerabilitySeverity: {
categories: ['2024Q1', '2024Q2', '2024Q3', '2024Q4', '2025Q1', '2025Q2', '2025Q3', '2025Q4'],
series: [
{
name: '高危漏洞',
type: 'bar',
stack: 'total',
itemStyle: { color: '#FF4D4F' },
barWidth: 16,
data: [35, 42, 58, 45, 67, 78, 62, 85, 92]
},
{
name: '中危漏洞',
type: 'bar',
stack: 'total',
itemStyle: { color: '#FAAD14' },
barWidth: 16,
data: [65, 72, 88, 95, 112, 105, 120, 135, 128]
},
{
name: '低危漏洞',
type: 'bar',
stack: 'total',
itemStyle: { color: '#36CFC9' },
barWidth: 16,
data: [120, 135, 150, 142, 165, 180, 175, 190, 210]
}
]
},
// 行业分布数据
industryDistribution: [
{ value: 25.8, name: '人工智能' },
{ value: 18.4, name: '云计算' },
{ value: 15.6, name: '大数据' },
{ value: 12.2, name: '物联网' },
{ value: 9.6, name: '网络安全' },
{ value: 7.1, name: '区块链' },
{ value: 6.4, name: '工业互联网' },
{ value: 4.9, name: '移动开发' }
],
// 科技感配色方案
industryColorPalette: [
'#00f0ff', '#00b03e', '#ffeb00', '#ffab00',
'#ff5100', '#c0232a', '#a262f2', '#379bff'
],
// CVE图表数据
cveData: {
// 全球CVE2024年实际数据基于Excel数据
global: {
cveCount: [98402, 418, 7025, 3711, 5115, 3228, 3044, 2957, 2592, 1455, 4826, 4661],
acceleration: [0, -99.58, 1580.62, -47.17, 37.83, -36.89, -5.70, -2.86, -12.34, -43.87, 231.68, -3.42] // 环比增长率(%
},
// 中国区域CVE2024年数据约占全球30%
regional: {
cveCount: [29521, 125, 2108, 1113, 1535, 968, 913, 887, 778, 437, 1448, 1398],
acceleration: [0, -99.58, 1580.62, -47.17, 37.83, -36.89, -5.70, -2.86, -12.34, -43.87, 231.68, -3.42] // 环比增长率(%
}
},
// 气泡图数据
// 基于真实数据:[代码活跃度(X), 漏洞密度(Y), 漏洞总数(Size), 语言名称(Name)]
// 活跃度基于新增项目数量漏洞密度基于CWE分布统计
scatterData: [
[95, 75, 450, 'JavaScript'], // 最高活跃度9,345,046个项目
[90, 70, 380, 'Python'], // 高活跃度9,261,587个项目
[85, 65, 320, 'TypeScript'], // 高增长78.1%增长率
[75, 80, 420, 'Java'], // 传统强势3,520,215个项目
[45, 60, 280, 'C++'], // 中等活跃1,701,552个项目
[40, 55, 250, 'C#'], // 中等活跃1,478,463个项目
[35, 45, 180, 'PHP'], // 传统Web语言
[25, 25, 80, 'Go'], // 新兴语言,低漏洞密度
[15, 20, 45, 'Rust'], // 安全性较好的新语言
[60, 50, 200, 'Shell'], // 脚本语言
],
// 四象限分割点
midX: 50,
midY: 50,
// 四象限颜色定义
quadrantColors: {
// [高密度, 低活跃度] - 极高风险
Q1: '#BF232A',
// [高密度, 高活跃度] - 高风险,需密切关注
Q2: '#FF4900',
// [低密度, 低活跃度] - 低风险,但维护成本高
Q3: '#00B43E',
// [低密度, 高活跃度] - 健康生态
Q4: '#0063D0'
}
});
// 视图切换状态变量global: 全球, regional: 区域)
const cveView = ref('global');
// 空数据标识
const isEmpty = ref({
university: false,
community: false,
ossCompass: false,
developer: false,
languageTech: false,
highRiskComponent: false,
industryDistribution: false,
cveData: false
});
// 图表实例存储用于resize时销毁重绘
const chartInstances = ref<{ [key: string]: echarts.ECharts | null }>({
universityChart: null,
communityChart: null,
ossCompassChart: null,
developerChart: null,
languageTechChart: null,
highRiskComponentRank: null,
cweAttackSurfaceRadar: null,
CVEChart: null
});
// ===================== 新增:列表自动滚动逻辑 =====================
// 1. 定义滚动容器的ref
const scrollContainer1 = ref<HTMLDivElement | null>(null); // 第一个列表容器
const scrollContainer2 = ref<HTMLDivElement | null>(null); // 第二个列表容器
// 2. 存储定时器1/2定时器ID
const scrollTimers: { [key: number]: NodeJS.Timeout | null } = {
1: null,
2: null
};
// 3. 滚动配置
const scrollStep = 1; // 每次滚动的步长(像素,越小越流畅)
const scrollInterval = 20; // 滚动间隔(毫秒,越小越流畅)
// 4. 通用自动滚动函数
const autoScroll = (containerKey: number) => {
// 获取对应容器
const container = containerKey === 1 ? scrollContainer1.value : scrollContainer2.value;
if (!container) return;
// 获取列表内容元素project-list
const projectList = container.querySelector('.project-list') as HTMLDivElement;
if (!projectList) return;
// 若内容高度 ≤ 容器高度,不执行滚动
if (projectList.scrollHeight <= container.clientHeight) {
// 清除现有定时器
if (scrollTimers[containerKey]) {
clearInterval(scrollTimers[containerKey]!);
scrollTimers[containerKey] = null;
}
return;
}
// 清除现有定时器(避免重复)
if (scrollTimers[containerKey]) {
clearInterval(scrollTimers[containerKey]!);
}
// 启动定时器滚动
scrollTimers[containerKey] = setInterval(() => {
// 向下滚动步长
container.scrollTop += scrollStep;
// 滚动到底部时重置为0循环滚动
// 加1是为了兼容像素精度问题
if (projectList.scrollHeight - container.scrollTop <= container.clientHeight + 1) {
container.scrollTop = 0; // 直接重置也可以用平滑滚动container.scrollTo({ top: 0, behavior: 'smooth' })
}
}, scrollInterval);
};
// 5. 暂停滚动
const pauseScroll = (containerKey: number) => {
if (scrollTimers[containerKey]) {
clearInterval(scrollTimers[containerKey]!);
scrollTimers[containerKey] = null;
}
};
// 6. 恢复滚动
const resumeScroll = (containerKey: number) => {
autoScroll(containerKey);
};
// 7. 组件卸载时清除所有定时器
onUnmounted(() => {
Object.keys(scrollTimers).forEach(key => {
const k = Number(key);
if (scrollTimers[k]) {
clearInterval(scrollTimers[k]!);
scrollTimers[k] = null;
}
});
});
// CVE视图切换方法
const switchCveView = (view: 'global' | 'regional') => {
cveView.value = view;
nextTick(() => {
initCVEChart();
});
};
// 初始化CVE图表柱状图+折线图)
const initCVEChart = () => {
const el = document.getElementById('CVEChart');
if (!el) return;
if (chartInstances.value.CVEChart) {
chartInstances.value.CVEChart.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.CVEChart = myChart;
// 获取当前视图的数据
const currentData = chartData.value.cveData[cveView.value === 'global' ? 'global' : 'regional'];
const cveCount = currentData.cveCount;
const acceleration = currentData.acceleration;
// 月份数据
const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
myChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
crossStyle: {
color: '#999'
}
},
formatter: (params: any) => {
let res = `<div>${params[0].name}</div>`;
params.forEach((p: any) => {
if (p.seriesName === 'CVE数量') {
res += `<div>${p.marker}${p.seriesName}: ${p.value}</div>`;
} else {
res += `<div>${p.marker}${p.seriesName}: ${p.value}%</div>`;
}
});
return res;
}
},
legend: {
top: 0,
textStyle: { fontSize: 12 },
data: ['CVE数量', '环比增长率']
},
grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
xAxis: [
{
type: 'category',
data: months,
axisPointer: {
type: 'shadow'
},
axisLabel: { fontSize: 12 }
}
],
yAxis: [
{
type: 'value',
name: 'CVE数量',
nameTextStyle: { fontSize: 12, color: '#333' },
axisLabel: { fontSize: 12, color: '#666' },
position: 'left',
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
},
{
type: 'value',
name: '环比增长率(%)',
nameTextStyle: { fontSize: 12, color: '#333' },
axisLabel: { fontSize: 12, color: '#666' },
position: 'right',
splitLine: {
show: false
}
}
],
series: [
{
name: 'CVE数量',
type: 'bar',
data: cveCount,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#3b82f6' },
{ offset: 1, color: '#60a5fa' }
])
},
barWidth: 20,
label: {
show: true,
position: 'top',
formatter: '{c}',
fontSize: 10,
color: '#333'
}
},
{
name: '环比增长率',
type: 'line',
yAxisIndex: 1,
data: acceleration,
itemStyle: {
color: '#ef4444',
borderWidth: 2
},
lineStyle: {
width: 2,
color: '#ef4444'
},
symbol: 'circle',
symbolSize: 6,
smooth: true,
label: {
show: true,
position: 'top',
formatter: (params: any) => `${params.value}%`,
fontSize: 10,
color: '#333'
}
}
]
});
};
// ===================== 新增结束 =====================
// 初始化高校参与分布饼图
const initUniversityChart = () => {
const el = document.getElementById('universityChart');
if (!el) return;
if (chartInstances.value.universityChart) {
chartInstances.value.universityChart.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.universityChart = myChart;
myChart.setOption({
tooltip: {
trigger: 'item',
formatter: (params: any) => `${params.name}: ${params.value}%`
},
legend: {
// orient: 'vertical',
// left: 'left',
bottom: '0',
textStyle: { fontSize: 12 },
itemWidth: 10,
itemHeight: 10
},
series: [
{
name: '参与度',
type: 'pie',
radius: ['40%', '70%'],
data: chartData.value.university,
label: {
show: true,
formatter: '{b}: {c}%',
fontSize: 10
},
color: ['#3b82f6', '#8b5cf6', '#ec4899', '#10b981', '#f59e0b', '#6366f1', '#14b8a6']
}
],
grid: { left: 0, bottom: 0, top: 0, right: 0 }
});
};
// 初始化社区区域分布柱状图
const initCommunityChart = () => {
const el = document.getElementById('communityChart');
if (!el) return;
if (chartInstances.value.communityChart) {
chartInstances.value.communityChart.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.communityChart = myChart;
myChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: (params: any) => `${params[0].name}: ${params[0].value}`
},
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: {
type: 'category',
data: chartData.value.community.xAxis,
axisLabel: { fontSize: 12 }
},
yAxis: {
type: 'value',
name: '社区数量',
nameTextStyle: { fontSize: 12 },
axisLabel: { fontSize: 12 }
},
series: [
{
name: '开源社区数量',
type: 'bar',
data: chartData.value.community.data,
itemStyle: {
color: (params: any) => {
const colorList = ['#3b82f6', '#8b5cf6', '#ec4899', '#10b981', '#f59e0b', '#6366f1'];
return colorList[params.dataIndex];
}
}
}
]
});
};
// 初始化OSS Compass开源项目排行横向柱状图
const initOssCompassChart = () => {
const el = document.getElementById('ossCompassChart');
if (!el) return;
if (chartInstances.value.ossCompassChart) {
chartInstances.value.ossCompassChart.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.ossCompassChart = myChart;
myChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: (params: any) => {
const index = params[0].dataIndex;
const data = chartData.value.ossCompass.fullData[index];
return `
<div>${params[0].name}</div>
<div>OpenRank: ${data.openRank}</div>
<div>代码内生质量: ${data.quality}</div>
<div>WXB分数: ${data.wxb}</div>
<div>综合分数: ${data.score}</div>
`;
}
},
grid: { left: '15%', right: '4%', bottom: '3%', top: '3%', containLabel: true },
xAxis: {
type: 'value',
name: 'OpenRank总分',
nameTextStyle: { fontSize: 12 },
axisLabel: { fontSize: 12 }
},
yAxis: {
type: 'category',
data: chartData.value.ossCompass.yAxis,
axisLabel: { fontSize: 10, overflow: 'truncate', width: 120 }
},
series: [
{
name: 'OpenRank总分',
type: 'bar',
data: chartData.value.ossCompass.data,
itemStyle: { color: '#2951E0' },
label: {
show: true,
position: 'right',
formatter: '{c}',
fontSize: 10
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: 'rgba(41, 81, 224, 0.24)' },
{ offset: 1, color: 'rgba(41, 81, 224, 0.04)' }
])
}
}
]
});
};
// 初始化武汉开源开发者排行榜(横向柱状图)
const initDeveloperChart = () => {
const el = document.getElementById('developerChart');
if (!el) return;
if (chartInstances.value.developerChart) {
chartInstances.value.developerChart.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.developerChart = myChart;
myChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: (params: any) => `${params[0].name}: ${params[0].value}贡献值`
},
grid: { left: '10%', right: '4%', bottom: '3%', top: '3%', containLabel: true },
xAxis: {
type: 'value',
name: '贡献值',
nameTextStyle: { fontSize: 12 },
axisLabel: { fontSize: 12 }
},
yAxis: {
type: 'category',
data: chartData.value.developer.yAxis,
axisLabel: { fontSize: 12 }
},
series: [
{
name: '贡献值',
type: 'bar',
data: chartData.value.developer.data,
itemStyle: { color: '#8b5cf6' },
label: {
show: true,
position: 'right',
formatter: '{c}',
fontSize: 12
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: 'rgba(139, 92, 246, 0.24)' },
{ offset: 1, color: 'rgba(139, 92, 246, 0.04)' }
])
}
}
]
});
};
// 初始化行业分布图表(饼图)
const initIndustryDistributionChart = () => {
const el = document.getElementById('cweAttackSurfaceRadar');
if (!el) return;
if (chartInstances.value.cweAttackSurfaceRadar) {
chartInstances.value.cweAttackSurfaceRadar.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.cweAttackSurfaceRadar = myChart;
myChart.setOption({
tooltip: {
trigger: 'item',
formatter: (params: any) => `${params.name}: ${params.percent}% (${params.value}%)`
},
legend: {
bottom: '0',
textStyle: { fontSize: 12 },
itemWidth: 10,
itemHeight: 10
},
color: chartData.value.industryColorPalette,
series: [
{
name: '行业分布',
type: 'pie',
radius: ['40%', '70%'],
data: chartData.value.industryDistribution,
label: {
show: true,
formatter: '{b}: {d}%',
fontSize: 10
}
}
],
grid: { left: 0, bottom: 0, top: 0, right: 0 }
});
};
// 初始化高危组件Top 10图表横向柱状图
const initHighRiskComponentChart = () => {
const el = document.getElementById('highRiskComponentRank');
if (!el) return;
if (chartInstances.value.highRiskComponentRank) {
chartInstances.value.highRiskComponentRank.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.highRiskComponentRank = myChart;
// 提取组件名称和CVSS评分
const yAxisData = chartData.value.highRiskComponent.map(item => item.name);
const seriesData = chartData.value.highRiskComponent.map(item => ({
value: item.cvss,
vulnCount: item.vulnCount,
usage: item.usage,
desc: item.desc
}));
myChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: (params: any) => {
const data = chartData.value.highRiskComponent[params[0].dataIndex];
return `
<div style="padding: 4px;">
<div style="font-weight: bold; font-size: 14px;">${data.name}</div>
<div>CVSS评分: ${data.cvss}</div>
<div>漏洞数量: ${data.vulnCount}个</div>
<div>使用量: ${data.usage}</div>
<div>漏洞描述: ${data.desc}</div>
</div>
`;
}
},
grid: { left: '15%', right: '4%', bottom: '3%', top: '3%', containLabel: true },
xAxis: {
type: 'value',
name: 'CVSS评分',
nameTextStyle: { fontSize: 12 },
axisLabel: { fontSize: 12 },
max: 10
},
yAxis: {
type: 'category',
data: yAxisData,
axisLabel: { fontSize: 12 }
},
series: [
{
name: 'CVSS评分',
type: 'bar',
data: seriesData,
itemStyle: {
color: (params: any) => {
const value = params.value;
if (value >= 9.0) return '#ef4444'; // 红色 - 严重
if (value >= 7.0) return '#f97316'; // 橙色 - 高危
if (value >= 4.0) return '#eab308'; // 黄色 - 中危
return '#22c55e'; // 绿色 - 低危
}
},
label: {
show: true,
position: 'right',
formatter: '{c}',
fontSize: 12
}
}
]
});
};
// 初始化编程语言使用趋势堆叠面积图
const initLanguageTrendChart = () => {
const el = document.getElementById('languageTrendChart');
if (!el) return;
if (chartInstances.value.languageTrendChart) {
chartInstances.value.languageTrendChart.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.languageTrendChart = myChart;
// 准备堆叠面积图数据
const series = chartData.value.languageTrend.series.map(item => ({
name: item.name,
type: 'line',
stack: '总量',
areaStyle: {
color: item.color
},
lineStyle: {
width: 1,
color: item.color
},
symbol: 'none',
data: item.data
}));
myChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
formatter: (params: any) => {
let res = `<div>${params[0].name}年</div>`;
params.forEach((p: any) => {
res += `<div style="display:flex;align-items:center;"><span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${p.color};"></span>${p.seriesName}: ${p.value}%</div>`;
});
return res;
}
},
legend: {
top: 0,
textStyle: { fontSize: 12 },
data: chartData.value.languageTrend.series.map(item => item.name)
},
grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
xAxis: [
{
type: 'category',
boundaryGap: false,
data: chartData.value.languageTrend.years,
axisLabel: { fontSize: 12 }
}
],
yAxis: [
{
type: 'value',
name: '使用率(%)',
nameTextStyle: { fontSize: 12 },
axisLabel: { fontSize: 12 }
}
],
series: series
});
};
// 初始化高危漏洞占比图表
const initVulnerabilitySeverityChart = () => {
const el = document.getElementById('vulnerabilitySeverityChart');
if (!el) return;
if (chartInstances.value.vulnerabilitySeverityChart) {
chartInstances.value.vulnerabilitySeverityChart.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.vulnerabilitySeverityChart = myChart;
myChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: (params: any) => {
let res = `<div>${params[0].name}</div>`;
let total = 0;
params.forEach((p: any) => {
total += p.value;
});
params.forEach((p: any) => {
const percentage = ((p.value / total) * 100).toFixed(1);
res += `<div style="display:flex;align-items:center;"><span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${p.color};"></span>${p.seriesName}: ${p.value} (${percentage}%)</div>`;
});
return res;
}
},
legend: {
top: 0,
textStyle: { fontSize: 12 },
data: chartData.value.vulnerabilitySeverity.series.map(item => item.name)
},
grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
xAxis: [
{
type: 'category',
data: chartData.value.vulnerabilitySeverity.categories,
axisLabel: { fontSize: 12 }
}
],
yAxis: [
{
type: 'value',
name: '漏洞数量',
nameTextStyle: { fontSize: 12 },
axisLabel: { fontSize: 12 }
}
],
series: chartData.value.vulnerabilitySeverity.series
});
};
// 初始化气泡图
const initScatterChart = () => {
const el = document.getElementById('scatterChart');
if (!el) return;
if (chartInstances.value.scatterChart) {
chartInstances.value.scatterChart.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.scatterChart = myChart;
// 数据处理
const data = chartData.value.scatterData.map(item => ({
value: [item[0], item[1], item[2]],
name: item[3]
}));
// 计算四象限颜色
const getColor = (x: number, y: number) => {
if (x < chartData.value.midX && y >= chartData.value.midY) return chartData.value.quadrantColors.Q1; // 高密度, 低活跃度 - 极高风险
if (x >= chartData.value.midX && y >= chartData.value.midY) return chartData.value.quadrantColors.Q2; // 高密度, 高活跃度 - 高风险
if (x < chartData.value.midX && y < chartData.value.midY) return chartData.value.quadrantColors.Q3; // 低密度, 低活跃度 - 低风险
return chartData.value.quadrantColors.Q4; // 低密度, 高活跃度 - 健康生态
};
myChart.setOption({
tooltip: {
trigger: 'item',
formatter: (params: any) => {
return `
<div>${params.data.name}</div>
<div>代码活跃度: ${params.data.value[0]}</div>
<div>漏洞密度: ${params.data.value[1]}</div>
<div>漏洞总数: ${params.data.value[2]}</div>
`;
}
},
grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
xAxis: {
type: 'value',
name: '代码活跃度',
nameTextStyle: { fontSize: 12 },
axisLabel: { fontSize: 12 },
splitLine: { show: true }
},
yAxis: {
type: 'value',
name: '漏洞密度',
nameTextStyle: { fontSize: 12 },
axisLabel: { fontSize: 12 },
splitLine: { show: true }
},
series: [{
type: 'scatter',
data: data,
symbolSize: (val: any) => val[2] / 2, // 根据漏洞总数调整气泡大小
itemStyle: {
color: (params: any) => getColor(params.value[0], params.value[1])
},
label: {
show: true,
formatter: (params: any) => params.data.name,
position: 'inside',
fontSize: 10,
color: '#fff'
}
}]
});
};
// 初始化武汉市语言与技术竞争趋势(折线图)
const initLanguageTechChart = () => {
const el = document.getElementById('languageTechChart');
if (!el) return;
if (chartInstances.value.languageTechChart) {
chartInstances.value.languageTechChart.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.languageTechChart = myChart;
// 定义系列颜色
const colorList = ['#3b82f6', '#10b981', '#f59e0b', '#ec4899', '#8b5cf6'];
const series = chartData.value.languageTech.series.map((item, index) => ({
name: item.name,
type: 'line',
data: item.data,
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: colorList[index], width: 2 },
itemStyle: { color: colorList[index] },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: `${colorList[index]}33` },
{ offset: 1, color: `${colorList[index]}08` }
])
}
}));
myChart.setOption({
tooltip: {
trigger: 'axis',
formatter: (params: any) => {
let res = `<div>${params[0].axisValue}</div>`;
params.forEach((p: any) => {
res += `<div>${p.seriesName}: ${p.value}</div>`;
});
return res;
}
},
legend: {
top: 0,
textStyle: { fontSize: 12 }
},
grid: { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true },
xAxis: {
type: 'category',
data: chartData.value.languageTech.xAxis,
axisLabel: { fontSize: 12 }
},
yAxis: {
type: 'value',
name: '使用占比/热度',
nameTextStyle: { fontSize: 12 },
axisLabel: { fontSize: 12 }
},
series
});
};
// 统一初始化图表
const initCharts = () => {
// 空数据判断
isEmpty.value = {
university: chartData.value.university.length === 0,
community: chartData.value.community.data.every(item => item === 0),
ossCompass: chartData.value.ossCompass.data.every(item => item === 0),
developer: chartData.value.developer.data.every(item => item === 0),
languageTech: chartData.value.languageTech.series.every(s => s.data.every(d => d === 0)),
highRiskComponent: chartData.value.highRiskComponent.length === 0,
industryDistribution: chartData.value.industryDistribution.length === 0,
cveData: chartData.value.cveData.global.cveCount.every(item => item === 0) &&
chartData.value.cveData.regional.cveCount.every(item => item === 0),
languageTrend: chartData.value.languageTrend.series.every(s => s.data.every(d => d === 0)),
vulnerabilitySeverity: chartData.value.vulnerabilitySeverity.series.every(s => s.data.every(d => d === 0)),
scatterData: chartData.value.scatterData.length === 0
};
// 初始化各图表
if (!isEmpty.value.university) nextTick(() => initUniversityChart());
if (!isEmpty.value.community) nextTick(() => initCommunityChart());
if (!isEmpty.value.ossCompass) nextTick(() => initOssCompassChart());
if (!isEmpty.value.developer) nextTick(() => initDeveloperChart());
if (!isEmpty.value.languageTech) nextTick(() => initLanguageTechChart());
if (!isEmpty.value.highRiskComponent) nextTick(() => initHighRiskComponentChart());
if (!isEmpty.value.industryDistribution) nextTick(() => initIndustryDistributionChart());
if (!isEmpty.value.cveData) nextTick(() => initCVEChart());
if (!isEmpty.value.languageTrend) nextTick(() => initLanguageTrendChart());
if (!isEmpty.value.vulnerabilitySeverity) nextTick(() => initVulnerabilitySeverityChart());
if (!isEmpty.value.scatterData) nextTick(() => initScatterChart());
};
// 页面resize时重绘图表
watch(widthType, () => {
initCharts();
// 重新启动滚动防止resize后滚动失效
autoScroll(1);
autoScroll(2);
});
// 挂载时初始化
onMounted(() => {
setTimeout(() => {
initCharts();
// 启动列表自动滚动
autoScroll(1);
autoScroll(2);
}, 100);
});
</script>
<style lang="scss" scoped>
.index-module__NV_5cW__container {
background: #020617;
min-height: 100vh;
padding: 2rem 1rem;
position: relative;
overflow-x: hidden;
}
.index-module__NV_5cW__header {
width: 1400px;
text-align: center;
padding: 1rem;
position: relative;
margin: 0 auto 3rem;
}
.index-module__NV_5cW__floatingElements {
pointer-events: none;
opacity: 0.3;
position: absolute;
inset: 0;
overflow: hidden;
}
.index-module__NV_5cW__floatingElement {
background: linear-gradient(135deg, #3b82f60d, #9333ea0d);
border-radius: 50%;
width: 120px;
height: 120px;
animation: 30s linear infinite index-module__NV_5cW__float;
position: absolute;
}
.index-module__NV_5cW__floatingElement:first-child {
animation-delay: 0s;
top: 10%;
left: 10%;
}
.index-module__NV_5cW__floatingElement:nth-child(2) {
animation-delay: -7s;
top: 60%;
right: 10%;
}
.index-module__NV_5cW__floatingElement:nth-child(3) {
animation-delay: -14s;
bottom: 20%;
left: 50%;
}
@keyframes index-module__NV_5cW__float {
0% {
transform: translateY(0) rotate(0);
}
33% {
transform: translateY(-20px) rotate(120deg);
}
66% {
transform: translateY(10px) rotate(240deg);
}
to {
transform: translateY(0) rotate(360deg);
}
}
.index-module__NV_5cW__titleSection {
color: #333;
z-index: 2;
flex-direction: column;
align-items: center;
gap: 1rem;
display: flex;
position: relative;
}
.index-module__NV_5cW__titleContainer {
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 1rem;
display: flex;
}
.index-module__NV_5cW__title {
background: #333;
-webkit-text-fill-color: transparent;
letter-spacing: -0.02em;
white-space: nowrap;
-webkit-background-clip: text;
background-clip: text;
margin: 0;
font-size: 2.8rem;
font-weight: 700;
}
.index-module__NV_5cW__titleBadge {
color: #fff;
letter-spacing: 0.05em;
white-space: nowrap;
background: #60a5fa;
border-radius: 1.5rem;
padding: 0.4rem 1rem;
font-size: 1rem;
font-weight: 600;
box-shadow: 0 2px 10px #3b82f64d;
}
.index-module__NV_5cW__subtitle {
color: #666;
text-align: center;
align-items: center;
gap: 0.5rem;
max-width: 500px;
font-size: 1.1rem;
font-weight: 400;
display: flex;
}
.index-module__NV_5cW__statsBar {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #ffffff1a;
border: 1px solid #fff3;
border-radius: 1.5rem;
align-items: center;
gap: 1.5rem;
width: fit-content;
max-width: 90%;
margin-top: 0.75rem;
padding: 0.75rem 1.5rem;
display: flex;
}
.index-module__NV_5cW__stat {
flex-direction: column;
align-items: center;
gap: 0.25rem;
display: flex;
}
.index-module__NV_5cW__statNumber {
color: #60a5fa;
font-size: 1.5rem;
font-weight: 700;
}
.index-module__NV_5cW__statLabel {
color: #333;
font-size: 0.875rem;
font-weight: 500;
}
.index-module__NV_5cW__statDivider {
background: #fff3;
width: 1px;
height: 2rem;
}
.index-module__NV_5cW__content {
max-width: 1400px; /* 加宽容器,适配更多图表 */
margin: 0 auto;
padding: 0 1rem;
}
/* 图表区域样式适配示例的num-empty */
.index-module__NV_5cW__chartsSection {
margin-bottom: 1.5rem;
}
.index-module__NV_5cW__chartsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); /* 调整列宽,适配列表 */
gap: 1.5rem;
padding: 0;
}
.index-module__NV_5cW__chartCard {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #ffffffe6;
border: 1px solid #ffffff4d;
border-radius: 1rem;
padding: 1.5rem;
box-shadow: 0 4px 16px #00000014;
transition: all 0.3s;
}
/* 列表卡片特殊样式 */
.index-module__NV_5cW__chartCard.list-card {
padding: 1.5rem;
}
.index-module__NV_5cW__chartCard:hover {
border-color: #3b82f64d;
transform: translateY(-4px);
box-shadow: 0 8px 24px #0000001f;
}
.chartCard__wide {
grid-column: 1 / -1; /* 跨列显示 */
}
.chartCard__title {
color: #333;
font-size: 1.1rem;
font-weight: 600;
margin: 0 0 1rem 0;
text-align: left;
}
.chartCard__container {
width: 100%;
height: 300px; /* 图表高度 */
position: relative;
}
/* 地图容器特殊样式 */
.chartCard__container.map-container {
height: 500px; /* 地图需要更大的高度 */
border-radius: 0.5rem;
overflow: hidden;
background: transparent; /* 让地图组件自己控制背景 */
}
/* 地图卡片标题样式优化 */
.index-module__NV_5cW__chartCard:has(.map-container) .chartCard__title {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(8px);
border-radius: 20px;
padding: 8px 16px;
margin: 0 auto 16px;
width: fit-content;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
}
/* 列表容器样式:隐藏原生滚动条(可选,视觉更美观) */
.chartCard__container.list-container {
height: auto;
max-height: 300px; /* 限制列表高度,超出滚动 */
overflow-y: auto;
padding: 0 0.5rem;
/* 隐藏滚动条 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
/* 隐藏webkit内核浏览器的滚动条 */
.chartCard__container.list-container::-webkit-scrollbar {
display: none;
}
/* 项目列表样式 */
.project-list {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.project-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.8rem;
background: #f8f9fa;
border-radius: 0.5rem;
border-left: 3px solid #2951E0;
transition: all 0.2s;
}
/* 鼠标悬停项目项时的样式(可选) */
.project-item:hover {
background: #f0f4ff;
transform: translateX(2px);
}
.project-rank {
min-width: 24px;
height: 24px;
border-radius: 4px;
background: #2951E0;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
padding: 0 4px;
}
.project-rank.cisa {
background: #ff0000;
}
.project-rank.poisoning {
background: #ffa500;
}
.project-rank.cnvd {
background: #ffff00;
color: #000;
}
.project-rank.github {
background: #0000ff;
}
.project-rank.fix {
background: #008000;
}
.project-rank.supply {
background: #800080;
}
.project-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.project-name {
font-size: 14px;
font-weight: 600;
color: #333;
}
.project-desc {
font-size: 12px;
color: #666;
font-style: italic;
}
.project-org {
font-size: 12px;
color: #888;
flex-shrink: 0;
white-space: nowrap;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
}
/* 与示例一致的空数据样式 */
.num-empty {
height: 10px;
border-top: 2px solid #2951E0;
width: 100%;
background: linear-gradient(180deg, rgba(41, 81, 224, 0.12) 0%, rgba(41, 81, 224, 0.04) 100%);
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
}
.chart {
width: 100%;
height: 100%;
}
.index-module__NV_5cW__sectionHeader {
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
display: flex;
position: relative;
}
.index-module__NV_5cW__sectionTitle {
color: #333;
text-align: center;
justify-content: center;
align-items: center;
gap: 0.75rem;
width: 100%;
margin: 0;
font-size: 1.5rem;
font-weight: 600;
display: flex;
}
.index-module__NV_5cW__actionSection {
text-align: center;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #ffffffe6;
border: 1px solid #ffffff4d;
border-radius: 1.5rem;
margin: 0;
padding: 2.5rem 2rem;
position: relative;
overflow: hidden;
box-shadow: 0 8px 24px #00000014;
}
.index-module__NV_5cW__actionBackground {
z-index: 1;
background: linear-gradient(135deg, #3b82f60d 0%, #9333ea0d 100%);
position: absolute;
inset: 0;
}
.index-module__NV_5cW__actionContent {
z-index: 2;
max-width: 600px;
margin: 0 auto;
position: relative;
}
.index-module__NV_5cW__actionIcon {
color: #fff;
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
border-radius: 50%;
justify-content: center;
align-items: center;
width: 3rem;
height: 3rem;
margin: 0 auto 1.5rem;
display: flex;
box-shadow: 0 4px 12px #3b82f633;
}
.index-module__NV_5cW__actionTitle {
background: linear-gradient(135deg, #1e293b 0%, #3b82f6 100%);
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
background-clip: text;
margin-bottom: 1rem;
font-size: 2rem;
font-weight: 700;
line-height: 1.2;
}
.index-module__NV_5cW__actionDescription {
color: #64748b;
margin-bottom: 2rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
}
.index-module__NV_5cW__actionFeatures {
flex-wrap: wrap;
justify-content: center;
gap: 1.5rem;
margin-bottom: 2rem;
display: flex;
}
.index-module__NV_5cW__feature {
color: #3b82f6;
background: #3b82f614;
border: 1px solid #3b82f626;
border-radius: 1.5rem;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 1rem;
font-size: 0.85rem;
font-weight: 500;
display: flex;
}
.index-module__NV_5cW__createButton {
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
border: none;
border-radius: 1.5rem;
align-items: center;
gap: 0.5rem;
height: auto;
margin: 0 auto;
padding: 0.75rem 2rem;
font-size: 1rem;
font-weight: 600;
transition: all 0.3s;
display: flex;
box-shadow: 0 4px 12px #3b82f64d;
}
.index-module__NV_5cW__createButton:hover {
background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%);
transform: translateY(-2px);
box-shadow: 0 6px 18px #3b82f666;
}
/* 响应式适配 */
@media (max-width: 768px) {
.index-module__NV_5cW__titleContainer {
text-align: center;
flex-direction: column;
gap: 0.5rem;
}
.index-module__NV_5cW__title {
font-size: 2.5rem;
}
.index-module__NV_5cW__titleBadge {
padding: 0.4rem 1.2rem;
font-size: 1rem;
}
.index-module__NV_5cW__container {
padding: 1rem;
}
.index-module__NV_5cW__header {
margin-bottom: 2rem;
padding: 1.5rem 1rem;
}
.index-module__NV_5cW__statsBar {
flex-wrap: nowrap;
justify-content: space-between;
gap: 0.8rem;
width: 100%;
max-width: 100%;
padding: 0.8rem 1.2rem;
}
.index-module__NV_5cW__stat {
text-align: center;
flex: 1;
min-width: 60px;
}
.index-module__NV_5cW__statNumber {
font-size: 1.1rem;
}
.index-module__NV_5cW__statLabel {
white-space: nowrap;
font-size: 0.7rem;
}
.index-module__NV_5cW__statDivider {
width: 1px;
height: 1.2rem;
}
.index-module__NV_5cW__sectionTitle {
text-align: left;
flex: 1;
justify-content: flex-start;
gap: 0.5rem;
font-size: 1.3rem;
}
.index-module__NV_5cW__actionSection {
border-radius: 1rem;
margin: 0;
padding: 2rem 1.5rem;
}
.index-module__NV_5cW__actionTitle {
font-size: 1.6rem;
}
.index-module__NV_5cW__actionFeatures {
flex-direction: column;
align-items: center;
gap: 1rem;
}
.index-module__NV_5cW__createButton {
padding: 0.65rem 1.5rem;
font-size: 0.95rem;
}
.chartCard__container {
height: 250px; /* 移动端图表高度调整 */
}
/* 移动端列表样式调整 */
.project-item {
flex-wrap: wrap;
gap: 0.5rem;
}
.project-org {
width: 100%;
max-width: 100%;
margin-top: 0.2rem;
}
}
@media (max-width: 480px) {
.index-module__NV_5cW__title {
font-size: 1.8rem;
}
.index-module__NV_5cW__actionSection {
padding: 1.5rem 1rem;
}
.index-module__NV_5cW__statsBar {
gap: 0.6rem;
padding: 0.6rem 0.8rem;
}
.index-module__NV_5cW__statNumber {
font-size: 1rem;
}
.index-module__NV_5cW__statLabel {
font-size: 0.65rem;
}
.index-module__NV_5cW__sectionTitle {
gap: 0.4rem;
font-size: 1.2rem;
}
.chartCard__container {
height: 200px; /* 小屏移动端图表高度调整 */
}
.index-module__NV_5cW__chartsGrid {
grid-template-columns: 1fr;
}
/* 小屏列表高度调整 */
.chartCard__container.list-container {
max-height: 300px;
}
}
/* 图表卡片头部样式 */
.chartCard__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
/* 图表操作按钮样式 */
.chartCard__actions {
display: flex;
gap: 8px;
}
.chartCard__actionBtn {
padding: 4px 12px;
border: 1px solid #d1d5db;
background-color: #c1c7cd;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.chartCard__actionBtn:hover {
background-color: #f3f4f6;
}
.chartCard__actionBtn--active {
background-color: #3b82f6;
color: white;
border-color: #3b82f6;
}
.chartCard__actionBtn--active:hover {
background-color: #2563eb;
}
.stats-glass-bar {
display: flex;
align-items: center;
justify-content: space-between;
/* 布局控制:撑满容器并增加内边距 */
width: 100%;
padding: 2rem 4rem;
//margin: 2rem 0;
/* 深色玻璃拟态背景 */
background: rgba(255, 255, 255, 0.02);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
/* 极细微的边框勾勒 */
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.08);
/* 外阴影与内发光,增加悬浮感 */
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3),
inset 0 0 20px rgba(255, 255, 255, 0.02);
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
border-color: rgba(56, 189, 248, 0.4);
background: rgba(255, 255, 255, 0.04);
transform: translateY(-2px);
}
}
.stat-node {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
transition: transform 0.3s ease;
&:hover {
transform: scale(1.05);
.stat-value {
text-shadow: 0 0 25px rgba(56, 189, 248, 0.8);
}
}
}
.stat-value {
/* 科技感亮蓝渐变 */
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 2.25rem;
font-weight: 800;
background: linear-gradient(180deg, #fff 0%, #38bdf8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
/* 基础发光效果 */
filter: drop-shadow(0 0 10px rgba(56, 189, 248, 0.3));
letter-spacing: -1px;
}
.stat-desc {
/* 在黑底上使用半透明白色,更有高级感 */
color: rgba(255, 255, 255, 0.6);
font-size: 0.875rem;
font-weight: 500;
letter-spacing: 0.1rem;
text-transform: uppercase;
}
.stat-separator {
/* 渐隐分割线 */
width: 1px;
height: 3rem;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0),
rgba(255, 255, 255, 0.2),
rgba(255, 255, 255, 0)
);
margin: 0 1rem;
}
/* 响应式适配 */
@media (max-width: 1024px) {
.stats-glass-bar {
padding: 2rem;
}
.stat-value {
font-size: 1.75rem;
}
}
@media (max-width: 768px) {
.stats-glass-bar {
flex-direction: column;
gap: 2rem;
}
.stat-separator {
width: 50%;
height: 1px;
background: linear-gradient(to right, transparent, rgba(255,255,255,0.2), transparent);
}
}
</style>