态势感知平台-全球开源风险态势感知监控中心CVE月度趋势图组件抽离
This commit is contained in:
324
src/views/Jyh/security/components/CveTrendChart.vue
Normal file
324
src/views/Jyh/security/components/CveTrendChart.vue
Normal file
@@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, nextTick, onMounted, onUnmounted } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
// 视图切换状态变量(global: 全球, regional: 区域)
|
||||
const cveView = ref('global');
|
||||
|
||||
// 空数据标识
|
||||
const isEmpty = ref({
|
||||
cveData: false
|
||||
});
|
||||
|
||||
// 图表实例存储(用于resize时销毁重绘)
|
||||
const chartInstances = ref<{ [key: string]: echarts.ECharts | null }>({
|
||||
CVEChart: null
|
||||
});
|
||||
|
||||
// CVE图表数据
|
||||
const 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] // 环比增长率(%)
|
||||
}
|
||||
};
|
||||
|
||||
// CVE视图切换方法
|
||||
const switchCveView = (view: 'global' | 'regional') => {
|
||||
cveView.value = view;
|
||||
nextTick(() => {
|
||||
initCVEChart();
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化CVE图表(柱状图+折线图)
|
||||
const initCVEChart = () => {
|
||||
const el = document.getElementById('CVEChart');
|
||||
if (!el) return;
|
||||
|
||||
// 延迟执行以确保DOM已渲染
|
||||
setTimeout(() => {
|
||||
const chartEl = document.getElementById('CVEChart');
|
||||
if (!chartEl) return;
|
||||
|
||||
if (chartInstances.value.CVEChart) {
|
||||
chartInstances.value.CVEChart.dispose();
|
||||
}
|
||||
|
||||
const myChart = echarts.init(chartEl);
|
||||
chartInstances.value.CVEChart = myChart;
|
||||
|
||||
// 获取当前视图的数据
|
||||
const currentData = 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'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 监听窗口大小变化,重新调整图表大小
|
||||
window.addEventListener('resize', () => {
|
||||
if (chartInstances.value.CVEChart) {
|
||||
chartInstances.value.CVEChart.resize();
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// 在组件挂载后初始化图表
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initCVEChart();
|
||||
});
|
||||
});
|
||||
|
||||
// 在组件卸载前清理资源
|
||||
onUnmounted(() => {
|
||||
if (chartInstances.value.CVEChart) {
|
||||
chartInstances.value.CVEChart.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
initCVEChart,
|
||||
switchCveView,
|
||||
cveView,
|
||||
cveData
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.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: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__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;
|
||||
}
|
||||
|
||||
/* 与示例一致的空数据样式 */
|
||||
.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%;
|
||||
}
|
||||
</style>
|
||||
@@ -46,32 +46,7 @@
|
||||
<!-- 情报流 -->
|
||||
<IntelligenceFeed />
|
||||
|
||||
<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>
|
||||
<CveTrendChart />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -113,6 +88,7 @@ import ThreatIntelMap from './components/ThreatIntelMap.vue';
|
||||
import IndustryDistribution from './components/IndustryDistribution.vue';
|
||||
import HighRiskComponentRank from './components/HighRiskComponentRank.vue';
|
||||
import IntelligenceFeed from './components/IntelligenceFeed.vue';
|
||||
import CveTrendChart from './components/CveTrendChart.vue';
|
||||
import StatsGlass from '@/views/Jyh/security/StatsGlass.vue';
|
||||
|
||||
// 页面尺寸响应式
|
||||
@@ -317,19 +293,7 @@ const chartData = ref({
|
||||
]
|
||||
},
|
||||
|
||||
// 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)]
|
||||
@@ -363,8 +327,7 @@ const chartData = ref({
|
||||
}
|
||||
});
|
||||
|
||||
// 视图切换状态变量(global: 全球, regional: 区域)
|
||||
const cveView = ref('global');
|
||||
|
||||
|
||||
// 空数据标识
|
||||
const isEmpty = ref({
|
||||
@@ -372,9 +335,7 @@ const isEmpty = ref({
|
||||
community: false,
|
||||
ossCompass: false,
|
||||
developer: false,
|
||||
languageTech: false,
|
||||
|
||||
cveData: false
|
||||
languageTech: false
|
||||
});
|
||||
|
||||
// 图表实例存储(用于resize时销毁重绘)
|
||||
@@ -384,149 +345,12 @@ const chartInstances = ref<{ [key: string]: echarts.ECharts | null }>({
|
||||
ossCompassChart: null,
|
||||
developerChart: null,
|
||||
languageTechChart: null,
|
||||
cweAttackSurfaceRadar: null,
|
||||
CVEChart: null
|
||||
cweAttackSurfaceRadar: 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'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
// ===================== 新增结束 =====================
|
||||
|
||||
@@ -735,10 +559,6 @@ const initDeveloperChart = () => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 初始化编程语言使用趋势堆叠面积图
|
||||
const initLanguageTrendChart = () => {
|
||||
const el = document.getElementById('languageTrendChart');
|
||||
@@ -1009,8 +829,7 @@ const initCharts = () => {
|
||||
languageTech: chartData.value.languageTech.series.every(s => s.data.every(d => d === 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
|
||||
@@ -1024,7 +843,7 @@ const initCharts = () => {
|
||||
if (!isEmpty.value.languageTech) nextTick(() => initLanguageTechChart());
|
||||
|
||||
|
||||
if (!isEmpty.value.cveData) nextTick(() => initCVEChart());
|
||||
|
||||
if (!isEmpty.value.languageTrend) nextTick(() => initLanguageTrendChart());
|
||||
if (!isEmpty.value.vulnerabilitySeverity) nextTick(() => initVulnerabilitySeverityChart());
|
||||
if (!isEmpty.value.scatterData) nextTick(() => initScatterChart());
|
||||
|
||||
Reference in New Issue
Block a user