Browse Source

feat(gameEvent): 优化排行榜页面功能和样式

- 重构个人和团队排行榜头部布局,支持控件分布和标题居中
- 添加排行榜显示数量控制功能,可自定义显示前N名数据
- 实现排行榜数据刷新功能,支持手动和自动刷新机制
- 增加团队排行榜分组名称显示和筛选优化
- 添加排行榜表头和数据行的响应式样式设计
- 设置默认赛事类型为'11',优化初始化配置
- 完善排行榜数据展示逻辑和计算精度处理
zhou 3 weeks ago
parent
commit
230731cfce
2 changed files with 467 additions and 30 deletions
  1. 465 28
      src/views/system/gameEvent/RankingBoardPage.vue
  2. 2 2
      src/views/system/gameEvent/edit.vue

+ 465 - 28
src/views/system/gameEvent/RankingBoardPage.vue

@@ -10,11 +10,51 @@
         <el-card shadow="hover" class="ranking-card">
           <template #header>
             <div class="card-header header-one">
-              <span>个人积分排行榜</span>
+              <div class="personal-ranking-header">
+                <!-- 标题单独一行,居中对齐 -->
+                <div class="header-title">
+                  <span>个人积分排行榜</span>
+                </div>
+                <!-- 控件在一行,均匀分布 -->
+                <div class="header-controls">
+                  <div class="display-count-control">
+                    <span class="control-label">前</span>
+                    <el-input-number
+                      v-model="personalDisplayCount"
+                      :min="1"
+                      :max="100"
+                      size="small"
+                      controls-position="right"
+                      style="width: 80px; margin: 0 5px;"
+                      @change="handlePersonalCountChange"
+                    />
+                    <span class="control-label">名</span>
+                  </div>
+                  <el-button
+                    type="primary"
+                    size="small"
+                    :icon="Refresh"
+                    :loading="personalRefreshing"
+                    @click="refreshPersonalRanking"
+                    class="refresh-btn"
+                  >
+                    刷新
+                  </el-button>
+                </div>
+              </div>
             </div>
           </template>
           <div class="ranking-list ranking-list-full">
-            <div v-for="(item, index) in athleteScoreList" :key="index" class="ranking-item">
+            <!-- 添加标题行 -->
+            <div class="ranking-header">
+              <div class="header-content">
+                <span class="header-rank">名次</span>
+                <span class="header-name">姓名</span>
+                <span class="header-team">队伍名称</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>
@@ -30,32 +70,70 @@
           <template #header>
             <div class="card-header header-three">
               <div class="team-ranking-header">
-                <span>团队积分排行榜</span>
-                <!-- 添加分组筛选下拉框 -->
-                <el-select 
-                  v-model="selectedRankGroupId" 
-                  placeholder="选择分组" 
-                  clearable 
-                  size="small" 
-                  @change="handleRankGroupChange"
-                  style="margin-left: 10px; width: 120px;"
-                >
-                  <el-option label="全部队伍" :value="null"></el-option>
-                  <el-option
-                    v-for="group in rankGroupOptions"
-                    :key="group.rgId"
-                    :label="group.rgName"
-                    :value="group.rgId"
-                  />
-                </el-select>
+                <!-- 标题单独一行,居中对齐 -->
+                <div class="header-title">
+                  <span>团队积分排行榜</span>
+                </div>
+                <!-- 控件在一行,均匀分布 -->
+                <div class="header-controls">
+                  <el-select 
+                    v-model="selectedRankGroupId" 
+                    placeholder="选择分组" 
+                    clearable 
+                    size="small" 
+                    @change="handleRankGroupChange"
+                    class="group-select"
+                  >
+                    <el-option label="全部队伍" :value="ALL_GROUPS_VALUE"></el-option>
+                    <el-option
+                      v-for="group in rankGroupOptions"
+                      :key="group.rgId"
+                      :label="group.rgName"
+                      :value="group.rgId"
+                    />
+                  </el-select>
+                  <div class="display-count-control">
+                    <span class="control-label">前</span>
+                    <el-input-number
+                      v-model="teamDisplayCount"
+                      :min="1"
+                      :max="100"
+                      size="small"
+                      controls-position="right"
+                      style="width: 80px; margin: 0 5px;"
+                      @change="handleTeamCountChange"
+                    />
+                    <span class="control-label">名</span>
+                  </div>
+                  <el-button
+                    type="primary"
+                    size="small"
+                    :icon="Refresh"
+                    :loading="teamRefreshing"
+                    @click="refreshTeamRanking"
+                    class="refresh-btn"
+                  >
+                    刷新
+                  </el-button>
+                </div>
               </div>
             </div>
           </template>
           <div class="ranking-list ranking-list-full">
-            <div v-for="(item, index) in filteredTeamScores" :key="index" class="ranking-item">
+            <!-- 添加标题行 -->
+            <div class="ranking-header">
+              <div class="header-content">
+                <span class="header-rank">名次</span>
+                <span class="header-team-name">队伍名称</span>
+                <span class="header-group">组别</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>
@@ -107,8 +185,9 @@ import { GameTeamVO } from '@/api/system/gameTeam/types';
 import { ProjectProgressVo, GroupProgressVo } from '@/api/system/gameEvent/types';
 import { useGameEventStore } from '@/store/modules/gameEvent';
 import { storeToRefs } from 'pinia';
-import { listRankGroup } from '@/api/system/rankGroup'; // 新增导入
-import { RankGroupVO } from '@/api/system/rankGroup/types'; // 新增导入
+import { listRankGroup } from '@/api/system/rankGroup'; 
+import { RankGroupVO } from '@/api/system/rankGroup/types'; 
+import { Refresh } from '@element-plus/icons-vue';
 
 // 定义队伍积分排行榜的数据结构
 interface TeamScore {
@@ -117,6 +196,7 @@ interface TeamScore {
   teamName: string;
   score: number;
   rank: number;
+  rgName?: string;
 }
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -133,16 +213,44 @@ const athleteScoreList = ref([]);
 
 const completedTasks = ref(0);
 const totalTasks = ref(0);
-const progressPercentage = computed(() => totalTasks.value > 0 ? (completedTasks.value / totalTasks.value) * 100 : 0);
+const progressPercentage = computed(() => totalTasks.value > 0 ? Math.round((completedTasks.value / totalTasks.value) * 100) : 0);
 
 const projectProgress = ref<ProjectProgressVo[]>([]);
 
 const teamScores = ref<TeamScore[]>([]);
 const filteredTeamScores = ref<TeamScore[]>([]); // 用于显示筛选后的队伍积分
 
+const ALL_GROUPS_VALUE = 'all';
 // 分组相关数据
 const rankGroupOptions = ref<RankGroupVO[]>([]);
-const selectedRankGroupId = ref<number | null>(null);
+const selectedRankGroupId = ref<string | number | null>(null);
+
+// 显示数量控制
+const personalDisplayCount = ref(10); // 个人排行榜显示数量,默认10
+const teamDisplayCount = ref(10); // 团队排行榜显示数量,默认10
+
+// 添加刷新状态控制
+const personalRefreshing = ref(false); // 个人排行榜刷新状态
+const teamRefreshing = ref(false); // 团队排行榜刷新状态
+
+// 根据显示数量过滤数据
+const displayedAthleteList = computed(() => {
+  return athleteScoreList.value.slice(0, personalDisplayCount.value);
+});
+
+const displayedTeamList = computed(() => {
+  return filteredTeamScores.value.slice(0, teamDisplayCount.value);
+});
+
+// 处理个人排行榜显示数量变化
+const handlePersonalCountChange = (value: number) => {
+  personalDisplayCount.value = value;
+};
+
+// 处理团队排行榜显示数量变化
+const handleTeamCountChange = (value: number) => {
+  teamDisplayCount.value = value;
+};
 
 // 获取队伍积分排行榜
 const loadTeamScores = async () => {
@@ -165,6 +273,17 @@ const loadTeamScores = async () => {
     isAsc: ''
   });
   const teams = teamRes.rows;
+
+  // 获取分组信息(如果还没有加载的话)
+  if (rankGroupOptions.value.length === 0) {
+    await loadRankGroupOptions();
+  }
+  
+  // 创建分组ID到分组名称的映射
+  const groupMap = new Map();
+  rankGroupOptions.value.forEach(group => {
+    groupMap.set(group.rgId, group.rgName);
+  });
   
   // 计算每个队伍的总积分
   const teamScoreMap = new Map();
@@ -186,7 +305,8 @@ const loadTeamScores = async () => {
       teamName: team.teamName,
       score: teamScoreMap.get(team.teamId) || 0,
       rank: 0, // 占位符,稍后设置
-      rgId: team.rgId // 添加分组ID
+      rgId: team.rgId, // 添加分组ID
+      rgName: groupMap.get(team.rgId) || '-' // 添加分组名称
     };
   });
   
@@ -216,8 +336,8 @@ const loadTeamScores = async () => {
 };
 
 // 处理分组筛选变化
-const handleRankGroupChange = (rgId: number | null) => {
-  if (rgId === null) {
+const handleRankGroupChange = (rgId: string | number | null) => {
+  if (rgId === null || rgId === ALL_GROUPS_VALUE) {
     // 显示所有队伍
     filteredTeamScores.value = teamScores.value;
   } else {
@@ -388,6 +508,101 @@ const getRankDisplay = (item: any, index: number, list: any[]) => {
   return `第${actualRank}名`;
 };
 
+// 刷新个人排行榜数据
+const refreshPersonalRanking = async () => {
+  personalRefreshing.value = true;
+  try {
+    const res = await listScoreRanking(defaultEventInfo.value.eventId.toString());
+    // 按照totalScore字段降序排序(分数高的在前面)
+    athleteScoreList.value = res.data.sort((a, b) => b.totalScore - a.totalScore);
+    
+    // 显示成功消息
+    ElMessage.success('个人排行榜数据已刷新');
+  } catch (error) {
+    console.error('刷新个人排行榜失败:', error);
+    ElMessage.error('刷新个人排行榜失败');
+  } finally {
+    personalRefreshing.value = false;
+  }
+};
+
+// 刷新团队排行榜数据
+const refreshTeamRanking = async () => {
+  teamRefreshing.value = true;
+  try {
+    // 重新加载队伍积分排行榜
+    await loadTeamScores();
+    
+    // 显示成功消息
+    ElMessage.success('团队排行榜数据已刷新');
+  } catch (error) {
+    console.error('刷新团队排行榜失败:', error);
+    ElMessage.error('刷新团队排行榜失败');
+  } finally {
+    teamRefreshing.value = false;
+  }
+};
+
+// 刷新所有数据
+const refreshAllData = async () => {
+  personalRefreshing.value = true;
+  teamRefreshing.value = true;
+  
+  try {
+    // 并行刷新所有数据
+    await Promise.all([
+      refreshPersonalRanking(),
+      refreshTeamRanking(),
+      loadProjectProgress()
+    ]);
+    
+    ElMessage.success('所有数据已刷新');
+  } catch (error) {
+    console.error('刷新数据失败:', error);
+    ElMessage.error('刷新数据失败');
+  } finally {
+    personalRefreshing.value = false;
+    teamRefreshing.value = false;
+  }
+};
+
+// 自动刷新相关
+const autoRefreshInterval = ref<NodeJS.Timeout | null>(null);
+const autoRefreshEnabled = ref(false);
+const autoRefreshSeconds = ref(60); // 默认60秒自动刷新
+
+// 开启自动刷新
+const startAutoRefresh = () => {
+  if (autoRefreshInterval.value) {
+    clearInterval(autoRefreshInterval.value);
+  }
+  
+  autoRefreshInterval.value = setInterval(() => {
+    refreshAllData();
+  }, autoRefreshSeconds.value * 1000);
+  
+  autoRefreshEnabled.value = true;
+  ElMessage.success(`已开启自动刷新,每${autoRefreshSeconds.value}秒刷新一次`);
+};
+
+// 停止自动刷新
+const stopAutoRefresh = () => {
+  if (autoRefreshInterval.value) {
+    clearInterval(autoRefreshInterval.value);
+    autoRefreshInterval.value = null;
+  }
+  
+  autoRefreshEnabled.value = false;
+  ElMessage.info('已停止自动刷新');
+};
+
+// 组件卸载时清理定时器
+onUnmounted(() => {
+  if (autoRefreshInterval.value) {
+    clearInterval(autoRefreshInterval.value);
+  }
+});
+
 onMounted(async () => {
   try{
     const res = await listScoreRanking(defaultEventInfo.value.eventId.toString());
@@ -399,6 +614,8 @@ onMounted(async () => {
     await loadRankGroupOptions();
     // 加载项目进度信息
     await loadProjectProgress();
+    // 开启自动刷新
+    startAutoRefresh();
   } catch (error) {
     console.error('加载数据失败:', error);
   }
@@ -490,6 +707,7 @@ onMounted(async () => {
 .item-type,
 .item-rank,
 .item-team-name,
+.item-group,
 .item-score {
   flex: 1;
   text-align: center;
@@ -543,4 +761,223 @@ onMounted(async () => {
   background-color: #f6ffed;
   color: #52c41a;
 }
+
+/* 标题行样式 */
+.ranking-header {
+  padding: 12px 0;
+  border-bottom: 2px solid #e0e0e0;
+  background-color: #f8f9fa;
+  font-weight: bold;
+  color: #333;
+  position: sticky;
+  top: 0;
+  z-index: 10;
+}
+
+.header-content {
+  display: flex;
+  justify-content: space-between;
+}
+
+/* 个人积分排行榜标题样式 */
+.header-rank,
+.header-team,
+.header-score {
+  flex: 1;
+  text-align: center;
+  font-size: 14px;
+}
+
+.header-name {
+  flex: 2;
+  text-align: left;
+  font-size: 14px;
+}
+
+/* 团队积分排行榜标题样式 */
+.header-team-name {
+  flex: 2;
+  text-align: left;
+  font-size: 14px;
+}
+
+.header-group {
+  flex: 1;
+  text-align: center;
+  font-size: 14px;
+}
+
+/* 数据行样式 */
+.item-content {
+  display: flex;
+  justify-content: space-between;
+}
+
+/* 个人积分排行榜数据样式 */
+.item-name {
+  flex: 2;
+  text-align: left;
+}
+
+.item-team,
+.item-time,
+.item-type,
+.item-rank {
+  flex: 1;
+  text-align: center;
+}
+
+/* 团队积分排行榜数据样式 */
+.item-team-name {
+  flex: 2;
+  text-align: left;
+}
+
+.item-rank,
+.item-group,
+.item-score {
+  flex: 1;
+  text-align: center;
+}
+
+/* 团队积分排行榜特殊样式优化 */
+.ranking-item {
+  padding: 10px 0;
+  border-bottom: 1px solid #f0f0f0;
+  transition: background-color 0.2s;
+}
+
+.ranking-item:hover {
+  background-color: #f8f9fa;
+}
+
+.ranking-item:last-child {
+  border-bottom: none;
+}
+
+/* 组别列样式优化 */
+.item-group {
+  color: #666;
+  font-size: 13px;
+}
+
+/* 总分列样式优化 */
+.item-score {
+  font-weight: 500;
+  /* color: #1890ff; */
+}
+
+/* 个人排行榜头部样式 */
+.personal-ranking-header {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  padding: 8px 0;
+}
+
+/* 团队排行榜头部样式 */
+.team-ranking-header {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  padding: 8px 0;
+}
+
+/* 标题样式 */
+.header-title {
+  text-align: center;
+  font-weight: bold;
+  font-size: 16px;
+  color: black;
+  margin-bottom: 4px;
+}
+
+/* 控件区域样式 */
+.header-controls {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 15px;
+  flex-wrap: wrap;
+  min-height: 32px;
+}
+
+/* 显示数量控制样式 */
+.display-count-control {
+  display: flex;
+  align-items: center;
+  white-space: nowrap;
+  background-color: #f8f9fa;
+  padding: 4px 8px;
+  border-radius: 4px;
+  border: 1px solid #e9ecef;
+}
+
+/* 分组选择框样式 */
+.group-select {
+  width: 120px;
+}
+
+/* 刷新按钮样式 */
+.refresh-btn {
+  min-width: 60px;
+  height: 28px;
+}
+
+.refresh-btn .el-icon {
+  margin-right: 4px;
+}
+
+.control-label {
+  font-size: 12px;
+  color: #666;
+  margin: 0 2px;
+  font-weight: 500;
+}
+
+/* 响应式设计 */
+@media (max-width: 1200px) {
+  .header-controls {
+    flex-direction: column;
+    gap: 10px;
+    align-items: center;
+  }
+  
+  .display-count-control {
+    order: 1;
+  }
+  
+  .group-select {
+    order: 2;
+  }
+  
+  .refresh-btn {
+    order: 3;
+  }
+}
+
+@media (max-width: 768px) {
+  .header-title {
+    font-size: 14px;
+  }
+  
+  .header-controls {
+    gap: 8px;
+  }
+  
+  .group-select {
+    width: 100%;
+    max-width: 200px;
+  }
+}
+
+/* 输入框样式优化 */
+.display-count-control .el-input-number {
+  --el-input-number-controls-width: 20px;
+}
+
+.display-count-control .el-input-number .el-input__inner {
+  text-align: center;
+  padding: 0 5px;
+}
 </style>

+ 2 - 2
src/views/system/gameEvent/edit.vue

@@ -310,7 +310,7 @@ const basicFormRef = ref<ElFormInstance>();
 const basicForm = ref<GameEventForm>({
   eventCode: '',
   eventName: '',
-  eventType: '',
+  eventType: '11',
   location: '',
   purpose: '',
   startTime: '',
@@ -350,7 +350,7 @@ onMounted(async () => {
     basicForm.value = {
       eventCode: '',
       eventName: '',
-      eventType: '',
+      eventType: '11',
       location: '',
       purpose: '',
       startTime: '',