From 9f071464bbc8add495a5468eea82102f99558f5b Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 6 May 2026 20:21:32 +0800 Subject: [PATCH] =?UTF-8?q?revert:=20=E6=81=A2=E5=A4=8D=20ECharts=20?= =?UTF-8?q?=E4=B8=96=E7=95=8C=E5=9C=B0=E5=9B=BE=E5=B1=95=E7=A4=BA=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E5=88=86=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/admin/index.vue | 441 ++++++++++++++++++++++++----- 1 file changed, 370 insertions(+), 71 deletions(-) diff --git a/frontend/src/pages/admin/index.vue b/frontend/src/pages/admin/index.vue index bc7ba29..752cf99 100644 --- a/frontend/src/pages/admin/index.vue +++ b/frontend/src/pages/admin/index.vue @@ -48,9 +48,9 @@ const provinces = ref([]) const onlineTrendData = ref([]) const recentTickets = ref([]) -const distributionChart = ref(null) +const mapChart = ref(null) const activityChart = ref(null) -let distributionChartInstance: any = null +let chartInstance: any = null let activityChartInstance: any = null const overseasUsers = computed(() => { @@ -231,77 +231,30 @@ function fetchCurrentUser() { } } -async function initDistributionChart() { - if (!distributionChart.value) +async function initMapChart() { + if (!mapChart.value) return try { - const { default: Chart } = await import('chart.js/auto') + const [echarts, worldResponse] = await Promise.all([ + import('echarts'), + fetch('/world.json'), + ]) - const ctx = distributionChart.value.getContext('2d') - if (!ctx) - return - - if (distributionChartInstance) { - distributionChartInstance.destroy() + if (!worldResponse.ok) { + throw new Error('Failed to load world map data') } - const data = provinces.value.filter((d: any) => d.count > 0) - const labels = data.map((d: any) => d.name) - const values = data.map((d: any) => d.count) - const dark = isDark.value + const worldData = await worldResponse.json() - const colors = [ - '#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', - '#06b6d4', '#ec4899', '#14b8a6', '#f97316', '#6366f1', - ] + echarts.registerMap('world', worldData) - distributionChartInstance = new Chart(ctx, { - type: 'doughnut', - data: { - labels, - datasets: [{ - data: values, - backgroundColor: colors.slice(0, values.length), - borderColor: dark ? '#1e293b' : '#ffffff', - borderWidth: 2, - hoverOffset: 8, - }], - }, - options: { - responsive: true, - maintainAspectRatio: false, - cutout: '55%', - plugins: { - legend: { - position: 'right', - labels: { - color: dark ? '#cbd5e1' : '#475569', - padding: 16, - usePointStyle: true, - pointStyleWidth: 12, - font: { - size: 13, - }, - }, - }, - tooltip: { - backgroundColor: 'rgba(15, 23, 42, 0.95)', - titleColor: '#f8fafc', - bodyColor: '#cbd5e1', - borderColor: '#334155', - borderWidth: 1, - padding: 12, - callbacks: { - label: (context: any) => { - const total = context.dataset.data.reduce((a: number, b: number) => a + b, 0) - const percentage = total > 0 ? ((context.parsed / total) * 100).toFixed(1) : '0' - return ` ${context.label}: ${context.parsed} (${percentage}%)` - }, - }, - }, - }, - }, + chartInstance = echarts.init(mapChart.value) + + updateMapOption() + + window.addEventListener('resize', () => { + chartInstance?.resize() }) } catch (error) { @@ -309,6 +262,352 @@ async function initDistributionChart() { } } +const worldNameMap: Record = { + 'Afghanistan': '阿富汗', + 'Albania': '阿尔巴尼亚', + 'Algeria': '阿尔及利亚', + 'American Samoa': '美属萨摩亚', + 'Andorra': '安道尔', + 'Angola': '安哥拉', + 'Anguilla': '安圭拉', + 'Antarctica': '南极洲', + 'Antigua and Barbuda': '安提瓜和巴布达', + 'Argentina': '阿根廷', + 'Armenia': '亚美尼亚', + 'Aruba': '阿鲁巴', + 'Australia': '澳大利亚', + 'Austria': '奥地利', + 'Azerbaijan': '阿塞拜疆', + 'Bahamas': '巴哈马', + 'Bahrain': '巴林', + 'Bangladesh': '孟加拉国', + 'Barbados': '巴巴多斯', + 'Belarus': '白俄罗斯', + 'Belgium': '比利时', + 'Belize': '伯利兹', + 'Benin': '贝宁', + 'Bermuda': '百慕大', + 'Bhutan': '不丹', + 'Bolivia': '玻利维亚', + 'Bosnia and Herzegovina': '波黑', + 'Botswana': '博茨瓦纳', + 'Brazil': '巴西', + 'British Indian Ocean Ter.': '英属印度洋领地', + 'Brunei': '文莱', + 'Bulgaria': '保加利亚', + 'Burkina Faso': '布基纳法索', + 'Burundi': '布隆迪', + 'Cambodia': '柬埔寨', + 'Cameroon': '喀麦隆', + 'Canada': '加拿大', + 'Cape Verde': '佛得角', + 'Cayman Is.': '开曼群岛', + 'Central African Rep.': '中非', + 'Chad': '乍得', + 'Chile': '智利', + 'China': '中国', + 'Colombia': '哥伦比亚', + 'Comoros': '科摩罗', + 'Congo': '刚果', + 'Dem. Rep. Congo': '刚果民主共和国', + 'Cook Is.': '库克群岛', + 'Costa Rica': '哥斯达黎加', + 'Croatia': '克罗地亚', + 'Cuba': '古巴', + 'Cyprus': '塞浦路斯', + 'Czech Rep.': '捷克', + 'Côte d\'Ivoire': '科特迪瓦', + 'Denmark': '丹麦', + 'Djibouti': '吉布提', + 'Dominica': '多米尼克', + 'Dominican Rep.': '多米尼加', + 'Ecuador': '厄瓜多尔', + 'Egypt': '埃及', + 'El Salvador': '萨尔瓦多', + 'Equatorial Guinea': '赤道几内亚', + 'Eritrea': '厄立特里亚', + 'Estonia': '爱沙尼亚', + 'Ethiopia': '埃塞俄比亚', + 'Falkland Is.': '福克兰群岛', + 'Faeroe Is.': '法罗群岛', + 'Fiji': '斐济', + 'Finland': '芬兰', + 'France': '法国', + 'French Guiana': '法属圭亚那', + 'French Polynesia': '法属波利尼西亚', + 'French Southern Ter.': '法属南方领地', + 'Gabon': '加蓬', + 'Gambia': '冈比亚', + 'Gaza': '加沙', + 'Georgia': '格鲁吉亚', + 'Germany': '德国', + 'Ghana': '加纳', + 'Gibraltar': '直布罗陀', + 'Greece': '希腊', + 'Greenland': '格陵兰', + 'Grenada': '格林纳达', + 'Guadeloupe': '瓜德罗普', + 'Guam': '关岛', + 'Guatemala': '危地马拉', + 'Guinea': '几内亚', + 'Guinea-Bissau': '几内亚比绍', + 'Guyana': '圭亚那', + 'Haiti': '海地', + 'Heard I. and McDonald Is.': '赫德岛和麦克唐纳群岛', + 'Honduras': '洪都拉斯', + 'Hong Kong': '香港', + 'Hungary': '匈牙利', + 'Iceland': '冰岛', + 'India': '印度', + 'Indonesia': '印度尼西亚', + 'Iran': '伊朗', + 'Iraq': '伊拉克', + 'Ireland': '爱尔兰', + 'Isle of Man': '马恩岛', + 'Israel': '以色列', + 'Italy': '意大利', + 'Jamaica': '牙买加', + 'Japan': '日本', + 'Jordan': '约旦', + 'Kazakhstan': '哈萨克斯坦', + 'Kenya': '肯尼亚', + 'Kiribati': '基里巴斯', + 'Korea': '韩国', + 'Dem. Rep. Korea': '朝鲜', + 'Kuwait': '科威特', + 'Kyrgyzstan': '吉尔吉斯斯坦', + 'Lao PDR': '老挝', + 'Latvia': '拉脱维亚', + 'Lebanon': '黎巴嫩', + 'Lesotho': '莱索托', + 'Liberia': '利比里亚', + 'Libya': '利比亚', + 'Liechtenstein': '列支敦士登', + 'Lithuania': '立陶宛', + 'Luxembourg': '卢森堡', + 'Macao': '澳门', + 'Macedonia': '马其顿', + 'Madagascar': '马达加斯加', + 'Malawi': '马拉维', + 'Malaysia': '马来西亚', + 'Maldives': '马尔代夫', + 'Mali': '马里', + 'Malta': '马耳他', + 'Marshall Is.': '马绍尔群岛', + 'Martinique': '马提尼克', + 'Mauritania': '毛里塔尼亚', + 'Mauritius': '毛里求斯', + 'Mexico': '墨西哥', + 'Micronesia': '密克罗尼西亚', + 'Moldova': '摩尔多瓦', + 'Monaco': '摩纳哥', + 'Mongolia': '蒙古', + 'Montenegro': '黑山', + 'Montserrat': '蒙特塞拉特', + 'Morocco': '摩洛哥', + 'Mozambique': '莫桑比克', + 'Myanmar': '缅甸', + 'Namibia': '纳米比亚', + 'Nauru': '瑙鲁', + 'Nepal': '尼泊尔', + 'Netherlands': '荷兰', + 'New Caledonia': '新喀里多尼亚', + 'New Zealand': '新西兰', + 'Nicaragua': '尼加拉瓜', + 'Niger': '尼日尔', + 'Nigeria': '尼日利亚', + 'Niue': '纽埃', + 'Norfolk Island': '诺福克岛', + 'Northern Mariana Is.': '北马里亚纳群岛', + 'Norway': '挪威', + 'Oman': '阿曼', + 'Pakistan': '巴基斯坦', + 'Palau': '帕劳', + 'Palestine': '巴勒斯坦', + 'Panama': '巴拿马', + 'Papua New Guinea': '巴布亚新几内亚', + 'Paraguay': '巴拉圭', + 'Peru': '秘鲁', + 'Philippines': '菲律宾', + 'Pitcairn Is.': '皮特凯恩群岛', + 'Poland': '波兰', + 'Portugal': '葡萄牙', + 'Puerto Rico': '波多黎各', + 'Qatar': '卡塔尔', + 'Réunion': '留尼汪', + 'Romania': '罗马尼亚', + 'Russia': '俄罗斯', + 'Rwanda': '卢旺达', + 'Saint Helena': '圣赫勒拿', + 'Saint Kitts and Nevis': '圣基茨和尼维斯', + 'Saint Lucia': '圣卢西亚', + 'Saint Pierre and Miquelon': '圣皮埃尔和密克隆', + 'Saint Vincent and the Grenadines': '圣文森特和格林纳丁斯', + 'Samoa': '萨摩亚', + 'San Marino': '圣马力诺', + 'Sao Tome and Principe': '圣多美和普林西比', + 'Saudi Arabia': '沙特阿拉伯', + 'Senegal': '塞内加尔', + 'Serbia': '塞尔维亚', + 'Seychelles': '塞舌尔', + 'Sierra Leone': '塞拉利昂', + 'Singapore': '新加坡', + 'Slovakia': '斯洛伐克', + 'Slovenia': '斯洛文尼亚', + 'Solomon Is.': '所罗门群岛', + 'Somalia': '索马里', + 'South Africa': '南非', + 'South Georgia and the South Sandwich Is.': '南乔治亚和南桑威奇群岛', + 'S. Sudan': '南苏丹', + 'Spain': '西班牙', + 'Sri Lanka': '斯里兰卡', + 'Sudan': '苏丹', + 'Suriname': '苏里南', + 'Swaziland': '斯威士兰', + 'Sweden': '瑞典', + 'Switzerland': '瑞士', + 'Syria': '叙利亚', + 'Taiwan': '台湾', + 'Tajikistan': '塔吉克斯坦', + 'Tanzania': '坦桑尼亚', + 'Thailand': '泰国', + 'Timor-Leste': '东帝汶', + 'Togo': '多哥', + 'Tokelau': '托克劳', + 'Tonga': '汤加', + 'Trinidad and Tobago': '特立尼达和多巴哥', + 'Tunisia': '突尼斯', + 'Turkey': '土耳其', + 'Turkmenistan': '土库曼斯坦', + 'Turks and Caicos Is.': '特克斯和凯科斯群岛', + 'Tuvalu': '图瓦卢', + 'Uganda': '乌干达', + 'Ukraine': '乌克兰', + 'United Arab Emirates': '阿联酋', + 'United Kingdom': '英国', + 'United States': '美国', + 'United States Minor Outlying Is.': '美属小离岛', + 'Uruguay': '乌拉圭', + 'Uzbekistan': '乌兹别克斯坦', + 'Vanuatu': '瓦努阿图', + 'Vatican City': '梵蒂冈', + 'Venezuela': '委内瑞拉', + 'Vietnam': '越南', + 'Virgin Is.': '维尔京群岛', + 'W. Sahara': '西撒哈拉', + 'Yemen': '也门', + 'Zambia': '赞比亚', + 'Zimbabwe': '津巴布韦', +} + +function updateMapOption() { + if (!chartInstance) + return + + const data = provinces.value.filter((d: any) => d.count > 0) + const maxCount = Math.max(...data.map((d: any) => d.count || 0), 1) + + const dark = isDark.value + + const seriesData = data.map((d: any) => { + const ratio = d.count / maxCount + let r: number, g: number, b: number + if (dark) { + r = Math.round(30 + (59 - 30) * ratio) + g = Math.round(58 + (130 - 58) * ratio) + b = Math.round(138 + (246 - 138) * ratio) + } + else { + r = Math.round(147 + (37 - 147) * ratio) + g = Math.round(197 + (99 - 197) * ratio) + b = Math.round(253 + (235 - 253) * ratio) + } + return { + name: d.name, + value: d.count, + count: d.count, + online: d.online || 0, + offline: d.offline || 0, + itemStyle: { + areaColor: `rgb(${r}, ${g}, ${b})`, + }, + } + }) + + const option = { + tooltip: { + trigger: 'item', + backgroundColor: dark ? 'rgba(15, 23, 42, 0.95)' : 'rgba(255, 255, 255, 0.95)', + borderColor: dark ? '#334155' : '#e2e8f0', + borderWidth: 1, + padding: 12, + textStyle: { + color: dark ? '#f8fafc' : '#0f172a', + }, + formatter: (params: any) => { + if (params.data && params.data.count > 0) { + return `
+
${params.name}
+
+ + ${t('admin.online')}: ${params.data.online} +
+
+ + ${t('admin.offline')}: ${params.data.offline} +
+
+ + ${t('admin.total')}: ${params.data.count} +
+
` + } + return `
+
${params.name}
+
${t('admin.noUserData')}
+
` + }, + }, + geo: { + map: 'world', + roam: true, + zoom: 1.2, + nameMap: worldNameMap, + label: { + show: false, + }, + emphasis: { + label: { + show: true, + color: dark ? '#f8fafc' : '#0f172a', + fontSize: 11, + fontWeight: 600, + }, + itemStyle: { + areaColor: dark ? '#334155' : '#f1f5f9', + borderColor: dark ? '#475569' : '#94a3b8', + borderWidth: 1, + }, + }, + itemStyle: { + areaColor: dark ? '#1e293b' : '#e2e8f0', + borderColor: dark ? '#334155' : '#cbd5e1', + borderWidth: 0.5, + }, + }, + series: [ + { + name: t('admin.userDistribution'), + type: 'map', + geoIndex: 0, + data: seriesData, + }, + ], + } + + chartInstance.setOption(option) +} + function initActivityChart() { if (!activityChart.value) return @@ -420,20 +719,20 @@ watch(loading, async (newVal) => { if (!newVal) { await nextTick() setTimeout(() => { - initDistributionChart() + initMapChart() initActivityChart() }, 100) } }) watch(isDark, () => { - initDistributionChart() + updateMapOption() }) onUnmounted(() => { - if (distributionChartInstance) { - distributionChartInstance.destroy() - distributionChartInstance = null + if (chartInstance) { + chartInstance.dispose() + chartInstance = null } if (activityChartInstance) { activityChartInstance.destroy() @@ -553,7 +852,7 @@ onUnmounted(() => {
- +