Преглед изворни кода

feat(game-score): 实现动态成绩录入和多轮次计数功能

- 新增多次计数类型的成绩明细录入界面,支持动态轮次展示
- 实现计时类成绩的时间格式转换(0:00.00 → 秒)
- 添加单次计数类的失误次数A/B录入字段
- 优化成绩录入界面布局,增加运动员/队伍信息展示
- 修改提交按钮文本为"保存成绩"

fix(game-event-project): 修复排名方式回显时机问题

- 将排名方式回显逻辑包装在nextTick中确保正确渲染
- 修复修改模式下成绩规则变更时的重置逻辑
- 调整多次计数默认轮次从1改为3轮
- 移除未使用的number导入

refactor(game-score): 重构成绩录入表单数据处理逻辑

- 添加scoreDetails响应式变量管理成绩明细
- 实现editScore方法中的异步数据加载和明细初始化
- 添加loadProjectInfo方法获取项目规则信息
- 移除冗余的成绩输入处理函数,简化数据验证流程
zhou пре 3 недеља
родитељ
комит
b82ad016b0

+ 2 - 0
src/api/system/gameEventProject/types.ts

@@ -1,3 +1,5 @@
+import { number } from "vue-types";
+
 export interface GameEventProjectVO {
   scheduleDate: any;
   /**

+ 21 - 16
src/views/system/gameEventProject/index.vue

@@ -282,7 +282,7 @@
                 </el-col>
                 <el-col :span="12" v-if="form.scoreRule === '4'">
                   <el-form-item label="成绩数量" prop="scoreCount">
-                    <el-input-number v-model="form.scoreCount" :min="1" :max="20" size="small" />
+                    <el-input-number v-model="form.scoreCount" :min="1" size="small" />
                   </el-form-item>
                 </el-col>
               </el-row>
@@ -598,21 +598,23 @@ const handleAdd = () => {
 const handleUpdate = (row?: GameEventProjectVO) => {
   reset();
   const projectId = row?.projectId || ids.value.at(0);
-  getGameEventProject(projectId).then(response => {
-    Object.assign(form.value, response.data);
-    // 处理排名方式回显
-    orderGroup.val1 = null; orderGroup.val2 = null; orderGroup.val3 = null;
-    if (form.value.orderType) {
-      const types = form.value.orderType.split(',');
-      types.forEach(t => {
-        if (['0', '1'].includes(t)) orderGroup.val1 = t;
-        if (['2', '3'].includes(t)) orderGroup.val2 = t;
-        if (['4', '5', '6', '7'].includes(t)) orderGroup.val3 = t;
+    getGameEventProject(projectId).then(response => {
+      Object.assign(form.value, response.data);
+      // 处理排名方式回显
+      nextTick(() => {
+        orderGroup.val1 = null; orderGroup.val2 = null; orderGroup.val3 = null;
+        if (form.value.orderType) {
+          const types = form.value.orderType.split(',');
+          types.forEach(t => {
+            if (['0', '1'].includes(t)) orderGroup.val1 = t;
+            if (['2', '3'].includes(t)) orderGroup.val2 = t;
+            if (['4', '5', '6', '7'].includes(t)) orderGroup.val3 = t;
+          });
+        }
       });
-    }
-    dialog.visible = true;
-    dialog.title = '修改赛事项目';
-  });
+      dialog.visible = true;
+      dialog.title = '修改赛事项目';
+    });
 };
 
 /** 提交按钮 */
@@ -657,6 +659,9 @@ const submitForm = (isContinue = false) => {
 
 /** 监听成绩类型变化,重置子配置和排名方式建议 */
 watch(() => form.value.scoreRule, (newVal) => {
+  // 如果是修改模式且已经有值,不进行重置初始化
+  if (form.value.projectId) return;
+
   orderGroup.val1 = null; orderGroup.val2 = null; orderGroup.val3 = null;
   if (newVal === '1') { // 计时类
     form.value.timingFormat = '0';
@@ -669,7 +674,7 @@ watch(() => form.value.scoreRule, (newVal) => {
     orderGroup.val1 = '1';
   } else if (newVal === '4') { // 多次计数
     form.value.countUnit = '个';
-    form.value.scoreCount = 1;
+    form.value.scoreCount = 3; // 多次计数默认给3轮
     orderGroup.val3 = '4'; // 默认求和
   } else if (newVal === '5') { // 排名类
     orderGroup.val1 = '0';

+ 163 - 123
src/views/system/gameScore/gameScoreEdit.vue

@@ -103,44 +103,80 @@
     <!-- 添加或修改裁判对话框 -->
     <el-dialog :title="dialog.title" v-model="dialog.visible" width="400px" append-to-body>
       <el-form ref="scoreFormRef" :model="form" :rules="rules" label-width="100px">
-        <!-- 个人项目显示个人成绩字段 -->
+        <!-- 个人项目录入 -->
         <template v-if="projectClassification === '0'">
-          <el-form-item label="运动员姓名:">
-            {{ pickAthletes || '--' }}
-          </el-form-item>
-          <el-form-item label="队伍名称:">
-            {{ form.teamName || '--' }}
-          </el-form-item>
-          <el-form-item label="个人成绩:" prop="individualPerformance">
+          <div style="margin-bottom: 15px; background: #f8f9fb; padding: 10px; border-radius: 4px;">
+            <p><strong>运动员:</strong>{{ pickAthletes || '--' }}</p>
+            <p><strong>队伍:</strong>{{ form.teamName || '--' }}</p>
+          </div>
+
+          <!-- 动态明细录入区域 -->
+          <div v-if="['2', '4', '6', '7'].includes(projectInfo.scoreRule)" class="detail-section">
+            <div v-for="(item, index) in scoreDetails" :key="index" style="margin-bottom: 10px; display: flex; align-items: center;">
+              <span style="width: 80px;">第 {{ index + 1 }} 轮:</span>
+              <el-input-number v-model="item.performanceValue" :precision="3" :step="0.1" style="flex: 1" />
+              <span style="margin-left: 8px; color: #666; width: 30px;">{{ projectInfo.countUnit || (projectInfo.scoreRule === '1' ? '秒' : '') }}</span>
+            </div>
+          </div>
+
+          <!-- 单次计数类额外录入失误 -->
+          <template v-else-if="projectInfo.scoreRule === '3'">
+            <el-form-item label="单次成绩" prop="individualPerformance">
+              <el-input-number v-model="form.individualPerformance" :precision="3" style="width: 100%" />
+            </el-form-item>
+            <el-form-item v-if="projectInfo.orderType && projectInfo.orderType.includes('2')" label="失误次数A">
+              <el-input-number v-model="form.faultA" :min="0" style="width: 100%" />
+            </el-form-item>
+            <el-form-item v-if="projectInfo.orderType && projectInfo.orderType.includes('3')" label="失误次数B">
+              <el-input-number v-model="form.faultB" :min="0" style="width: 100%" />
+            </el-form-item>
+          </template>
+
+          <!-- 基础成绩录入(计时/排名/默认) -->
+          <el-form-item v-else label="个人成绩" prop="individualPerformance">
             <el-input 
               v-model="form.individualPerformance" 
-              placeholder="输入个人成绩" 
-              type="number"
-              step="0.01"
-              @input="handleIndividualPerformanceInput"
-              @blur="handleIndividualPerformanceBlur" />
+              :placeholder="projectInfo.scoreRule === '1' ? '支持输入秒数或 00:00.00 格式' : '输入成绩'"
+            >
+              <template #append v-if="projectInfo.scoreRule === '1'">秒</template>
+              <template #append v-else-if="projectInfo.countUnit">{{ projectInfo.countUnit }}</template>
+            </el-input>
           </el-form-item>
         </template>
         
-        <!-- 团体项目显示团队成绩字段 -->
+        <!-- 团体项目录入 -->
         <template v-else>
-          <el-form-item label="队伍名称:">
-            {{ form.teamName || '--' }}
-          </el-form-item>
-          <el-form-item label="团队成绩" prop="teamPerformance">
+          <div style="margin-bottom: 15px; background: #f8f9fb; padding: 10px; border-radius: 4px;">
+            <p><strong>队伍:</strong>{{ form.teamName || '--' }}</p>
+          </div>
+
+          <!-- 动态明细录入区域 (如团体项目也有多轮) -->
+          <div v-if="['2', '4', '6', '7'].includes(projectInfo.scoreRule)" class="detail-section">
+            <div v-for="(item, index) in scoreDetails" :key="index" style="margin-bottom: 10px; display: flex; align-items: center;">
+              <span style="width: 80px;">第 {{ index + 1 }} 轮:</span>
+              <el-input-number v-model="item.performanceValue" :precision="3" :step="0.1" style="flex: 1" />
+            </div>
+          </div>
+
+          <!-- 基础成绩录入(计时类支持字符串,其他数字) -->
+          <el-form-item v-else label="团队成绩" prop="teamPerformance">
             <el-input 
+              v-if="projectInfo.scoreRule === '1'"
               v-model="form.teamPerformance" 
-              placeholder="输入团队成绩" 
-              type="number"
-              step="0.01"
-              @input="handleTeamPerformanceInput"
-              @blur="handleTeamPerformanceBlur" />
+              placeholder="支持输入 00:00.00 格式"
+            >
+              <template #append>秒</template>
+            </el-input>
+            <div v-else style="display: flex; align-items: center;">
+              <el-input-number v-model="form.teamPerformance" :precision="3" style="flex: 1" />
+              <span style="margin-left: 8px; color: #666;">{{ projectInfo.countUnit }}</span>
+            </div>
           </el-form-item>
         </template>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
-          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">保存成绩</el-button>
           <el-button @click="cancel">取 消</el-button>
         </div>
       </template>
@@ -170,6 +206,8 @@
 import { onMounted, onUnmounted, ref, reactive, getCurrentInstance, toRefs } from 'vue';
 import { useRoute } from 'vue-router';
 import { getProjectScoreData, updateScoreAndRecalculate, exportProjectScore } from '@/api/system/gameScore/index';
+import { getGameEventProject } from '@/api/system/gameEventProject';
+import { listScoreDetail } from '@/api/system/scoreDetail';
 import { GameScoreForm } from '@/api/system/gameScore/types';
 import Pagination from '@/components/Pagination/index.vue';
 import type { ComponentInternalInstance } from 'vue';
@@ -180,6 +218,11 @@ const { game_project_type, sys_user_sex } = toRefs<any>(proxy?.useDict('game_pro
 const route = useRoute();
 const buttonLoading = ref(false);
 const rankingLoading = ref(false);
+const projectInfo = ref<any>({}); // 存储项目规则信息
+
+// 成绩明细列表
+const scoreDetails = ref<any[]>([]);
+
 const dataList = ref<any[]>([]);
 const loading = ref(true);
 const total = ref(0);
@@ -319,8 +362,19 @@ const form = reactive({
   userId: 0,
   teamName: '',
   updateTime: '',
+  faultA: 0,
+  faultB: 0
 });
 
+const reset = () => {
+  Object.assign(form, {
+    scorePoint: null, individualPerformance: 0, teamPerformance: 0, scoreRank: null,
+    scoreId: 0, athleteId: 0, teamId: 0, eventId: '', projectId: '',
+    athleteCode: '', userId: 0, teamName: '', updateTime: '', faultA: 0, faultB: 0
+  });
+  scoreDetails.value = [];
+};
+
 const rules = {
   individualPerformance: [
     { required: true, message: "个人成绩不能为空", trigger: "blur" },
@@ -364,123 +418,89 @@ const rules = {
   ],
 }
 
-// 成绩输入处理函数
-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 = () => {
   dialog.visible = false;
 }
 
-/** 编辑按钮 */
-const editScore = (row: any) => {
-  dialog.visible = true;
-  // 将当前行数据复制到编辑表单中,确保成绩数据类型正确
-  Object.assign(form, {
-    scoreId: row.scoreId,
-    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,
-    projectId: row.projectId,
-    teamName: row.teamName || '',
-    updateTime: row.updateTime,
-  });
+/** 修改成绩 */
+const editScore = async (row: any) => {
+  reset();
+  
+  // 强制重新加载项目规则信息,确保获取到最新的 scoreCount 和 countUnit
+  await loadProjectInfo();
+  
+  Object.assign(form, { ...row });
   pickAthletes.value = row.name;
+  
+  // 如果是需要明细的项目(远度、多次计数等),加载明细
+  if (['2', '4', '6', '7'].includes(projectInfo.value.scoreRule)) {
+    // 改为按维度查询,不再单纯依赖 scoreId
+    const queryParams: any = { 
+      projectId: row.projectId 
+    };
+    if (projectClassification === '0') {
+      queryParams.athleteId = row.athleteId;
+    } else {
+      queryParams.teamId = row.teamId;
+    }
+    
+    const res = await listScoreDetail(queryParams);
+    const existingDetails = res.rows || [];
+    // 根据项目配置的次数初始化明细数组
+    const count = projectInfo.value.scoreCount || 1;
+    scoreDetails.value = [];
+    for (let i = 1; i <= count; i++) {
+      const detail = existingDetails.find((d: any) => d.attemptIndex === i);
+      scoreDetails.value.push({
+        scoreId: row.scoreId,
+        attemptIndex: i,
+        performanceValue: detail ? detail.performanceValue : 0,
+        faultA: detail ? detail.faultA : 0,
+        faultB: detail ? detail.faultB : 0
+      });
+    }
+  }
+  
+  dialog.visible = true;
+  dialog.title = "录入成绩";
 };
 
-/** 提交按钮 */
-const submitForm = async () => {
+/** 提交表单 */
+const submitForm = () => {
   scoreFormRef.value?.validate(async (valid: boolean) => {
     if (valid) {
+      buttonLoading.value = true;
       try {
-        buttonLoading.value = true;
-        
-        // 确保成绩数据类型正确
-        let individualPerformance = 0;
-        let teamPerformance = 0;
+        // 使用 any 避开 TS 对 details 的类型检查
+        const postData: any = { ...form };
         
-        if (projectClassification === '0') {
-          // 个人项目
-          individualPerformance = parseFloat(String(form.individualPerformance)) || 0;
-          if (isNaN(individualPerformance)) {
-            individualPerformance = 0;
+        // 计时类单位转换 (统一处理两个字段,确保不携带时间格式字符串)
+        if (projectInfo.value.scoreRule === '1') {
+          if (typeof postData.individualPerformance === 'string') {
+            postData.individualPerformance = timeToSeconds(postData.individualPerformance);
           }
-        } else {
-          // 团体项目
-          teamPerformance = parseFloat(String(form.teamPerformance)) || 0;
-          if (isNaN(teamPerformance)) {
-            teamPerformance = 0;
+          if (typeof postData.teamPerformance === 'string') {
+            postData.teamPerformance = timeToSeconds(postData.teamPerformance);
           }
+        } else {
+          // 非计时类,也确保是数值,防止占位符干扰
+          if (isNaN(parseFloat(postData.individualPerformance))) postData.individualPerformance = 0;
+          if (isNaN(parseFloat(postData.teamPerformance))) postData.teamPerformance = 0;
         }
+
+        // 挂载成绩明细
+        postData.details = scoreDetails.value;
         
-        // 构建提交数据,确保使用正确的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);
+        // 调用后端接口更新成绩(后端已支持 recalculate)
+        await updateScoreAndRecalculate(postData);
         
-        proxy?.$modal.msgSuccess("操作成功");
+        proxy?.$modal.msgSuccess("保存成功,排名已重新计算");
         dialog.visible = false;
-        loadData(false); // 提交成功后不自动计算排名,用户需要手动点击排名按钮
-      } catch (error) {
-        // console.error("提交失败:", error);
-        proxy?.$modal.msgError("操作失败,请稍后再试");
+        loadData(false);
+      } catch (err) {
+        console.error("提交失败:", err);
+        proxy?.$modal.msgError("保存失败,请检查网络或数据格式");
       } finally {
         buttonLoading.value = false;
       }
@@ -579,6 +599,25 @@ const handlePagination = (paginationData: { page: number, limit: number }) => {
   loadData(false); // 分页加载不自动计算排名
 };
 
+/** 时间字符串转秒 (00:00.00 -> 60.00) */
+const timeToSeconds = (timeStr: string) => {
+  if (!timeStr || !timeStr.includes(':')) return parseFloat(timeStr) || 0;
+  const parts = timeStr.split(':');
+  let seconds = 0;
+  if (parts.length === 3) { // 00:00:00.000
+    seconds = parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseFloat(parts[2]);
+  } else if (parts.length === 2) { // 00:00.00
+    seconds = parseInt(parts[0]) * 60 + parseFloat(parts[1]);
+  }
+  return seconds;
+};
+
+/** 加载项目规则信息 */
+const loadProjectInfo = async () => {
+  const res = await getGameEventProject(projectId);
+  projectInfo.value = res.data;
+};
+
 // 组件卸载时清理定时器
 onUnmounted(() => {
   if (autoRefreshInterval.value) {
@@ -588,6 +627,7 @@ onUnmounted(() => {
 });
 
 onMounted(() => {
+  loadProjectInfo(); // 加载规则
   loadData(false); // 页面初始化时不自动计算排名,需要手动点击计算排名按钮
   // 开启自动刷新
   startAutoRefresh();