Merge remote-tracking branch 'origin/SAP' into SAP
This commit is contained in:
@@ -66,7 +66,7 @@ export function mapDataPage1(params?: any): Promise<any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 全球开源风险态势感知监控中心-行业分布饼图
|
// 全球开源风险态势感知监控中心-行业分布饼图
|
||||||
export function dustryDistributionPage1(params?: any): Promise<any> {
|
export function industryDistributionPage1(params?: any): Promise<any> {
|
||||||
return request({
|
return request({
|
||||||
url: `/api/v1/page1/industryDistribution`,
|
url: `/api/v1/page1/industryDistribution`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<div class="stat-separator"></div>
|
<div class="stat-separator"></div>
|
||||||
|
|
||||||
<div class="stat-node">
|
<div class="stat-node">
|
||||||
<span class="stat-value">{{ formatProjectValue(stats.project) }}</span>
|
<span class="stat-value">{{ stats.project.toLocaleString() }}亿+</span>
|
||||||
<span class="stat-desc">全球开源项目</span>
|
<span class="stat-desc">全球开源项目</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue';
|
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue';
|
||||||
import * as echarts from 'echarts';
|
import * as echarts from 'echarts';
|
||||||
|
import { industryDistributionPage1 } from '@/api/jyh/situation';
|
||||||
|
|
||||||
// 定义数据类型
|
// 定义数据类型
|
||||||
interface IndustryData {
|
interface IndustryData {
|
||||||
@@ -20,18 +21,6 @@ interface IndustryData {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定义行业分布数据
|
|
||||||
const industryDistribution: IndustryData[] = [
|
|
||||||
{ value: 25.8, name: '人工智能' },
|
|
||||||
{ value: 18.4, name: '云计算' },
|
|
||||||
{ value: 15.6, name: '大数据' },
|
|
||||||
{ value: 12.2, name: '物联网' },
|
|
||||||
{ value: 9.6, name: '网络安全' },
|
|
||||||
{ value: 7.1, name: '区块链' },
|
|
||||||
{ value: 6.4, name: '工业互联网' },
|
|
||||||
{ value: 4.9, name: '移动开发' }
|
|
||||||
];
|
|
||||||
|
|
||||||
// 定义配色方案
|
// 定义配色方案
|
||||||
const industryColorPalette = [
|
const industryColorPalette = [
|
||||||
'#00f0ff', '#00b03e', '#ffeb00', '#ffab00',
|
'#00f0ff', '#00b03e', '#ffeb00', '#ffab00',
|
||||||
@@ -39,7 +28,7 @@ const industryColorPalette = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
// 内部数据
|
// 内部数据
|
||||||
const internalData = ref<IndustryData[]>(industryDistribution);
|
const internalData = ref<IndustryData[]>([]);
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const chartDiv = ref<HTMLDivElement | null>(null);
|
const chartDiv = ref<HTMLDivElement | null>(null);
|
||||||
@@ -103,11 +92,47 @@ const handleResize = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取行业分布数据
|
||||||
|
const fetchIndustryData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await industryDistributionPage1();
|
||||||
|
if (response.code === 200) {
|
||||||
|
internalData.value = response.data;
|
||||||
|
} else {
|
||||||
|
console.error('获取行业分布数据失败:', response.msg);
|
||||||
|
// 如果接口调用失败,使用默认数据
|
||||||
|
internalData.value = [
|
||||||
|
{ value: 25.8, name: '人工智能' },
|
||||||
|
{ value: 18.4, name: '云计算' },
|
||||||
|
{ value: 15.6, name: '大数据' },
|
||||||
|
{ value: 12.2, name: '物联网' },
|
||||||
|
{ value: 9.6, name: '网络安全' },
|
||||||
|
{ value: 7.1, name: '区块链' },
|
||||||
|
{ value: 6.4, name: '工业互联网' },
|
||||||
|
{ value: 4.9, name: '移动开发' }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('请求行业分布数据时发生错误:', error);
|
||||||
|
// 发生异常时使用默认数据
|
||||||
|
internalData.value = [
|
||||||
|
{ value: 25.8, name: '人工智能' },
|
||||||
|
{ value: 18.4, name: '云计算' },
|
||||||
|
{ value: 15.6, name: '大数据' },
|
||||||
|
{ value: 12.2, name: '物联网' },
|
||||||
|
{ value: 9.6, name: '网络安全' },
|
||||||
|
{ value: 7.1, name: '区块链' },
|
||||||
|
{ value: 6.4, name: '工业互联网' },
|
||||||
|
{ value: 4.9, name: '移动开发' }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 生命周期钩子
|
// 生命周期钩子
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
nextTick(() => {
|
await fetchIndustryData(); // 获取行业分布数据
|
||||||
initChart();
|
await nextTick();
|
||||||
});
|
initChart();
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
272
src/views/Jyh/security/components/IntelligenceFeed.vue
Normal file
272
src/views/Jyh/security/components/IntelligenceFeed.vue
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
<template>
|
||||||
|
<div class="index-module__NV_5cW__chartCard list-card">
|
||||||
|
<h3 class="chartCard__title">情报流</h3>
|
||||||
|
<!-- 新增:ref + 鼠标事件 -->
|
||||||
|
<div class="chartCard__container list-container"
|
||||||
|
ref="scrollContainer1"
|
||||||
|
@mouseenter="pauseScroll(1)"
|
||||||
|
@mouseleave="resumeScroll(1)">
|
||||||
|
<div class="project-list">
|
||||||
|
<div class="project-item" v-for="(item, index) in feedList" :key="index">
|
||||||
|
<span class="project-rank" :class="item.type">{{ item.type }}</span>
|
||||||
|
<div class="project-info">
|
||||||
|
<span class="project-name">{{ item.content }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
||||||
|
|
||||||
|
// 接收外部数据作为属性
|
||||||
|
const props = defineProps<{
|
||||||
|
initialData?: Array<any>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 情报流数据
|
||||||
|
const feedList = ref([
|
||||||
|
{
|
||||||
|
type: 'CISA',
|
||||||
|
content: 'CISA KEV新增: WordPress 的 OneClick Chat to Order 插件,存在不安全的直接对象引用漏洞 (CVE-2025-13526) '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: '投毒',
|
||||||
|
content: '投毒监控: npm 基建级包大规模投毒事件,影响chalk, debug, ansi-styles 等 18 个包'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'CNVD',
|
||||||
|
content: 'CNVD预警: Google Chrome信息泄露漏洞(CNVD-2025-3038304)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'GitHub',
|
||||||
|
content: 'GitHub安全通告: MovableType 软件的 ContentType 页面 "编辑分类集" 功能存在存储型跨站脚本漏洞'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: '修复',
|
||||||
|
content: '漏洞修复: Node.js 发布 v20.11.0 版本,修复 3 个高危漏洞'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: '供应链',
|
||||||
|
content: '供应链预警: Maven 中央仓库发现 12 个伪造开源组件,包含后门程序'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 如果提供了初始数据,则使用提供的数据
|
||||||
|
watch(() => props.initialData, (newData) => {
|
||||||
|
if (newData && newData.length > 0) {
|
||||||
|
feedList.value = newData;
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// 1. 定义滚动容器的ref
|
||||||
|
const scrollContainer1 = ref<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
// 2. 存储定时器(键:1/2,值:定时器ID)
|
||||||
|
const scrollTimers: { [key: number]: NodeJS.Timeout | null } = {
|
||||||
|
1: null,
|
||||||
|
2: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. 滚动配置
|
||||||
|
const scrollStep = 1; // 每次滚动的步长(像素,越小越流畅)
|
||||||
|
const scrollInterval = 20; // 滚动间隔(毫秒,越小越流畅)
|
||||||
|
|
||||||
|
// 4. 通用自动滚动函数
|
||||||
|
const autoScroll = (containerKey: number) => {
|
||||||
|
// 获取对应容器
|
||||||
|
const container = containerKey === 1 ? scrollContainer1.value : null;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// 获取列表内容元素(project-list)
|
||||||
|
const projectList = container.querySelector('.project-list') as HTMLDivElement;
|
||||||
|
if (!projectList) return;
|
||||||
|
|
||||||
|
// 若内容高度 ≤ 容器高度,不执行滚动
|
||||||
|
if (projectList.scrollHeight <= container.clientHeight) {
|
||||||
|
// 清除现有定时器
|
||||||
|
if (scrollTimers[containerKey]) {
|
||||||
|
clearInterval(scrollTimers[containerKey]!);
|
||||||
|
scrollTimers[containerKey] = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除现有定时器(避免重复)
|
||||||
|
if (scrollTimers[containerKey]) {
|
||||||
|
clearInterval(scrollTimers[containerKey]!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动定时器滚动
|
||||||
|
scrollTimers[containerKey] = setInterval(() => {
|
||||||
|
// 向下滚动步长
|
||||||
|
container.scrollTop += scrollStep;
|
||||||
|
|
||||||
|
// 滚动到底部时,重置为0(循环滚动)
|
||||||
|
// 加1是为了兼容像素精度问题
|
||||||
|
if (projectList.scrollHeight - container.scrollTop <= container.clientHeight + 1) {
|
||||||
|
container.scrollTop = 0; // 直接重置(也可以用平滑滚动:container.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
}, scrollInterval);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 5. 暂停滚动
|
||||||
|
const pauseScroll = (containerKey: number) => {
|
||||||
|
if (scrollTimers[containerKey]) {
|
||||||
|
clearInterval(scrollTimers[containerKey]!);
|
||||||
|
scrollTimers[containerKey] = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 6. 恢复滚动
|
||||||
|
const resumeScroll = (containerKey: number) => {
|
||||||
|
autoScroll(containerKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 7. 组件挂载时启动滚动
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
autoScroll(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 8. 组件卸载时清除定时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
Object.keys(scrollTimers).forEach(key => {
|
||||||
|
const k = Number(key);
|
||||||
|
if (scrollTimers[k]) {
|
||||||
|
clearInterval(scrollTimers[k]!);
|
||||||
|
scrollTimers[k] = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 使用主页面相同的CSS类定义以确保样式一致性 */
|
||||||
|
.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.list-card {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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__container.list-container {
|
||||||
|
height: auto;
|
||||||
|
max-height: 300px; /* 限制列表高度,超出滚动 */
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
/* 隐藏滚动条 */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE/Edge */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏webkit内核浏览器的滚动条 */
|
||||||
|
.chartCard__container.list-container::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 项目列表样式 */
|
||||||
|
.project-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.8rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border-left: 3px solid #2951E0;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 鼠标悬停项目项时的样式 */
|
||||||
|
.project-item:hover {
|
||||||
|
background: #f0f4ff;
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-rank {
|
||||||
|
min-width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #2951E0;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 特定类型的颜色定义 - 使用中文类名 */
|
||||||
|
/* .project-rank.CISA {
|
||||||
|
background: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-rank.投毒 {
|
||||||
|
background: #ffa500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-rank.CNVD {
|
||||||
|
background: #ffff00;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-rank.GitHub {
|
||||||
|
background: #0000ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-rank.修复 {
|
||||||
|
background: #008000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-rank.供应链 {
|
||||||
|
background: #800080;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.project-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -60,19 +60,14 @@ Vue 3 适配版代码
|
|||||||
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||||
import * as echarts from 'echarts';
|
import * as echarts from 'echarts';
|
||||||
import worldJson from './world.json';
|
import worldJson from './world.json';
|
||||||
|
import { mapVulnStatsPage1, mapRegionStatsPage1, mapDataPage1, mapVulnTypeStatsPage1 } from '@/api/jyh/situation';
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const chartDiv = ref(null);
|
const chartDiv = ref(null);
|
||||||
let myChart = null;
|
let myChart = null;
|
||||||
let timer = null;
|
let timer = null;
|
||||||
|
|
||||||
const vulnStats = ref([
|
const vulnStats = ref([]);
|
||||||
{ 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 regionStats = ref([
|
const regionStats = ref([
|
||||||
{ name: 'United States', score: 435202 },
|
{ name: 'United States', score: 435202 },
|
||||||
@@ -82,11 +77,8 @@ const regionStats = ref([
|
|||||||
{ name: 'Germany', score: 126397 },
|
{ name: 'Germany', score: 126397 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const alerts = [
|
// 实时情报数据状态
|
||||||
{ title: "高危漏洞通告", desc: "Apache Log4j2 远程代码执行漏洞复现" },
|
const alerts = ref([]);
|
||||||
{ title: "僵尸网络活动", desc: "Mirai 变种正在扫描 IoT 设备端口 23" },
|
|
||||||
{ title: "APT 组织活动", desc: "OceanLotus 组织针对金融行业的钓鱼攻击" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const currentAlert = ref(null);
|
const currentAlert = ref(null);
|
||||||
let alertIndex = 0;
|
let alertIndex = 0;
|
||||||
@@ -100,35 +92,116 @@ const geoCoordMap = {
|
|||||||
'Moscow': [37.6173, 55.7558],
|
'Moscow': [37.6173, 55.7558],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取实时情报数据
|
||||||
|
const fetchAlertsData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await mapVulnTypeStatsPage1();
|
||||||
|
if (response.code === 200) {
|
||||||
|
alerts.value = response.data || [];
|
||||||
|
if (alerts.value.length > 0) {
|
||||||
|
currentAlert.value = alerts.value[0];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('获取实时情报数据失败:', response.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 = () => {
|
const startAlertRotation = () => {
|
||||||
currentAlert.value = alerts[0];
|
if (alerts.value.length > 0) {
|
||||||
|
currentAlert.value = alerts.value[0];
|
||||||
|
}
|
||||||
timer = setInterval(() => {
|
timer = setInterval(() => {
|
||||||
alertIndex = (alertIndex + 1) % alerts.length;
|
if (alerts.value.length > 0) {
|
||||||
currentAlert.value = null;
|
alertIndex = (alertIndex + 1) % alerts.value.length;
|
||||||
setTimeout(() => {
|
currentAlert.value = null;
|
||||||
currentAlert.value = alerts[alertIndex];
|
setTimeout(() => {
|
||||||
}, 500);
|
currentAlert.value = alerts.value[alertIndex];
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
}, 3500);
|
}, 3500);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setChart = () => {
|
// 地图数据状态
|
||||||
|
const mapData = ref([]);
|
||||||
|
const scatterData = ref([]);
|
||||||
|
|
||||||
|
// 获取地图数据
|
||||||
|
const fetchMapData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await mapDataPage1();
|
||||||
|
if (response.code === 200) {
|
||||||
|
mapData.value = response.data;
|
||||||
|
} else {
|
||||||
|
console.error('获取地图数据失败:', response.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;
|
if (!chartDiv.value) return;
|
||||||
echarts.registerMap('world', worldJson);
|
echarts.registerMap('world', worldJson);
|
||||||
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 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const scatterData = [
|
// 初始化散点数据
|
||||||
|
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['Beijing'], 100] },
|
||||||
{ name: '上海节点', value: [...geoCoordMap['Shanghai'], 80] },
|
{ name: '上海节点', value: [...geoCoordMap['Shanghai'], 80] },
|
||||||
{ name: '广州节点', value: [...geoCoordMap['Guangzhou'], 70] },
|
{ name: '广州节点', value: [...geoCoordMap['Guangzhou'], 70] },
|
||||||
@@ -186,13 +259,13 @@ const setChart = () => {
|
|||||||
name: 'Threat Distribution',
|
name: 'Threat Distribution',
|
||||||
type: 'map',
|
type: 'map',
|
||||||
geoIndex: 0,
|
geoIndex: 0,
|
||||||
data: mapData
|
data: mapData.value
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Key Nodes',
|
name: 'Key Nodes',
|
||||||
type: 'effectScatter',
|
type: 'effectScatter',
|
||||||
coordinateSystem: 'geo',
|
coordinateSystem: 'geo',
|
||||||
data: scatterData,
|
data: scatterDataLocal,
|
||||||
symbolSize: (val) => val[2] / 5,
|
symbolSize: (val) => val[2] / 5,
|
||||||
showEffectOn: 'render',
|
showEffectOn: 'render',
|
||||||
rippleEffect: {
|
rippleEffect: {
|
||||||
@@ -222,10 +295,76 @@ const setChart = () => {
|
|||||||
myChart.setOption(option);
|
myChart.setOption(option);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取漏洞类型统计数据
|
||||||
|
const fetchVulnStats = async () => {
|
||||||
|
try {
|
||||||
|
const response = await mapVulnStatsPage1();
|
||||||
|
if (response.code === 200) {
|
||||||
|
// 为每个数据项添加颜色
|
||||||
|
const colors = ['#ef4444', '#f97316', '#eab308', '#3b82f6', '#10b981', '#8b5cf6', '#ec4899', '#06b6d4'];
|
||||||
|
vulnStats.value = response.data.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
color: colors[index % colors.length]
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
console.error('获取漏洞类型统计数据失败:', response.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();
|
||||||
|
if (response.code === 200) {
|
||||||
|
regionStats.value = response.data;
|
||||||
|
} else {
|
||||||
|
console.error('获取威胁情报地域排行数据失败:', response.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 () => {
|
onMounted(async () => {
|
||||||
await nextTick(); // 确保 DOM 已渲染
|
await nextTick(); // 确保 DOM 已渲染
|
||||||
setChart();
|
await Promise.all([fetchVulnStats(), fetchRegionStats(), fetchMapData(), fetchAlertsData()]); // 同时获取所有统计数据
|
||||||
|
await setChart(); // 等待图表设置完成
|
||||||
startAlertRotation();
|
startAlertRotation();
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,23 +43,7 @@
|
|||||||
<!-- 高危组件Top 10 -->
|
<!-- 高危组件Top 10 -->
|
||||||
<HighRiskComponentRank />
|
<HighRiskComponentRank />
|
||||||
|
|
||||||
<div class="index-module__NV_5cW__chartCard list-card">
|
<IntelligenceFeed />
|
||||||
<h3 class="chartCard__title">情报流</h3>
|
|
||||||
<!-- 新增:ref + 鼠标事件 -->
|
|
||||||
<div class="chartCard__container list-container"
|
|
||||||
ref="scrollContainer1"
|
|
||||||
@mouseenter="pauseScroll(1)"
|
|
||||||
@mouseleave="resumeScroll(1)">
|
|
||||||
<div class="project-list">
|
|
||||||
<div class="project-item" v-for="(item, index) in feedList" :key="index">
|
|
||||||
<span class="project-rank" :class="item.type">{{ item.type }}</span>
|
|
||||||
<div class="project-info">
|
|
||||||
<span class="project-name">{{ item.content }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="index-module__NV_5cW__chartCard chartCard__wide">
|
<div class="index-module__NV_5cW__chartCard chartCard__wide">
|
||||||
<div class="chartCard__header">
|
<div class="chartCard__header">
|
||||||
@@ -127,6 +111,7 @@ import { usePageResize } from '@/utils/hooks/usePageResize';
|
|||||||
import ThreatIntelMap from './components/ThreatIntelMap.vue';
|
import ThreatIntelMap from './components/ThreatIntelMap.vue';
|
||||||
import IndustryDistribution from './components/IndustryDistribution.vue';
|
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 StatsGlass from '@/views/Jyh/security/StatsGlass.vue';
|
import StatsGlass from '@/views/Jyh/security/StatsGlass.vue';
|
||||||
|
|
||||||
// 页面尺寸响应式
|
// 页面尺寸响应式
|
||||||
@@ -146,33 +131,7 @@ const excellentProjects = ref([
|
|||||||
{ name: 'NextDenovo', desc: '高效且精确的长读长纠错与组装工具', org: '武汉希望组生物科技有限公司' }
|
{ name: 'NextDenovo', desc: '高效且精确的长读长纠错与组装工具', org: '武汉希望组生物科技有限公司' }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 情报流数据
|
|
||||||
const feedList = ref([
|
|
||||||
{
|
|
||||||
type: 'CISA',
|
|
||||||
content: 'CISA KEV新增: WordPress 的 OneClick Chat to Order 插件,存在不安全的直接对象引用漏洞 (CVE-2025-13526) '
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: '投毒',
|
|
||||||
content: '投毒监控: npm 基建级包大规模投毒事件,影响chalk, debug, ansi-styles 等 18 个包'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'CNVD',
|
|
||||||
content: 'CNVD预警: Google Chrome信息泄露漏洞(CNVD-2025-3038304)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'GitHub',
|
|
||||||
content: 'GitHub安全通告: MovableType 软件的 ContentType 页面 "编辑分类集" 功能存在存储型跨站脚本漏洞'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: '修复',
|
|
||||||
content: '漏洞修复: Node.js 发布 v20.11.0 版本,修复 3 个高危漏洞'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: '供应链',
|
|
||||||
content: '供应链预警: Maven 中央仓库发现 12 个伪造开源组件,包含后门程序'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 2. 2025武汉市优秀开源软件项目
|
// 2. 2025武汉市优秀开源软件项目
|
||||||
const excellentProjects2025 = ref([
|
const excellentProjects2025 = ref([
|
||||||
@@ -428,82 +387,7 @@ const chartInstances = ref<{ [key: string]: echarts.ECharts | null }>({
|
|||||||
CVEChart: null
|
CVEChart: null
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===================== 新增:列表自动滚动逻辑 =====================
|
|
||||||
// 1. 定义滚动容器的ref
|
|
||||||
const scrollContainer1 = ref<HTMLDivElement | null>(null); // 第一个列表容器
|
|
||||||
const scrollContainer2 = ref<HTMLDivElement | null>(null); // 第二个列表容器
|
|
||||||
|
|
||||||
// 2. 存储定时器(键:1/2,值:定时器ID)
|
|
||||||
const scrollTimers: { [key: number]: NodeJS.Timeout | null } = {
|
|
||||||
1: null,
|
|
||||||
2: null
|
|
||||||
};
|
|
||||||
|
|
||||||
// 3. 滚动配置
|
|
||||||
const scrollStep = 1; // 每次滚动的步长(像素,越小越流畅)
|
|
||||||
const scrollInterval = 20; // 滚动间隔(毫秒,越小越流畅)
|
|
||||||
|
|
||||||
// 4. 通用自动滚动函数
|
|
||||||
const autoScroll = (containerKey: number) => {
|
|
||||||
// 获取对应容器
|
|
||||||
const container = containerKey === 1 ? scrollContainer1.value : scrollContainer2.value;
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
// 获取列表内容元素(project-list)
|
|
||||||
const projectList = container.querySelector('.project-list') as HTMLDivElement;
|
|
||||||
if (!projectList) return;
|
|
||||||
|
|
||||||
// 若内容高度 ≤ 容器高度,不执行滚动
|
|
||||||
if (projectList.scrollHeight <= container.clientHeight) {
|
|
||||||
// 清除现有定时器
|
|
||||||
if (scrollTimers[containerKey]) {
|
|
||||||
clearInterval(scrollTimers[containerKey]!);
|
|
||||||
scrollTimers[containerKey] = null;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清除现有定时器(避免重复)
|
|
||||||
if (scrollTimers[containerKey]) {
|
|
||||||
clearInterval(scrollTimers[containerKey]!);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动定时器滚动
|
|
||||||
scrollTimers[containerKey] = setInterval(() => {
|
|
||||||
// 向下滚动步长
|
|
||||||
container.scrollTop += scrollStep;
|
|
||||||
|
|
||||||
// 滚动到底部时,重置为0(循环滚动)
|
|
||||||
// 加1是为了兼容像素精度问题
|
|
||||||
if (projectList.scrollHeight - container.scrollTop <= container.clientHeight + 1) {
|
|
||||||
container.scrollTop = 0; // 直接重置(也可以用平滑滚动:container.scrollTo({ top: 0, behavior: 'smooth' }))
|
|
||||||
}
|
|
||||||
}, scrollInterval);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 5. 暂停滚动
|
|
||||||
const pauseScroll = (containerKey: number) => {
|
|
||||||
if (scrollTimers[containerKey]) {
|
|
||||||
clearInterval(scrollTimers[containerKey]!);
|
|
||||||
scrollTimers[containerKey] = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 6. 恢复滚动
|
|
||||||
const resumeScroll = (containerKey: number) => {
|
|
||||||
autoScroll(containerKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 7. 组件卸载时清除所有定时器
|
|
||||||
onUnmounted(() => {
|
|
||||||
Object.keys(scrollTimers).forEach(key => {
|
|
||||||
const k = Number(key);
|
|
||||||
if (scrollTimers[k]) {
|
|
||||||
clearInterval(scrollTimers[k]!);
|
|
||||||
scrollTimers[k] = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// CVE视图切换方法
|
// CVE视图切换方法
|
||||||
const switchCveView = (view: 'global' | 'regional') => {
|
const switchCveView = (view: 'global' | 'regional') => {
|
||||||
@@ -1148,18 +1032,12 @@ const initCharts = () => {
|
|||||||
// 页面resize时重绘图表
|
// 页面resize时重绘图表
|
||||||
watch(widthType, () => {
|
watch(widthType, () => {
|
||||||
initCharts();
|
initCharts();
|
||||||
// 重新启动滚动(防止resize后滚动失效)
|
|
||||||
autoScroll(1);
|
|
||||||
autoScroll(2);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 挂载时初始化
|
// 挂载时初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
initCharts();
|
initCharts();
|
||||||
// 启动列表自动滚动
|
|
||||||
autoScroll(1);
|
|
||||||
autoScroll(2);
|
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user