Browse Source

feat(game): 添加赛事项目报名限制功能

- 新增每人限报项目数和每项限报人数配置
- 实现前端项目选择校验逻辑
- 添加项目满员状态实时检测
- 完善报名表单验证和提示信息
- 更新相关API接口和数据模型定义
- 优化项目选择组件交互体验
zhou 2 weeks ago
parent
commit
79e3f3f33d

+ 12 - 0
src/api/system/gameAthlete/index.ts

@@ -62,3 +62,15 @@ export const delGameAthlete = (athleteId: string | number | Array<string | numbe
   });
 };
 
+/**
+ * 校验项目选择
+ * @param data 
+ * @returns 
+ */
+export function validateProjectSelection(data: any) {
+  return request({
+    url: '/system/gameAthlete/validateProjectSelection',
+    method: 'post',
+    data: data
+  });
+}

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

@@ -39,6 +39,11 @@ export interface GameEventVO {
    */
   endTime: string;
 
+  /**
+   * 每人限报项目数
+   */
+  limitApplication: number;
+
   /**
    * 赛事链接
    */
@@ -128,6 +133,11 @@ export interface GameEventForm extends BaseEntity {
    */
   endTime?: string;
 
+  /**
+   * 每人限报项目数
+   */
+  limitApplication?: number;
+
   /**
    * 赛事链接
    */

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

@@ -63,6 +63,11 @@ export interface GameEventProjectVO {
    */
   participateNum: number;
 
+  /**
+   * 每个项目限报人数
+   */
+  limitPerson: number;
+
   /**
    * 录取名次
    */
@@ -170,6 +175,11 @@ export interface GameEventProjectForm extends BaseEntity {
    */
   participateNum?: number;
 
+  /**
+   * 每个项目限报人数
+   */
+  limitPerson: number;
+
   /**
    * 录取名次
    */

+ 195 - 22
src/views/system/gameAthlete/index.vue

@@ -132,12 +132,34 @@
         <el-form-item label="参与项目" prop="projectList">
           <el-transfer
             v-model="projectListStr"
-            :data="gameEventProjectList"
+            :data="filteredGameEventProjectList"
             :titles="['可选项目', '已选项目']"
             :button-texts="['移除', '添加']"
             filterable
             style="width: 100%"
+            @change="handleProjectChange"
           />
+          <!-- 添加校验提示 -->
+          <div v-if="projectValidation.errors.length > 0" class="validation-errors">
+            <el-alert
+              v-for="error in projectValidation.errors"
+              :key="error"
+              :title="error"
+              type="error"
+              :closable="false"
+              show-icon
+            />
+          </div>
+          <div v-if="projectValidation.warnings.length > 0" class="validation-warnings">
+            <el-alert
+              v-for="warning in projectValidation.warnings"
+              :key="warning"
+              :title="warning"
+              type="warning"
+              :closable="false"
+              show-icon
+            />
+          </div>
         </el-form-item>
         <el-form-item label="备注" prop="remark">
           <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
@@ -191,9 +213,9 @@
 
 <script setup name="GameAthlete" lang="ts">
 import { nextTick, ref, onMounted } from 'vue';
-import { listGameAthlete, getGameAthlete, delGameAthlete, addGameAthlete, updateGameAthlete } from '@/api/system/gameAthlete';
+import { listGameAthlete, getGameAthlete, delGameAthlete, addGameAthlete, updateGameAthlete, validateProjectSelection } from '@/api/system/gameAthlete';
 import { listGameTeam, updateTeamAthletes } from '@/api/system/gameTeam';
-import { listGameEventProject } from '@/api/system/gameEventProject';
+import { listGameEventProject, getGameEventProject } from '@/api/system/gameEventProject';
 // import { getDefaultEvent } from '@/api/system/gameEvent';
 import { GameAthleteVO, GameAthleteQuery, GameAthleteForm } from '@/api/system/gameAthlete/types';
 import { GameTeamVO } from '@/api/system/gameTeam/types';
@@ -206,7 +228,7 @@ const defaultEvent = ref<GameEventVO | null>(null); // 默认赛事信息
 
 const gameTeamList = ref<GameTeamVO[]>([]); // 队伍列表
 const gameAthleteList = ref<GameAthleteVO[]>([]);
-const gameEventProjectList = ref<Array<{ key: string; label: string }>>([]); // 赛事项目列表(用于穿梭框)
+const gameEventProjectList = ref<Array<{ key: string; label: string; disabled: boolean }>>([]); // 赛事项目列表(用于穿梭框)
 const buttonLoading = ref(false);
 const loading = ref(true);
 const showSearch = ref(true);
@@ -311,6 +333,8 @@ const data = reactive<PageData<GameAthleteForm, GameAthleteQuery>>({
 
 // 添加额外的ref用于处理默认事件ID
 const defaultEventId = computed(() => defaultEvent.value?.eventId);
+// 添加赛事限制信息
+const eventLimitInfo = defaultEvent.value?.limitApplication || 0;
 
 // 监听默认事件变化
 watchEffect(() => {
@@ -322,6 +346,129 @@ watchEffect(() => {
 
 const { queryParams, form, rules } = toRefs(data);
 
+// 使用计算属性实时计算校验状态
+const validationStatus = computed(() => {
+  const selectedCount = form.value.selectedProjects.length;
+  
+  // 如果有限制且超额
+  if (eventLimitInfo > 0 && selectedCount > eventLimitInfo) {
+    return {
+      errors: [`每人限报项目数为${eventLimitInfo}个,您已选择${selectedCount}个项目`],
+      warnings: [],
+      isValid: false,
+      isExceeded: true
+    };
+  }
+  
+  // 正常状态
+  return {
+    errors: [],
+    warnings: [],
+    isValid: true,
+    isExceeded: false
+  };
+});
+
+const projectValidation = reactive({
+  errors: [] as string[],
+  warnings: [] as string[],
+  isValid: true,
+  isExceeded: false
+});
+
+// 添加过滤后的项目列表(排除已满的项目)
+const filteredGameEventProjectList = computed(() => {
+  return gameEventProjectList.value.map(project => ({
+    ...project,
+    disabled: false // 先设为false,在组件加载时异步更新
+  }));
+});
+
+const projectDetailsCache = ref<Map<number, any>>(new Map());
+
+// 获取项目详情
+const getProjectDetails = async (projectId: number) => {
+  if (projectDetailsCache.value.has(projectId)) {
+    return projectDetailsCache.value.get(projectId);
+  }
+
+  try {
+    const response = await getGameEventProject(projectId);
+    projectDetailsCache.value.set(projectId, response.data);
+    return response.data;
+  } catch (error) {
+    console.error('获取项目详情失败:', error);
+    return null;
+  }
+};
+
+// 判断项目是否已满
+const isProjectFull = async (projectId: number): Promise<boolean> => {
+  const projectDetails = await getProjectDetails(projectId);
+  if (!projectDetails || !projectDetails.limitPerson || projectDetails.limitPerson === 0) {
+    return false; // 无限制或未设置限制
+  }
+
+  // 这里需要调用后端接口获取当前报名人数
+  // 可以通过批量查询优化性能
+  try {
+    const response = await validateProjectSelection({
+      eventId: form.value.eventId,
+      athleteId: form.value.athleteId || null,
+      selectedProjectIds: [projectId]
+    });
+    
+    // 如果该项目报名人数已满,返回true
+    return response.data.errors.some((error: string) => 
+      error.includes('报名人数已满')
+    );
+  } catch (error) {
+    console.error('检查项目是否已满失败:', error);
+    return false;
+  }
+};
+
+// 项目选择变化处理
+const handleProjectChange = async (value: string[], direction: string, movedKeys: string[]) => {
+  // 立即更新已选项目
+  form.value.selectedProjects = value.map(id => Number(id));
+  
+  // 如果从超额状态恢复正常,进行后端校验
+  if (validationStatus.value.isExceeded === false && form.value.selectedProjects.length > 0) {
+    await validateProjectSelection2();
+  }
+};
+
+// 校验项目选择
+const validateProjectSelection2 = async () => {
+  if (!form.value.eventId || !form.value.selectedProjects.length) {
+    projectValidation.errors = [];
+    projectValidation.warnings = [];
+    projectValidation.isValid = true;
+    return;
+  }
+
+  try {
+    const response = await validateProjectSelection({
+      eventId: form.value.eventId,
+      athleteId: form.value.athleteId || null,
+      selectedProjectIds: form.value.selectedProjects
+    });
+
+    // 合并前端和后端的校验结果
+    projectValidation.errors = [...validationStatus.value.errors, ...(response.data.errors || [])];
+    projectValidation.warnings = response.data.warnings || [];
+    projectValidation.isValid = response.data.valid && validationStatus.value.isValid;
+    projectValidation.isExceeded = validationStatus.value.isExceeded;
+  } catch (error) {
+    console.error('校验项目选择失败:', error);
+    projectValidation.errors = [...validationStatus.value.errors, '校验失败,请重试'];
+    projectValidation.warnings = [];
+    projectValidation.isValid = false;
+    projectValidation.isExceeded = validationStatus.value.isExceeded;
+  }
+};
+
 /** 获取默认赛事 */
 // const getDefaultEventInfo = async () => {
 //   try {
@@ -340,29 +487,25 @@ const getTeamNameById = (teamId: string | number | null | undefined) => {
 };
 
 // 获取赛事项目列表
-const getProjectList = async (eventId?: string) => {
-  const res = await listGameEventProject({
-    pageNum: 1,
-    pageSize: 1000,
-    orderByColumn: '',
-    isAsc: ''
-  });
-  console.log(res);
-  gameEventProjectList.value = res.rows.map((item) => ({
-    key: String(item.projectId),
-    label: `${item.projectName}`
-  }));
+const getProjectList = async () => {
+  try {
+    const res = await listGameEventProject();
+    gameEventProjectList.value = res.rows.map((item) => ({
+      key: String(item.projectId),
+      label: item.projectName,
+      disabled: false
+    }));
+    
+    // 更新项目状态
+    await updateProjectStatus();
+  } catch (error) {
+    console.error('获取项目列表失败:', error);
+  }
 };
 
 // 格式化项目列表显示
 const formatProjectList = (projectList: number[]) => {
   if (!projectList) return '';
-  // 将逗号分隔的ID列表转换为项目名称列表
-  // const projectIds = projectValue.split(',');
-  // const projectNames = projectIds.map((id) => {
-  //   const project = gameEventProjectList.value.find((p) => p.key === id);
-  //   return project ? project.label : id;
-  // });
   const projectNames = gameEventProjectList.value.filter((p) => projectList.includes(Number(p.key))).map((p) => p.label);
   return projectNames.join(',');
 };
@@ -471,6 +614,13 @@ const projectListStr = computed({
 const submitForm = () => {
   gameAthleteFormRef.value?.validate(async (valid: boolean) => {
     if (valid) {
+      // 提交前再次校验
+      await validateProjectSelection2();
+      
+      if (!projectValidation.isValid) {
+        proxy?.$modal.msgError('项目选择不符合要求,请检查后重试');
+        return;
+      }
       buttonLoading.value = true;
       try {
         // 处理项目列表数据,将数组转换为逗号分隔的字符串
@@ -554,6 +704,17 @@ const submitForm = () => {
   });
 };
 
+// 在组件加载时更新项目状态
+const updateProjectStatus = async () => {
+  if (gameEventProjectList.value.length > 0) {
+    for (const project of gameEventProjectList.value) {
+      const projectId = Number(project.key);
+      const isFull = await isProjectFull(projectId);
+      project.disabled = isFull;
+    }
+  }
+};
+
 /** 删除按钮操作 */
 const handleDelete = async (row?: GameAthleteVO) => {
   const _athleteIds = row?.athleteId || ids.value;
@@ -645,3 +806,15 @@ onMounted(() => {
   getProjectList();
 });
 </script>
+
+<style scoped>
+.validation-errors,
+.validation-warnings {
+  margin-top: 8px;
+}
+
+.validation-errors .el-alert,
+.validation-warnings .el-alert {
+  margin-bottom: 4px;
+}
+</style>

+ 16 - 0
src/views/system/gameEvent/edit.vue

@@ -81,6 +81,20 @@
             </el-row>
 
             <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="限报项目数" prop="limitApplication">
+                  <template #label>
+                    <span>
+                      <el-tooltip content="每人限报项目的数量,0表示不限。" placement="top">
+                        <el-icon>
+                          <question-filled />
+                        </el-icon>
+                      </el-tooltip>
+                    限报项目数</span>
+                  </template>
+                  <el-input-number v-model="basicForm.limitApplication" :min="0" :max="100" size="small" controls-position="right" />
+                </el-form-item>
+              </el-col>
               <el-col :span="12">
                 <el-form-item label="状态" prop="status">
                   <el-radio-group v-model="basicForm.status">
@@ -315,6 +329,7 @@ const basicForm = ref<GameEventForm>({
   purpose: '',
   startTime: '',
   endTime: '',
+  limitApplication: 0,
   eventUrl: '',
   refereeUrl: '',
   registerUrl: '',
@@ -355,6 +370,7 @@ onMounted(async () => {
       purpose: '',
       startTime: '',
       endTime: '',
+      limitApplication: 0,
       eventUrl: '',
       refereeUrl: '',
       registerUrl: '',

+ 14 - 1
src/views/system/gameEventProject/index.vue

@@ -178,6 +178,18 @@
             <el-radio value="1">降序</el-radio>
           </el-radio-group>
         </el-form-item>
+        <el-form-item label="限报人数" prop="limitPerson">
+          <template #label>
+            <span>
+              <el-tooltip content="项目限制报名的运动员数量,0表示不限。" placement="top">
+                <el-icon>
+                  <question-filled />
+                </el-icon>
+              </el-tooltip>
+            限报人数</span>
+          </template>
+          <el-input-number v-model="form.limitPerson" :min="0" :max="100" size="small" controls-position="right" />
+        </el-form-item>
         <el-form-item label="录取名次" prop="roundType">
           <el-input v-model="form.roundType" placeholder="请输入录取名次" />
         </el-form-item>
@@ -303,6 +315,7 @@ const initFormData: GameEventProjectForm = {
   endTime: undefined,
   groupNum: undefined,
   participateNum: undefined,
+  limitPerson: 0,
   roundType: undefined,
   orderType: '0',
   scoreRule: undefined,
@@ -310,7 +323,7 @@ const initFormData: GameEventProjectForm = {
   award: undefined,
   gameRound: undefined,
   gameStage: undefined,
-  status: undefined,
+  status: '0',
   remark: undefined
 };
 const data = reactive<PageData<GameEventProjectForm, GameEventProjectQuery>>({