Переглянути джерело

feat(index): 添加项目进度展示功能并重构底部布局

- 新增项目进度卡片,显示项目完成统计和进度条
- 重构底部详情区域布局,分为左侧项目进度和右侧动态公告
- 添加项目类型、分类和时间格式化功能
- 集成游戏事件存储和项目进度API调用

feat(ranking-board): 优化排行榜页面结构和项目选择功能

- 调整排行榜列布局,从三列改为两列布局
- 添加个人项目和团队项目的下拉选择功能
- 更新排行榜表头和数据显示,增加项目名称列
- 实现项目切换时的排行榜数据刷新功能
- 重构排序和排名计算逻辑,支持升序和降序排列
zhou 1 місяць тому
батько
коміт
1035b5d626
2 змінених файлів з 572 додано та 205 видалено
  1. 316 47
      src/views/index.vue
  2. 256 158
      src/views/system/gameEvent/RankingBoardPage.vue

+ 316 - 47
src/views/index.vue

@@ -157,51 +157,92 @@
         </el-card>
       </el-col>
     </el-row>
-
-    <!-- 最新动态 -->
-    <el-row :gutter="20" class="activity-section">
-      <el-col :xs="24" :sm="24" :md="12">
-        <el-card class="activity-card">
+    
+    <!-- 底部详情区域 -->
+    <el-row :gutter="20" class="detail-section" style="margin-top: 20px;">
+      <!-- 左侧:项目进度 -->
+      <el-col :xs="24" :lg="14">
+        <el-card class="progress-card fixed-height-card">
           <template #header>
             <div class="card-header">
-              <span>最新赛事</span>
-              <el-icon>
-                <Calendar />
-              </el-icon>
+              <span class="header-title-with-icon">
+                <el-icon><Operation /></el-icon>
+                项目进度
+              </span>
+              <div class="progress-stats">
+                <span>{{ completedTasks }} / {{ totalTasks }}</span>
+              </div>
             </div>
           </template>
-          <div class="activity-list">
-            <div v-for="event in recentEvents" :key="event.id" class="activity-item">
-              <div class="activity-dot"></div>
-              <div class="activity-content">
-                <div class="activity-title">{{ event.eventName }}</div>
-                <div class="activity-time">{{ event.updateTime }}</div>
+          <div class="progress-summary">
+            <el-progress :percentage="progressPercentage" :stroke-width="12" />
+          </div>
+          <div class="project-vertical-list">
+            <div v-for="(item, index) in projectProgress" :key="index" class="project-list-row">
+              <div class="row-left">
+                <span class="project-name">{{ item.projectName }}</span>
+              </div>
+              <div class="row-center">
+                <span class="project-type">{{ getProjectTypeText(item.projectType) }}</span>
+                <span class="project-classification">{{ item.classification === '0' ? '个人' : '团体' }}</span>
+              </div>
+              <div class="row-right">
+                <span class="project-time">{{ formatTime(item.startTime) }}</span>
+              </div>
+              <!-- 组别详情 -->
+              <div v-if="item.groups && item.groups.length > 0" class="row-groups">
+                <div v-for="group in item.groups" :key="group.groupId" class="group-row">
+                  <span class="group-name">{{ group.groupName }}</span>
+                  <span class="group-time">{{ formatTimeOnly(group.beginTime) }}</span>
+                  <span class="group-status" :class="'status-' + group.status">{{ group.statusText }}</span>
+                </div>
               </div>
             </div>
           </div>
         </el-card>
       </el-col>
 
-      <el-col :xs="24" :sm="24" :md="12">
-        <el-card class="activity-card">
-          <template #header>
-            <div class="card-header">
-              <span>系统公告</span>
-              <el-icon>
-                <Bell />
-              </el-icon>
+      <!-- 右侧:动态与公告 -->
+      <el-col :xs="24" :lg="10">
+        <div class="right-stack">
+          <!-- 最新赛事 -->
+          <el-card class="activity-card mb-20">
+            <template #header>
+              <div class="card-header">
+                <el-icon><Calendar /></el-icon>
+                <span>最新赛事</span>
+              </div>
+            </template>
+            <div class="activity-list">
+              <div v-for="event in recentEvents" :key="event.id" class="activity-item">
+                <div class="activity-dot"></div>
+                <div class="activity-content">
+                  <div class="activity-title">{{ event.eventName }}</div>
+                  <div class="activity-time">{{ event.updateTime }}</div>
+                </div>
+              </div>
             </div>
-          </template>
-          <div class="activity-list">
-            <div v-for="notice in systemNotices" :key="notice.id" class="activity-item">
-              <div class="activity-dot notice"></div>
-              <div class="activity-content">
-                <div class="activity-title">{{ notice.noticeTitle }}</div>
-                <div class="activity-time">{{ notice.createTime }}</div>
+          </el-card>
+
+          <!-- 系统公告 -->
+          <el-card class="activity-card">
+            <template #header>
+              <div class="card-header">
+                <el-icon><Bell /></el-icon>
+                <span>系统公告</span>
+              </div>
+            </template>
+            <div class="activity-list">
+              <div v-for="notice in systemNotices" :key="notice.id" class="activity-item">
+                <div class="activity-dot notice"></div>
+                <div class="activity-content">
+                  <div class="activity-title">{{ notice.noticeTitle }}</div>
+                  <div class="activity-time">{{ notice.createTime }}</div>
+                </div>
               </div>
             </div>
-          </div>
-        </el-card>
+          </el-card>
+        </div>
       </el-col>
     </el-row>
 
@@ -269,21 +310,8 @@ import { ElMessage } from 'element-plus';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 import {
-  TrendCharts,
-  PieChart,
-  Monitor,
-  Operation,
-  Calendar,
-  Bell,
-  Trophy,
-  Clock,
-  VideoPlay,
-  CircleCheck,
-  DataBoard,
-  InfoFilled,
-  FolderAdd,
-  RefreshLeft,
-  UploadFilled
+  TrendCharts,PieChart,Monitor,Operation,Calendar,Bell,Trophy,Clock,VideoPlay,CircleCheck,DataBoard,
+  InfoFilled,FolderAdd,RefreshLeft,UploadFilled,
 } from '@element-plus/icons-vue';
 import * as echarts from 'echarts';
 import { getArticleCount } from '@/api/system/article';
@@ -294,6 +322,10 @@ import { getTeamCount } from '@/api/system/gameTeam';
 import { listNotice } from '@/api/system/notice';
 import { countOnlineUser } from '@/api/monitor/online';
 import { backupDatabase, restoreDatabase } from '@/api/system/backup';
+import { getProjectProgress } from '@/api/system/gameEvent/projectProgress';
+import { ProjectProgressVo } from '@/api/system/gameEvent/types';
+import { useGameEventStore } from '@/store/modules/gameEvent';
+import { storeToRefs } from 'pinia';
 import { useRouter } from 'vue-router';
 
 // 响应式数据
@@ -342,6 +374,8 @@ const backupLoading = ref(false);
 const uploadRef = ref();
 const lastUpdateTime = ref('');
 
+const { game_project_type } = toRefs<any>(proxy?.useDict('game_project_type'));
+
 const quickActions = ref([
   { name: '新增赛事', type: 'primary' as const, icon: 'Plus', action: 'addEvent' },
   { name: '参赛队伍管理', type: 'success' as const, icon: 'User', action: 'manageTeam' },
@@ -361,6 +395,14 @@ const eventStatus = ref({
   total: 0
 });
 
+// 项目进度相关数据
+const gameEventStore = useGameEventStore();
+const { defaultEventInfo } = storeToRefs(gameEventStore);
+const projectProgress = ref<ProjectProgressVo[]>([]);
+const completedTasks = ref(0);
+const totalTasks = ref(0);
+const progressPercentage = computed(() => totalTasks.value > 0 ? Math.round((completedTasks.value / totalTasks.value) * 100) : 0);
+
 /**
  * 获取最新赛事
  */
@@ -601,12 +643,81 @@ const updateTime = () => {
   lastUpdateTime.value = now.toLocaleTimeString();
 };
 
+/**
+ * 加载项目进度信息
+ */
+const loadProjectProgress = async () => {
+  try {
+    await gameEventStore.fetchDefaultEvent();
+    if (!defaultEventInfo.value || !defaultEventInfo.value.eventId) return;
+    
+    const res = await getProjectProgress(defaultEventInfo.value.eventId);
+    projectProgress.value = res.data || [];
+    
+    // 计算已完成和总任务数
+    let completed = 0;
+    let total = 0;
+    
+    projectProgress.value.forEach(project => {
+      if (project.groups && project.groups.length > 0) {
+        project.groups.forEach(group => {
+          total++;
+          if (group.status === '2') completed++;
+        });
+      } else {
+        total++;
+        if (project.status === '2') completed++;
+      }
+    });
+    
+    completedTasks.value = completed;
+    totalTasks.value = total;
+  } catch (error) {
+    console.error('加载项目进度失败:', error);
+  }
+};
+
+// 格式化函数
+const formatTime = (timeStr: string) => {
+  if (!timeStr) return '-';
+  try {
+    const date = new Date(timeStr);
+    return date.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false });
+  } catch (e) { return timeStr; }
+};
+
+const formatTimeOnly = (timeStr: string) => {
+  if (!timeStr) return '-';
+  try {
+    const date = new Date(timeStr);
+    return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false });
+  } catch (e) { return timeStr; }
+};
+
+// 获取项目类型文本
+const getProjectTypeText = (projectType: string) => {
+  if (!game_project_type.value || !projectType) return '未知';
+  
+  const typeItem = game_project_type.value.find((item: any) => item.value === projectType);
+  return typeItem ? typeItem.label : '未知';
+};
+
+const getStatusType = (status: string) => {
+  switch(status) {
+    case '0': return 'info';
+    case '1': return 'primary';
+    case '2': return 'success';
+    default: return 'info';
+  }
+};
+
 const getAllData = async () => {
   loadStatistics();
   loadRecentEvents();
   loadNotice();
   loadEventStatus();
   getOnlineUser();
+  loadProjectProgress();
 };
 
 // 弹窗相关方法
@@ -1130,4 +1241,162 @@ onMounted(() => {
     }
   }
 }
+.fixed-height-card {
+  height: 600px;
+}
+
+.fixed-height-card :deep(.el-card__body) {
+  height: calc(100% - 55px);
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+
+.progress-summary {
+  padding: 10px 0;
+}
+
+.project-vertical-list {
+  flex: 1;
+  overflow-y: auto;
+}
+
+.project-list-row {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  padding: 15px 0;
+  padding-right: 80px; /* 增加右侧内边距,使内容往中间靠 */
+  border-bottom: 1px solid #f0f2f5;
+}
+
+.row-left {
+  flex: 1; /* 减小比例,让出更多空间给中间和右侧 */
+  font-weight: 500;
+  color: #303133;
+  padding-right: 20px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.row-center {
+  flex: 1;
+  text-align: center;
+  color: #606266;
+  font-size: 13px;
+  line-height: 1.4;
+}
+
+.row-center span {
+  display: block;
+}
+
+.row-right {
+  width: 100px;
+  flex:  1;
+  text-align: right;
+  color: #909399;
+  font-size: 13px;
+  white-space: nowrap;
+  padding-right: 5px;
+}
+
+.row-groups {
+  width: 100%;
+  margin-top: 10px;
+  padding-left: 20px;
+  border-left: 2px solid #e4e7ed;
+}
+
+.group-row {
+  display: flex;
+  align-items: center;
+  padding: 5px 0;
+  font-size: 13px;
+}
+
+.group-name {
+  width: 100px;
+  color: #666;
+}
+
+.group-time {
+  width: 100px;
+  color: #999;
+}
+
+.group-status {
+  margin-left: auto;
+}
+
+.status-0 { color: #909399; }
+.status-1 { color: #409eff; }
+.status-2 { color: #67c23a; }
+
+/* 右侧堆叠 */
+.right-stack {
+  height: 600px;
+  display: flex;
+  flex-direction: column;
+}
+
+.mb-20 {
+  margin-bottom: 20px;
+}
+
+.activity-card {
+  flex: 1;
+}
+
+.activity-card :deep(.el-card__body) {
+  height: calc(100% - 55px);
+  overflow-y: auto;
+}
+
+/* 恢复之前的 activity 样式 */
+.activity-list {
+  padding: 10px 0;
+}
+
+.activity-item {
+  display: flex;
+  align-items: center;
+  padding: 12px 0;
+  border-bottom: 1px solid #f0f2f5;
+}
+
+.activity-item:last-child {
+  border-bottom: none;
+}
+
+.activity-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background-color: #409eff;
+  margin-right: 15px;
+  flex-shrink: 0;
+}
+
+.activity-dot.notice {
+  background-color: #f56c6c;
+}
+
+.activity-content {
+  flex: 1;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.activity-title {
+  font-size: 14px;
+  color: #303133;
+}
+
+.activity-time {
+  font-size: 12px;
+  color: #999;
+}
 </style>

+ 256 - 158
src/views/system/gameEvent/RankingBoardPage.vue

@@ -18,7 +18,7 @@
       <p>随时掌握运动会情况</p>
     </div>
     <el-row :gutter="20" class="ranking-row">
-      <el-col :span="8">
+      <el-col :span="12">
         <el-card shadow="hover" class="ranking-card">
           <template #header>
             <div class="card-header header-one">
@@ -29,6 +29,22 @@
                 </div>
                 <!-- 控件在一行,均匀分布 -->
                 <div class="header-controls">
+                  <el-select
+                    v-model="selectedProjectId"
+                    placeholder="选择项目"
+                    size="small"
+                    @change="handleProjectChange"
+                    class="project-select"
+                    filterable
+                    style="width: 150px;"
+                  >
+                    <el-option
+                      v-for="project in personalProjectOptions"
+                      :key="project.projectId"
+                      :label="project.projectName"
+                      :value="project.projectId"
+                    />
+                  </el-select>
                   <div class="display-count-control">
                     <span class="control-label">前</span>
                     <el-input-number
@@ -63,31 +79,50 @@
                 <span class="header-rank">名次</span>
                 <span class="header-name">姓名</span>
                 <span class="header-team">队伍名称</span>
-                <span class="header-score">积分</span>
+                <span class="header-project">项目</span>
+                <span class="header-score">成绩</span>
               </div>
             </div>
             <div v-for="(item, index) in displayedAthleteList" :key="index" class="ranking-item">
               <div class="item-content">
                 <span class="item-rank">{{ getRankDisplay(item, index, athleteScoreList) }}</span>
-                <span class="item-name">{{ item.athleteName }}</span>
+                <span class="item-name">{{ item.name || item.athleteName }}</span>
                 <span class="item-team">{{ item.teamName }}</span>
-                <span class="item-time">{{ item.totalScore }}</span>
+                <span class="item-project-name">{{ selectedProjectName }}</span>
+                <span class="item-score" v-if="item.classification=='0'">{{item.individualPerformance || item.score || item.totalScore }}</span>
+                <span class="item-score" v-else-if="item.classification=='1'">{{item.teamPerformance || item.score || item.totalScore }}</span>
               </div>
             </div>
           </div>
         </el-card>
       </el-col>
-      <el-col :span="8">
+      <el-col :span="12">
         <el-card shadow="hover" class="ranking-card">
           <template #header>
             <div class="card-header header-three">
               <div class="team-ranking-header">
                 <!-- 标题单独一行,居中对齐 -->
                 <div class="header-title">
-                  <span>团队积分排行榜</span>
+                  <span>团队项目排行榜</span>
                 </div>
                 <!-- 控件在一行,均匀分布 -->
                 <div class="header-controls">
+                  <el-select
+                    v-model="selectedTeamProjectId"
+                    placeholder="选择项目"
+                    size="small"
+                    @change="handleTeamProjectChange"
+                    filterable
+                    class="project-select"
+                    style="width: 150px;"
+                  >
+                    <el-option
+                      v-for="project in teamProjectOptions"
+                      :key="project.projectId"
+                      :label="project.projectName"
+                      :value="project.projectId"
+                    />
+                  </el-select>
                   <el-select 
                     v-model="selectedRankGroupId" 
                     placeholder="选择分组" 
@@ -136,47 +171,20 @@
             <div class="ranking-header">
               <div class="header-content">
                 <span class="header-rank">名次</span>
+                <span class="header-project">项目</span>
                 <span class="header-team-name">队伍名称</span>
                 <span class="header-group">组别</span>
-                <span class="header-score">总分</span>
+                <span class="header-score">成绩</span>
               </div>
             </div>
             <div v-for="(item, index) in displayedTeamList" :key="index" class="ranking-item">
               <div class="item-content">
                 <span class="item-rank">{{ getRankDisplay(item, index, filteredTeamScores) }}</span>
-                <span class="item-team-name">{{ item.teamName }}</span>
-                <span class="item-group">{{ item.rgName }}</span>
-                <span class="item-score">{{ item.score }}分</span>
-              </div>
-            </div>
-          </div>
-        </el-card>
-      </el-col>
-      <el-col :span="8">
-        <el-card shadow="hover" class="ranking-card">
-          <template #header>
-            <div class="card-header header-two">
-              <span>项目进度</span>
-            </div>
-          </template>
-          <div class="progress-info">
-            <p>{{ completedTasks }} / {{ totalTasks }}</p>
-            <el-progress :percentage="progressPercentage" />
-          </div>
-          <div class="ranking-list ranking-list-full">
-            <div v-for="(item, index) in projectProgress" :key="index" class="ranking-item">
-              <div class="item-content">
-                <span class="item-name">{{ item.projectName }}</span>
-                <span class="item-type">{{ getProjectTypeText(item.projectType) }} {{ item.classification === '0' ? '个人' : '团体' }}</span>
-                <span class="item-time">{{ formatTime(item.startTime) }}</span>
-              </div>
-              <!-- 如果有组别信息,显示组别详情 -->
-              <div v-if="item.groups && item.groups.length > 0" class="group-details">
-                <div v-for="group in item.groups" :key="group.groupId" class="group-item">
-                  <span class="group-name">{{ group.groupName }}</span>
-                  <span class="group-time">{{ formatTimeOnly(group.beginTime) }}</span>
-                  <span class="group-status" :class="'status-' + group.status">{{ group.statusText }}</span>
-                </div>
+                <span class="item-project-name">{{ selectedTeamProjectName }}</span>
+                <span class="item-team-name">{{ item.name || item.teamName }}</span>
+                <span class="item-group">{{ item.rgName || '-' }}</span>
+                <span class="item-score" v-if="item.classification=='1'">{{ item.teamPerformance || item.score || item.totalScore }}</span>
+                <span class="item-score" v-else>{{ item.score || item.totalScore }}</span>
               </div>
             </div>
           </div>
@@ -189,7 +197,8 @@
 <script setup lang="ts">
 import { ref, computed, onMounted, onUnmounted, getCurrentInstance, toRefs } from 'vue';
 import { listScoreRanking, listPersonalRanking, listTeamRanking } from '@/api/system/gameEvent/eventRank';
-import { listGameScore, getBonusData } from '@/api/system/gameScore';
+import { listGameScore, getBonusData, getProjectScoreData } from '@/api/system/gameScore';
+import { listGameEventProject } from '@/api/system/gameEventProject';
 import { getProjectProgress } from '@/api/system/gameEvent/projectProgress';
 import { ProjectProgressVo, GroupProgressVo } from '@/api/system/gameEvent/types';
 import { useGameEventStore } from '@/store/modules/gameEvent';
@@ -227,13 +236,30 @@ const progressPercentage = computed(() => totalTasks.value > 0 ? Math.round((com
 const projectProgress = ref<ProjectProgressVo[]>([]);
 
 const teamScores = ref<TeamScore[]>([]);
-const filteredTeamScores = ref<TeamScore[]>([]); // 用于显示筛选后的队伍积分
+const filteredTeamScores = ref<any[]>([]); // 用于显示筛选后的队伍积分
 
 const ALL_GROUPS_VALUE = 'all';
 // 分组相关数据
 const rankGroupOptions = ref<RankGroupVO[]>([]);
 const selectedRankGroupId = ref<string | number | null>(null);
 
+const personalProjectOptions = ref<any[]>([]);
+const selectedProjectId = ref<string | number | null>(null);
+
+const selectedProjectName = computed(() => {
+  const project = personalProjectOptions.value.find(p => p.projectId === selectedProjectId.value);
+  return project ? project.projectName : '-';
+});
+
+// 团队项目选择相关
+const teamProjectOptions = ref<any[]>([]);
+const selectedTeamProjectId = ref<string | number | null>(null);
+
+const selectedTeamProjectName = computed(() => {
+  const project = teamProjectOptions.value.find(p => p.projectId === selectedTeamProjectId.value);
+  return project ? project.projectName : '-';
+});
+
 // 显示数量控制
 const personalDisplayCount = ref(10); // 个人排行榜显示数量,默认10
 const teamDisplayCount = ref(10); // 团队排行榜显示数量,默认10
@@ -251,13 +277,14 @@ const countdownInterval = ref<NodeJS.Timeout | null>(null); // 倒计时定时
 
 // 启动倒计时
 const startCountdown = () => {
-  // stopCountdown();
+  stopCountdown();
   countdownSeconds.value = autoRefreshSeconds.value;
   
   countdownInterval.value = setInterval(() => {
     countdownSeconds.value--;
     
     if (countdownSeconds.value <= 0) {
+      refreshAllData();
       countdownSeconds.value = autoRefreshSeconds.value;
     }
   }, 1000);
@@ -291,10 +318,15 @@ const handleTeamCountChange = (value: number) => {
   teamDisplayCount.value = value;
 };
 
-// 获取队伍积分排行榜
-const loadTeamScores = async () => {
-  const teamScoreRes = await getBonusData({
+// 获取团队项目排行榜数据
+const loadTeamProjectScores = async () => {
+  if (!selectedTeamProjectId.value) return;
+  const res = await getProjectScoreData({
     eventId: defaultEventInfo.value.eventId,
+    projectId: selectedTeamProjectId.value,
+    classification: '1',
+    pageNum: 1,
+    pageSize: 1000 // 获取全量以便筛选
   });
   
   // 创建分组ID到分组名称的映射
@@ -303,20 +335,21 @@ const loadTeamScores = async () => {
     groupMap.set(group.rgId, group.rgName);
   });
   
-  // // 将队伍信息和积分结合
-  const teamScoreList = teamScoreRes.data.rows.map(teamScore => {
+  const teamScoreList = (res.rows || []).map(item => {
     return {
-      teamId: teamScore.teamId,
-      teamName: teamScore.teamName,
-      score: teamScore.totalScore,
-      rank: teamScore.rank,
-      rgId: teamScore.rgId,
-      rgName: groupMap.get(teamScore.rgId) || '-' // 添加分组名称
+      ...item,
+      classification: '1',
+      rgName: groupMap.get(item.rgId) || item.rgName || '-'
     }
-  })
-  // // 默认显示所有队伍
-  filteredTeamScores.value = teamScoreList;
-  teamScores.value = teamScoreList;
+  });
+
+  // 获取当前项目的排序方式
+  const currentProject = teamProjectOptions.value.find(p => p.projectId == selectedTeamProjectId.value);
+  const orderType = currentProject ? currentProject.orderType : '1'; // 默认降序
+
+  teamScores.value = sortAndRankList(teamScoreList, orderType);
+  // 应用分组筛选
+  handleRankGroupChange(selectedRankGroupId.value);
 };
 
 // 处理分组筛选变化
@@ -329,21 +362,9 @@ const handleRankGroupChange = (rgId: string | number | null | undefined) => {
     filteredTeamScores.value = teamScores.value.filter(team => team.rgId === rgId);
   }
   
-  // 重新计算排名
-  let currentRank = 1;
-  for (let i = 0; i < filteredTeamScores.value.length; i++) {
-    const team = filteredTeamScores.value[i];
-    const currentScore = team.score || 0;
-    
-    if (i > 0) {
-      const previousScore = filteredTeamScores.value[i - 1].score || 0;
-      if (currentScore !== previousScore) {
-        currentRank = i + 1;
-      }
-    }
-    
-    team.rank = currentRank;
-  }
+  // 重新计算排名(基于已有的 rank 字段进行筛选显示,不重新计算,因为数据已经是有序的了)
+  // 只是同步到 filteredTeamScores 供前端展示
+  // 注意:如果是从 teamScores 中过滤出来的,它们已经带有了全局排名信息
 };
 
 // 获取分组选项
@@ -363,6 +384,46 @@ const loadRankGroupOptions = async () => {
   }
 };
 
+// 获取项目选项(个人和团队)
+const loadProjectOptions = async () => {
+  try {
+    const res = await listGameEventProject({
+      eventId: defaultEventInfo.value.eventId,
+      orderByColumn: 'createTime',
+      isAsc: 'desc',
+      pageNum: 1,
+      pageSize: 1000
+    });
+    const allProjects = res.rows || [];
+    
+    // 个人项目过滤
+    personalProjectOptions.value = allProjects.filter(p => p.classification === '0');
+    if (personalProjectOptions.value.length > 0 && !selectedProjectId.value) {
+      selectedProjectId.value = personalProjectOptions.value[0].projectId;
+    }
+    
+    // 团队项目过滤
+    teamProjectOptions.value = allProjects.filter(p => p.classification === '1');
+    if (teamProjectOptions.value.length > 0 && !selectedTeamProjectId.value) {
+      selectedTeamProjectId.value = teamProjectOptions.value[0].projectId;
+    }
+  } catch (error) {
+    console.error('获取项目列表失败:', error);
+  }
+};
+
+// 处理项目切换
+const handleProjectChange = (projectId: string | number) => {
+  selectedProjectId.value = projectId;
+  refreshPersonalRanking();
+};
+
+// 处理团队项目切换
+const handleTeamProjectChange = (projectId: string | number) => {
+  selectedTeamProjectId.value = projectId;
+  refreshTeamRanking();
+};
+
 // 加载项目进度信息
 const loadProjectProgress = async () => {
   try {
@@ -421,90 +482,123 @@ const loadProjectProgress = async () => {
   }
 };
 
-// 获取项目类型文本
-const getProjectTypeText = (projectType: string) => {
-  if (!game_project_type.value || !projectType) return '未知';
+// 获取成绩的数值表示(用于排序)
+const getNumericValue = (val: string | number | null | undefined) => {
+  if (val === null || val === undefined || val === '' || val === '0' || val === '00:00:00.000') return 0;
+  if (typeof val === 'number') return val;
   
-  const typeItem = game_project_type.value.find((item: any) => item.value === projectType);
-  return typeItem ? typeItem.label : '未知';
-};
-
-// 格式化时间显示(包含日期)
-const formatTime = (timeStr: string) => {
-  if (!timeStr) return '-';
-  try {
-    const date = new Date(timeStr);
-    // 检查是否是有效日期
-    if (isNaN(date.getTime())) {
-      return timeStr;
+  const valStr = String(val);
+  // 处理计时格式 00:00:00.000
+  if (valStr.includes(':')) {
+    const parts = valStr.split(':');
+    if (parts.length === 3) {
+      const hours = parseInt(parts[0]) || 0;
+      const minutes = parseInt(parts[1]) || 0;
+      const secondsWithMs = parts[2];
+      let seconds = 0;
+      let ms = 0;
+      if (secondsWithMs.includes('.')) {
+        const secondsParts = secondsWithMs.split('.');
+        seconds = parseInt(secondsParts[0]) || 0;
+        ms = parseInt(secondsParts[1]) || 0;
+      } else {
+        seconds = parseInt(secondsWithMs) || 0;
+      }
+      return hours * 3600000 + minutes * 60000 + seconds * 1000 + ms;
     }
-    return date.toLocaleString('zh-CN', { 
-      month: '2-digit',
-      day: '2-digit',
-      hour: '2-digit', 
-      minute: '2-digit',
-      hour12: false 
-    });
-  } catch (error) {
-    return timeStr;
   }
+  
+  const parsed = parseFloat(valStr);
+  return isNaN(parsed) ? 0 : parsed;
 };
 
-// 格式化时间显示(仅时间,用于组别详情)
-const formatTimeOnly = (timeStr: string) => {
-  if (!timeStr) return '-';
-  try {
-    const date = new Date(timeStr);
-    // 检查是否是有效日期
-    if (isNaN(date.getTime())) {
-      return timeStr;
+// 前端排序与排名处理
+const sortAndRankList = (list: any[], orderType: string) => {
+  if (!list || list.length === 0) return [];
+  
+  // 成绩取值助手函数
+  const getScoreValue = (item: any) => {
+    if (item.classification == '0') {
+      return item.individualPerformance ?? item.score ?? item.totalScore;
+    } else {
+      return item.teamPerformance ?? item.score ?? item.totalScore;
     }
-    return date.toLocaleTimeString('zh-CN', { 
-      hour: '2-digit', 
-      minute: '2-digit',
-      hour12: false 
-    });
-  } catch (error) {
-    return timeStr;
-  }
-};
+  };
 
-// 获取排名显示文本(支持并列排名,排名数值连续)
-const getRankDisplay = (item: any, index: number, list: any[]) => {
-  // 如果项目有rank字段(如团队排名),直接使用
-  if (item.rank !== undefined) {
-    return `第${item.rank}名`;
-  }
+  // 1. 排序逻辑
+  const sorted = [...list].sort((a, b) => {
+    const valA = getScoreValue(a);
+    const valB = getScoreValue(b);
+    
+    const scoreA = getNumericValue(valA);
+    const scoreB = getNumericValue(valB);
+    
+    // 0 值(未参赛/无效成绩)永远排在最后
+    if (scoreA === 0 && scoreB === 0) return 0;
+    if (scoreA === 0) return 1;
+    if (scoreB === 0) return -1;
+    
+    if (orderType == '0') { // 升序(计时类,用时越短越好)
+      return scoreA - scoreB;
+    } else { // 降序(远度/积分,分值越高越好)
+      return scoreB - scoreA;
+    }
+  });
   
-  // 个人排名逻辑(排名数值连续)
-  // 计算当前项目的实际排名
-  // 排名 = 比当前积分高的不同积分数量 + 1
-  const currentScore = item.totalScore || 0;
-  const higherScores = new Set();
+  // 2. 计算排名(支持并列)
+  let currentRank = 0;
+  let lastScoreValue = -1;
   
-  for (let i = 0; i < list.length; i++) {
-    if (list[i].totalScore > currentScore) {
-      higherScores.add(list[i].totalScore);
+  return sorted.map((item, index) => {
+    const val = getScoreValue(item);
+    const scoreValue = getNumericValue(val);
+    
+    if (scoreValue === 0) {
+      item.rank = '-'; // 无效成绩不计排名
+    } else if (scoreValue !== lastScoreValue) {
+      currentRank = index + 1;
+      lastScoreValue = scoreValue;
+      item.rank = currentRank;
+    } else {
+      item.rank = currentRank;
     }
+    return item;
+  });
+};
+
+// 获取排名显示文本
+const getRankDisplay = (item: any, index: number, list: any[]) => {
+  if (item.rank === '-' || item.rank === undefined) {
+    return '未入榜';
   }
-  
-  const actualRank = higherScores.size + 1;
-  return `第${actualRank}名`;
+  return `第${item.rank}名`;
 };
 
 // 刷新个人排行榜数据
 const refreshPersonalRanking = async () => {
+  if (!selectedProjectId.value) return;
   personalRefreshing.value = true;
   try {
-    const res = await listScoreRanking(defaultEventInfo.value.eventId.toString());
-    // 按照totalScore字段降序排序(分数高的在前面)
-    athleteScoreList.value = res.data.sort((a, b) => b.totalScore - a.totalScore);
+    const res = await getProjectScoreData({
+      eventId: defaultEventInfo.value.eventId,
+      projectId: selectedProjectId.value,
+      classification: '0',
+      pageNum: 1,
+      pageSize: personalDisplayCount.value
+    });
+    // 接口返回的是分页数据,取 rows
+    const rows = (res.rows || []).map(item => ({ ...item, classification: '0' }));
+    
+    // 获取当前项目的排序方式
+    const currentProject = personalProjectOptions.value.find(p => p.projectId == selectedProjectId.value);
+    const orderType = currentProject ? currentProject.orderType : '1'; // 默认降序
+    
+    athleteScoreList.value = sortAndRankList(rows, orderType);
     // 手动刷新时重置倒计时
     startCountdown();
     // 显示成功消息
     ElMessage.success('个人排行榜数据已刷新');
   } catch (error) {
-    console.error('刷新个人排行榜失败:', error);
     ElMessage.error('刷新个人排行榜失败');
   } finally {
     personalRefreshing.value = false;
@@ -515,8 +609,8 @@ const refreshPersonalRanking = async () => {
 const refreshTeamRanking = async () => {
   teamRefreshing.value = true;
   try {
-    // 重新加载队伍积分排行榜
-    await loadTeamScores();
+    // 重新加载团队项目排行榜
+    await loadTeamProjectScores();
     // 手动刷新时重置倒计时
     startCountdown();
     
@@ -539,12 +633,10 @@ const refreshAllData = async () => {
     // 并行刷新所有数据
     await Promise.all([
       refreshPersonalRanking(),
-      refreshTeamRanking(),
-      loadProjectProgress()
+      refreshTeamRanking()
     ]);
-    ElMessage.success('所有数据已刷新');
+    // ElMessage.success('所有数据已刷新');
   } catch (error) {
-    console.error('刷新数据失败:', error);
     ElMessage.error('刷新数据失败');
   } finally {
     personalRefreshing.value = false;
@@ -554,16 +646,8 @@ const refreshAllData = async () => {
 
 // 开启自动刷新
 const startAutoRefresh = () => {
-  if (autoRefreshInterval.value) {
-    clearInterval(autoRefreshInterval.value);
-  }
-  
-  autoRefreshInterval.value = setInterval(() => {
-    // refreshAllData();
-  }, autoRefreshSeconds.value * 1000);
-  // 开启倒计时
+  // 开启倒计时,倒计时结束会自动触发刷新
   startCountdown();
-  ElMessage.success(`已开启自动刷新,每${autoRefreshSeconds.value}秒刷新一次`);
 };
 
 // 组件卸载时清理定时器
@@ -579,18 +663,21 @@ onMounted(async () => {
   try{
     startAutoRefresh();
     await gameEventStore.fetchDefaultEvent();
-    const res = await listScoreRanking(defaultEventInfo.value.eventId.toString());
-    // 按照totalScore字段降序排序(分数高的在前面)
-    athleteScoreList.value = res.data.sort((a, b) => b.totalScore - a.totalScore);
     // 并行加载其他数据
     await Promise.all([
-      // 加载队伍积分排行榜
-      loadTeamScores(),
+      // 加载所有项目选项并进行前端分类
+      loadProjectOptions(),
       // 加载分组选项
       loadRankGroupOptions(),
-      // 加载项目进度信息
-      loadProjectProgress()
     ]);
+
+    // 在获取到项目列表并设置默认项目后,加载对应项目的排名
+    if (selectedProjectId.value || selectedTeamProjectId.value) {
+      await Promise.all([
+        refreshPersonalRanking(),
+        refreshTeamRanking()
+      ]);
+    }
   } catch (error) {
     console.error('加载数据失败:', error);
   }
@@ -764,6 +851,12 @@ onMounted(async () => {
   font-size: 14px;
 }
 
+.header-project {
+  flex: 1.5;
+  text-align: center;
+  font-size: 14px;
+}
+
 .header-name {
   flex: 2;
   text-align: left;
@@ -803,6 +896,11 @@ onMounted(async () => {
   text-align: center;
 }
 
+.item-project-name {
+  flex: 1.5;
+  text-align: center;
+}
+
 /* 团队积分排行榜数据样式 */
 .item-team-name {
   flex: 2;
@@ -871,7 +969,7 @@ onMounted(async () => {
 /* 控件区域样式 */
 .header-controls {
   display: flex;
-  justify-content: space-between;
+  justify-content: center;
   align-items: center;
   gap: 15px;
   flex-wrap: wrap;