Codex Assistant
1 天以前 ba94ceae1315174798ae1967ef62268c6d16cd5b
web/src/views/dashboard/index.vue
@@ -1,88 +1,123 @@
<template>
  <div>
    <el-row :gutter="20">
  <div class="dashboard">
    <el-row :gutter="20" class="stats-row">
      <el-col :span="6">
        <el-card class="box-card">
          <template #header>
            <div class="card-header">
              <span>当前进行比赛</span>
            </div>
          </template>
          <div class="text item">
            {{ stats.activeActivities }}
        <div class="stat-card">
          <div class="icon-container blue">
            <el-icon><Trophy /></el-icon>
          </div>
        </el-card>
          <div class="stat-number">{{ stats.activeActivities }}</div>
          <div class="stat-title">当前比赛</div>
        </div>
      </el-col>
      <el-col :span="6">
        <el-card class="box-card">
          <template #header>
            <div class="card-header">
              <span>参赛总人数</span>
            </div>
          </template>
          <div class="text item">
            {{ stats.totalPlayers }}
        <div class="stat-card">
          <div class="icon-container green">
            <el-icon><UserFilled /></el-icon>
          </div>
        </el-card>
          <div class="stat-number">{{ stats.totalPlayers }}</div>
          <div class="stat-title">参赛总人数</div>
        </div>
      </el-col>
      <el-col :span="6">
        <el-card class="box-card">
          <template #header>
            <div class="card-header">
              <span>报名待审核</span>
            </div>
          </template>
          <div class="text item">
            {{ stats.pendingReviews }}
        <div class="stat-card">
          <div class="icon-container yellow">
            <el-icon><Clock /></el-icon>
          </div>
        </el-card>
          <div class="stat-number">{{ stats.pendingReviews }}</div>
          <div class="stat-title">报名待审核</div>
        </div>
      </el-col>
      <el-col :span="6">
        <el-card class="box-card">
          <template #header>
            <div class="card-header">
              <span>评委总数</span>
            </div>
          </template>
          <div class="text item">
            {{ stats.totalJudges }}
        <div class="stat-card">
          <div class="icon-container red">
            <el-icon><User /></el-icon>
          </div>
        </el-card>
          <div class="stat-number">{{ stats.totalJudges }}</div>
          <div class="stat-title">评委总数</div>
        </div>
      </el-col>
    </el-row>
    <el-card class="box-card" style="margin-top: 20px;">
      <template #header>
        <div class="card-header">
          <span>最近比赛</span>
          <el-button type="primary" @click="$router.push('/activity')">查看全部</el-button>
    <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>
      </template>
      <el-table :data="recentActivities" style="width: 100%">
        <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="endTime" label="结束时间" width="180" />
        <el-table-column prop="status" label="状态" width="100" />
        <el-table-column prop="status" label="状态" width="100">
          <template #default="{ row }">
            <span :class="getStatusClass(row.status)">{{ row.status }}</span>
          </template>
        </el-table-column>
        <el-table-column label="操作">
          <template #default="scope">
            <el-button size="small" @click="viewActivity(scope.row)">查看</el-button>
            <el-button size="small" type="primary" @click="manageActivity(scope.row)">管理</el-button>
            <a class="action-link" @click="viewActivity(scope.row)">查看</a>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
    </div>
  </div>
</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 { getDashboardStats } from '@/api/dashboard'
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 = ref({
@@ -93,7 +128,7 @@
})
// 最近活动数据
const recentActivities = ref([])
const recentActivities = ref<any[]>([])
// 加载统计数据
const loadStats = async () => {
@@ -109,14 +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,
      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)
@@ -124,40 +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)
})
// 查看比赛
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-published',
    未发布: 'status-unpublished',
    报名中: 'status-unpublished',
    待开始: 'status-unpublished',
    关闭: 'status-closed',
    已结束: 'status-closed'
  }
  return statusMap[status] || 'status-unpublished'
}
</script>
<style scoped>
.card-header {
/* 页面整体样式 */
.dashboard {
  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;
}
.text {
  font-size: 24px;
  font-weight: bold;
.table-title {
  font-size: 18px;
  font-weight: 600;
  color: #1f2937;
  margin: 0;
}
.item {
  margin-bottom: 18px;
}
.box-card {
.recent-table {
  width: 100%;
}
</style>
: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 {
    margin-bottom: 16px;
  }
}
</style>