4 次代码提交 16e45aec01 ... 3a570ceed9

作者 SHA1 备注 提交日期
  wenkai 3a570ceed9 feat:修改字体 6 天之前
  wenkai d89e0389c1 Merge branch 'dev' into wk-dev 6 天之前
  wenkai 94ada099a7 feat:导出号码布zip、生成二维码、增加权限校验 6 天之前
  zhou d6383cc347 feat(gameScore): 重构成绩管理功能 6 天之前

+ 1 - 0
.gitignore

@@ -12,6 +12,7 @@ tests/e2e/reports
 selenium-debug.log
 
 # Editor directories and files
+.md
 .idea
 .vscode
 *.suo

+ 6 - 6
src/api/system/gameEventProject/types.ts

@@ -19,9 +19,9 @@ export interface GameEventProjectVO {
   projectType: string;
 
   /**
-   * 项目组别
+   * 归类(0个人项目/1团体项目)
    */
-  groupType: string;
+  classification: string;
 
   /**
    * 裁判组员
@@ -125,9 +125,9 @@ export interface GameEventProjectForm extends BaseEntity {
   projectType?: string;
 
   /**
-   * 项目组别
+   * 归类(0个人项目/1团体项目)
    */
-  groupType?: string;
+  classification?: string;
 
   /**
    * 裁判组员
@@ -237,9 +237,9 @@ export interface GameEventProjectQuery extends PageQuery {
   projectType?: string;
 
   /**
-   * 项目组别
+   * 归类(0个人项目/1团体项目)
    */
-  groupType?: string;
+  classification?: string;
 
   /**
    * 开始时间

+ 61 - 0
src/api/system/gameScore/index.ts

@@ -27,6 +27,19 @@ export const getGameScore = (scoreId: string | number): AxiosPromise<GameScoreVO
   });
 };
 
+/**
+ * 根据运动员ID和项目ID查询成绩
+ * @param athleteId
+ * @param projectId
+ */
+export const getScoreByAthleteIdAndProjectId = (athleteId: string | number, projectId: string | number): AxiosPromise<GameScoreVO> => {
+  return request({
+    url: '/system/gameScore/getScoreByAthleteIdAndProjectId',
+    method: 'get',
+    params: { athleteId, projectId }
+  });
+};
+
 /**
  * 查询项目成绩详细
  * @param projectId
@@ -72,3 +85,51 @@ export const delGameScore = (scoreId: string | number | Array<string | number>)
     method: 'delete'
   });
 };
+
+/**
+ * 获取项目成绩综合数据(包含运动员/队伍信息)
+ * @param eventId 赛事ID
+ * @param projectId 项目ID
+ * @param classification 项目分类
+ * @param searchValue 搜索值
+ * @param pageQuery 分页参数
+ */
+export const getProjectScoreData = (params: {
+  eventId: string | number;
+  projectId: string | number;
+  classification: string;
+  searchValue?: string;
+  pageNum?: number;
+  pageSize?: number;
+}) => {
+  return request({
+    url: '/system/gameScore/getProjectScoreData',
+    method: 'get',
+    params
+  });
+};
+
+/**
+ * 更新成绩并重新计算排名积分
+ * @param data 成绩数据
+ */
+export const updateScoreAndRecalculate = (data: GameScoreForm) => {
+  return request({
+    url: '/system/gameScore/updateScoreAndRecalculate',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 重新计算项目排名和积分
+ * @param eventId 赛事ID
+ * @param projectId 项目ID
+ */
+export const recalculateRankingsAndPoints = (eventId: string | number, projectId: string | number) => {
+  return request({
+    url: '/system/gameScore/recalculateRankingsAndPoints',
+    method: 'post',
+    params: { eventId, projectId }
+  });
+};

+ 14 - 4
src/api/system/gameScore/types.ts

@@ -24,9 +24,14 @@ export interface GameScoreVO {
   teamId: string | number;
 
   /**
-   * 成绩
+   * 个人成绩
    */
-  scoreValue: number;
+  individualPerformance: number;
+
+  /**
+   * 团队成绩
+   */
+  teamPerformance: number;
 
   /**
    * 成绩类型
@@ -107,9 +112,14 @@ export interface GameScoreForm extends BaseEntity {
   teamId?: string | number;
 
   /**
-   * 成绩值
+   * 个人成绩
+   */
+  individualPerformance?: number;
+
+  /**
+   * 团队成绩
    */
-  scoreValue?: number;
+  teamPerformance?: number;
 
   /**
    * 成绩类型

+ 1 - 1
src/router/index.ts

@@ -138,7 +138,7 @@ export const constantRoutes: RouteRecordRaw[] = [
     hidden: true,
     children: [
       {
-        path: 'edit/:projectId/:projectName/:projectType/:eventId',
+        path: 'edit/:projectId/:projectName/:projectType/:eventId/:classification',
         component: () => import('@/views/system/gameScore/gameScoreEdit.vue'),
         name: 'GameScoreEdit',
         meta: { title: '修改成绩', icon: 'form' }

+ 51 - 53
src/views/system/gameEvent/index.vue

@@ -356,6 +356,7 @@
                     <i-ep-upload-filled />
                   </el-icon>
                   <div class="el-upload__text">拖拽背景图片到此处,或<em>点击上传</em></div>
+                  <div class="el-upload__tip">建议尺寸:842×595px (横向A4比例)</div>
                 </el-upload>
               </el-form-item>
 
@@ -365,17 +366,18 @@
                     <i-ep-upload-filled />
                   </el-icon>
                   <div class="el-upload__text">拖拽Logo图片到此处,或<em>点击上传</em></div>
+                  <div class="el-upload__tip">建议尺寸:80×80px</div>
                 </el-upload>
               </el-form-item>
 
               <el-form-item label="字体设置">
-                <div style="display: flex; gap: 15px; align-items: center;">
-                  <el-select v-model="bibForm.fontName" placeholder="字体" style="width: 100px;">
+                <div style="display: flex; gap: 15px; align-items: center">
+                  <el-select v-model="bibForm.fontName" placeholder="字体" style="width: 100px">
                     <el-option label="黑体" value="simhei"></el-option>
                     <el-option label="宋体" value="simsun"></el-option>
-                    <el-option label="微软雅黑" value="microsoft-yahei"></el-option>
+                    <el-option label="微软雅黑" value="yahei"></el-option>
                   </el-select>
-                  <el-input-number v-model="bibForm.fontSize" :min="38" :max="198" placeholder="字体大小" style="width: 140px;"></el-input-number>
+                  <el-input-number v-model="bibForm.fontSize" :min="38" :max="198" placeholder="字体大小" style="width: 140px"></el-input-number>
                   <el-color-picker v-model="bibForm.fontColor" @change="handleFontColorChange"></el-color-picker>
                 </div>
               </el-form-item>
@@ -408,26 +410,32 @@
                   }"
                   @mousedown="startDrag($event, 'barcode')"
                 >
-                  <svg width="100" height="30" viewBox="0 0 100 30">
-                    <rect x="0" y="0" width="2" height="30" fill="black" />
-                    <rect x="4" y="0" width="1" height="30" fill="black" />
-                    <rect x="7" y="0" width="3" height="30" fill="black" />
-                    <rect x="12" y="0" width="1" height="30" fill="black" />
-                    <rect x="15" y="0" width="2" height="30" fill="black" />
-                    <rect x="19" y="0" width="1" height="30" fill="black" />
-                    <rect x="22" y="0" width="3" height="30" fill="black" />
-                    <rect x="27" y="0" width="2" height="30" fill="black" />
-                    <rect x="31" y="0" width="1" height="30" fill="black" />
-                    <rect x="34" y="0" width="2" height="30" fill="black" />
-                    <rect x="37" y="0" width="2" height="30" fill="black" />
-                    <rect x="42" y="0" width="2" height="30" fill="black" />
-                    <rect x="46" y="0" width="2" height="30" fill="black" />
-                    <rect x="49" y="0" width="2" height="30" fill="black" />
-                    <rect x="50" y="0" width="2" height="30" fill="black" />
-                    <rect x="52" y="0" width="2" height="30" fill="black" />
-                    <rect x="54" y="0" width="2" height="30" fill="black" />
-                    <rect x="58" y="0" width="2" height="30" fill="black" />
-                    <rect x="60" y="0" width="2" height="30" fill="black" />
+                  <svg
+                    t="1755833734016"
+                    class="icon"
+                    viewBox="0 0 1024 1024"
+                    version="1.1"
+                    xmlns="http://www.w3.org/2000/svg"
+                    p-id="2399"
+                    width="32"
+                    height="32"
+                  >
+                    <path
+                      d="M540.9 866h59v59h-59v-59zM422.8 423.1V98.4H98.1v324.8h59v59h59v-59h206.7z m-265.7-59V157.4h206.7v206.7H157.1z m0 0"
+                      p-id="2400"
+                    ></path>
+                    <path
+                      d="M216.2 216.4h88.6V305h-88.6v-88.6zM600 98.4v324.8h324.8V98.4H600z m265.7 265.7H659V157.4h206.7v206.7z m0 0"
+                      p-id="2401"
+                    ></path>
+                    <path
+                      d="M718.1 216.4h88.6V305h-88.6v-88.6zM216.2 718.3h88.6v88.6h-88.6v-88.6zM98.1 482.2h59v59h-59v-59z m118.1 0h59.1v59h-59.1v-59z m0 0"
+                      p-id="2402"
+                    ></path>
+                    <path
+                      d="M275.2 600.2H98.1V925h324.8V600.2h-88.6v-59h-59v59z m88.6 59.1V866H157.1V659.3h206.7z m118.1-531.4h59v88.6h-59v-88.6z m0 147.6h59v59h-59v-59zM659 482.2H540.9v-88.6h-59v88.6H334.3v59H600v59h59v-118z m0 118h59.1v59H659v-59z m-177.1 0h59v88.6h-59v-88.6z m0 147.7h59V866h-59V747.9zM600 688.8h59V866h-59V688.8z m177.1-88.6h147.6v59H777.1v-59z m88.6-118h59v59h-59v-59z m-147.6 0h118.1v59H718.1v-59z m0 206.6h59v59h-59v-59z m147.6 59.1h-29.5v59h59v-59h29.5v-59h-59v59z m-147.6 59h59V866h-59v-59.1z m59 59.1h147.6v59H777.1v-59z m0 0"
+                      p-id="2403"
+                    ></path>
                   </svg>
                 </div>
 
@@ -435,9 +443,9 @@
                 <div
                   class="event-name-preview"
                   :style="{
-                    fontSize: Math.round(bibForm.fontSize * 0.8) + 'px',
-                    color: bibForm.fontColorHex,
-                    fontFamily: bibForm.fontName
+                    fontSize: 32 + 'px',
+                    color: 'black',
+                    fontFamily: '黑体'
                   }"
                 >
                   赛事名称
@@ -1125,7 +1133,6 @@ const dragState = reactive({
 
 // 生成参赛证按钮处理
 const handleGenerateBib = () => {
-
   // 强制设置默认值,不使用条件判断(像素单位)
   bibForm.logoX = bibForm.logoX || 50;
   bibForm.logoY = bibForm.logoY || 50;
@@ -1154,7 +1161,6 @@ const handleCloseBibDialog = () => {
 
 // 重置表单
 const resetBibForm = () => {
-
   // 设置默认值(像素单位)
   bibForm.logoX = 50;
   bibForm.logoY = 50;
@@ -1176,7 +1182,6 @@ const resetBibForm = () => {
   if (logoUploadRef.value) {
     logoUploadRef.value.clearFiles();
   }
-
 };
 
 // 背景图片改变处理
@@ -1347,7 +1352,6 @@ const handleGenerateBibFile = async () => {
       console.warn('qRCodeY值异常,使用默认值200px');
     }
 
-
     // 获取预览容器的高度用于坐标转换
     const containerHeight = previewContainer.value?.clientHeight || 400;
 
@@ -1366,15 +1370,15 @@ const handleGenerateBibFile = async () => {
     // 二维码坐标(左上角)
     const qrCoords = convertCoordinatesWithScale(qRCodeX, qRCodeY);
 
-          const bibParams = {
-        logoX: logoCoords.x,
-        logoY: logoCoords.y,
-        qRCodeX: qrCoords.x,
-        qRCodeY: qrCoords.y,
-        fontName: bibForm.fontName || 'simhei',
-        fontSize: Math.round((bibForm.fontSize || 36) * 0.75), // 字体大小转换为PDF点并四舍五入为整数
-        fontColor: parseInt((bibForm.fontColor || '#000000').replace('#', ''), 16)
-      };
+    const bibParams = {
+      logoX: logoCoords.x,
+      logoY: logoCoords.y,
+      qRCodeX: qrCoords.x,
+      qRCodeY: qrCoords.y,
+      fontName: bibForm.fontName || 'simhei',
+      fontSize: Math.round((bibForm.fontSize || 36) * 0.75), // 字体大小转换为PDF点并四舍五入为整数
+      fontColor: parseInt((bibForm.fontColor || '#000000').replace('#', ''), 16)
+    };
 
     // 最后一次检查,确保二维码坐标不为null
     if (bibParams.qRCodeX === null || bibParams.qRCodeY === undefined) {
@@ -1411,19 +1415,6 @@ onMounted(() => {
   // 获取默认赛事信息
   gameEventStore.fetchDefaultEvent();
   getList();
-  generateNumberTable();
-
-  // 强制确保bibForm的值正确初始化
-
-  // 使用nextTick确保在下一个tick执行
-  nextTick(() => {
-    if (bibForm.qRCodeX === null || bibForm.qRCodeX === undefined) {
-      bibForm.qRCodeX = 100; // 像素单位
-    }
-    if (bibForm.qRCodeY === null || bibForm.qRCodeY === undefined) {
-      bibForm.qRCodeY = 200; // 像素单位
-    }
-  });
 });
 
 // 监听路由变化,当从编辑页返回时检查是否需要刷新列表
@@ -1472,6 +1463,13 @@ onActivated(() => {
   padding: 20px;
 }
 
+.bib-generator .el-upload__tip {
+  color: #909399;
+  font-size: 12px;
+  margin-top: 8px;
+  text-align: center;
+}
+
 .preview-container {
   border: 2px dashed #ddd;
   border-radius: 8px;

+ 38 - 39
src/views/system/gameEventGroup/detail.vue

@@ -359,48 +359,47 @@ const generateGroups = async () => {
     // 随机打乱运动员顺序
     const shuffledAthletes = [...eligibleAthletes].sort(() => Math.random() - 0.5);
     
-    // 按组别和道次分配运动员
-    let athleteIndex = 0;
+    // 记录已分配的运动员ID,避免重复分配
+    const assignedAthleteIds = new Set();
     
+    // 按组别和道次分配运动员
     for (let groupIndex = 1; groupIndex <= groupInfo.value.includeGroupNum; groupIndex++) {
       for (let track = 1; track <= groupInfo.value.trackNum; track++) {
-        if (athleteIndex < shuffledAthletes.length) {
-           let selectedAthlete = shuffledAthletes[athleteIndex];
-           
-           // 检查同一组中是否已有同一队伍的运动员
-           const hasSameTeamInGroup = Array.from(groupResult.value.entries())
-             .some(([key, existingAthlete]) => {
-               const [existingGroup] = key.split('-');
-               return existingGroup === groupIndex.toString() && 
-                      existingAthlete.teamId === selectedAthlete.teamId;
-             });
-           
-           // 如果同一组中已有同一队伍的运动员,尝试找下一个不同队伍的运动员
-           if (hasSameTeamInGroup) {
-             let nextAthleteIndex = athleteIndex + 1;
-             let found = false;
-             
-             while (nextAthleteIndex < shuffledAthletes.length && !found) {
-               const nextAthlete = shuffledAthletes[nextAthleteIndex];
-               const hasSameTeam = Array.from(groupResult.value.entries())
-                 .some(([key, existingAthlete]) => {
-                   const [existingGroup] = key.split('-');
-                   return existingGroup === groupIndex.toString() && 
-                          existingAthlete.teamId === nextAthlete.teamId;
-                 });
-               
-               if (!hasSameTeam) {
-                 selectedAthlete = nextAthlete;
-                 found = true;
-               }
-               nextAthleteIndex++;
-             }
-           }
-           
-           const key = `${groupIndex}-${track}`;
-           groupResult.value.set(key, selectedAthlete);
-           athleteIndex++;
-         }
+        // 寻找可用的运动员
+        let selectedAthlete = null;
+        let athleteIndex = 0;
+        
+        while (athleteIndex < shuffledAthletes.length && !selectedAthlete) {
+          const candidateAthlete = shuffledAthletes[athleteIndex];
+          
+          // 检查运动员是否已经被分配
+          if (assignedAthleteIds.has(candidateAthlete.athleteId)) {
+            athleteIndex++;
+            continue;
+          }
+          
+          // 检查同一组中是否已有同一队伍的运动员
+          const hasSameTeamInGroup = Array.from(groupResult.value.entries())
+            .some(([key, existingAthlete]) => {
+              const [existingGroup] = key.split('-');
+              return existingGroup === groupIndex.toString() && 
+                     existingAthlete.teamId === candidateAthlete.teamId;
+            });
+          
+          if (!hasSameTeamInGroup) {
+            selectedAthlete = candidateAthlete;
+            // 标记运动员为已分配
+            assignedAthleteIds.add(candidateAthlete.athleteId);
+          }
+          
+          athleteIndex++;
+        }
+        
+        // 如果找到了合适的运动员,分配到当前组和道次
+        if (selectedAthlete) {
+          const key = `${groupIndex}-${track}`;
+          groupResult.value.set(key, selectedAthlete);
+        }
       }
     }
     

+ 2 - 2
src/views/system/gameEventGroup/index.vue

@@ -52,9 +52,9 @@
               >删除
             </el-button>
           </el-col>
-          <el-col :span="1.5">
+          <!-- <el-col :span="1.5">
             <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:gameEventGroup:export']">导出 </el-button>
-          </el-col>
+          </el-col> -->
           <right-toolbar v-model:showSearch="showSearch" :columns="columns" @queryTable="getList"></right-toolbar>
         </el-row>
       </template>

+ 25 - 15
src/views/system/gameEventProject/index.vue

@@ -7,11 +7,11 @@
             <el-form-item label="项目名称" prop="projectName">
               <el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable @keyup.enter="handleQuery" />
             </el-form-item>
-            <!-- <el-form-item label="项目组别" prop="groupType">
-              <el-select v-model="queryParams.groupType" placeholder="请选择项目组别" clearable @change="handleQuery">
-                <el-option v-for="group in gameEventGroupList" :key="group.groupName" :label="group.groupName" :value="group.groupName" />
+            <el-form-item label="项目归类" prop="classification">
+              <el-select v-model="queryParams.classification" placeholder="请选择项目归类" clearable @change="handleQuery">
+                <el-option v-for="dict in game_project_classification" :key="dict.value" :label="dict.label" :value="dict.value" />
               </el-select>
-            </el-form-item> -->
+            </el-form-item>
             <el-form-item>
               <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
               <el-button icon="Refresh" @click="resetQuery">重置</el-button>
@@ -37,9 +37,9 @@
               >删除
             </el-button>
           </el-col>
-          <el-col :span="1.5">
+          <!-- <el-col :span="1.5">
             <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:gameEventProject:export']">导出 </el-button>
-          </el-col>
+          </el-col> -->
           <right-toolbar v-model:showSearch="showSearch" :columns="columns" @queryTable="getList"></right-toolbar>
         </el-row>
       </template>
@@ -53,7 +53,11 @@
             <dict-tag :options="game_project_type" :value="scope.row.projectType || ''" />
           </template>
         </el-table-column>
-        <!-- <el-table-column label="项目组别" align="center" prop="groupType" v-if="columns[3].visible" /> -->
+        <el-table-column label="项目归类" align="center" prop="classification" v-if="columns[3].visible" >
+          <template #default="scope">
+            <dict-tag :options="game_project_classification" :value="scope.row.classification || '未知'" />
+          </template>
+        </el-table-column>
         <el-table-column label="比赛场地" align="center" prop="location" v-if="columns[4].visible" />
         <el-table-column label="计算规则" align="center" prop="scoreRule" v-if="columns[5].visible">
           <template #default="scope">
@@ -131,7 +135,11 @@
         <el-form-item label="比赛场地" prop="location">
           <el-input v-model="form.location" placeholder="请输入比赛场地" />
         </el-form-item>
-
+        <el-form-item label="归类" prop="classification">
+          <el-radio-group v-model="form.classification" style="width: 100%">
+            <el-radio v-for="dict in game_project_classification" :key="dict.value" :label="dict.label" :value="dict.value" />
+          </el-radio-group>
+        </el-form-item>
         <!-- <el-form-item label="参赛组数" prop="groupNum">
           <el-input v-model="form.groupNum" placeholder="请输入参赛组数" />
         </el-form-item>
@@ -200,9 +208,10 @@ import {
 import { listGameEventGroup } from '@/api/system/gameEventGroup';
 import { GameEventProjectVO, GameEventProjectQuery, GameEventProjectForm } from '@/api/system/gameEventProject/types';
 import RefereeGroupDialog from './RefereeGroupDialog.vue';
+import { orderBy } from 'element-plus/es/components/table/src/util';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const { game_score_type, game_project_type } = toRefs<any>(proxy?.useDict('game_score_type', 'game_project_type'));
+const { game_score_type, game_project_type, game_project_classification } = toRefs<any>(proxy?.useDict('game_score_type', 'game_project_type','game_project_classification'));
 
 const gameEventGroupList = ref<any[]>([]); // 赛事分组列表
 
@@ -235,7 +244,7 @@ const columns = ref<FieldOption[]>([
   { key: 0, label: '主键', visible: false },
   { key: 1, label: '项目名称', visible: true },
   { key: 2, label: '项目类型', visible: true },
-  { key: 3, label: '项目组别', visible: true },
+  { key: 3, label: '归类', visible: true },
   { key: 4, label: '比赛场地', visible: true },
   { key: 5, label: '计算规则', visible: true },
   { key: 6, label: '开始时间', visible: true },
@@ -251,7 +260,7 @@ const initFormData: GameEventProjectForm = {
   eventId: undefined,
   projectName: undefined,
   projectType: undefined,
-  groupType: undefined,
+  classification: '0',
   refereeGroups: undefined,
   location: undefined,
   startTime: undefined,
@@ -259,7 +268,7 @@ const initFormData: GameEventProjectForm = {
   groupNum: undefined,
   participateNum: undefined,
   roundType: undefined,
-  orderType: undefined,
+  orderType: '0',
   scoreRule: undefined,
   scoreValue: undefined,
   award: undefined,
@@ -273,14 +282,15 @@ const data = reactive<PageData<GameEventProjectForm, GameEventProjectQuery>>({
   queryParams: {
     pageNum: 1,
     pageSize: 10,
+    classification: undefined,
     orderByColumn: undefined,
     isAsc: undefined,
   },
   rules: {
     projectName: [{ required: true, message: '项目名称不能为空', trigger: 'blur' }],
     projectType: [{ required: true, message: '项目类型不能为空', trigger: 'change' }],
-    groupType: [{ required: true, message: '项目组别不能为空', trigger: 'change' }],
-    location: [{ required: true, message: '比赛场地不能为空', trigger: 'blur' }]
+    classification: [{ required: true, message: '归类不能为空', trigger: 'change' }],
+    orderType: [{ required: true, message: '排序方式不能为空', trigger: 'change' }],
   }
 });
 
@@ -410,7 +420,7 @@ const handleViewRefereeGroup = (refereeGroups: string[], projectName: string) =>
 
 onMounted(() => {
   getList();
-  getGameEventGroupList();
+  // getGameEventGroupList();
 });
 
 </script>

+ 277 - 144
src/views/system/gameScore/gameScoreEdit.vue

@@ -3,29 +3,54 @@
     <!-- 新增:顶部搜索框 -->
     <div class="flex items-center mb-4">
       <el-button type="primary" @click="refreshData"><el-icon><Refresh /></el-icon> 刷新</el-button>
-      <el-input v-model="searchValue" placeholder="输入姓名搜索" style="margin-left: 20px; width: 200px;" clearable @keyup.enter="loadAthleteScores">
+      <el-button type="success" @click="calculateRankings" :loading="rankingLoading">
+        <el-icon><Trophy /></el-icon> 计算排名
+      </el-button>
+      <el-input 
+        v-model="searchValue" 
+        :placeholder="projectClassification === '0' ? '输入运动员姓名搜索' : '输入队伍名称搜索'" 
+        style="margin-left: 20px; width: 200px;" 
+        clearable 
+        @keyup.enter="loadData">
         <template #prefix>
           <el-icon><Search /></el-icon>
         </template>
       </el-input>
+      <div class="ml-4 text-gray-600">
+        <el-tag :type="projectClassification === '0' ? 'success' : 'warning'" class="mr-2">
+          {{ projectClassification === '0' ? '个人项目' : '团体项目' }}
+        </el-tag>
+        <span>{{ projectClassification === '0' ? '管理运动员个人成绩' : '管理队伍团队成绩' }}</span>
+      </div>
     </div>
 
     <el-card shadow="never">
-      <el-table v-loading="loading" border :data="athleteScores">
+      <el-table v-loading="loading" border :data="dataList">
         <el-table-column type="selection" width="55" align="center" />
-        <el-table-column label="ID" align="center" prop="projectId" />
+        <el-table-column label="队伍ID" align="center" prop="teamId" />
         <el-table-column label="项目" align="center" prop="projectName" />
-        <!-- <el-table-column label="分组" align="center" prop="groupType" /> -->
         <el-table-column label="项目类型" align="center" prop="projectType" >
           <template #default="scope">
             <dict-tag :options="game_project_type" :value="scope.row.projectType" />
           </template>
         </el-table-column>
-        <el-table-column label="单位" align="center" prop="unit" />
-        <el-table-column label="姓名" align="center" prop="name" />
-        <el-table-column label="号码" align="center" prop="athleteCode" />
+        <el-table-column label="队伍名称" align="center" prop="teamName" />
+        
+        <!-- 个人项目显示运动员信息 -->
+        <template v-if="projectClassification === '0'">
+          <el-table-column label="姓名" align="center" prop="name" />
+          <el-table-column label="号码" align="center" prop="athleteCode" />
+          <el-table-column label="个人成绩" align="center" prop="individualPerformance" />
+        </template>
+        
+        <!-- 团体项目显示队伍信息 -->
+        <template v-else>
+          <!-- <el-table-column label="队伍名称" align="center" prop="teamName" /> -->
+          <!-- <el-table-column label="队伍编号" align="center" prop="teamCode" /> -->
+          <el-table-column label="团队成绩" align="center" prop="teamPerformance" />
+        </template>
+        
         <el-table-column label="积分" align="center" prop="scorePoint" />
-        <el-table-column label="成绩" align="center" prop="scoreValue" />
         <el-table-column label="排名" align="center" prop="scoreRank" />
         <el-table-column label="更新时间" align="center" prop="updateTime" />
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
@@ -39,17 +64,33 @@
     </el-card>
 
     <!-- 添加或修改裁判对话框 -->
-    <el-dialog :title="dialog.title" v-model="dialog.visible" width="800px" append-to-body>
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="400px" append-to-body>
       <el-form ref="scoreFormRef" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="积分" prop="scorePoint">
-          <el-input v-model="form.scorePoint" 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="scoreRank">
-          <el-input v-model="form.scoreRank" placeholder="输入排名" />
-        </el-form-item>
+        <!-- 个人项目显示个人成绩字段 -->
+        <template v-if="projectClassification === '0'">
+          <el-form-item label="个人成绩" prop="individualPerformance">
+            <el-input 
+              v-model="form.individualPerformance" 
+              placeholder="输入个人成绩" 
+              type="number"
+              step="0.01"
+              @input="handleIndividualPerformanceInput"
+              @blur="handleIndividualPerformanceBlur" />
+          </el-form-item>
+        </template>
+        
+        <!-- 团体项目显示团队成绩字段 -->
+        <template v-else>
+          <el-form-item label="团队成绩" prop="teamPerformance">
+            <el-input 
+              v-model="form.teamPerformance" 
+              placeholder="输入团队成绩" 
+              type="number"
+              step="0.01"
+              @input="handleTeamPerformanceInput"
+              @blur="handleTeamPerformanceBlur" />
+          </el-form-item>
+        </template>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
@@ -59,15 +100,14 @@
       </template>
     </el-dialog>
 
-    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="loadAthleteScores" />
+    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="handlePagination({page: queryParams.pageNum, limit: queryParams.pageSize})" />
   </div>
 </template>
 
 <script setup name="GameScoreEdit" lang="ts">
 import { onMounted, ref } from 'vue';
 import { useRoute } from 'vue-router';
-import { listGameAthlete } from '@/api/system/gameAthlete/index'; 
-import { listGameScore, delGameScore, getGameScore, addGameScore, updateGameScore } from '@/api/system/gameScore/index';
+import { getProjectScoreData, updateScoreAndRecalculate } from '@/api/system/gameScore/index';
 import { GameScoreForm } from '@/api/system/gameScore/types';
 import Pagination from '@/components/Pagination/index.vue';
 
@@ -76,43 +116,19 @@ const { game_project_type } = toRefs<any>(proxy?.useDict('game_project_type'));
 
 const route = useRoute();
 const buttonLoading = ref(false);
-const athleteScores = ref<AthleteScore[]>([]);
+const rankingLoading = ref(false);
+const dataList = ref<any[]>([]);
 const loading = ref(true);
 const total = ref(0);
 
 // 定义搜索框状态变量
 const searchValue = ref('');
 
-// 定义一个简化版的类型,只包含需要的属性
-interface AthleteScore {
-  // GameAthleteVO 属性
-  athleteId?: string | number;
-  userId?: string | number;
-  eventId?: string | number;
-  teamId?: string | number;
-  athleteCode?: string;
-  name?: string;
-  unit?: string;
-  groupType?: string;
-  number?: string;
-  // GameEventProjectVO 属性 (排除scoreValue)
-  projectId?: string | number;
-  projectName?: string;
-  projectType?: string;
-  startTime?: string;
-  endTime?: string;
-  // GameScoreVO 属性
-  scoreId?: string | number;
-  scoreValue?: number; 
-  scoreRank?: number;
-  scorePoint?: number;
-  updateTime?: string;
-}
-
 const eventId = route.params.eventId as string;
 const projectId = route.params.projectId as string;
 const projectName = route.params.projectName as string;
 const projectType = route.params.projectType as string;
+const projectClassification = route.params.classification as string || '0'; // 默认为个人项目
 
 const queryParams = reactive({
   pageNum: 1,
@@ -121,13 +137,42 @@ const queryParams = reactive({
   projectId: projectId,
   projectName: projectName,
   projectType: projectType,
+  classification: projectClassification,
 });
 
 //刷新数据方法
 const refreshData = () => {
   searchValue.value = '';
-  loadAthleteScores();
+  loadData(true); // 刷新时自动计算排名
 };
+
+// 计算排名方法
+const calculateRankings = async () => {
+  try {
+    rankingLoading.value = true;
+    
+    // 调用后端接口重新计算排名和积分
+    await updateScoreAndRecalculate({
+      eventId: eventId,
+      projectId: projectId,
+      scoreType: projectClassification === '0' ? 'individual' : 'team',
+      statusFlag: '0',
+      status: '0',
+      remark: '手动触发排名计算'
+    } as GameScoreForm);
+    
+    proxy?.$modal.msgSuccess("排名计算完成");
+    
+    // 重新加载数据以显示新的排名和积分
+    await loadData(false); // 不自动计算排名,避免循环
+  } catch (error) {
+    console.error("排名计算失败:", error);
+    proxy?.$modal.msgError("排名计算失败,请稍后再试");
+  } finally {
+    rankingLoading.value = false;
+  }
+};
+
 const scoreFormRef = ref<ElFormInstance>();
 const dialog = reactive<DialogOption>({
   visible: false,
@@ -135,150 +180,238 @@ const dialog = reactive<DialogOption>({
 });
 
 const form = reactive({
-  scorePoint: 0,
-  scoreValue: 0.0,
-  scoreRank: 0,
+  scorePoint: null,
+  individualPerformance: 0,
+  teamPerformance: 0,
+  scoreRank: null,
   scoreId: 0,
+  athleteId: 0,
+  teamId: 0,
+  eventId: '',
+  projectId: '',
+  athleteCode: '',
+  userId: 0,
+  teamName: '', // 个人项目和团体项目都使用
+  updateTime: '',
 });
 
 const rules = {
-  scorePoint: [
-    { required: true, message: "积分不能为空", trigger: "blur" }
-  ],
-  scoreValue: [
-    { required: true, message: "成绩不能为空", trigger: "blur" }
+  individualPerformance: [
+    { required: true, message: "个人成绩不能为空", trigger: "blur" },
+    { 
+      validator: (rule: any, value: any, callback: any) => {
+        if (value !== '' && value !== null && value !== undefined) {
+          const numValue = parseFloat(value);
+          if (isNaN(numValue)) {
+            callback(new Error('请输入有效的数字'));
+          } else if (numValue < 0) {
+            callback(new Error('成绩不能为负数'));
+          } else {
+            callback();
+          }
+        } else {
+          callback();
+        }
+      }, 
+      trigger: "blur" 
+    }
   ],
-  scoreRank: [
-    { required: true, message: "排名不能为空", trigger: "change" }
+  teamPerformance: [
+    { required: true, message: "团队成绩不能为空", trigger: "blur" },
+    { 
+      validator: (rule: any, value: any, callback: any) => {
+        if (value !== '' && value !== null && value !== undefined) {
+          const numValue = parseFloat(value);
+          if (isNaN(numValue)) {
+            callback(new Error('请输入有效的数字'));
+          } else if (numValue < 0) {
+            callback(new Error('成绩不能为负数'));
+          } else {
+            callback();
+          }
+        } else {
+          callback();
+        }
+      }, 
+      trigger: "blur" 
+    }
   ],
 }
 
+// 成绩输入处理函数
+const handleIndividualPerformanceInput = (value: string) => {
+  // 确保输入的是有效数字
+  if (value && !isNaN(parseFloat(value))) {
+    form.individualPerformance = parseFloat(value);
+  }
+};
+
+const handleIndividualPerformanceBlur = () => {
+  // 失焦时确保数据类型正确
+  if (form.individualPerformance !== null && form.individualPerformance !== undefined) {
+    const numValue = parseFloat(String(form.individualPerformance));
+    if (!isNaN(numValue)) {
+      form.individualPerformance = numValue;
+    } else {
+      form.individualPerformance = 0;
+    }
+  }
+};
+
+const handleTeamPerformanceInput = (value: string) => {
+  // 确保输入的是有效数字
+  if (value && !isNaN(parseFloat(value))) {
+    form.teamPerformance = parseFloat(value);
+  }
+};
+
+const handleTeamPerformanceBlur = () => {
+  // 失焦时确保数据类型正确
+  if (form.teamPerformance !== null && form.teamPerformance !== undefined) {
+    const numValue = parseFloat(String(form.teamPerformance));
+    if (!isNaN(numValue)) {
+      form.teamPerformance = numValue;
+    } else {
+      form.teamPerformance = 0;
+    }
+  }
+};
+
 /** 取消按钮 */
 const cancel = () => {
-  // reset();
   dialog.visible = false;
 }
+
 /** 编辑按钮 */
-const editScore = (row: AthleteScore) => {
+const editScore = (row: any) => {
   dialog.visible = true;
-  // 将当前行数据复制到编辑表单中
+  // 将当前行数据复制到编辑表单中,确保成绩数据类型正确
   Object.assign(form, {
     scoreId: row.scoreId,
-    scorePoint: row.scorePoint,
-    scoreValue: row.scoreValue,
-    scoreRank: row.scoreRank,
+    scorePoint: row.scorePoint || null,
+    individualPerformance: parseFloat(String(row.individualPerformance)) || 0,
+    teamPerformance: parseFloat(String(row.teamPerformance)) || 0,
+    scoreRank: row.scoreRank || null,
     athleteId: row.athleteId,
+    teamId: row.teamId,
     athleteCode: row.athleteCode,
     userId: row.userId,
     eventId: row.eventId,
-    teamId: row.teamId,
     projectId: row.projectId,
-    unit: row.unit,
+    teamName: row.teamName || '', // 统一使用teamName字段
     updateTime: row.updateTime,
   });
 };
 
 /** 提交按钮 */
-const submitForm = () => {
+const submitForm = async () => {
   scoreFormRef.value?.validate(async (valid: boolean) => {
     if (valid) {
       try {
         buttonLoading.value = true;
         
-        // 根据 form 中的数据创建提交对象
-        const submitForm: GameScoreForm = {
-          ...form,
-          // updateTime: new Date().toISOString() // 在提交时设置updateTime
-        };
-
-        // 调用更新或添加接口
-        if (form.scoreId) { 
-          await updateGameScore(submitForm);
+        // 确保成绩数据类型正确
+        let individualPerformance = 0;
+        let teamPerformance = 0;
+        
+        if (projectClassification === '0') {
+          // 个人项目
+          individualPerformance = parseFloat(String(form.individualPerformance)) || 0;
+          if (isNaN(individualPerformance)) {
+            individualPerformance = 0;
+          }
         } else {
-          await addGameScore(submitForm);
+          // 团体项目
+          teamPerformance = parseFloat(String(form.teamPerformance)) || 0;
+          if (isNaN(teamPerformance)) {
+            teamPerformance = 0;
+          }
         }
-
+        
+        // 构建提交数据,确保使用正确的scoreId
+        const submitForm: GameScoreForm = {
+          scoreId: form.scoreId, // 确保使用正确的scoreId
+          scorePoint: form.scorePoint || 0,
+          individualPerformance: individualPerformance,
+          teamPerformance: teamPerformance,
+          scoreRank: form.scoreRank || 0,
+          athleteId: form.athleteId,
+          teamId: form.teamId,
+          eventId: eventId,
+          projectId: projectId,
+          scoreType: projectClassification === '0' ? 'individual' : 'team',
+          statusFlag: '0',
+          status: '0',
+          remark: projectClassification === '0' ? '个人项目成绩' : '团体项目成绩'
+        };
+        
+        // 调用后端接口更新成绩并重新计算排名积分
+        await updateScoreAndRecalculate(submitForm);
+        
         proxy?.$modal.msgSuccess("操作成功");
         dialog.visible = false;
-        await loadAthleteScores(); // 重新加载数据以反映更改
+        loadData(false); // 提交成功后不自动计算排名,用户需要手动点击排名按钮
       } catch (error) {
+        console.error("提交失败:", error);
         proxy?.$modal.msgError("操作失败,请稍后再试");
       } finally {
         buttonLoading.value = false;
       }
     }
   });
-}
+};
 
-// 加载运动员成绩列表方法,支持搜索
-const loadAthleteScores = async () => {
+// 加载数据方法,支持搜索
+const loadData = async (autoCalculateRanking = false) => {
   loading.value = true;
-  const resAthletes = await listGameAthlete({
-    eventId: eventId,
-    pageNum: queryParams.pageNum,
-    pageSize: queryParams.pageSize,
-    // 使用name属性替代keyword进行搜索
-    name: searchValue.value,
-    orderByColumn: '',
-    isAsc: ''
-  });
-  const athletes = resAthletes.rows;
-  total.value = resAthletes.total;
-
-  const athleteScoresData: AthleteScore[] = [];
-
-  for (const athlete of athletes) {
-    try {
-      // 使用 listGameScore 替换 getGameScore
-      const resScores = await listGameScore({
-        athleteId: athlete.athleteId,
-        pageNum: queryParams.pageNum,
-        pageSize: queryParams.pageSize,
-        orderByColumn: '',
-        isAsc: ''
-      });
-      const scores = resScores.rows;
-
-      // 找到与当前 athleteId 匹配的成绩记录
-      const score = scores.find(scoreItem => scoreItem.athleteId === athlete.athleteId);
-      //如果运动员的参与项目列表中没有项目,则跳过
-      if (athlete.projectValue === null || athlete.projectValue === '') {
-        continue;
-      }
-      athleteScoresData.push({
-        ...athlete,
-        scoreId: score?.scoreId ?? 0,
-        scorePoint: score?.scorePoint ?? 0,
-        scoreValue: score?.scoreValue ?? 0.0,
-        scoreRank: score?.scoreRank ?? 0,
-        updateTime: score?.updateTime ?? '',
-        eventId: eventId,
-        projectId: projectId, // 直接使用路由传来的 projectId
-        projectName: projectName,
-        projectType: projectType,
-      });
-    } catch (error) {
-      // 如果获取成绩失败,使用默认值
-      athleteScoresData.push({
-        ...athlete,
-        scoreId: 0,
-        scorePoint: 0,
-        scoreValue: 0.0,
-        scoreRank: 0,
-        updateTime: '',
-        eventId: eventId,
-        projectId: projectId, // 直接使用路由传来的 projectId
-        projectName: projectName,
-        projectType: projectType,
-      });
+  
+  try {
+    // 调用后端接口获取综合数据
+    const response = await getProjectScoreData({
+      eventId: eventId,
+      projectId: projectId,
+      classification: projectClassification,
+      searchValue: searchValue.value,
+      pageNum: queryParams.pageNum,
+      pageSize: queryParams.pageSize
+    });
+    
+    // 处理返回的数据
+    const rows = response.rows || [];
+    
+    // 为每行数据添加项目相关信息
+    dataList.value = rows.map((row: any) => ({
+      ...row,
+      projectName: projectName,
+      projectType: projectType,
+    }));
+    
+    total.value = response.total || 0;
+    
+    // 只有在自动计算排名的情况下才进行排名计算
+    if (autoCalculateRanking) {
+      await calculateRankings();
     }
+  } catch (error) {
+    console.error("加载数据失败:", error);
+    proxy?.$modal.msgError("数据加载失败");
+    dataList.value = [];
+    total.value = 0;
+  } finally {
+    loading.value = false;
   }
+};
 
-  athleteScores.value = athleteScoresData;
-  loading.value = false;
+// 分页组件事件处理
+const handlePagination = (paginationData: { page: number, limit: number }) => {
+  console.log('分页事件数据:', paginationData); // 添加调试日志
+  queryParams.pageNum = paginationData.page;
+  queryParams.pageSize = paginationData.limit;
+  console.log('更新后的查询参数:', queryParams); // 添加调试日志
+  loadData(false); // 分页加载不自动计算排名
 };
 
 onMounted(() => {
-  loadAthleteScores();
+  loadData(true); // 页面初始化时自动计算排名
 });
 </script>

+ 22 - 11
src/views/system/gameScore/index.vue

@@ -69,14 +69,22 @@
       <el-table v-loading="loading" border :data="projectList" @selection-change="handleSelectionChange">
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="ID" align="center" prop="projectId" v-if="columns[0].visible" />
-        <el-table-column label="项目" align="center" prop="projectName" v-if="columns[1].visible" />
-        <!-- <el-table-column label="分组" align="center" prop="groupType" v-if="columns[2].visible" /> -->
         <el-table-column label="项目类型" align="center" prop="projectType" v-if="columns[2].visible" >
           <template #default="scope">
             <dict-tag :options="game_project_type" :value="scope.row.projectType" />
           </template>
         </el-table-column>
-        <el-table-column label="状态" align="center" prop="status" v-if="columns[3].visible">
+        <el-table-column label="归类" align="center" prop="classification" v-if="columns[3].visible" >
+          <template #default="scope">
+            <el-tag :type="scope.row.classification === '0' ? 'success' : 'warning'">
+              {{ scope.row.classification === '0' ? '个人项目' : '团体项目' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="项目" align="center" prop="projectName" v-if="columns[1].visible" />
+        <!-- <el-table-column label="分组" align="center" prop="groupType" v-if="columns[2].visible" /> -->
+        
+        <el-table-column label="状态" align="center" prop="status" v-if="columns[4].visible">
           <template #default="scope">
             <el-select v-model="scope.row.status" placeholder="请选择状态">
               <el-option label="等待处理" value="0"></el-option>
@@ -84,8 +92,8 @@
             </el-select>
           </template>
         </el-table-column>
-        <el-table-column label="比赛时间" align="center" prop="startTime" v-if="columns[4].visible" />
-        <el-table-column label="更新时间" align="center" prop="updateTime" v-if="columns[5].visible" />
+        <el-table-column label="比赛时间" align="center" prop="startTime" v-if="columns[5].visible" />
+        <el-table-column label="更新时间" align="center" prop="updateTime" v-if="columns[6].visible" />
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
           <template #default="scope">
             <el-tooltip content="查看成绩详情" placement="top">
@@ -128,10 +136,11 @@ const total = ref(0);
 const columns = ref<FieldOption[]>([
   { key: 0, label: 'ID', visible: false },
   { key: 1, label: '项目', visible: true },
-  { key: 2, label: '分组', visible: true },
-  { key: 3, label: '状态', visible: true },
-  { key: 4, label: '比赛时间', visible: true },
-  { key: 5, label: '更新时间', visible: true },
+  { key: 2, label: '项目类型', visible: true },
+  { key: 3, label: '归类', visible: true },
+  { key: 4, label: '状态', visible: true },
+  { key: 5, label: '比赛时间', visible: true },
+  { key: 6, label: '更新时间', visible: true },
 ]);
 
 // 下拉框数据
@@ -145,7 +154,8 @@ const initFormData: GameScoreForm = {
   projectId: undefined,
   athleteId: undefined,
   teamId: undefined,
-  scoreValue: undefined,
+  individualPerformance: undefined,
+  teamPerformance: undefined,
   scoreType: undefined,
   scoreRank: undefined,
   scorePoint: undefined,
@@ -314,9 +324,10 @@ const navigateToEditPage = (row: GameEventProjectVO) => {
   const projectName = row.projectName;
   const projectType = row.projectType;
   const eventId = row.eventId;
+  const classification = row.classification;
   router.push({
     name: 'GameScoreEdit',
-    params: { projectId,projectName,projectType,eventId}
+    params: { projectId, projectName, projectType, eventId, classification }
   });
 };