数据结构优化
This commit is contained in:
@@ -347,7 +347,7 @@ const runCode = async () => {
|
||||
doc.write(loadingTemplate)
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:8081/api/run', {
|
||||
const response = await fetch('/api/run', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
@@ -115,7 +115,7 @@ const uploadFile = async (file: File) => {
|
||||
formData.append('storageType', 'local')
|
||||
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await fetch('http://localhost:8081/api/admin/attachments/upload', {
|
||||
const response = await fetch('/api/admin/attachments/upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {})
|
||||
|
||||
@@ -95,12 +95,16 @@ const drawStars = () => {
|
||||
}
|
||||
}
|
||||
|
||||
ctx.globalAlpha = star.alpha
|
||||
ctx.beginPath()
|
||||
ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
if (ctx) {
|
||||
ctx.globalAlpha = star.alpha
|
||||
ctx.beginPath()
|
||||
ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
}
|
||||
})
|
||||
ctx.globalAlpha = 1
|
||||
if (ctx) {
|
||||
ctx.globalAlpha = 1
|
||||
}
|
||||
}
|
||||
|
||||
// 生成流星
|
||||
|
||||
103310
client/src/composables/full.json
Normal file
103310
client/src/composables/full.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -191,7 +191,7 @@ const uploadCategoryOptions = computed(() => {
|
||||
})
|
||||
|
||||
|
||||
const API_BASE = 'http://localhost:8081/api'
|
||||
const API_BASE = '/api'
|
||||
const getAuthHeaders = () => {
|
||||
const token = localStorage.getItem('token')
|
||||
return {
|
||||
|
||||
@@ -344,7 +344,7 @@ const handlePaste = async (e: ClipboardEvent) => {
|
||||
formData.append('storageType', 'local')
|
||||
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await fetch('http://localhost:8081/api/admin/attachments/upload', {
|
||||
const response = await fetch('/api/admin/attachments/upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {})
|
||||
|
||||
@@ -64,12 +64,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Charts Row 2 -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- User Regions -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
<!-- Operation Trend -->
|
||||
<div class="chart-card relative">
|
||||
<h3 class="chart-title">用户地域分布</h3>
|
||||
<div class="h-80" v-if="!loading && hasData(stats.userRegions)">
|
||||
<v-chart class="chart" :option="regionOption" autoresize />
|
||||
<h3 class="chart-title">管理员操作统计</h3>
|
||||
<div class="h-80" v-if="!loading && hasData(stats.operationTrend)">
|
||||
<v-chart class="chart" :option="operationTrendOption" autoresize />
|
||||
</div>
|
||||
<div v-else-if="loading" class="h-80 flex items-center justify-center text-white/20">
|
||||
加载中...
|
||||
@@ -93,6 +93,26 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Regions - Full Width -->
|
||||
<div class="chart-card relative">
|
||||
<h3 class="chart-title">用户地域分布</h3>
|
||||
<div class="h-[800px]" v-if="!loading && hasData(stats.userRegions) && mapDataLoaded">
|
||||
<v-chart class="chart" :option="regionOption" autoresize />
|
||||
</div>
|
||||
<div v-else-if="loading" class="h-[800px] flex items-center justify-center text-white/20">
|
||||
加载中...
|
||||
</div>
|
||||
<div v-else-if="!mapDataLoaded" class="h-[800px] flex items-center justify-center text-white/40">
|
||||
<div class="text-center">
|
||||
<p class="mb-2">地图数据加载失败</p>
|
||||
<p class="text-sm text-white/60">请检查网络连接或稍后重试</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="h-[800px] flex items-center justify-center text-white/20">
|
||||
暂无数据
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -113,6 +133,7 @@ import * as echarts from 'echarts/core'
|
||||
import VChart from 'vue-echarts'
|
||||
import { getDashboardStats } from '../../../services/api'
|
||||
import { useToast } from '../../../composables/useToast'
|
||||
import chinaMapData from '../../../composables/full.json'
|
||||
|
||||
use([
|
||||
CanvasRenderer,
|
||||
@@ -278,44 +299,6 @@ const uvTrendOption = computed(() => ({
|
||||
}]
|
||||
}))
|
||||
|
||||
// 省份名称映射(确保与地图数据一致)
|
||||
const provinceNameMap: Record<string, string> = {
|
||||
'北京': '北京',
|
||||
'天津': '天津',
|
||||
'河北': '河北',
|
||||
'山西': '山西',
|
||||
'内蒙古': '内蒙古',
|
||||
'辽宁': '辽宁',
|
||||
'吉林': '吉林',
|
||||
'黑龙江': '黑龙江',
|
||||
'上海': '上海',
|
||||
'江苏': '江苏',
|
||||
'浙江': '浙江',
|
||||
'安徽': '安徽',
|
||||
'福建': '福建',
|
||||
'江西': '江西',
|
||||
'山东': '山东',
|
||||
'河南': '河南',
|
||||
'湖北': '湖北',
|
||||
'湖南': '湖南',
|
||||
'广东': '广东',
|
||||
'广西': '广西',
|
||||
'海南': '海南',
|
||||
'重庆': '重庆',
|
||||
'四川': '四川',
|
||||
'贵州': '贵州',
|
||||
'云南': '云南',
|
||||
'西藏': '西藏',
|
||||
'陕西': '陕西',
|
||||
'甘肃': '甘肃',
|
||||
'青海': '青海',
|
||||
'宁夏': '宁夏',
|
||||
'新疆': '新疆',
|
||||
'台湾': '台湾',
|
||||
'香港': '香港',
|
||||
'澳门': '澳门'
|
||||
}
|
||||
|
||||
// 所有省份列表(用于设置默认值)
|
||||
const allProvinces = [
|
||||
'北京', '天津', '河北', '山西', '内蒙古', '辽宁', '吉林', '黑龙江',
|
||||
@@ -354,6 +337,20 @@ const getMapData = () => {
|
||||
}
|
||||
|
||||
const regionOption = computed(() => {
|
||||
// 如果地图数据未加载,返回空配置
|
||||
if (!mapDataLoaded.value) {
|
||||
return {
|
||||
title: {
|
||||
text: '地图数据加载中...',
|
||||
left: 'center',
|
||||
top: 'middle',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mapData = getMapData()
|
||||
const maxValue = mapData.length > 0 ? Math.max(...mapData.map((d: any) => d[1] || 0)) : 1
|
||||
|
||||
@@ -372,7 +369,7 @@ const regionOption = computed(() => {
|
||||
min: 0,
|
||||
max: maxValue || 100,
|
||||
left: 'left',
|
||||
top: 'bottom',
|
||||
top: 'middle',
|
||||
text: ['高', '低'],
|
||||
inRange: {
|
||||
color: ['#1a1a1a', '#d4b383']
|
||||
@@ -380,11 +377,15 @@ const regionOption = computed(() => {
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
calculable: true
|
||||
calculable: true,
|
||||
itemWidth: 12,
|
||||
itemHeight: 200
|
||||
},
|
||||
geo: {
|
||||
map: 'china',
|
||||
roam: false,
|
||||
layoutCenter: ['55%', '50%'],
|
||||
layoutSize: '95%',
|
||||
itemStyle: {
|
||||
areaColor: 'rgba(255, 255, 255, 0.05)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||
@@ -398,7 +399,7 @@ const regionOption = computed(() => {
|
||||
label: {
|
||||
show: true,
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
fontSize: 10
|
||||
fontSize: 14
|
||||
}
|
||||
},
|
||||
series: [
|
||||
@@ -421,7 +422,7 @@ const regionOption = computed(() => {
|
||||
},
|
||||
label: {
|
||||
color: '#fff',
|
||||
fontSize: 12
|
||||
fontSize: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -429,6 +430,33 @@ const regionOption = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const operationTrendOption = computed(() => ({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(0,0,0,0.8)',
|
||||
borderColor: '#333',
|
||||
textStyle: { color: '#fff' }
|
||||
},
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: stats.value.operationTrend?.map((i: any) => i.date) || [],
|
||||
axisLabel: { color: '#ccc' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: { color: '#ccc' },
|
||||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
|
||||
},
|
||||
series: [{
|
||||
name: '操作次数',
|
||||
data: stats.value.operationTrend?.map((i: any) => i.value) || [],
|
||||
type: 'bar',
|
||||
itemStyle: { color: '#f59e0b' },
|
||||
barWidth: '40%'
|
||||
}]
|
||||
}))
|
||||
|
||||
const topPostsOption = computed(() => ({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
@@ -475,21 +503,69 @@ const loadData = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 地图数据加载状态
|
||||
const mapDataLoaded = ref(false)
|
||||
|
||||
// 加载中国地图数据
|
||||
const loadChinaMap = async () => {
|
||||
try {
|
||||
// 使用 echarts 官方的中国地图数据(从 CDN 加载)
|
||||
const response = await fetch('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json')
|
||||
if (response.ok) {
|
||||
const geoJson = await response.json()
|
||||
// 注册地图数据
|
||||
echarts.registerMap('china', geoJson)
|
||||
} else {
|
||||
console.warn('Failed to load China map data, using fallback')
|
||||
// 优先使用本地地图数据
|
||||
if (chinaMapData && chinaMapData.type === 'FeatureCollection') {
|
||||
echarts.registerMap('china', chinaMapData as any)
|
||||
mapDataLoaded.value = true
|
||||
console.log('China map data loaded successfully from local file')
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading China map:', error)
|
||||
console.warn('Failed to load local map data:', error)
|
||||
}
|
||||
|
||||
// 如果本地数据加载失败,尝试备用数据源
|
||||
const mapDataSources = [
|
||||
'https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json',
|
||||
'https://cdn.jsdelivr.net/npm/echarts@5/map/json/china.json',
|
||||
'https://unpkg.com/echarts@5/map/json/china.json'
|
||||
]
|
||||
|
||||
for (const url of mapDataSources) {
|
||||
try {
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000) // 10秒超时
|
||||
|
||||
const response = await fetch(url, {
|
||||
mode: 'cors',
|
||||
credentials: 'omit',
|
||||
signal: controller.signal
|
||||
})
|
||||
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
if (response.ok) {
|
||||
const geoJson = await response.json()
|
||||
// 验证数据格式
|
||||
if (geoJson && geoJson.type === 'FeatureCollection') {
|
||||
// 注册地图数据
|
||||
echarts.registerMap('china', geoJson)
|
||||
mapDataLoaded.value = true
|
||||
console.log('China map data loaded successfully from:', url)
|
||||
return
|
||||
} else {
|
||||
console.warn(`Invalid map data format from ${url}`)
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.name === 'AbortError') {
|
||||
console.warn(`Timeout loading map data from ${url}`)
|
||||
} else {
|
||||
console.warn(`Failed to load map data from ${url}:`, error)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 所有数据源都失败
|
||||
console.error('All map data sources failed, map will not be displayed')
|
||||
mapDataLoaded.value = false
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// export const API_BASE = 'http://localhost:8081/api'
|
||||
export const API_BASE = '/api'
|
||||
|
||||
// 通用请求头配置
|
||||
@@ -145,6 +144,7 @@ export interface OperationLog {
|
||||
userId: number
|
||||
username: string
|
||||
ip: string
|
||||
region?: string
|
||||
path: string
|
||||
method: string
|
||||
params: string
|
||||
|
||||
Reference in New Issue
Block a user