2135 lines
59 KiB
Vue
2135 lines
59 KiB
Vue
<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: {
|
||
// 全球CVE:2024年实际数据(基于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] // 环比增长率(%)
|
||
},
|
||
// 中国区域CVE:2024年数据(约占全球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>
|