Merge remote-tracking branch 'origin/SAP' into SAP

This commit is contained in:
fmk1023
2026-01-26 18:03:55 +08:00
6 changed files with 493 additions and 179 deletions

View File

@@ -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({
url: `/api/v1/page1/industryDistribution`,
method: 'get',

View File

@@ -23,7 +23,7 @@
<div class="stat-separator"></div>
<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>
</div>
</div>

View File

@@ -13,6 +13,7 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue';
import * as echarts from 'echarts';
import { industryDistributionPage1 } from '@/api/jyh/situation';
// 定义数据类型
interface IndustryData {
@@ -20,18 +21,6 @@ interface IndustryData {
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 = [
'#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);
@@ -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(() => {
nextTick(() => {
initChart();
});
onMounted(async () => {
await fetchIndustryData(); // 获取行业分布数据
await nextTick();
initChart();
window.addEventListener('resize', handleResize);
});

View 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>

View File

@@ -60,19 +60,14 @@ Vue 3 适配版代码
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([
{ 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 vulnStats = ref([]);
const regionStats = ref([
{ name: 'United States', score: 435202 },
@@ -82,11 +77,8 @@ const regionStats = ref([
{ name: 'Germany', score: 126397 },
]);
const alerts = [
{ title: "高危漏洞通告", desc: "Apache Log4j2 远程代码执行漏洞复现" },
{ title: "僵尸网络活动", desc: "Mirai 变种正在扫描 IoT 设备端口 23" },
{ title: "APT 组织活动", desc: "OceanLotus 组织针对金融行业的钓鱼攻击" },
];
// 实时情报数据状态
const alerts = ref([]);
const currentAlert = ref(null);
let alertIndex = 0;
@@ -100,35 +92,116 @@ const geoCoordMap = {
'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 = () => {
currentAlert.value = alerts[0];
if (alerts.value.length > 0) {
currentAlert.value = alerts.value[0];
}
timer = setInterval(() => {
alertIndex = (alertIndex + 1) % alerts.length;
currentAlert.value = null;
setTimeout(() => {
currentAlert.value = alerts[alertIndex];
}, 500);
if (alerts.value.length > 0) {
alertIndex = (alertIndex + 1) % alerts.value.length;
currentAlert.value = null;
setTimeout(() => {
currentAlert.value = alerts.value[alertIndex];
}, 500);
}
}, 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;
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['Shanghai'], 80] },
{ name: '广州节点', value: [...geoCoordMap['Guangzhou'], 70] },
@@ -186,13 +259,13 @@ const setChart = () => {
name: 'Threat Distribution',
type: 'map',
geoIndex: 0,
data: mapData
data: mapData.value
},
{
name: 'Key Nodes',
type: 'effectScatter',
coordinateSystem: 'geo',
data: scatterData,
data: scatterDataLocal,
symbolSize: (val) => val[2] / 5,
showEffectOn: 'render',
rippleEffect: {
@@ -222,10 +295,76 @@ const setChart = () => {
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 () => {
await nextTick(); // 确保 DOM 已渲染
setChart();
await Promise.all([fetchVulnStats(), fetchRegionStats(), fetchMapData(), fetchAlertsData()]); // 同时获取所有统计数据
await setChart(); // 等待图表设置完成
startAlertRotation();
window.addEventListener('resize', handleResize);
});

View File

@@ -43,23 +43,7 @@
<!-- 高危组件Top 10 -->
<HighRiskComponentRank />
<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>
<IntelligenceFeed />
<div class="index-module__NV_5cW__chartCard chartCard__wide">
<div class="chartCard__header">
@@ -127,6 +111,7 @@ import { usePageResize } from '@/utils/hooks/usePageResize';
import ThreatIntelMap from './components/ThreatIntelMap.vue';
import IndustryDistribution from './components/IndustryDistribution.vue';
import HighRiskComponentRank from './components/HighRiskComponentRank.vue';
import IntelligenceFeed from './components/IntelligenceFeed.vue';
import StatsGlass from '@/views/Jyh/security/StatsGlass.vue';
// 页面尺寸响应式
@@ -146,33 +131,7 @@ const excellentProjects = ref([
{ 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武汉市优秀开源软件项目
const excellentProjects2025 = ref([
@@ -428,82 +387,7 @@ const chartInstances = ref<{ [key: string]: echarts.ECharts | 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视图切换方法
const switchCveView = (view: 'global' | 'regional') => {
@@ -1148,18 +1032,12 @@ const initCharts = () => {
// 页面resize时重绘图表
watch(widthType, () => {
initCharts();
// 重新启动滚动防止resize后滚动失效
autoScroll(1);
autoScroll(2);
});
// 挂载时初始化
onMounted(() => {
setTimeout(() => {
initCharts();
// 启动列表自动滚动
autoScroll(1);
autoScroll(2);
}, 100);
});
</script>