删除第一张风险监控地图(SecurityRiskMap)及相关代码

This commit is contained in:
gcw_yTjNWok9
2025-12-18 13:37:44 +08:00
parent bb17c69ba9
commit 7e6321a7b9
11 changed files with 2084 additions and 89 deletions

373
GlobalThreatMap.vue Normal file
View File

@@ -0,0 +1,373 @@
<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>

46
china.js Normal file

File diff suppressed because one or more lines are too long

521
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -32,6 +32,7 @@
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"devui-theme": "^0.0.7", "devui-theme": "^0.0.7",
"dompurify": "^3.0.5", "dompurify": "^3.0.5",
"echarts": "^6.0.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"highlightjs-line-numbers.js": "^2.9.0", "highlightjs-line-numbers.js": "^2.9.0",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",

268
src/utils/mapLoader.js Normal file
View File

@@ -0,0 +1,268 @@
/**
* 地图数据加载工具
* 统一管理世界地图和中国地图数据的导入
*/
import * as echarts from 'echarts'
/**
* 加载世界地图数据
* @returns {Promise<boolean>} 是否加载成功
*/
export const loadWorldMap = async () => {
try {
// 第一优先从根目录加载原始世界地图数据ECharts压缩格式
await import('/world.js')
// world.js 文件会自动注册地图,无需手动处理
console.log('World map loaded from root world.js file (ECharts compressed format)')
return true
} catch (error) {
console.warn('Failed to load world map from root world.js file:', error)
}
try {
// 第三优先从CDN加载真实的世界地图数据完整的国家轮廓
const response = await fetch('https://geo.datav.aliyun.com/areas_v3/bound/world.json')
if (response.ok) {
const worldGeoJSON = await response.json()
echarts.registerMap('world', worldGeoJSON)
console.log('World map loaded from CDN with real country boundaries')
return true
}
} catch (error) {
console.warn('Failed to load world map from CDN:', error)
}
// 最后的备用方案:使用简化的手动地图数据(仅作为最后选择)
console.warn('Using fallback world map data - this will show simplified rectangles')
const fallbackWorldGeoJSON = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": { "name": "China" },
"geometry": {
"type": "Polygon",
"coordinates": [[[73.5, 53.5], [134.8, 53.5], [134.8, 18.2], [73.5, 18.2], [73.5, 53.5]]]
}
},
{
"type": "Feature",
"properties": { "name": "United States" },
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[[[-158.2, 21.8], [-154.8, 21.8], [-154.8, 22.2], [-158.2, 22.2], [-158.2, 21.8]]],
[[[-178.3, 18.9], [-154.8, 18.9], [-154.8, 28.4], [-178.3, 28.4], [-178.3, 18.9]]],
[[[-171.8, 63.8], [-129.9, 63.8], [-129.9, 71.4], [-171.8, 71.4], [-171.8, 63.8]]],
[[[-125.0, 32.5], [-66.9, 32.5], [-66.9, 49.4], [-125.0, 49.4], [-125.0, 32.5]]]
]
}
},
{
"type": "Feature",
"properties": { "name": "Russia" },
"geometry": {
"type": "Polygon",
"coordinates": [[[19.6, 41.2], [180.0, 41.2], [180.0, 81.9], [19.6, 81.9], [19.6, 41.2]]]
}
},
{
"type": "Feature",
"properties": { "name": "Brazil" },
"geometry": {
"type": "Polygon",
"coordinates": [[[-73.9, -33.7], [-34.8, -33.7], [-34.8, 5.3], [-73.9, 5.3], [-73.9, -33.7]]]
}
},
{
"type": "Feature",
"properties": { "name": "Germany" },
"geometry": {
"type": "Polygon",
"coordinates": [[[5.9, 47.3], [15.0, 47.3], [15.0, 55.1], [5.9, 55.1], [5.9, 47.3]]]
}
},
{
"type": "Feature",
"properties": { "name": "India" },
"geometry": {
"type": "Polygon",
"coordinates": [[[68.2, 6.8], [97.4, 6.8], [97.4, 37.1], [68.2, 37.1], [68.2, 6.8]]]
}
},
{
"type": "Feature",
"properties": { "name": "Australia" },
"geometry": {
"type": "Polygon",
"coordinates": [[[113.3, -43.6], [153.6, -43.6], [153.6, -10.7], [113.3, -10.7], [113.3, -43.6]]]
}
},
{
"type": "Feature",
"properties": { "name": "Japan" },
"geometry": {
"type": "Polygon",
"coordinates": [[[129.4, 31.0], [145.8, 31.0], [145.8, 45.6], [129.4, 45.6], [129.4, 31.0]]]
}
},
{
"type": "Feature",
"properties": { "name": "United Kingdom" },
"geometry": {
"type": "Polygon",
"coordinates": [[[-8.6, 49.9], [1.8, 49.9], [1.8, 60.8], [-8.6, 60.8], [-8.6, 49.9]]]
}
},
{
"type": "Feature",
"properties": { "name": "France" },
"geometry": {
"type": "Polygon",
"coordinates": [[[-5.1, 41.3], [9.6, 41.3], [9.6, 51.1], [-5.1, 51.1], [-5.1, 41.3]]]
}
}
]
}
echarts.registerMap('world', fallbackWorldGeoJSON)
return true
}
/**
* 加载中国地图数据
* @returns {Promise<boolean>} 是否加载成功
*/
export const loadChinaMap = async () => {
try {
// 优先动态导入本地中国地图数据
await import('/china.js')
// china.js 文件会自动注册地图,无需手动处理
console.log('China map loaded from local file')
return true
} catch (error) {
console.warn('Failed to load china map from local file:', error)
}
try {
// 备用方案从CDN加载中国地图数据
const response = await fetch('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json')
if (response.ok) {
const chinaGeoJSON = await response.json()
echarts.registerMap('china', chinaGeoJSON)
console.log('China map loaded from CDN')
return true
}
} catch (error) {
console.warn('Failed to load china map from CDN:', error)
}
// 最后的备用方案:使用简化的手动地图数据
console.warn('Using fallback china map data')
const fallbackChinaGeoJSON = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": { "name": "北京" },
"geometry": {
"type": "Polygon",
"coordinates": [[[115.4, 39.4], [117.5, 39.4], [117.5, 41.1], [115.4, 41.1], [115.4, 39.4]]]
}
},
{
"type": "Feature",
"properties": { "name": "上海" },
"geometry": {
"type": "Polygon",
"coordinates": [[[120.9, 30.7], [122.2, 30.7], [122.2, 31.9], [120.9, 31.9], [120.9, 30.7]]]
}
},
{
"type": "Feature",
"properties": { "name": "广东" },
"geometry": {
"type": "Polygon",
"coordinates": [[[109.7, 20.2], [117.2, 20.2], [117.2, 25.5], [109.7, 25.5], [109.7, 20.2]]]
}
},
{
"type": "Feature",
"properties": { "name": "浙江" },
"geometry": {
"type": "Polygon",
"coordinates": [[[118.0, 27.1], [123.2, 27.1], [123.2, 31.2], [118.0, 31.2], [118.0, 27.1]]]
}
},
{
"type": "Feature",
"properties": { "name": "江苏" },
"geometry": {
"type": "Polygon",
"coordinates": [[[116.4, 30.8], [121.9, 30.8], [121.9, 35.3], [116.4, 35.3], [116.4, 30.8]]]
}
},
{
"type": "Feature",
"properties": { "name": "山东" },
"geometry": {
"type": "Polygon",
"coordinates": [[[114.8, 34.4], [122.7, 34.4], [122.7, 38.4], [114.8, 38.4], [114.8, 34.4]]]
}
},
{
"type": "Feature",
"properties": { "name": "四川" },
"geometry": {
"type": "Polygon",
"coordinates": [[[97.3, 26.0], [108.5, 26.0], [108.5, 34.3], [97.3, 34.3], [97.3, 26.0]]]
}
}
]
}
echarts.registerMap('china', fallbackChinaGeoJSON)
return true
}
/**
* 预加载所有地图数据
* @returns {Promise<{world: boolean, china: boolean}>} 加载结果
*/
export const preloadAllMaps = async () => {
const results = await Promise.allSettled([
loadWorldMap(),
loadChinaMap()
])
return {
world: results[0].status === 'fulfilled' ? results[0].value : false,
china: results[1].status === 'fulfilled' ? results[1].value : false
}
}
/**
* 检查地图是否已注册
* @param {string} mapName - 地图名称 ('world' 或 'china')
* @returns {boolean} 是否已注册
*/
export const isMapRegistered = (mapName) => {
try {
// 尝试获取地图数据来检查是否已注册
const mapData = echarts.getMap(mapName)
return !!mapData
} catch (error) {
return false
}
}
/**
* 获取地图加载状态
* @returns {{world: boolean, china: boolean}} 地图注册状态
*/
export const getMapStatus = () => {
return {
world: isMapRegistered('world'),
china: isMapRegistered('china')
}
}

View File

@@ -0,0 +1,468 @@
<template>
<div class="china-security-map">
<div ref="chartDiv" class="map-chart"></div>
<!-- 左侧省份威胁排行 -->
<div class="info-panel panel-left">
<div class="panel-title">🏛 省份威胁排行</div>
<div class="province-ranking">
<div class="rank-item" v-for="(item, index) in provinceRanking" :key="index">
<div class="rank-badge" :class="'rank-' + (index + 1)">{{ index + 1 }}</div>
<div class="rank-info">
<div class="rank-province">{{ item.province }}</div>
<div class="rank-threats">{{ item.threats.toLocaleString() }} 威胁</div>
</div>
<div class="rank-indicator" :style="{ background: item.riskLevel === 'high' ? '#ef4444' : item.riskLevel === 'medium' ? '#f59e0b' : '#10b981' }"></div>
</div>
</div>
</div>
<!-- 右侧安全态势面板 -->
<div class="info-panel panel-right">
<div class="panel-title">📈 安全态势分析</div>
<div class="security-stats">
<div class="stat-item" v-for="(stat, index) in securityStats" :key="index">
<div class="stat-icon" :style="{ color: stat.color }">{{ stat.icon }}</div>
<div class="stat-content">
<div class="stat-value" :style="{ color: stat.color }">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
<div class="stat-trend" :class="stat.trend">
{{ stat.trend === 'up' ? '↗' : stat.trend === 'down' ? '↘' : '→' }} {{ stat.change }}%
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
// 响应式数据
const chartDiv = ref(null)
const myChart = ref(null)
// 省份威胁排行
const provinceRanking = ref([
{ province: '北京', threats: 8420, riskLevel: 'high' },
{ province: '上海', threats: 7350, riskLevel: 'high' },
{ province: '广东', threats: 6870, riskLevel: 'high' },
{ province: '浙江', threats: 5540, riskLevel: 'medium' },
{ province: '江苏', threats: 4320, riskLevel: 'medium' },
{ province: '山东', threats: 3890, riskLevel: 'medium' },
{ province: '四川', threats: 2650, riskLevel: 'low' }
])
// 安全态势统计
const securityStats = ref([
{ icon: '🔥', label: '活跃攻击', value: '2,847', color: '#ef4444', trend: 'up', change: 15 },
{ icon: '🛡️', label: '防护成功', value: '15,692', color: '#10b981', trend: 'up', change: 8 },
{ icon: '⚠️', label: '高危漏洞', value: '156', color: '#f59e0b', trend: 'down', change: 12 },
{ icon: '🎯', label: '精准拦截', value: '98.7%', color: '#8b5cf6', trend: 'up', change: 2 }
])
// 主要城市坐标映射
const geoCoordMap = {
'北京': [116.4074, 39.9042],
'上海': [121.4737, 31.2304],
'广州': [113.2644, 23.1291],
'深圳': [114.0579, 22.5431],
'杭州': [120.1551, 30.2741],
'南京': [118.7969, 32.0603],
'武汉': [114.3054, 30.5931],
'成都': [104.0665, 30.5723],
'西安': [108.9398, 34.3416],
'重庆': [106.5516, 29.5630],
'天津': [117.2008, 39.0842],
'沈阳': [123.4315, 41.8057],
'长沙': [112.9388, 28.2282],
'郑州': [113.6254, 34.7466],
'济南': [117.1205, 36.6519],
'哈尔滨': [126.5358, 45.8023],
'长春': [125.3245, 43.8868],
'石家庄': [114.5149, 38.0428],
'太原': [112.5489, 37.8706],
'呼和浩特': [111.7519, 40.8414]
}
import { loadChinaMap } from '@/utils/mapLoader'
// 初始化图表
const initChart = () => {
if (!chartDiv.value) return
// 省份威胁热力数据
const provinceData = [
{ name: '北京', value: 100 },
{ name: '上海', value: 95 },
{ name: '广东', value: 90 },
{ name: '浙江', value: 75 },
{ name: '江苏', value: 70 },
{ name: '山东', value: 65 },
{ name: '四川', value: 60 },
{ name: '湖北', value: 55 },
{ name: '河南', value: 50 },
{ name: '湖南', value: 45 },
{ name: '安徽', value: 40 },
{ name: '河北', value: 38 },
{ name: '福建', value: 35 },
{ name: '江西', value: 32 },
{ name: '重庆', value: 30 },
{ name: '陕西', value: 28 },
{ name: '辽宁', value: 25 },
{ name: '天津', value: 22 },
{ name: '山西', value: 20 },
{ name: '吉林', value: 18 },
{ name: '黑龙江', value: 15 },
{ name: '内蒙古', value: 12 },
{ name: '广西', value: 25 },
{ name: '海南', value: 10 },
{ name: '贵州', value: 15 },
{ name: '云南', value: 20 },
{ name: '西藏', value: 5 },
{ name: '甘肃', value: 8 },
{ name: '青海', value: 3 },
{ name: '宁夏', value: 6 },
{ name: '新疆', value: 10 },
{ name: '台湾', value: 30 },
{ name: '香港', value: 25 },
{ name: '澳门', value: 15 }
]
// 安全监控节点
const securityNodes = [
{ name: '北京安全中心', value: [...geoCoordMap['北京'], 100] },
{ name: '上海监控点', value: [...geoCoordMap['上海'], 85] },
{ name: '广州分析中心', value: [...geoCoordMap['广州'], 75] },
{ name: '深圳预警站', value: [...geoCoordMap['深圳'], 70] },
{ name: '杭州情报点', value: [...geoCoordMap['杭州'], 65] },
{ name: '南京监测站', value: [...geoCoordMap['南京'], 60] },
{ name: '武汉防护点', value: [...geoCoordMap['武汉'], 55] },
{ name: '成都安全站', value: [...geoCoordMap['成都'], 50] }
]
// 攻击路径数据
const attackPaths = [
{ coords: [geoCoordMap['北京'], geoCoordMap['上海']] },
{ coords: [geoCoordMap['广州'], geoCoordMap['深圳']] },
{ coords: [geoCoordMap['杭州'], geoCoordMap['南京']] },
{ coords: [geoCoordMap['武汉'], geoCoordMap['成都']] }
]
const option = {
backgroundColor: 'transparent',
// 视觉映射组件
visualMap: {
min: 0,
max: 100,
left: '20',
bottom: '20',
text: ['高风险', '低风险'],
textStyle: { color: '#64748b', fontSize: 10 },
calculable: true,
inRange: {
color: ['#e0f2fe', '#bae6fd', '#7dd3fc', '#38bdf8', '#0ea5e9']
}
},
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(255,255,255,0.95)',
borderColor: '#e2e8f0',
textStyle: { color: '#1f2937' },
formatter: function(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: 'china',
roam: true,
zoom: 1.2,
label: {
show: true,
fontSize: 10,
color: '#64748b'
},
itemStyle: {
areaColor: '#f8fafc',
borderColor: '#cbd5e1',
borderWidth: 1
},
emphasis: {
label: {
show: true,
color: '#1f2937'
},
itemStyle: {
areaColor: '#0ea5e9',
shadowBlur: 10,
shadowColor: 'rgba(14, 165, 233, 0.3)'
}
}
},
series: [
// 省份威胁热力图
{
name: '威胁分布',
type: 'map',
geoIndex: 0,
data: provinceData
},
// 安全监控节点
{
name: '安全节点',
type: 'effectScatter',
coordinateSystem: 'geo',
data: securityNodes,
symbolSize: function (val) {
return val[2] / 8
},
showEffectOn: 'render',
rippleEffect: {
brushType: 'stroke',
scale: 2.5
},
label: {
show: true,
formatter: '{b}',
position: 'right',
color: '#1f2937',
fontSize: 9,
textBorderColor: '#fff',
textBorderWidth: 1
},
itemStyle: {
color: '#10b981',
shadowBlur: 10,
shadowColor: '#10b981'
},
zlevel: 2
},
// 攻击路径 (红色飞线)
{
type: 'lines',
zlevel: 1,
effect: {
show: true,
period: 4,
trailLength: 0.7,
color: '#ef4444',
symbolSize: 3
},
lineStyle: {
color: '#ef4444',
width: 0,
curveness: 0.2
},
data: attackPaths
},
// 静态连线背景
{
type: 'lines',
zlevel: 0,
symbol: ['none', 'arrow'],
symbolSize: 6,
effect: { show: false },
lineStyle: {
color: '#ef4444',
width: 1,
opacity: 0.3,
curveness: 0.2
},
data: attackPaths
}
]
}
myChart.value = echarts.init(chartDiv.value)
myChart.value.setOption(option)
}
// 组件挂载
onMounted(async () => {
await loadChinaMap()
initChart()
window.addEventListener('resize', () => {
myChart.value && myChart.value.resize()
})
})
// 组件卸载
onUnmounted(() => {
if (myChart.value) {
myChart.value.dispose()
}
window.removeEventListener('resize', () => {
myChart.value && myChart.value.resize()
})
})
</script>
<style lang="scss" scoped>
.china-security-map {
position: relative;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border-radius: 0.5rem;
overflow: hidden;
}
.map-chart {
width: 100%;
height: 100%;
}
/* 信息面板通用样式 */
.info-panel {
position: absolute;
top: 20px;
width: 260px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(8px);
border: 1px solid rgba(14, 165, 233, 0.2);
border-radius: 0.5rem;
padding: 16px;
z-index: 10;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
.panel-title {
font-size: 14px;
font-weight: 600;
color: #0ea5e9;
margin-bottom: 12px;
border-bottom: 1px solid rgba(14, 165, 233, 0.2);
padding-bottom: 8px;
}
}
.panel-left { left: 20px; }
.panel-right { right: 20px; }
/* 省份威胁排行 */
.province-ranking {
.rank-item {
display: flex;
align-items: center;
margin-bottom: 10px;
padding: 8px;
border-radius: 6px;
transition: background-color 0.2s;
&:hover {
background: rgba(14, 165, 233, 0.05);
}
.rank-badge {
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 700;
color: white;
margin-right: 12px;
&.rank-1 { background: #ef4444; }
&.rank-2 { background: #f97316; }
&.rank-3 { background: #eab308; }
&.rank-4, &.rank-5, &.rank-6, &.rank-7 { background: #6b7280; }
}
.rank-info {
flex: 1;
.rank-province {
font-size: 12px;
font-weight: 600;
color: #1f2937;
margin-bottom: 2px;
}
.rank-threats {
font-size: 10px;
color: #6b7280;
}
}
.rank-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
}
}
}
/* 安全态势统计 */
.security-stats {
.stat-item {
display: flex;
align-items: center;
margin-bottom: 12px;
padding: 8px;
border-radius: 6px;
transition: background-color 0.2s;
&:hover {
background: rgba(14, 165, 233, 0.05);
}
.stat-icon {
font-size: 20px;
margin-right: 12px;
}
.stat-content {
flex: 1;
.stat-value {
font-size: 16px;
font-weight: 700;
margin-bottom: 2px;
}
.stat-label {
font-size: 11px;
color: #6b7280;
margin-bottom: 2px;
}
.stat-trend {
font-size: 10px;
font-weight: 600;
&.up { color: #ef4444; }
&.down { color: #10b981; }
&.stable { color: #6b7280; }
}
}
}
}
/* 响应式适配 */
@media (max-width: 768px) {
.info-panel {
width: 220px;
padding: 12px;
top: 15px;
}
.panel-left { left: 15px; }
.panel-right { right: 15px; }
}
</style>

View File

@@ -0,0 +1,418 @@
<template>
<div class="threat-intel-map">
<div ref="chartDiv" class="map-chart"></div>
<!-- 左侧漏洞类型面板 -->
<div class="info-panel panel-left">
<div class="panel-title">🔍 漏洞类型分布</div>
<div class="vuln-list">
<div class="vuln-item" v-for="(item, index) in vulnTypes" :key="index">
<div class="vuln-header">
<span class="vuln-name">{{ item.name }}</span>
<span class="vuln-count">{{ item.count }}</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: item.percentage + '%', background: item.color }"></div>
</div>
<div class="vuln-trend" :class="item.trend">
{{ item.trend === 'up' ? '↗' : item.trend === 'down' ? '↘' : '→' }} {{ item.change }}%
</div>
</div>
</div>
</div>
<!-- 右侧威胁情报排行 -->
<div class="info-panel panel-right">
<div class="panel-title">📊 威胁情报排行</div>
<div class="intel-ranking">
<div class="rank-item" v-for="(item, index) in intelRanking" :key="index">
<div class="rank-badge" :class="'rank-' + (index + 1)">{{ index + 1 }}</div>
<div class="rank-info">
<div class="rank-country">{{ item.country }}</div>
<div class="rank-threats">{{ item.threats.toLocaleString() }} 威胁</div>
</div>
<div class="rank-indicator" :style="{ background: item.riskLevel === 'high' ? '#ef4444' : item.riskLevel === 'medium' ? '#f59e0b' : '#10b981' }"></div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
// 响应式数据
const chartDiv = ref(null)
const myChart = ref(null)
// 漏洞类型数据
const vulnTypes = ref([
{ name: 'XSS 跨站脚本', count: 1247, percentage: 35, color: '#ef4444', trend: 'up', change: 12 },
{ name: 'SQL 注入', count: 892, percentage: 25, color: '#f97316', trend: 'down', change: 8 },
{ name: '缓冲区溢出', count: 634, percentage: 18, color: '#eab308', trend: 'up', change: 15 },
{ name: '输入验证', count: 445, percentage: 12, color: '#3b82f6', trend: 'stable', change: 2 },
{ name: '信息泄露', count: 356, percentage: 10, color: '#10b981', trend: 'down', change: 5 }
])
// 威胁情报排行
const intelRanking = ref([
{ country: 'United States', threats: 15420, riskLevel: 'high' },
{ country: 'China', threats: 12350, riskLevel: 'high' },
{ country: 'Russia', threats: 9870, riskLevel: 'high' },
{ country: 'Germany', threats: 6540, riskLevel: 'medium' },
{ country: 'Brazil', threats: 4320, riskLevel: 'medium' }
])
// 地理坐标映射 - 主要城市坐标
const geoCoordMap = {
'Beijing': [116.4074, 39.9042], // 北京
'Shanghai': [121.4737, 31.2304], // 上海
'Guangzhou': [113.2644, 23.1291], // 广州
'Shenzhen': [114.0579, 22.5431], // 深圳
'Washington': [-77.0369, 38.9072], // 华盛顿
'NewYork': [-74.0060, 40.7128], // 纽约
'Moscow': [37.6173, 55.7558], // 莫斯科
'Berlin': [13.4050, 52.5200], // 柏林
'Tokyo': [139.6917, 35.6895], // 东京
'London': [-0.1278, 51.5074], // 伦敦
'Paris': [2.3522, 48.8566], // 巴黎
'Sydney': [151.2093, -33.8688], // 悉尼
'Mumbai': [72.8777, 19.0760], // 孟买
'Dubai': [55.2708, 25.2048], // 迪拜
'Toronto': [-79.3832, 43.6532], // 多伦多
'SaoPaulo': [-46.6333, -23.5505], // 圣保罗
'Cairo': [31.2357, 30.0444], // 开罗
'Istanbul': [28.9784, 41.0082], // 伊斯坦布尔
'Tehran': [51.3890, 35.6892], // 德黑兰
'Riyadh': [46.6753, 24.7136] // 利雅得
}
import { loadWorldMap } from '@/utils/mapLoader'
// 初始化图表
const initChart = () => {
if (!chartDiv.value) return
// 地图热力数据
const mapData = [
{ name: 'China', value: 100 },
{ name: 'United States', value: 95 },
{ name: 'Russia', value: 80 },
{ name: 'Germany', value: 65 },
{ name: 'Brazil', value: 45 },
{ name: 'India', value: 55 },
{ name: 'Japan', value: 40 },
{ name: 'United Kingdom', value: 35 },
{ name: 'France', value: 30 },
{ name: 'Australia', value: 25 },
{ name: 'Canada', value: 35 },
{ name: 'Mexico', value: 20 },
{ name: 'Argentina', value: 15 },
{ name: 'South Africa', value: 18 },
{ name: 'Egypt', value: 22 },
{ name: 'Turkey', value: 28 },
{ name: 'Iran', value: 32 },
{ name: 'Saudi Arabia', value: 25 },
{ name: 'Kazakhstan', value: 12 },
{ name: 'Mongolia', value: 8 }
]
// 威胁情报节点
const intelNodes = [
{ name: '北京情报中心', value: [...geoCoordMap['Beijing'], 100] },
{ name: '上海监测点', value: [...geoCoordMap['Shanghai'], 85] },
{ name: '广州分析中心', value: [...geoCoordMap['Guangzhou'], 75] },
{ name: '深圳预警站', value: [...geoCoordMap['Shenzhen'], 70] },
{ name: '华盛顿情报站', value: [...geoCoordMap['Washington'], 90] },
{ name: '纽约监控点', value: [...geoCoordMap['NewYork'], 80] },
{ name: '莫斯科分析点', value: [...geoCoordMap['Moscow'], 85] },
{ name: '柏林预警中心', value: [...geoCoordMap['Berlin'], 65] },
{ name: '东京监测站', value: [...geoCoordMap['Tokyo'], 60] },
{ name: '伦敦情报点', value: [...geoCoordMap['London'], 55] }
]
const option = {
backgroundColor: 'transparent',
// 视觉映射组件
visualMap: {
min: 0,
max: 100,
left: '20',
bottom: '20',
text: ['高风险', '低风险'],
textStyle: { color: '#64748b', fontSize: 10 },
calculable: true,
inRange: {
color: ['#e0e7ff', '#c7d2fe', '#a5b4fc', '#818cf8', '#6366f1']
}
},
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(255,255,255,0.95)',
borderColor: '#e2e8f0',
textStyle: { color: '#1f2937' },
formatter: function(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: '#f8fafc',
borderColor: '#e2e8f0',
borderWidth: 1
},
emphasis: {
label: { show: false },
itemStyle: {
areaColor: '#3b82f6',
shadowBlur: 10,
shadowColor: 'rgba(59, 130, 246, 0.3)'
}
}
},
series: [
// 地域威胁热力图
{
name: '威胁分布',
type: 'map',
geoIndex: 0,
data: mapData
},
// 情报节点
{
name: '情报节点',
type: 'effectScatter',
coordinateSystem: 'geo',
data: intelNodes,
symbolSize: function (val) {
return val[2] / 6
},
showEffectOn: 'render',
rippleEffect: {
brushType: 'stroke',
scale: 2.5
},
label: {
show: true,
formatter: '{b}',
position: 'right',
color: '#1f2937',
fontSize: 9,
textBorderColor: '#fff',
textBorderWidth: 1
},
itemStyle: {
color: '#f59e0b',
shadowBlur: 10,
shadowColor: '#f59e0b'
},
zlevel: 1
}
]
}
myChart.value = echarts.init(chartDiv.value)
myChart.value.setOption(option)
}
// 组件挂载
onMounted(async () => {
await loadWorldMap()
initChart()
window.addEventListener('resize', () => {
myChart.value && myChart.value.resize()
})
})
// 组件卸载
onUnmounted(() => {
if (myChart.value) {
myChart.value.dispose()
}
window.removeEventListener('resize', () => {
myChart.value && myChart.value.resize()
})
})
</script>
<style lang="scss" scoped>
.threat-intel-map {
position: relative;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #f8fafc 0%, #e0e7ff 100%);
border-radius: 0.5rem;
overflow: hidden;
}
.map-chart {
width: 100%;
height: 100%;
}
/* 信息面板通用样式 */
.info-panel {
position: absolute;
top: 20px;
width: 260px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(8px);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: 0.5rem;
padding: 16px;
z-index: 10;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
.panel-title {
font-size: 14px;
font-weight: 600;
color: #3b82f6;
margin-bottom: 12px;
border-bottom: 1px solid rgba(59, 130, 246, 0.2);
padding-bottom: 8px;
}
}
.panel-left { left: 20px; }
.panel-right { right: 20px; }
/* 漏洞类型列表 */
.vuln-list {
.vuln-item {
margin-bottom: 12px;
.vuln-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
.vuln-name {
font-size: 12px;
color: #374151;
font-weight: 500;
}
.vuln-count {
font-size: 11px;
color: #6b7280;
font-weight: 600;
}
}
.progress-bar {
height: 4px;
background: rgba(59, 130, 246, 0.1);
border-radius: 2px;
overflow: hidden;
margin-bottom: 4px;
.progress-fill {
height: 100%;
border-radius: 2px;
transition: width 1s ease;
}
}
.vuln-trend {
font-size: 10px;
font-weight: 600;
&.up { color: #ef4444; }
&.down { color: #10b981; }
&.stable { color: #6b7280; }
}
}
}
/* 威胁情报排行 */
.intel-ranking {
.rank-item {
display: flex;
align-items: center;
margin-bottom: 10px;
padding: 8px;
border-radius: 6px;
transition: background-color 0.2s;
&:hover {
background: rgba(59, 130, 246, 0.05);
}
.rank-badge {
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 700;
color: white;
margin-right: 12px;
&.rank-1 { background: #ef4444; }
&.rank-2 { background: #f97316; }
&.rank-3 { background: #eab308; }
&.rank-4, &.rank-5 { background: #6b7280; }
}
.rank-info {
flex: 1;
.rank-country {
font-size: 12px;
font-weight: 600;
color: #1f2937;
margin-bottom: 2px;
}
.rank-threats {
font-size: 10px;
color: #6b7280;
}
}
.rank-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
}
}
}
/* 响应式适配 */
@media (max-width: 768px) {
.info-panel {
width: 220px;
padding: 12px;
top: 15px;
}
.panel-left { left: 15px; }
.panel-right { right: 15px; }
}
</style>

View File

@@ -66,7 +66,15 @@
<!-- 图表网格布局新增多个模块 --> <!-- 图表网格布局新增多个模块 -->
<div class="index-module__NV_5cW__chartsGrid"> <div class="index-module__NV_5cW__chartsGrid">
<!-- 1. 武汉优秀开源项目列表展示--> <!-- 1. 全球威胁情报分析地图 -->
<div class="index-module__NV_5cW__chartCard chartCard__wide">
<h3 class="chartCard__title">🛡 全球威胁情报分析与监测</h3>
<div class="chartCard__container map-container">
<ThreatIntelMap />
</div>
</div>
<!-- 2. 武汉优秀开源项目列表展示-->
<div class="index-module__NV_5cW__chartCard list-card"> <div class="index-module__NV_5cW__chartCard list-card">
<h3 class="chartCard__title">武汉优秀开源项目</h3> <h3 class="chartCard__title">武汉优秀开源项目</h3>
<!-- 新增ref + 鼠标事件 --> <!-- 新增ref + 鼠标事件 -->
@@ -87,7 +95,7 @@
</div> </div>
</div> </div>
<!-- 2. 高校开源参与分布饼图--> <!-- 3. 高校开源参与分布饼图-->
<div class="index-module__NV_5cW__chartCard"> <div class="index-module__NV_5cW__chartCard">
<h3 class="chartCard__title">高校开源参与分布</h3> <h3 class="chartCard__title">高校开源参与分布</h3>
<div class="chartCard__container"> <div class="chartCard__container">
@@ -96,7 +104,7 @@
</div> </div>
</div> </div>
<!-- 3. 开源社区区域分布柱状图--> <!-- 4. 开源社区区域分布柱状图-->
<div class="index-module__NV_5cW__chartCard"> <div class="index-module__NV_5cW__chartCard">
<h3 class="chartCard__title">开源社区区域分布</h3> <h3 class="chartCard__title">开源社区区域分布</h3>
<div class="chartCard__container"> <div class="chartCard__container">
@@ -105,7 +113,7 @@
</div> </div>
</div> </div>
<!-- 4. OSS Compass开源项目排行横向柱状图--> <!-- 5. OSS Compass开源项目排行横向柱状图-->
<div class="index-module__NV_5cW__chartCard"> <div class="index-module__NV_5cW__chartCard">
<h3 class="chartCard__title">OSS Compass 开源项目排行</h3> <h3 class="chartCard__title">OSS Compass 开源项目排行</h3>
<div class="chartCard__container"> <div class="chartCard__container">
@@ -114,7 +122,7 @@
</div> </div>
</div> </div>
<!-- 5. 武汉开源开发者排行榜横向柱状图--> <!-- 6. 武汉开源开发者排行榜横向柱状图-->
<div class="index-module__NV_5cW__chartCard"> <div class="index-module__NV_5cW__chartCard">
<h3 class="chartCard__title">武汉开源开发者排行榜</h3> <h3 class="chartCard__title">武汉开源开发者排行榜</h3>
<div class="chartCard__container"> <div class="chartCard__container">
@@ -123,7 +131,7 @@
</div> </div>
</div> </div>
<!-- 6. 2025武汉市优秀开源软件项目列表--> <!-- 7. 2025武汉市优秀开源软件项目列表-->
<div class="index-module__NV_5cW__chartCard list-card"> <div class="index-module__NV_5cW__chartCard list-card">
<h3 class="chartCard__title">2025武汉市优秀开源软件项目</h3> <h3 class="chartCard__title">2025武汉市优秀开源软件项目</h3>
<!-- 新增ref + 鼠标事件 --> <!-- 新增ref + 鼠标事件 -->
@@ -144,7 +152,7 @@
</div> </div>
</div> </div>
<!-- 7. 武汉市语言与技术竞争趋势折线图近五年--> <!-- 8. 武汉市语言与技术竞争趋势折线图近五年-->
<div class="index-module__NV_5cW__chartCard chartCard__wide"> <div class="index-module__NV_5cW__chartCard chartCard__wide">
<h3 class="chartCard__title">武汉市语言与技术竞争趋势2021-2025</h3> <h3 class="chartCard__title">武汉市语言与技术竞争趋势2021-2025</h3>
<div class="chartCard__container"> <div class="chartCard__container">
@@ -239,6 +247,7 @@
import { onMounted, ref, nextTick, watch, onUnmounted } from 'vue'; import { onMounted, ref, nextTick, watch, onUnmounted } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import { usePageResize } from '@/utils/hooks/usePageResize'; import { usePageResize } from '@/utils/hooks/usePageResize';
import ThreatIntelMap from './components/ThreatIntelMap.vue';
// 页面尺寸响应式 // 页面尺寸响应式
const { widthType } = usePageResize(); const { widthType } = usePageResize();
@@ -952,6 +961,26 @@ onMounted(() => {
position: relative; position: relative;
} }
/* 地图容器特殊样式 */
.chartCard__container.map-container {
height: 500px; /* 地图需要更大的高度 */
border-radius: 0.5rem;
overflow: hidden;
background: transparent; /* 让地图组件自己控制背景 */
}
/* 地图卡片标题样式优化 */
.index-module__NV_5cW__chartCard:has(.map-container) .chartCard__title {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(8px);
border-radius: 20px;
padding: 8px 16px;
margin: 0 auto 16px;
width: fit-content;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
}
/* 列表容器样式:隐藏原生滚动条(可选,视觉更美观) */ /* 列表容器样式:隐藏原生滚动条(可选,视觉更美观) */
.chartCard__container.list-container { .chartCard__container.list-container {
height: auto; height: auto;

20
world.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -2866,6 +2866,14 @@ eastasianwidth@^0.2.0:
resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
echarts@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz"
integrity sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==
dependencies:
tslib "2.3.0"
zrender "6.0.0"
echarts@5.3.3: echarts@5.3.3:
version "5.3.3" version "5.3.3"
resolved "https://registry.npmjs.org/echarts/-/echarts-5.3.3.tgz" resolved "https://registry.npmjs.org/echarts/-/echarts-5.3.3.tgz"
@@ -5596,3 +5604,10 @@ zrender@5.3.2:
integrity sha512-8IiYdfwHj2rx0UeIGZGGU4WEVSDEdeVCaIg/fomejg1Xu6OifAL1GVzIPHg2D+MyUkbNgPWji90t0a8IDk+39w== integrity sha512-8IiYdfwHj2rx0UeIGZGGU4WEVSDEdeVCaIg/fomejg1Xu6OifAL1GVzIPHg2D+MyUkbNgPWji90t0a8IDk+39w==
dependencies: dependencies:
tslib "2.3.0" tslib "2.3.0"
zrender@6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz"
integrity sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==
dependencies:
tslib "2.3.0"