态势感知平台-全球开源风险态势感知监控中心漏洞语言分布图组件抽离
This commit is contained in:
263
src/views/Jyh/security/components/VulnerabilityLanguageChart.vue
Normal file
263
src/views/Jyh/security/components/VulnerabilityLanguageChart.vue
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
<template>
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, nextTick, onMounted, onUnmounted } from 'vue';
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
|
// 空数据标识
|
||||||
|
const isEmpty = ref({
|
||||||
|
languageTrend: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 图表实例存储(用于resize时销毁重绘)
|
||||||
|
const chartInstances = ref<{ [key: string]: echarts.ECharts | null }>({
|
||||||
|
languageTrendChart: null
|
||||||
|
});
|
||||||
|
|
||||||
|
// 漏洞语言分布数据
|
||||||
|
const languageTrendData = {
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化漏洞语言分布图表(堆叠面积图)
|
||||||
|
const initLanguageTrendChart = () => {
|
||||||
|
const el = document.getElementById('languageTrendChart');
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
// 延迟执行以确保DOM已渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
const chartEl = document.getElementById('languageTrendChart');
|
||||||
|
if (!chartEl) return;
|
||||||
|
|
||||||
|
if (chartInstances.value.languageTrendChart) {
|
||||||
|
chartInstances.value.languageTrendChart.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
const myChart = echarts.init(chartEl);
|
||||||
|
chartInstances.value.languageTrendChart = myChart;
|
||||||
|
|
||||||
|
// 准备堆叠面积图数据
|
||||||
|
const series = languageTrendData.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: languageTrendData.series.map(item => item.name)
|
||||||
|
},
|
||||||
|
grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
|
||||||
|
xAxis: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
data: languageTrendData.years,
|
||||||
|
axisLabel: { fontSize: 12 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '使用率(%)',
|
||||||
|
nameTextStyle: { fontSize: 12 },
|
||||||
|
axisLabel: { fontSize: 12 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: series
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听窗口大小变化,重新调整图表大小
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
if (chartInstances.value.languageTrendChart) {
|
||||||
|
chartInstances.value.languageTrendChart.resize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 在组件挂载后初始化图表
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
initLanguageTrendChart();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 在组件卸载前清理资源
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (chartInstances.value.languageTrendChart) {
|
||||||
|
chartInstances.value.languageTrendChart.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
initLanguageTrendChart
|
||||||
|
});
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-\[1\.5rem\] {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 与示例一致的空数据样式 */
|
||||||
|
.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,18 +46,13 @@
|
|||||||
<!-- 情报流 -->
|
<!-- 情报流 -->
|
||||||
<IntelligenceFeed />
|
<IntelligenceFeed />
|
||||||
|
|
||||||
|
<!-- CVE 月度趋势:全球图 区域图 -->
|
||||||
<CveTrendChart />
|
<CveTrendChart />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 编程语言使用趋势堆叠面积图 -->
|
<!-- 编程语言使用趋势堆叠面积图-->
|
||||||
<div class="index-module__NV_5cW__chartCard chartCard__wide mb-[1.5rem]">
|
<VulnerabilityLanguageChart />
|
||||||
<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]">
|
<div class="index-module__NV_5cW__chartCard mb-[1.5rem]">
|
||||||
@@ -89,6 +84,7 @@ import IndustryDistribution from './components/IndustryDistribution.vue';
|
|||||||
import HighRiskComponentRank from './components/HighRiskComponentRank.vue';
|
import HighRiskComponentRank from './components/HighRiskComponentRank.vue';
|
||||||
import IntelligenceFeed from './components/IntelligenceFeed.vue';
|
import IntelligenceFeed from './components/IntelligenceFeed.vue';
|
||||||
import CveTrendChart from './components/CveTrendChart.vue';
|
import CveTrendChart from './components/CveTrendChart.vue';
|
||||||
|
import VulnerabilityLanguageChart from './components/VulnerabilityLanguageChart.vue';
|
||||||
import StatsGlass from '@/views/Jyh/security/StatsGlass.vue';
|
import StatsGlass from '@/views/Jyh/security/StatsGlass.vue';
|
||||||
|
|
||||||
// 页面尺寸响应式
|
// 页面尺寸响应式
|
||||||
@@ -183,84 +179,7 @@ const chartData = ref({
|
|||||||
{ name: 'Go', data: [60, 65, 70, 72, 75] }
|
{ 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]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
// 漏洞占比数据
|
// 漏洞占比数据
|
||||||
vulnerabilitySeverity: {
|
vulnerabilitySeverity: {
|
||||||
@@ -559,76 +478,7 @@ const initDeveloperChart = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化编程语言使用趋势堆叠面积图
|
|
||||||
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 initVulnerabilitySeverityChart = () => {
|
||||||
@@ -830,7 +680,7 @@ const initCharts = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
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)),
|
vulnerabilitySeverity: chartData.value.vulnerabilitySeverity.series.every(s => s.data.every(d => d === 0)),
|
||||||
scatterData: chartData.value.scatterData.length === 0
|
scatterData: chartData.value.scatterData.length === 0
|
||||||
};
|
};
|
||||||
@@ -844,7 +694,7 @@ const initCharts = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (!isEmpty.value.languageTrend) nextTick(() => initLanguageTrendChart());
|
|
||||||
if (!isEmpty.value.vulnerabilitySeverity) nextTick(() => initVulnerabilitySeverityChart());
|
if (!isEmpty.value.vulnerabilitySeverity) nextTick(() => initVulnerabilitySeverityChart());
|
||||||
if (!isEmpty.value.scatterData) nextTick(() => initScatterChart());
|
if (!isEmpty.value.scatterData) nextTick(() => initScatterChart());
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user