From afeeed281e60466b576fbe74d339634cc5d07b82 Mon Sep 17 00:00:00 2001 From: Codex Assistant <codex@example.com> Date: 星期三, 08 十月 2025 08:56:42 +0800 Subject: [PATCH] 修复评审功能和用户认证问题 --- web/src/views/dashboard/index.vue | 655 +++++++++++++++++++++++++++++++++++++++++++++-------------- 1 files changed, 497 insertions(+), 158 deletions(-) diff --git a/web/src/views/dashboard/index.vue b/web/src/views/dashboard/index.vue index 26e53e0..1254a5e 100644 --- a/web/src/views/dashboard/index.vue +++ b/web/src/views/dashboard/index.vue @@ -1,103 +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.activeCompetitions }}</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-row :gutter="20"> - <el-col :span="6"> - <el-button type="primary" size="large" class="quick-btn" @click="$router.push('/activity')"> - <el-icon><Plus /></el-icon> - 鏂板姣旇禌 - </el-button> - </el-col> - <el-col :span="6"> - <el-button type="success" size="large" class="quick-btn" @click="$router.push('/judge')"> - <el-icon><Plus /></el-icon> - 鏂板璇勫 - </el-button> - </el-col> - <el-col :span="6"> - <el-button type="warning" size="large" class="quick-btn" @click="$router.push('/player')"> - <el-icon><View /></el-icon> - 瀹℃牳鎶ュ悕 - </el-button> - </el-col> - <el-col :span="6"> - <el-button type="info" size="large" class="quick-btn" @click="$router.push('/carousel')"> - <el-icon><Plus /></el-icon> - 鍙戝竷鏂伴椈 - </el-button> - </el-col> - </el-row> + <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="page-card"> - <h3 class="card-title">鏈�杩戞瘮璧�</h3> - <el-table :data="recentActivities" style="width: 100%"> - <el-table-column prop="name" label="姣旇禌鍚嶇О" /> + <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> @@ -106,113 +85,473 @@ </template> <script setup lang="ts"> -import { reactive } 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 * 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 = reactive({ - activeCompetitions: 5, - totalPlayers: 128, - pendingReviews: 23, - totalJudges: 15 +const stats = ref({ + activeActivities: 0, + totalPlayers: 0, + pendingReviews: 0, + totalJudges: 0 }) -// 鏈�杩戞瘮璧涙暟鎹� -const recentActivities = reactive([ - { - id: 1, - name: '2024骞村垱鏂板垱涓氬ぇ璧�', - playerCount: 45, - startTime: '2024-01-15 09:00', - status: '杩涜涓�' - }, - { - id: 2, - name: '鎶�鑳界珵璧涘垵璧�', - playerCount: 32, - startTime: '2024-01-20 14:00', - status: '鎶ュ悕涓�' - }, - { - id: 3, - name: '璁捐澶ц禌鍐宠禌', - playerCount: 18, - startTime: '2024-01-25 10:00', - status: '寰呭紑濮�' - } -]) +// 鏈�杩戞椿鍔ㄦ暟鎹� +const recentActivities = ref<any[]>([]) -// 鑾峰彇鐘舵�佹爣绛剧被鍨� -const getStatusType = (status: string) => { - const typeMap: Record<string, string> = { - '杩涜涓�': 'success', - '鎶ュ悕涓�': 'warning', - '寰呭紑濮�': 'info', - '宸茬粨鏉�': 'info' +// 鍔犺浇缁熻鏁版嵁 +const loadStats = async () => { + try { + const data = await getDashboardStats() + stats.value = data + } catch (error) { + console.error('鍔犺浇缁熻鏁版嵁澶辫触:', error) + ElMessage.error('鍔犺浇缁熻鏁版嵁澶辫触') } - return typeMap[status] || 'info' } -// 鏌ョ湅姣旇禌 +// 鍔犺浇鏈�杩戞椿鍔� +const loadRecentActivities = async () => { + try { + 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) + ElMessage.error('鍔犺浇鏈�杩戞椿鍔ㄥけ璐�') + } +} + +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) +}) + +onBeforeUnmount(() => { + window.removeEventListener('resize', handleResize) + if (trendChartInstance) { + trendChartInstance.dispose() + trendChartInstance = null + } + if (regionChartInstance) { + regionChartInstance.dispose() + regionChartInstance = null + } +}) + const viewActivity = (activity: any) => { - ElMessage.info(`鏌ョ湅姣旇禌锛�${activity.name}`) + router.push(`/activity/${activity.id}`) } -// 绠$悊姣旇禌 -const manageActivity = (activity: any) => { - ElMessage.info(`绠$悊姣旇禌锛�${activity.name}`) +// 鑾峰彇鐘舵�佹牱寮忕被 +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