From 3714621173c606c4c58439ed8941100ce9ddea14 Mon Sep 17 00:00:00 2001
From: Codex Assistant <codex@example.com>
Date: 星期三, 05 十一月 2025 15:10:49 +0800
Subject: [PATCH] bug
---
web/src/views/dashboard/index.vue | 570 ++++++++++++++++++++++++++++++++++++++++++++++----------
1 files changed, 461 insertions(+), 109 deletions(-)
diff --git a/web/src/views/dashboard/index.vue b/web/src/views/dashboard/index.vue
index d2a3ba5..1254a5e 100644
--- a/web/src/views/dashboard/index.vue
+++ b/web/src/views/dashboard/index.vue
@@ -1,72 +1,82 @@
<template>
<div class="dashboard">
- <!-- 鏁版嵁缁熻鍗$墖 -->
<el-row :gutter="20" class="stats-row">
<el-col :span="6">
<div class="stat-card">
- <div class="stat-icon">
- <el-icon color="#409eff"><Trophy /></el-icon>
+ <div class="icon-container blue">
+ <el-icon><Trophy /></el-icon>
</div>
- <div class="stat-content">
- <div class="stat-number">{{ stats.activeActivities }}</div>
- <div class="stat-label">褰撳墠杩涜姣旇禌</div>
- </div>
+ <div class="stat-number">{{ stats.activeActivities }}</div>
+ <div class="stat-title">褰撳墠姣旇禌</div>
</div>
</el-col>
-
<el-col :span="6">
<div class="stat-card">
- <div class="stat-icon">
- <el-icon color="#67c23a"><UserFilled /></el-icon>
+ <div class="icon-container green">
+ <el-icon><UserFilled /></el-icon>
</div>
- <div class="stat-content">
- <div class="stat-number">{{ stats.totalPlayers }}</div>
- <div class="stat-label">鍙傝禌鎬讳汉鏁�</div>
- </div>
+ <div class="stat-number">{{ stats.totalPlayers }}</div>
+ <div class="stat-title">鍙傝禌鎬讳汉鏁�</div>
</div>
</el-col>
-
<el-col :span="6">
<div class="stat-card">
- <div class="stat-icon">
- <el-icon color="#e6a23c"><Clock /></el-icon>
+ <div class="icon-container yellow">
+ <el-icon><Clock /></el-icon>
</div>
- <div class="stat-content">
- <div class="stat-number">{{ stats.pendingReviews }}</div>
- <div class="stat-label">鎶ュ悕寰呭鏍�</div>
- </div>
+ <div class="stat-number">{{ stats.pendingReviews }}</div>
+ <div class="stat-title">鎶ュ悕寰呭鏍�</div>
</div>
</el-col>
-
<el-col :span="6">
<div class="stat-card">
- <div class="stat-icon">
- <el-icon color="#f56c6c"><User /></el-icon>
+ <div class="icon-container red">
+ <el-icon><User /></el-icon>
</div>
- <div class="stat-content">
- <div class="stat-number">{{ stats.totalJudges }}</div>
- <div class="stat-label">璇勫鎬绘暟</div>
- </div>
+ <div class="stat-number">{{ stats.totalJudges }}</div>
+ <div class="stat-title">璇勫鎬绘暟</div>
</div>
</el-col>
</el-row>
- <!-- 鏈�杩戞椿鍔� -->
- <div class="page-card">
- <h3 class="card-title">鏈�杩戞瘮璧�</h3>
- <el-table :data="recentActivities" style="width: 100%">
- <el-table-column prop="name" label="姣旇禌鍚嶇О" />
+ <div class="chart-section">
+ <div class="chart-card">
+ <div v-if="trendChartLoading" class="chart-mask">鍔犺浇涓�...</div>
+ <div class="chart-header">
+ <h3 class="chart-header-title">鎶ュ悕瓒嬪娍</h3>
+ <span class="chart-header-desc">鏈�杩� 15 澶�</span>
+ </div>
+ <div class="chart-container" ref="trendChartRef"></div>
+ </div>
+ <div class="chart-card">
+ <div v-if="regionChartLoading" class="chart-mask">鍔犺浇涓�...</div>
+ <div class="chart-header">
+ <h3 class="chart-header-title">鍖哄煙鎶ュ悕鍒嗗竷</h3>
+ <span class="chart-header-desc">鎸夊彾瀛愬湴鍖虹粺璁�</span>
+ </div>
+ <div class="chart-container" ref="regionChartRef"></div>
+ </div>
+ </div>
+
+ <div class="table-card">
+ <div class="table-header">
+ <h3 class="table-title">鏈�杩戞瘮璧�</h3>
+ <el-button type="primary" @click="$router.push('/activity')">鏌ョ湅鍏ㄩ儴</el-button>
+ </div>
+
+ <el-table :data="recentActivities" class="recent-table">
+ <el-table-column prop="name" label="姣旇禌鍚嶇О" width="180" />
<el-table-column prop="playerCount" label="鎶ュ悕浜烘暟" width="120" />
- <el-table-column prop="startTime" label="姣旇禌鏃堕棿" width="180" />
+ <el-table-column prop="startTime" label="寮�濮嬫椂闂�" width="180" />
+ <el-table-column prop="endTime" label="缁撴潫鏃堕棿" width="180" />
<el-table-column prop="status" label="鐘舵��" width="100">
<template #default="{ row }">
- <el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
+ <span :class="getStatusClass(row.status)">{{ row.status }}</span>
</template>
</el-table-column>
- <el-table-column label="鎿嶄綔" width="150">
- <template #default="{ row }">
- <el-button type="primary" size="small" @click="viewActivity(row)">鏌ョ湅</el-button>
- <el-button type="success" size="small" @click="manageActivity(row)">绠$悊</el-button>
+ <el-table-column label="鎿嶄綔">
+ <template #default="scope">
+ <a class="action-link" @click="viewActivity(scope.row)">鏌ョ湅</a>
</template>
</el-table-column>
</el-table>
@@ -75,14 +85,39 @@
</template>
<script setup lang="ts">
-import { ref, onMounted } from 'vue'
+import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Trophy, UserFilled, Clock, User } from '@element-plus/icons-vue'
-import { getDashboardStats } from '@/api/dashboard'
+import * as echarts from 'echarts'
+import { getDashboardStats, getRegistrationTrend, getRegionRegistrationStats } from '@/api/dashboard'
import { getActivities } from '@/api/activity'
const router = useRouter()
+
+interface RegistrationTrendPoint {
+ date: string
+ count: number
+}
+
+interface RegionRegistrationStat {
+ regionId: number | null
+ regionName: string
+ leafFlag?: boolean | null
+ count: number
+}
+
+const UNASSIGNED_REGION_LABEL = '鏈�夋嫨鍖哄煙'
+
+const registrationTrend = ref<RegistrationTrendPoint[]>([])
+const regionStats = ref<RegionRegistrationStat[]>([])
+const trendChartRef = ref<HTMLDivElement | null>(null)
+const regionChartRef = ref<HTMLDivElement | null>(null)
+const trendChartLoading = ref(false)
+const regionChartLoading = ref(false)
+
+let trendChartInstance: echarts.ECharts | null = null
+let regionChartInstance: echarts.ECharts | null = null
// 缁熻鏁版嵁
const stats = ref({
@@ -93,7 +128,7 @@
})
// 鏈�杩戞椿鍔ㄦ暟鎹�
-const recentActivities = ref([])
+const recentActivities = ref<any[]>([])
// 鍔犺浇缁熻鏁版嵁
const loadStats = async () => {
@@ -109,13 +144,15 @@
// 鍔犺浇鏈�杩戞椿鍔�
const loadRecentActivities = async () => {
try {
- const data = await getActivities(0, 5) // 鑾峰彇鍓�5鏉℃椿鍔�
- recentActivities.value = data.content.map(activity => ({
- id: activity.id,
- name: activity.name,
- playerCount: activity.playerCount || 0,
- startTime: activity.matchTime || activity.createTime,
- status: activity.stateName || '鏈煡'
+ const data = await getActivities(0, 5)
+ const { content } = data || {}
+ recentActivities.value = (content || []).map(item => ({
+ id: item.id,
+ name: item.name,
+ playerCount: item.playerCount || 0,
+ startTime: item.matchTime,
+ endTime: item.matchTime,
+ status: item.stateName
}))
} catch (error) {
console.error('鍔犺浇鏈�杩戞椿鍔ㄥけ璐�:', error)
@@ -123,83 +160,398 @@
}
}
+const renderTrendChart = () => {
+ if (!trendChartRef.value) return
+ if (!trendChartInstance) {
+ trendChartInstance = echarts.init(trendChartRef.value)
+ }
+
+ const dates = registrationTrend.value.map(item => item.date)
+ const values = registrationTrend.value.map(item => Number(item.count || 0))
+
+ trendChartInstance.setOption({
+ grid: { left: 16, right: 16, top: 32, bottom: 24, containLabel: true },
+ tooltip: { trigger: 'axis' },
+ xAxis: {
+ type: 'category',
+ boundaryGap: false,
+ data: dates
+ },
+ yAxis: {
+ type: 'value',
+ minInterval: 1
+ },
+ series: [
+ {
+ name: '鎶ュ悕鏁伴噺',
+ type: 'line',
+ smooth: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ lineStyle: { width: 3, color: '#409EFF' },
+ areaStyle: { color: 'rgba(64, 158, 255, 0.2)' },
+ data: values
+ }
+ ]
+ })
+}
+
+const renderRegionChart = () => {
+ if (!regionChartRef.value) return
+ if (!regionChartInstance) {
+ regionChartInstance = echarts.init(regionChartRef.value)
+ }
+
+ const pieData = regionStats.value.map(item => ({
+ name: item.regionName || UNASSIGNED_REGION_LABEL,
+ value: Number(item.count || 0)
+ }))
+
+ regionChartInstance.setOption({
+ tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
+ legend: { orient: 'vertical', right: 10, top: 'center' },
+ series: [
+ {
+ name: '鍖哄煙鎶ュ悕',
+ type: 'pie',
+ radius: ['40%', '70%'],
+ avoidLabelOverlap: false,
+ itemStyle: {
+ borderRadius: 8,
+ borderColor: '#fff',
+ borderWidth: 2
+ },
+ label: { formatter: '{b}\n{c}浜�' },
+ data: pieData.length > 0 ? pieData : [{ name: UNASSIGNED_REGION_LABEL, value: 0 }]
+ }
+ ]
+ })
+}
+
+const handleResize = () => {
+ trendChartInstance?.resize()
+ regionChartInstance?.resize()
+}
+
+const loadRegistrationTrend = async () => {
+ trendChartLoading.value = true
+ try {
+ const data = await getRegistrationTrend(15)
+ registrationTrend.value = data || []
+ await nextTick()
+ renderTrendChart()
+ } catch (error) {
+ console.error('鍔犺浇鎶ュ悕瓒嬪娍澶辫触:', error)
+ ElMessage.error('鍔犺浇鎶ュ悕瓒嬪娍澶辫触')
+ } finally {
+ trendChartLoading.value = false
+ }
+}
+
+const loadRegionStats = async () => {
+ regionChartLoading.value = true
+ try {
+ const data = await getRegionRegistrationStats()
+ regionStats.value = (data || [])
+ .filter(item => item.leafFlag !== false)
+ .map(item => ({
+ regionId: item.regionId ?? null,
+ regionName: item.regionName || UNASSIGNED_REGION_LABEL,
+ leafFlag: item.leafFlag,
+ count: item.count ?? 0
+ }))
+ await nextTick()
+ renderRegionChart()
+ } catch (error) {
+ console.error('鍔犺浇鍖哄煙鎶ュ悕缁熻澶辫触:', error)
+ ElMessage.error('鍔犺浇鍖哄煙鎶ュ悕缁熻澶辫触')
+ } finally {
+ regionChartLoading.value = false
+ }
+}
+
// 椤甸潰鍔犺浇鏃惰幏鍙栨暟鎹�
onMounted(() => {
loadStats()
loadRecentActivities()
+ loadRegistrationTrend()
+ loadRegionStats()
+ window.addEventListener('resize', handleResize)
})
-// 鑾峰彇鐘舵�佹爣绛剧被鍨�
-const getStatusType = (status: string) => {
- const typeMap: Record<string, string> = {
- '杩涜涓�': 'success',
- '鎶ュ悕涓�': 'warning',
- '寰呭紑濮�': 'info',
- '宸茬粨鏉�': 'info'
+onBeforeUnmount(() => {
+ window.removeEventListener('resize', handleResize)
+ if (trendChartInstance) {
+ trendChartInstance.dispose()
+ trendChartInstance = null
}
- return typeMap[status] || 'info'
-}
+ if (regionChartInstance) {
+ regionChartInstance.dispose()
+ regionChartInstance = null
+ }
+})
-// 鏌ョ湅姣旇禌
const viewActivity = (activity: any) => {
router.push(`/activity/${activity.id}`)
}
-// 绠$悊姣旇禌
-const manageActivity = (activity: any) => {
- router.push('/activity')
+// 鑾峰彇鐘舵�佹牱寮忕被
+const getStatusClass = (status: string) => {
+ const statusMap: Record<string, string> = {
+ 宸插彂甯�: 'status-published',
+ 鍙戝竷: 'status-published',
+ 杩涜涓�: 'status-published',
+ 鏈彂甯�: 'status-unpublished',
+ 鎶ュ悕涓�: 'status-unpublished',
+ 寰呭紑濮�: 'status-unpublished',
+ 鍏抽棴: 'status-closed',
+ 宸茬粨鏉�: 'status-closed'
+ }
+ return statusMap[status] || 'status-unpublished'
}
</script>
-<style lang="scss" scoped>
+<style scoped>
+/* 椤甸潰鏁翠綋鏍峰紡 */
.dashboard {
- .stats-row {
- margin-bottom: 20px;
+ padding: 24px;
+ background-color: #ffffff;
+ min-height: 100vh;
+}
+
+/* 缁熻鍗$墖鍖哄煙 */
+.stats-row {
+ margin-bottom: 20px;
+}
+
+/* 缁熻鍗$墖鏍峰紡 */
+.stat-card {
+ background: #ffffff;
+ border-radius: 12px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+ border: none;
+ padding: 24px;
+ height: 120px;
+ position: relative;
+ overflow: hidden;
+ transition: all 0.3s ease;
+}
+
+.stat-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
+}
+
+.icon-container {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ top: 24px;
+ left: 24px;
+}
+
+.icon-container.blue {
+ background-color: #e0e7ff;
+ color: #6366f1;
+}
+
+.icon-container.green {
+ background-color: #d1fae5;
+ color: #10b981;
+}
+
+.icon-container.yellow {
+ background-color: #fef3c7;
+ color: #f59e0b;
+}
+
+.icon-container.red {
+ background-color: #fecaca;
+ color: #ef4444;
+}
+
+.stat-number {
+ font-size: 32px;
+ font-weight: 700;
+ color: #1f2937;
+ position: absolute;
+ top: 24px;
+ right: 24px;
+}
+
+.stat-title {
+ font-size: 14px;
+ font-weight: 500;
+ color: #6b7280;
+ position: absolute;
+ bottom: 24px;
+ left: 24px;
+}
+
+.chart-section {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
+ gap: 20px;
+ margin-top: 24px;
+}
+
+.chart-card {
+ background: #ffffff;
+ border-radius: 12px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+ border: none;
+ padding: 20px;
+ min-height: 340px;
+ position: relative;
+}
+
+.chart-mask {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: rgba(255, 255, 255, 0.8);
+ z-index: 1;
+ font-size: 14px;
+ color: #6b7280;
+}
+
+.chart-header {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ margin-bottom: 12px;
+}
+
+.chart-header-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #1f2937;
+ margin: 0;
+}
+
+.chart-header-desc {
+ font-size: 13px;
+ color: #9ca3af;
+}
+
+.chart-container {
+ width: 100%;
+ height: 280px;
+}
+
+.table-card {
+ background: #ffffff;
+ border-radius: 12px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+ border: none;
+ padding: 24px;
+ margin-top: 20px;
+}
+
+.table-header {
+ margin-bottom: 20px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.table-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #1f2937;
+ margin: 0;
+}
+
+.recent-table {
+ width: 100%;
+}
+
+:deep(.el-table__header) {
+ background-color: #f9fafb;
+}
+
+:deep(.el-table__header th) {
+ background-color: #f9fafb !important;
+ color: #374151;
+ font-size: 14px;
+ font-weight: 500;
+ height: 48px;
+ border-bottom: 1px solid #e5e7eb;
+}
+
+:deep(.el-table__row) {
+ height: 56px;
+}
+
+:deep(.el-table__row:nth-child(even)) {
+ background-color: #f9fafb;
+}
+
+:deep(.el-table__row:nth-child(odd)) {
+ background-color: #ffffff;
+}
+
+:deep(.el-table td) {
+ color: #1f2937;
+ font-size: 14px;
+ font-weight: 400;
+ border-bottom: 1px solid #f3f4f6;
+}
+
+.status-published {
+ color: #67c23a;
+ background-color: #f0f9ff;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+}
+
+.status-unpublished {
+ color: #e6a23c;
+ background-color: #fdf6ec;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+}
+
+.status-closed {
+ color: #f56c6c;
+ background-color: #fef0f0;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+}
+
+.action-link {
+ color: #409eff;
+ cursor: pointer;
+ font-size: 14px;
+ margin: 0 8px;
+ text-decoration: none;
+}
+
+.action-link:hover {
+ color: #66b1ff;
+ text-decoration: underline;
+}
+
+.action-link:first-child {
+ margin-left: 0;
+}
+
+@media (max-width: 768px) {
+ .dashboard {
+ padding: 16px;
}
-
+
.stat-card {
- background: white;
- border-radius: 8px;
- padding: 20px;
- display: flex;
- align-items: center;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-
- .stat-icon {
- font-size: 40px;
- margin-right: 16px;
- }
-
- .stat-content {
- .stat-number {
- font-size: 24px;
- font-weight: bold;
- color: #303133;
- margin-bottom: 4px;
- }
-
- .stat-label {
- font-size: 14px;
- color: #909399;
- }
- }
- }
-
- .card-title {
- margin-bottom: 20px;
- color: #303133;
- font-size: 16px;
- font-weight: 500;
- }
-
- .quick-btn {
- width: 100%;
- height: 60px;
- font-size: 14px;
-
- .el-icon {
- margin-right: 8px;
- }
+ margin-bottom: 16px;
}
}
-</style>
\ No newline at end of file
+</style>
--
Gitblit v1.8.0