更改文件路径
This commit is contained in:
@@ -1,373 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -10,13 +10,13 @@ import * as echarts from 'echarts'
|
|||||||
*/
|
*/
|
||||||
export const loadWorldMap = async () => {
|
export const loadWorldMap = async () => {
|
||||||
try {
|
try {
|
||||||
// 第一优先:从根目录加载原始世界地图数据(ECharts压缩格式)
|
// 第一优先:从components目录加载原始世界地图数据(ECharts压缩格式)
|
||||||
await import('/world.js')
|
await import('../views/Jyh/security/components/world.js')
|
||||||
// world.js 文件会自动注册地图,无需手动处理
|
// world.js 文件会自动注册地图,无需手动处理
|
||||||
console.log('World map loaded from root world.js file (ECharts compressed format)')
|
console.log('World map loaded from components world.js file (ECharts compressed format)')
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to load world map from root world.js file:', error)
|
console.warn('Failed to load world map from components world.js file:', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user