Files
Situation-Awareness-Platfor…/GlobalThreatMap.vue

374 lines
11 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="dashboard-container">
<div ref="chartDiv" class="global-map"></div>
<div class="panel panel-left">
<div class="panel-title">🛡 漏洞类型分布 (Top 5)</div>
<div class="stat-list">
<div class="stat-item" v-for="(item, index) in vulnStats" :key="index">
<div class="stat-label">
<span>{{ item.name }}</span>
<span class="stat-value">{{ item.value }}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: item.value + '%', background: item.color }"></div>
</div>
</div>
</div>
</div>
<div class="panel panel-right">
<div class="panel-title">🌍 威胁情报地域排行</div>
<div class="rank-list">
<div class="rank-item" v-for="(item, index) in regionStats" :key="index">
<span class="rank-num" :class="'top-' + (index + 1)">{{ index + 1 }}</span>
<span class="rank-name">{{ item.name }}</span>
<span class="rank-score">{{ item.score.toLocaleString() }} </span>
</div>
</div>
</div>
<transition name="fade-slide">
<div v-if="currentAlert" class="alert-card">
<div class="alert-header">
<span class="alert-icon"></span> 实时情报监测
</div>
<div class="alert-content">
<div class="alert-title">{{ currentAlert.title }}</div>
<div class="alert-desc">{{ currentAlert.desc }}</div>
</div>
</div>
</transition>
</div>
</template>
<script>
import * as echarts from 'echarts';
import "@/lib/world.js"; // 必须确保引入世界地图数据
export default {
name: 'ThreatIntelligenceMap',
data() {
return {
myChart: null,
timer: null,
// 基于真实CWE TOPN分布数据的漏洞类型统计
vulnStats: [
{ name: 'XSS 跨站脚本', value: 32.1, color: '#ef4444' }, // CWE-79: 39,435个
{ name: 'SQL 注入', value: 11.9, color: '#f97316' }, // CWE-89: 14,657个
{ name: '内存缓冲区溢出', value: 10.9, color: '#eab308' }, // CWE-119: 13,390个
{ name: '输入验证不当', value: 9.3, color: '#3b82f6' }, // CWE-20: 11,424个
{ name: '信息泄露', value: 8.0, color: '#10b981' }, // CWE-200: 9,781个
],
// 基于真实开源贡献者数据的威胁情报排行
regionStats: [
{ name: 'United States', score: 435202 },
{ name: 'India', score: 252054 },
{ name: 'China (中国)', score: 184085 },
{ name: 'Brazil', score: 174811 },
{ name: 'Germany', score: 126397 },
],
// 告警轮播
alerts: [
{ title: "高危漏洞通告", desc: "Apache Log4j2 远程代码执行漏洞复现" },
{ title: "僵尸网络活动", desc: "Mirai 变种正在扫描 IoT 设备端口 23" },
{ title: "APT 组织活动", desc: "OceanLotus 组织针对金融行业的钓鱼攻击" },
],
currentAlert: null,
alertIndex: 0,
// 坐标映射
geoCoordMap: {
'Beijing': [116.407395, 39.904211],
'Shanghai': [121.473701, 31.230416],
'Guangzhou': [113.264385, 23.129112],
'Chengdu': [104.066541, 30.572269],
'Washington': [-77.036871, 38.907192],
'Moscow': [37.6173, 55.7558],
}
};
},
methods: {
startAlertRotation() {
this.currentAlert = this.alerts[0];
this.timer = setInterval(() => {
this.alertIndex = (this.alertIndex + 1) % this.alerts.length;
this.currentAlert = null;
setTimeout(() => {
this.currentAlert = this.alerts[this.alertIndex];
}, 500);
}, 3500);
},
setChart() {
// 1. 准备地图热力数据 (对应 VisualMap)
// ECharts Map Series 需要 name 与 world.js 中的国家英文名一致
const mapData = [
{ name: 'China', value: 100 },
{ name: 'United States', value: 80 },
{ name: 'Russia', value: 60 },
{ name: 'Brazil', value: 40 },
{ name: 'Canada', value: 30 },
{ name: 'Australia', value: 20 },
{ name: 'India', value: 50 },
{ name: 'Germany', value: 45 },
{ name: 'United Kingdom', value: 40 },
{ name: 'France', value: 35 },
];
// 2. 准备关键节点呼吸点 (EffectScatter)
const scatterData = [
{ name: '北京中心', value: [...this.geoCoordMap['Beijing'], 100] },
{ name: '上海节点', value: [...this.geoCoordMap['Shanghai'], 80] },
{ name: '广州节点', value: [...this.geoCoordMap['Guangzhou'], 70] },
{ name: '成都节点', value: [...this.geoCoordMap['Chengdu'], 60] },
// 标记一些海外威胁源头
{ name: '威胁源-US', value: [...this.geoCoordMap['Washington'], 90] },
{ name: '威胁源-RU', value: [...this.geoCoordMap['Moscow'], 85] },
];
const option = {
backgroundColor: 'transparent', // 背景交由 CSS 处理
// 视觉映射组件:决定地图区域颜色
visualMap: {
min: 0,
max: 100,
left: '30',
bottom: '30',
text: ['高风险', '低风险'],
textStyle: { color: '#fff' },
calculable: true,
inRange: {
// 定义热力颜色渐变:深蓝 -> 蓝 -> 青 -> 黄 -> 红
color: ['#1e3c72', '#2a5298', '#24b6d8', '#facc15', '#ef4444']
}
},
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(0,0,0,0.8)',
borderColor: '#415A77',
textStyle: { color: '#fff' },
formatter: function(params) {
if(params.seriesType === 'effectScatter') {
return `${params.marker} ${params.name}<br/>威胁指数: ${params.value[2]}`;
}
// 地图区域 Hover
if(!params.value) return params.name + ': 无数据';
return `${params.name}<br/>受影响程度: ${params.value}`;
}
},
geo: {
map: 'world',
roam: true, // 允许缩放和平移
zoom: 1.2,
label: { show: false },
// 地图基础样式
itemStyle: {
areaColor: '#0f172a', // 默认无数据区域颜色 (深色)
borderColor: '#1e293b', // 边框颜色
borderWidth: 1
},
// 高亮样式
emphasis: {
label: { show: false },
itemStyle: {
areaColor: '#3b82f6', // 鼠标悬浮颜色
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
},
series: [
// 图层1地域分布热力图
{
name: 'Threat Distribution',
type: 'map',
geoIndex: 0, // 绑定到上面的 geo 配置
data: mapData
},
// 图层2关键节点呼吸点 (比飞线更清晰)
{
name: 'Key Nodes',
type: 'effectScatter', // 带有涟漪效果的散点
coordinateSystem: 'geo',
data: scatterData,
symbolSize: function (val) {
return val[2] / 5; // 根据数值大小调整圆点大小
},
showEffectOn: 'render',
rippleEffect: {
brushType: 'stroke',
scale: 3 // 涟漪扩散范围
},
label: {
show: true,
formatter: '{b}',
position: 'right',
color: '#fff',
fontSize: 10,
textBorderColor: '#000',
textBorderWidth: 2
},
itemStyle: {
color: '#facc15', // 节点颜色 (黄色高亮)
shadowBlur: 10,
shadowColor: '#facc15'
},
zlevel: 1
}
]
};
if (!this.myChart) {
this.myChart = echarts.init(this.$refs.chartDiv);
}
this.myChart.setOption(option);
}
},
mounted() {
this.setChart();
this.startAlertRotation();
window.addEventListener('resize', () => this.myChart && this.myChart.resize());
},
beforeDestroy() {
if (this.timer) clearInterval(this.timer);
window.removeEventListener('resize', () => this.myChart && this.myChart.resize());
}
};
</script>
<style lang="less" scoped>
.dashboard-container {
position: relative;
width: 100%;
height: 100%;
// 背景:深邃的科技蓝黑渐变
background: radial-gradient(circle at center, #1e293b 0%, #020617 100%);
overflow: hidden;
font-family: 'Arial', sans-serif;
color: #fff;
}
.global-map {
width: 100%;
height: 100%;
}
/* --- 侧边面板通用样式 --- */
.panel {
position: absolute;
top: 20px;
width: 260px;
background: rgba(15, 23, 42, 0.8); /* 半透明深色背景 */
backdrop-filter: blur(8px);
border: 1px solid rgba(56, 189, 248, 0.2); /* 科技蓝边框 */
border-radius: 8px;
padding: 15px;
z-index: 10;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
.panel-title {
font-size: 14px;
font-weight: bold;
color: #38bdf8;
margin-bottom: 15px;
border-bottom: 1px solid rgba(56, 189, 248, 0.2);
padding-bottom: 8px;
}
}
.panel-left { left: 20px; }
.panel-right { right: 20px; }
/* 左侧:统计列表 */
.stat-item {
margin-bottom: 12px;
.stat-label {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #cbd5e1;
margin-bottom: 4px;
}
.progress-bar {
height: 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
overflow: hidden;
.progress-fill {
height: 100%;
border-radius: 3px;
transition: width 1s ease;
}
}
}
/* 右侧:排行列表 */
.rank-item {
display: flex;
align-items: center;
margin-bottom: 10px;
font-size: 12px;
.rank-num {
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
margin-right: 10px;
color: #94a3b8;
font-weight: bold;
&.top-1 { background: #ef4444; color: white; }
&.top-2 { background: #f97316; color: white; }
&.top-3 { background: #eab308; color: white; }
}
.rank-name { flex: 1; color: #e2e8f0; }
.rank-score { color: #38bdf8; font-weight: bold; }
}
/* 底部告警卡片 (保持原有风格) */
.alert-card {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%); /* 居中显示 */
width: 400px;
background: rgba(17, 25, 40, 0.9);
border: 1px solid #ef4444;
border-radius: 8px;
padding: 12px 20px;
display: flex;
align-items: center;
gap: 15px;
box-shadow: 0 0 20px rgba(239, 68, 68, 0.3);
z-index: 10;
.alert-icon { font-size: 20px; animation: pulse 1.5s infinite; }
.alert-content {
.alert-title { color: #ef4444; font-weight: bold; font-size: 14px; margin-bottom: 2px; }
.alert-desc { color: #fff; font-size: 12px; }
}
}
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.2); opacity: 0.8; }
100% { transform: scale(1); opacity: 1; }
}
.fade-slide-enter-active, .fade-slide-leave-active { transition: all 0.5s ease; }
.fade-slide-enter-from, .fade-slide-leave-to { opacity: 0; transform: translate(-50%, 20px); }
</style>