| | |
| | | </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="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 label="操作"> |
| | | <template #default="scope"> |
| | | <a class="action-link" @click="viewActivity(scope.row)">查看</a> |
| | | <a class="action-link" @click="manageActivity(scope.row)">管理</a> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | |
| | | </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({ |
| | |
| | | }) |
| | | |
| | | // 最近活动数据 |
| | | const recentActivities = ref([]) |
| | | const recentActivities = ref<any[]>([]) |
| | | |
| | | // 加载统计数据 |
| | | const loadStats = async () => { |
| | |
| | | // 加载最近活动 |
| | | 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, |
| | | endTime: activity.endTime || '待定', |
| | | 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) |
| | |
| | | } |
| | | } |
| | | |
| | | 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) => { |
| | | 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-unpublished', |
| | | '报名中': 'status-unpublished', |
| | | '关闭': 'status-closed', |
| | | '已结束': 'status-closed', |
| | | '待开始': 'status-unpublished' |
| | | 已发布: 'status-published', |
| | | 发布: 'status-published', |
| | | 进行中: 'status-published', |
| | | 未发布: 'status-unpublished', |
| | | 报名中: 'status-unpublished', |
| | | 待开始: 'status-unpublished', |
| | | 关闭: 'status-closed', |
| | | 已结束: 'status-closed' |
| | | } |
| | | return statusMap[status] || 'status-unpublished' |
| | | } |
| | |
| | | /* 页面整体样式 */ |
| | | .dashboard { |
| | | padding: 24px; |
| | | background-color: #FFFFFF; |
| | | background-color: #ffffff; |
| | | min-height: 100vh; |
| | | } |
| | | |
| | | /* 统计卡片行 */ |
| | | /* 统计卡片区域 */ |
| | | .stats-row { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | /* 统计卡片样式 */ |
| | | .stat-card { |
| | | background: #FFFFFF; |
| | | background: #ffffff; |
| | | border-radius: 12px; |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); |
| | | border: none; |
| | |
| | | box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); |
| | | } |
| | | |
| | | /* 图标容器 */ |
| | | .icon-container { |
| | | width: 48px; |
| | | height: 48px; |
| | |
| | | } |
| | | |
| | | .icon-container.blue { |
| | | background-color: #E0E7FF; |
| | | color: #6366F1; |
| | | background-color: #e0e7ff; |
| | | color: #6366f1; |
| | | } |
| | | |
| | | .icon-container.green { |
| | | background-color: #D1FAE5; |
| | | color: #10B981; |
| | | background-color: #d1fae5; |
| | | color: #10b981; |
| | | } |
| | | |
| | | .icon-container.yellow { |
| | | background-color: #FEF3C7; |
| | | color: #F59E0B; |
| | | background-color: #fef3c7; |
| | | color: #f59e0b; |
| | | } |
| | | |
| | | .icon-container.red { |
| | | background-color: #FECACA; |
| | | color: #EF4444; |
| | | background-color: #fecaca; |
| | | color: #ef4444; |
| | | } |
| | | |
| | | /* 统计数字 */ |
| | | .stat-number { |
| | | font-size: 32px; |
| | | font-weight: 700; |
| | | color: #1F2937; |
| | | color: #1f2937; |
| | | position: absolute; |
| | | top: 24px; |
| | | right: 24px; |
| | | line-height: 1; |
| | | } |
| | | |
| | | /* 统计标题 */ |
| | | .stat-title { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #6B7280; |
| | | 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; |
| | | background: #ffffff; |
| | | border-radius: 12px; |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); |
| | | border: none; |
| | |
| | | margin-top: 20px; |
| | | } |
| | | |
| | | /* 表格头部 */ |
| | | .table-header { |
| | | margin-bottom: 20px; |
| | | display: flex; |
| | |
| | | .table-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #1F2937; |
| | | color: #1f2937; |
| | | margin: 0; |
| | | } |
| | | |
| | | /* 表格样式 */ |
| | | .recent-table { |
| | | width: 100%; |
| | | } |
| | | |
| | | :deep(.el-table__header) { |
| | | background-color: #F9FAFB; |
| | | background-color: #f9fafb; |
| | | } |
| | | |
| | | :deep(.el-table__header th) { |
| | | background-color: #F9FAFB !important; |
| | | background-color: #f9fafb !important; |
| | | color: #374151; |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | height: 48px; |
| | | border-bottom: 1px solid #E5E7EB; |
| | | border-bottom: 1px solid #e5e7eb; |
| | | } |
| | | |
| | | :deep(.el-table__row) { |
| | |
| | | } |
| | | |
| | | :deep(.el-table__row:nth-child(even)) { |
| | | background-color: #F9FAFB; |
| | | background-color: #f9fafb; |
| | | } |
| | | |
| | | :deep(.el-table__row:nth-child(odd)) { |
| | | background-color: #FFFFFF; |
| | | background-color: #ffffff; |
| | | } |
| | | |
| | | :deep(.el-table td) { |
| | | color: #1F2937; |
| | | color: #1f2937; |
| | | font-size: 14px; |
| | | font-weight: 400; |
| | | border-bottom: 1px solid #F3F4F6; |
| | | border-bottom: 1px solid #f3f4f6; |
| | | } |
| | | |
| | | /* 状态标签样式 */ |
| | | .status-published { |
| | | color: #67C23A; |
| | | background-color: #F0F9FF; |
| | | color: #67c23a; |
| | | background-color: #f0f9ff; |
| | | padding: 4px 8px; |
| | | border-radius: 4px; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .status-unpublished { |
| | | color: #E6A23C; |
| | | background-color: #FDF6EC; |
| | | color: #e6a23c; |
| | | background-color: #fdf6ec; |
| | | padding: 4px 8px; |
| | | border-radius: 4px; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .status-closed { |
| | | color: #F56C6C; |
| | | background-color: #FEF0F0; |
| | | color: #f56c6c; |
| | | background-color: #fef0f0; |
| | | padding: 4px 8px; |
| | | border-radius: 4px; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | /* 操作链接样式 */ |
| | | .action-link { |
| | | color: #409EFF; |
| | | color: #409eff; |
| | | cursor: pointer; |
| | | font-size: 14px; |
| | | margin: 0 8px; |
| | |
| | | } |
| | | |
| | | .action-link:hover { |
| | | color: #66B1FF; |
| | | color: #66b1ff; |
| | | text-decoration: underline; |
| | | } |
| | | |
| | |
| | | margin-left: 0; |
| | | } |
| | | |
| | | /* 响应式设计 */ |
| | | @media (max-width: 768px) { |
| | | .dashboard { |
| | | padding: 16px; |
| | | } |
| | | |
| | | |
| | | .stat-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | } |
| | | </style> |
| | | </style> |