Files
Situation-Awareness-Platfor…/src/views/Jyh/security/components/ThreatIntelMap.vue

515 lines
16 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.
Vue 3 项目中直接搬运 Vue 2 代码不生效主要原因有三点生命周期钩子的变化Ref 获取方式的变化以及 全局变量挂载时机
以下是针对 Vue 3 组合式 API (Composition API) 优化后的代码建议直接替换
核心修改说明
ref 声明Vue 3 需要使用 const chartDiv = ref(null) 来声明模板引用
生命周期mounted 变为 onMountedbeforeDestroy 变为 onBeforeUnmount
挂载时机确保在 import world.js 之前window.echarts 已经被赋值否则 world.js 执行时依然会因为找不到 echarts 而报错
Vue 3 适配版代码
代码段
<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 setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
import * as echarts from 'echarts';
import worldJson from './world.json';
import { mapVulnStatsPage1, mapRegionStatsPage1, mapDataPage1, mapVulnTypeStatsPage1 } from '@/api/jyh/situation';
// 响应式数据
const chartDiv = ref(null);
let myChart = null;
let timer = null;
const vulnStats = ref([]);
const regionStats = ref([
{ name: 'United States', score: 435202 },
{ name: 'India', score: 252054 },
{ name: 'China (中国)', score: 184085 },
{ name: 'Brazil', score: 174811 },
{ name: 'Germany', score: 126397 },
]);
// 实时情报数据状态
const alerts = ref([]);
const currentAlert = ref(null);
let alertIndex = 0;
const 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],
};
// 获取实时情报数据
const fetchAlertsData = async () => {
try {
const response = await mapVulnTypeStatsPage1();
const businessData = response.data; // Axios响应格式业务数据在response.data中
if (businessData?.code === 200) {
alerts.value = businessData.data || [];
if (alerts.value.length > 0) {
currentAlert.value = alerts.value[0];
}
} else {
console.error('获取实时情报数据失败:', businessData?.msg || '未知错误');
// 如果接口调用失败,使用默认数据
alerts.value = [
{ title: "高危漏洞通告", desc: "Apache Log4j2 远程代码执行漏洞复现" },
{ title: "僵尸网络活动", desc: "Mirai 变种正在扫描 IoT 设备端口 23" },
{ title: "APT 组织活动", desc: "OceanLotus 组织针对金融行业的钓鱼攻击" },
];
if (alerts.value.length > 0) {
currentAlert.value = alerts.value[0];
}
}
} catch (error) {
console.error('请求实时情报数据时发生错误:', error);
// 发生异常时使用默认数据
alerts.value = [
{ title: "高危漏洞通告", desc: "Apache Log4j2 远程代码执行漏洞复现" },
{ title: "僵尸网络活动", desc: "Mirai 变种正在扫描 IoT 设备端口 23" },
{ title: "APT 组织活动", desc: "OceanLotus 组织针对金融行业的钓鱼攻击" },
];
if (alerts.value.length > 0) {
currentAlert.value = alerts.value[0];
}
}
};
// 逻辑方法
const startAlertRotation = () => {
if (alerts.value.length > 0) {
currentAlert.value = alerts.value[0];
}
timer = setInterval(() => {
if (alerts.value.length > 0) {
alertIndex = (alertIndex + 1) % alerts.value.length;
currentAlert.value = null;
setTimeout(() => {
currentAlert.value = alerts.value[alertIndex];
}, 500);
}
}, 3500);
};
// 地图数据状态
const mapData = ref([]);
const scatterData = ref([]);
// 获取地图数据
const fetchMapData = async () => {
try {
const response = await mapDataPage1();
const businessData = response.data; // Axios响应格式业务数据在response.data中
if (businessData?.code === 200) {
mapData.value = businessData.data;
} else {
console.error('获取地图数据失败:', businessData?.msg || '未知错误');
// 如果接口调用失败,使用默认数据
mapData.value = [
{ 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 },
];
}
} catch (error) {
console.error('请求地图数据时发生错误:', error);
// 发生异常时使用默认数据
mapData.value = [
{ 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 },
];
}
};
const setChart = async () => {
if (!chartDiv.value) return;
echarts.registerMap('world', worldJson);
// 初始化散点数据
const 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],
};
const scatterDataLocal = [
{ name: '北京中心', value: [...geoCoordMap['Beijing'], 100] },
{ name: '上海节点', value: [...geoCoordMap['Shanghai'], 80] },
{ name: '广州节点', value: [...geoCoordMap['Guangzhou'], 70] },
{ name: '成都节点', value: [...geoCoordMap['Chengdu'], 60] },
{ name: '威胁源-US', value: [...geoCoordMap['Washington'], 90] },
{ name: '威胁源-RU', value: [...geoCoordMap['Moscow'], 85] },
];
const option = {
backgroundColor: 'transparent',
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: (params) => {
if (params.seriesType === 'effectScatter') {
return `${params.marker} ${params.name}<br/>威胁指数: ${params.value[2]}`;
}
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',
}
}
},
series: [
{
name: 'Threat Distribution',
type: 'map',
geoIndex: 0,
data: mapData.value
},
{
name: 'Key Nodes',
type: 'effectScatter',
coordinateSystem: 'geo',
data: scatterDataLocal,
symbolSize: (val) => 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
}
]
};
myChart = echarts.init(chartDiv.value);
myChart.setOption(option);
};
// 获取漏洞类型统计数据
const fetchVulnStats = async () => {
try {
const response = await mapVulnStatsPage1();
const businessData = response.data; // Axios响应格式业务数据在response.data中
if (businessData?.code === 200) {
// 为每个数据项添加颜色
const colors = ['#ef4444', '#f97316', '#eab308', '#3b82f6', '#10b981', '#8b5cf6', '#ec4899', '#06b6d4'];
vulnStats.value = businessData.data.map((item, index) => ({
...item,
color: colors[index % colors.length]
}));
} else {
console.error('获取漏洞类型统计数据失败:', businessData?.msg || '未知错误');
// 如果接口调用失败,使用默认数据
vulnStats.value = [
{ name: 'XSS 跨站脚本', value: 32.1, color: '#ef4444' },
{ name: 'SQL 注入', value: 11.9, color: '#f97316' },
{ name: '内存缓冲区溢出', value: 10.9, color: '#eab308' },
{ name: '输入验证不当', value: 9.3, color: '#3b82f6' },
{ name: '信息泄露', value: 8.0, color: '#10b981' },
];
}
} catch (error) {
console.error('请求漏洞类型统计数据时发生错误:', error);
// 发生异常时使用默认数据
vulnStats.value = [
{ name: 'XSS 跨站脚本', value: 32.1, color: '#ef4444' },
{ name: 'SQL 注入', value: 11.9, color: '#f97316' },
{ name: '内存缓冲区溢出', value: 10.9, color: '#eab308' },
{ name: '输入验证不当', value: 9.3, color: '#3b82f6' },
{ name: '信息泄露', value: 8.0, color: '#10b981' },
];
}
};
// 获取威胁情报地域排行数据
const fetchRegionStats = async () => {
try {
const response = await mapRegionStatsPage1();
const businessData = response.data; // Axios响应格式业务数据在response.data中
if (businessData?.code === 200) {
regionStats.value = businessData.data;
} else {
console.error('获取威胁情报地域排行数据失败:', businessData?.msg || '未知错误');
// 如果接口调用失败,使用默认数据
regionStats.value = [
{ name: 'United States', score: 435202 },
{ name: 'India', score: 252054 },
{ name: 'China (中国)', score: 184085 },
{ name: 'Brazil', score: 174811 },
{ name: 'Germany', score: 126397 },
];
}
} catch (error) {
console.error('请求威胁情报地域排行数据时发生错误:', error);
// 发生异常时使用默认数据
regionStats.value = [
{ name: 'United States', score: 435202 },
{ name: 'India', score: 252054 },
{ name: 'China (中国)', score: 184085 },
{ name: 'Brazil', score: 174811 },
{ name: 'Germany', score: 126397 },
];
}
};
// 生命周期钩子
onMounted(async () => {
await nextTick(); // 确保 DOM 已渲染
await Promise.all([fetchVulnStats(), fetchRegionStats(), fetchMapData(), fetchAlertsData()]); // 同时获取所有统计数据
await setChart(); // 等待图表设置完成
startAlertRotation();
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
if (timer) clearInterval(timer);
window.removeEventListener('resize', handleResize);
if (myChart) {
myChart.dispose();
}
});
const handleResize = () => {
myChart && myChart.resize();
};
</script>
<style lang="scss" 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>