Browse Source

feat(system): 增加项目进度展示功能

- 新增项目进度视图对象和组别进度视图对象
- 实现项目进度数据加载和展示逻辑
- 优化排名显示和项目类型显示
- 调整组别详情展示样式
- 修复项目进度相关页面的样式问题
zhou 2 days ago
parent
commit
c707c3b9e8

+ 10 - 0
src/api/system/gameEvent/projectProgress.ts

@@ -0,0 +1,10 @@
+import request from '@/utils/request';
+import { ProjectProgressVo } from './types';
+
+// 获取赛事项目进度信息
+export function getProjectProgress(eventId: string | number) {
+  return request({
+    url: `/system/gameEvent/projectProgress/${eventId}`,
+    method: 'get'
+  });
+} 

+ 42 - 0
src/api/system/gameEvent/types.ts

@@ -240,3 +240,45 @@ export interface GameEventQuery extends PageQuery {
    */
   params?: any;
 }
+
+// 项目进度视图对象
+export interface ProjectProgressVo {
+  /** 项目ID */
+  projectId: number;
+  /** 项目名称 */
+  projectName: string;
+  /** 项目类型 */
+  projectType: string;
+  /** 归类(0个人项目/1团体项目) */
+  classification: string;
+  /** 比赛场地 */
+  location: string;
+  /** 项目开始时间 */
+  startTime: string;
+  /** 项目结束时间 */
+  endTime: string;
+  /** 状态(0未开始 1进行中 2已完成) */
+  status: string;
+  /** 状态描述 */
+  statusText: string;
+  /** 组别信息列表 */
+  groups: GroupProgressVo[];
+  /** 项目进度百分比 */
+  progressPercentage: number;
+}
+
+// 组别进度视图对象
+export interface GroupProgressVo {
+  /** 组别ID */
+  groupId: number;
+  /** 组别名称 */
+  groupName: string;
+  /** 组别开始时间 */
+  beginTime: string;
+  /** 组别结束时间 */
+  endTime: string;
+  /** 状态(0未开始 1进行中 2已完成) */
+  status: string;
+  /** 状态描述 */
+  statusText: string;
+}

+ 225 - 25
src/views/system/gameEvent/RankingBoardPage.vue

@@ -16,6 +16,7 @@
           <div class="ranking-list ranking-list-full">
             <div v-for="(item, index) in athleteScoreList" :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-team">{{ item.teamName }}</span>
                 <span class="item-time">{{ item.totalScore }}</span>
@@ -34,7 +35,7 @@
           <div class="ranking-list ranking-list-full">
             <div v-for="(item, index) in teamScores" :key="index" class="ranking-item">
               <div class="item-content">
-                <span class="item-rank">第{{ item.rank }}名</span>
+                <span class="item-rank">{{ getRankDisplay(item, index, teamScores) }}</span>
                 <span class="item-team-name">{{ item.teamName }}</span>
                 <span class="item-score">{{ item.score }}分</span>
               </div>
@@ -56,9 +57,17 @@
           <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.name }}</span>
-                <span class="item-type">{{ item.type }}</span>
-                <span class="item-time">{{ item.time }}</span>
+                <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>
               </div>
             </div>
           </div>
@@ -73,8 +82,10 @@ import { ref, computed, onMounted } from 'vue';
 import { listScoreRanking, listPersonalRanking, listTeamRanking } from '@/api/system/gameEvent/eventRank';
 import { listGameScore } from '@/api/system/gameScore';
 import { listGameTeam } from '@/api/system/gameTeam';
+import { getProjectProgress } from '@/api/system/gameEvent/projectProgress';
 import { useRouter,useRoute } from 'vue-router';
 import { GameTeamVO } from '@/api/system/gameTeam/types';
+import { ProjectProgressVo, GroupProgressVo } from '@/api/system/gameEvent/types';
 
 // 定义队伍积分排行榜的数据结构
 interface TeamScore {
@@ -84,27 +95,19 @@ interface TeamScore {
   rank: number;
 }
 
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { game_project_type } = toRefs<any>(proxy?.useDict('game_project_type'));
+
 const route = useRoute();
 const eventId = route.params.eventId as string;
 
 const athleteScoreList = ref([]);
 
-const completedTasks = ref(22);
-const totalTasks = ref(33);
-const progressPercentage = computed(() => (completedTasks.value / totalTasks.value) * 100);
-
-const projectProgress = ref([
-  { name: '全部', type: '', time: '' },
-  { name: '砥砺前行', type: '团体趣味u竞赛', time: '16:24' },
-  { name: '同舟共济', type: '团体趣味u竞赛', time: '16:26' },
-  { name: '跳绳', type: '个人趣味', time: '13:25' },
-  { name: '托球跑', type: '个人趣味', time: '15:08' },
-  { name: '100m', type: '田径丙组', time: '10:18' },
-  { name: '200m', type: '田径丙组', time: '10:24' },
-  { name: '400m', type: '田径丙组', time: '09:26' },
-  { name: '800m', type: '田径丙组', time: '10:28' },
-  { name: '1500m', type: '田径丙组', time: '10:17' }
-]);
+const completedTasks = ref(0);
+const totalTasks = ref(0);
+const progressPercentage = computed(() => totalTasks.value > 0 ? (completedTasks.value / totalTasks.value) * 100 : 0);
+
+const projectProgress = ref<ProjectProgressVo[]>([]);
 
 const teamScores = ref<TeamScore[]>([]);
 
@@ -156,14 +159,155 @@ const loadTeamScores = async () => {
   // 按积分从高到低排序
   teamScoreList.sort((a, b) => b.score - a.score);
   
-  // 添加排名
-  teamScoreList.forEach((team, index) => {
-    team.rank = index + 1;
-  });
+  // 添加排名(支持并列排名)
+  let currentRank = 1;
+  for (let i = 0; i < teamScoreList.length; i++) {
+    const team = teamScoreList[i];
+    const currentScore = team.score || 0;
+    
+    // 如果不是第一个,检查是否与前一个积分相同
+    if (i > 0) {
+      const previousScore = teamScoreList[i - 1].score || 0;
+      if (currentScore !== previousScore) {
+        currentRank = i + 1;
+      }
+    }
+    
+    team.rank = currentRank;
+  }
   
   teamScores.value = teamScoreList;
 };
 
+// 加载项目进度信息
+const loadProjectProgress = async () => {
+  try {
+    const res = await getProjectProgress(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;
+    
+    // 前端也可以做一次排序确保,虽然后端已经排序了
+    // 按完整时间排序(项目时间或最早组别时间)
+    projectProgress.value.sort((a, b) => {
+      const getEarliestTime = (project: ProjectProgressVo) => {
+        if (project.groups && project.groups.length > 0) {
+          // 有组别,找到最早的组别时间
+          const groupTimes = project.groups
+            .map(g => g.beginTime ? new Date(g.beginTime).getTime() : 0)
+            .filter(time => time > 0);
+          if (groupTimes.length > 0) {
+            return Math.min(...groupTimes);
+          }
+        }
+        // 没有组别或组别时间无效,使用项目时间
+        return project.startTime ? new Date(project.startTime).getTime() : 0;
+      };
+      
+      const aTime = getEarliestTime(a);
+      const bTime = getEarliestTime(b);
+      
+      return aTime - bTime;
+    });
+  } catch (error) {
+    console.error('加载项目进度失败:', error);
+  }
+};
+
+// 获取项目类型文本
+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 formatTime = (timeStr: string) => {
+  if (!timeStr) return '-';
+  try {
+    const date = new Date(timeStr);
+    // 检查是否是有效日期
+    if (isNaN(date.getTime())) {
+      return timeStr;
+    }
+    return date.toLocaleString('zh-CN', { 
+      month: '2-digit',
+      day: '2-digit',
+      hour: '2-digit', 
+      minute: '2-digit',
+      hour12: false 
+    });
+  } catch (error) {
+    return timeStr;
+  }
+};
+
+// 格式化时间显示(仅时间,用于组别详情)
+const formatTimeOnly = (timeStr: string) => {
+  if (!timeStr) return '-';
+  try {
+    const date = new Date(timeStr);
+    // 检查是否是有效日期
+    if (isNaN(date.getTime())) {
+      return timeStr;
+    }
+    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 currentScore = item.totalScore || 0;
+  const higherScores = new Set();
+  
+  for (let i = 0; i < list.length; i++) {
+    if (list[i].totalScore > currentScore) {
+      higherScores.add(list[i].totalScore);
+    }
+  }
+  
+  const actualRank = higherScores.size + 1;
+  return `第${actualRank}名`;
+};
+
 onMounted(async () => {
   const res = await listScoreRanking(eventId);
   // 按照totalScore字段降序排序(分数高的在前面)
@@ -171,6 +315,9 @@ onMounted(async () => {
   
   // 加载队伍积分排行榜
   await loadTeamScores();
+  
+  // 加载项目进度信息
+  await loadProjectProgress();
 });
 </script>
 
@@ -242,7 +389,11 @@ onMounted(async () => {
   justify-content: space-between;
 }
 
-.item-name,
+.item-name {
+  flex: 2;
+  text-align: left;
+}
+
 .item-team,
 .item-time,
 .item-type,
@@ -252,4 +403,53 @@ onMounted(async () => {
   flex: 1;
   text-align: center;
 }
+
+/* 组别详情样式 */
+.group-details {
+  margin-top: 8px;
+  padding-left: 20px;
+  border-left: 2px solid #e0e0e0;
+}
+
+.group-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 4px 0;
+  font-size: 12px;
+  color: #666;
+}
+
+.group-name {
+  flex: 3;
+  text-align: left;
+}
+
+.group-time {
+  flex: 2;
+  text-align: center;
+}
+
+.group-status {
+  flex: 1;
+  text-align: center;
+  padding: 2px 6px;
+  border-radius: 10px;
+  font-size: 10px;
+}
+
+.status-0 {
+  background-color: #f0f0f0;
+  color: #999;
+}
+
+.status-1 {
+  background-color: #e6f7ff;
+  color: #1890ff;
+}
+
+.status-2 {
+  background-color: #f6ffed;
+  color: #52c41a;
+}
 </style>

+ 8 - 1
src/views/system/gameEventGroup/detail.vue

@@ -95,9 +95,13 @@
           <div class="text-2xl font-bold text-purple-600">{{ groupInfo.trackNum }}</div>
           <div class="text-sm text-gray-600">道数</div>
         </div>
-        <div class="text-center p-3 bg-orange-50 rounded-lg">
+        <!-- <div class="text-center p-3 bg-orange-50 rounded-lg">
           <div class="text-2xl font-bold text-orange-600">{{ groupInfo.personNum }}</div>
           <div class="text-sm text-gray-600">{{ groupInfo.groupName }}人数</div>
+        </div> -->
+        <div class="text-center p-3 bg-orange-50 rounded-lg">
+          <div class="text-2xl font-bold text-orange-600">{{ roundType }}</div>
+          <div class="text-sm text-gray-600">录取人数</div>
         </div>
       </div>
     </el-card>
@@ -152,10 +156,13 @@ const groupResult = ref<Map<string, GameAthleteVO>>(new Map());
 const loading = ref(false);
 // 生成分组状态
 const generating = ref(false);
+
+const roundType = ref();
 // 项目名称(需要从项目信息中获取)
 const projectName = computed(() => {
   if (!groupInfo.value.projectId) return '';
   const project = projects.value.find(p => p.projectId === groupInfo.value.projectId);
+  roundType.value = project?.roundType || 0;
   return project?.projectName || '';
 });
 

+ 39 - 12
src/views/system/gameEventGroup/index.vue

@@ -202,13 +202,15 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="组别开始时间" prop="beginTime">
-              <el-time-picker
+              <el-date-picker
                 v-model="form.beginTime"
+                type="datetime"
                 placeholder="选择开始时间"
-                format="HH:mm"
-                value-format="HH:mm"
+                format="YYYY-MM-DD HH:mm"
+                value-format="YYYY-MM-DD HH:mm:ss"
                 style="width: 100%"
                 :disabled="!form.projectId"
+                :disabled-date="disabledDate"
               />
             </el-form-item>
           </el-col>
@@ -365,13 +367,32 @@ const calculatedEndTime = computed(() => {
     return '';
   }
   
-  const beginTime = new Date(`2000-01-01 ${form.value.beginTime}`);
+  const beginTime = new Date(form.value.beginTime);
   const totalMinutes = form.value.duration * form.value.includeGroupNum;
   const endTime = new Date(beginTime.getTime() + totalMinutes * 60 * 1000);
   
-  return endTime.toTimeString().slice(0, 5); // 返回 HH:mm 格式
+  return endTime.toLocaleString('zh-CN', {
+    month: '2-digit',
+    day: '2-digit',
+    hour: '2-digit',
+    minute: '2-digit',
+    hour12: false
+  });
 });
 
+// 禁用日期(限制在项目时间范围内)
+const disabledDate = (time: Date) => {
+  if (!form.value.projectId) return false;
+  const project = projectList.value.find(p => p.projectId === form.value.projectId);
+  if (!project || !project.startTime || !project.endTime) return false;
+  
+  const projectStart = new Date(project.startTime);
+  const projectEnd = new Date(project.endTime);
+  
+  // 禁用项目时间范围外的日期
+  return time < projectStart || time > projectEnd;
+};
+
 /** 查询赛事分组列表 */
 const getList = async () => {
   loading.value = true;
@@ -405,6 +426,9 @@ const handleFormProjectTypeFilterChange = () => {
   form.value.trackNum = undefined;
   form.value.fieldNum = undefined;
   form.value.duration = undefined;
+  
+  // 重新获取项目列表以更新过滤
+  getProjectList();
 };
 
 // 表单中项目变化
@@ -412,7 +436,10 @@ const handleFormProjectChange = () => {
   if (form.value.projectId) {
     const selectedProject = projectList.value.find(p => p.projectId === form.value.projectId);
     if (selectedProject) {
-      // 可以在这里设置一些默认值或者进行其他处理
+      // 自动设置默认开始时间为项目开始时间
+      if (selectedProject.startTime && !form.value.beginTime) {
+        form.value.beginTime = selectedProject.startTime;
+      }
       console.log('选中的项目:', selectedProject);
     }
   }
@@ -501,16 +528,16 @@ const submitForm = () => {
 
       // 自动计算结束时间
       if (form.value.beginTime && form.value.duration && form.value.includeGroupNum) {
-        const beginTime = new Date(`2000-01-01 ${form.value.beginTime}`);
+        const beginTime = new Date(form.value.beginTime);
         const totalMinutes = form.value.duration * form.value.includeGroupNum;
         const endTime = new Date(beginTime.getTime() + totalMinutes * 60 * 1000);
-        form.value.endTime = endTime.toTimeString().slice(0, 5);
+        form.value.endTime = endTime.toISOString().slice(0, 19).replace('T', ' ');
       }
 
       // 验证时间范围
       if (form.value.beginTime && form.value.endTime) {
-        const beginTime = new Date(`2000-01-01 ${form.value.beginTime}`);
-        const endTime = new Date(`2000-01-01 ${form.value.endTime}`);
+        const beginTime = new Date(form.value.beginTime);
+        const endTime = new Date(form.value.endTime);
         
         if (beginTime >= endTime) {
           proxy?.$modal.msgError('组别结束时间必须晚于开始时间');
@@ -521,8 +548,8 @@ const submitForm = () => {
         if (form.value.projectId) {
           const selectedProject = projectList.value.find(p => p.projectId === form.value.projectId);
           if (selectedProject && selectedProject.startTime && selectedProject.endTime) {
-            const projectStart = new Date(`2000-01-01 ${selectedProject.startTime}`);
-            const projectEnd = new Date(`2000-01-01 ${selectedProject.endTime}`);
+            const projectStart = new Date(selectedProject.startTime);
+            const projectEnd = new Date(selectedProject.endTime);
             
             if (beginTime < projectStart || endTime > projectEnd) {
               proxy?.$modal.msgError('组别比赛时间必须在项目比赛时间范围内');

+ 9 - 9
src/views/system/gameEventProject/index.vue

@@ -89,13 +89,13 @@
         </el-table-column>
         <!-- <el-table-column label="参赛组数" align="center" prop="groupNum" />
         <el-table-column label="参赛人数" align="center" prop="participateNum" /> -->
-        <el-table-column label="轮次" align="center" prop="roundType" v-if="columns[9].visible" />
-        <el-table-column label="排序方式" align="center" prop="orderType" v-if="columns[10].visible">
+        <el-table-column label="录取名次" align="center" prop="roundType" v-if="columns[9].visible" />
+        <el-table-column label="积分分值" align="center" prop="scoreValue" v-if="columns[10].visible" />
+        <el-table-column label="排序方式" align="center" prop="orderType" v-if="columns[11].visible">
           <template #default="scope">
             {{ scope.row.orderType === '0' ? '升序' : '降序' }}
           </template>
         </el-table-column>
-        <el-table-column label="积分分值" align="center" prop="scoreValue" v-if="columns[11].visible" />
         <!-- <el-table-column label="奖项" align="center" prop="award" />
         <el-table-column label="计时点名称" align="center" prop="timePoint" />
         <el-table-column label="控制盒编号" align="center" prop="boxCode" />
@@ -157,12 +157,12 @@
             <el-radio value="1">降序</el-radio>
           </el-radio-group>
         </el-form-item>
+        <el-form-item label="录取名次" prop="roundType">
+          <el-input v-model="form.roundType" placeholder="请输入录取名次" />
+        </el-form-item>
         <el-form-item label="积分分值" prop="scoreValue">
           <el-input v-model="form.scoreValue" placeholder="请输入积分分值" />
         </el-form-item>
-        <el-form-item label="轮次" prop="roundType">
-          <el-input v-model="form.roundType" placeholder="请输入轮次" />
-        </el-form-item>
         <el-form-item label="开始时间" prop="startTime">
           <el-date-picker clearable v-model="form.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择开始时间">
           </el-date-picker>
@@ -249,9 +249,9 @@ const columns = ref<FieldOption[]>([
   { key: 6, label: '开始时间', visible: true },
   { key: 7, label: '结束时间', visible: true },
   { key: 8, label: '裁判组', visible: true },
-  { key: 9, label: '次', visible: true },
-  { key: 10, label: '排序方式', visible: true },
-  { key: 11, label: '积分分值', visible: true },
+  { key: 9, label: '录取名次', visible: true },
+  { key: 10, label: '积分分值', visible: true },
+  { key: 11, label: '排序方式', visible: true },
 ]);
 
 const initFormData: GameEventProjectForm = {

+ 4 - 4
src/views/system/gameEventSchedule/index.vue

@@ -265,7 +265,7 @@ const saveSchedule = async () => {
         eventId: defaultEvent.value.eventId,
         projectName: project.projectName,
         projectType: project.projectType,
-        groupType: project.groupType
+        // groupType: project.groupType
       } as any);
     }
 
@@ -297,7 +297,7 @@ const deleteSchedule = async (project: GameEventProjectVO) => {
       eventId: defaultEvent.value.eventId,
       projectName: project.projectName,
       projectType: project.projectType,
-      groupType: project.groupType
+      // groupType: project.groupType
     } as any);
 
     // 更新本地数据
@@ -332,7 +332,7 @@ const exportSchedule = () => {
   }
 
   // 创建表格数据
-  const headers = ['日期', '时间', '项目名称', '项目类型', '项目组别', '比赛场地', '分组数', '每组人数'];
+  const headers = ['日期', '时间', '项目名称', '项目类型', '比赛场地', '分组数', '每组人数'];
   let csvContent = headers.join(',') + '\n';
 
   scheduleData.value.forEach((item) => {
@@ -341,7 +341,7 @@ const exportSchedule = () => {
       `${item.startTime}-${item.endTime}`,
       item.projectName,
       item.projectType === '0' ? '个人' : '团体',
-      item.groupType,
+      // item.groupType,
       item.location,
       item.groupNum,
       item.participateNum

+ 4 - 7
src/views/system/gameScore/index.vue

@@ -467,13 +467,10 @@ const formatScore = (score: number | string) => {
  * 获取项目类型名称
  */
 const getProjectTypeName = (type: string) => {
-  const typeMap: Record<string, string> = {
-    '1': '田径',
-    '2': '游泳', 
-    '3': '球类',
-    '4': '其他'
-  };
-  return typeMap[type] || '未知';
+  if (!game_project_type.value || !type) return '未知';
+  
+  const typeItem = game_project_type.value.find((item: any) => item.value === type);
+  return typeItem ? typeItem.label : '未知';
 };
 
 const exportScoresNames = () => {

+ 7 - 7
src/views/system/gameScore/print.vue

@@ -58,6 +58,9 @@ import { getGameEventProject } from '@/api/system/gameEventProject'
 import { GameScoreVO } from '@/api/system/gameScore/types'
 import { GameEventProjectVO } from '@/api/system/gameEventProject/types'
 
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { game_project_type } = toRefs<any>(proxy?.useDict('game_project_type'));
+
 const route = useRoute()
 const router = useRouter()
 
@@ -133,13 +136,10 @@ const loadProjectScores = async () => {
 
 // 获取项目类型名称
 const getProjectTypeName = (type: string) => {
-  const typeMap: Record<string, string> = {
-    '1': '田径',
-    '2': '游泳',
-    '3': '球类',
-    '4': '其他'
-  }
-  return typeMap[type] || '未知'
+  if (!game_project_type.value || !type) return '未知';
+  
+  const typeItem = game_project_type.value.find((item: any) => item.value === type);
+  return typeItem ? typeItem.label : '未知';
 }
 
 // 获取分组类型名称