From 8ecd8ffe45d3f628c3c6d76234d98ecc09843a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E6=A2=93=E5=BD=AD?= Date: Wed, 15 Apr 2026 09:58:29 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B5=9B=E4=BA=8B=E6=B5=81=E7=A8=8B=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/match/usereventregistration.js | 35 +++++++ src/views/match/manage/work.vue | 115 +++++++++++++++++++--- src/views/user/record/index.vue | 18 ++-- src/views/user/registration/index.vue | 127 ++++++++++++++++++++----- 4 files changed, 248 insertions(+), 47 deletions(-) diff --git a/src/api/match/usereventregistration.js b/src/api/match/usereventregistration.js index 0f63490..ce9afa3 100644 --- a/src/api/match/usereventregistration.js +++ b/src/api/match/usereventregistration.js @@ -294,3 +294,38 @@ export const usereventregistrationOrderStatusGet = (orderNo) => { return http.get(`/usereventregistration/order/status/${orderNo}`); }; +/** +* 检查组别选择限制 +* @param {Object} data - 检查信息 +* @param {number} data.eventId - 赛事ID +* @param {number} data.groupId - 要检查的组别ID +* @param {Array} data.selectedGroupIds - 当前已选择的组别ID列表 +* @returns {Promise<{ +* canSelect: boolean, +* limitMessage: string, +* groupLimit: { +* groupLimitCount: number, +* currentRegistrations: number, +* isLimitReached: boolean +* }, +* projectLimit: { +* projectLimitCount: number, +* currentSelectedCount: number, +* isLimitReached: boolean +* }, +* teamProjectLimit: { +* limitCount: number, +* currentSelectedCount: number, +* isLimitReached: boolean +* }, +* individualProjectLimit: { +* limitCount: number, +* currentSelectedCount: number, +* isLimitReached: boolean +* } +* }>} +*/ +export const usereventregistrationCheckGroupSelectionLimitPost = (data) => { + return http.post('/usereventregistration/check-group-selection-limit', data); +}; + diff --git a/src/views/match/manage/work.vue b/src/views/match/manage/work.vue index 672dba9..6210fc2 100644 --- a/src/views/match/manage/work.vue +++ b/src/views/match/manage/work.vue @@ -75,11 +75,11 @@ 拖动项目卡片可调整顺序,按最终顺序导出
-
+
{{ item.projectName }}
类型: {{ item.projectType }}
-
时长: {{ formatDuration(item.durationSeconds) }}
+
时长: {{ formatDurationChinese(item.durationSeconds) }}
@@ -106,7 +106,7 @@
项目所需时长
-
{{ formatDuration(item.totalDurationSeconds || 0) }}
+
{{ formatDurationSymbol(item.totalDurationSeconds || 0) }}
@@ -114,7 +114,6 @@
预览 - 保存修改 导出
@@ -264,7 +263,7 @@ :rowspan="getTimeRowspan(index)" class="time-cell" > - {{ formatTime(item.timeRange) }} + {{ item.projectTotalDuration }} {{ item.projectName }} {{ item.groupName }} @@ -312,6 +311,7 @@ const importingScore = ref(false); const issuingPoints = ref(false); const arrangementContainerRef = ref(null); let sortableInstance = null; +let isSorting = false; // 标志位,标记是否正在排序中 // 预览相关 const previewDialogVisible = ref(false); @@ -418,7 +418,10 @@ const openDialog = async (id) => { } const res2 = await eventarrangementListGet(id); - eventlist.value = filterProjectsWithParticipants(res2.data); + eventlist.value = filterProjectsWithParticipants(res2.data).map((item, idx) => ({ + ...item, + _tempId: item.id || `temp-${idx}-${Date.now()}` + })); await nextTick(); initSortable(); @@ -440,12 +443,24 @@ const initSortable = () => { ghostClass: 'sort-ghost', dragClass: 'sort-drag', handle: '.arrangement-card', + onStart: () => { + isSorting = true; + }, onEnd: (evt) => { const { oldIndex, newIndex } = evt; - if (oldIndex === undefined || newIndex === undefined || oldIndex === newIndex) return; + if (oldIndex === undefined || newIndex === undefined || oldIndex === newIndex) { + isSorting = false; + return; + } + // 更新数据 const item = eventlist.value.splice(oldIndex, 1)[0]; eventlist.value.splice(newIndex, 0, item); + + // 延迟设置 isSorting 为 false,确保 Vue 不会立即重新渲染 + setTimeout(() => { + isSorting = false; + }, 200); } }); }; @@ -614,6 +629,22 @@ const previewArrangement = async () => { return; } + // 为每个项目设置正确的 SortIndex(根据当前在数组中的位置) + const settingsWithSortIndex = eventlist.value.map((item, index) => ({ + ...item, + SortIndex: index + 1 // 从 1 开始排序 + })); + + // 先保存当前修改 + const saveRes = await eventarrangementPut({ + eventId: initParam.value.id, + settings: settingsWithSortIndex + }); + if (!saveRes.succeeded) { + ElMessage.error('保存失败,无法预览'); + return; + } + const res = await eventarrangementPreviewPost(initParam.value.id); if (res.succeeded) { previewData.value = res.data || []; @@ -637,7 +668,7 @@ const isTimeMerged = (index) => { if (index === 0) return false; const current = previewData.value[index]; const prev = previewData.value[index - 1]; - return current?.projectName === prev?.projectName && current?.timeRange === prev?.timeRange; + return current?.projectName === prev?.projectName; } // 获取时间列的合并行数 @@ -648,7 +679,7 @@ const getTimeRowspan = (index) => { let count = 1; for (let i = index + 1; i < previewData.value.length; i++) { const next = previewData.value[i]; - if (next?.projectName === current.projectName && next?.timeRange === current.timeRange) { + if (next?.projectName === current.projectName) { count++; } else { break; @@ -659,9 +690,15 @@ const getTimeRowspan = (index) => { //更新赛事编排 const onSave = async () => { + // 为每个项目设置正确的 SortIndex(根据当前在数组中的位置) + const settingsWithSortIndex = eventlist.value.map((item, index) => ({ + ...item, + SortIndex: index + 1 // 从 1 开始排序 + })); + const res = await eventarrangementPut({ eventId: initParam.value.id, - settings: eventlist.value + settings: settingsWithSortIndex }); if (res.succeeded) { ElMessage.success('操作成功!'); @@ -690,6 +727,22 @@ const getArr = async () => { return; } + // 为每个项目设置正确的 SortIndex(根据当前在数组中的位置) + const settingsWithSortIndex = eventlist.value.map((item, index) => ({ + ...item, + SortIndex: index + 1 // 从 1 开始排序 + })); + + // 先保存当前修改 + const saveRes = await eventarrangementPut({ + eventId: initParam.value.id, + settings: settingsWithSortIndex + }); + if (!saveRes.succeeded) { + ElMessage.error('保存失败,无法导出'); + return; + } + const { succeeded, data, errors } = await eventarrangementExportPost(initParam.value.id); if (!succeeded) { ElMessage.error(errors || '导出失败'); @@ -1097,8 +1150,8 @@ const isTeamProject = (projectType) => { return projectType && projectType.includes('团队'); }; -// 格式化时长显示 -const formatDuration = (totalSeconds) => { +// 格式化时长显示(中文格式:X分X秒) +const formatDurationChinese = (totalSeconds) => { if (!totalSeconds || totalSeconds < 0) { return '0分0秒'; } @@ -1115,6 +1168,41 @@ const formatDuration = (totalSeconds) => { } }; +// 格式化时长显示(符号格式:X'X") +const formatDurationSymbol = (totalSeconds) => { + if (!totalSeconds || totalSeconds < 0) { + return '0\'0"'; + } + + // 计算小时、分钟、秒 + const hours = Math.floor(totalSeconds / 3600); + const remainingSecondsAfterHours = totalSeconds % 3600; + const minutes = Math.floor(remainingSecondsAfterHours / 60); + const seconds = remainingSecondsAfterHours % 60; + + // 如果不足1小时,只显示分钟和秒 + if (hours === 0) { + if (seconds === 0) { + return `${minutes}'`; + } else { + // 秒数使用两位数显示,不足补零 + const paddedSeconds = seconds.toString().padStart(2, '0'); + return `${minutes}'${paddedSeconds}"`; + } + } else { + // 有小时的情况 + let result = `${hours}h`; + if (minutes > 0 || seconds > 0) { + result += `${minutes}'`; + if (seconds > 0) { + const paddedSeconds = seconds.toString().padStart(2, '0'); + result += `${paddedSeconds}"`; + } + } + return result; + } +}; + // 过滤有有效参与者的项目 const filterProjectsWithParticipants = (projects) => { if (!projects || !Array.isArray(projects)) { @@ -1204,6 +1292,9 @@ const validateVenueCount = () => { watch( eventlist, (newList) => { + // 如果正在排序中,不执行计算,避免 Vue 立即重新渲染 + if (isSorting) return; + if (newList && newList.length > 0) { newList.forEach((item) => { const result = calculateProjectDuration(item); diff --git a/src/views/user/record/index.vue b/src/views/user/record/index.vue index f1e4199..12d0081 100644 --- a/src/views/user/record/index.vue +++ b/src/views/user/record/index.vue @@ -2,9 +2,7 @@
- + @@ -81,9 +79,7 @@ {{ currentRow.eventName ?? '-' }} {{ currentRow.teamName ?? '-' }} - - {{ tagTextForStatus(currentRow.status) }} - + {{ tagTextForPayStatus(currentRow.payStatus) }} @@ -646,18 +642,16 @@ const getEventStatusText = (status) => { const disableCancel = computed(() => { if (!currentRow.value) return true; - const status = normalizeToNumber(currentRow.value.status); const payStatus = normalizeToNumber(currentRow.value.payStatus); - // 只有报名状态为已支付(1) 且 支付状态为已支付(1),才能取消/退款 - return !(status === 1 && payStatus === 1); + // 只有支付状态为已支付(1),才能取消/退款 + return !(payStatus === 1); }); const isCancelDisabledByRow = (row) => { if (!row) return true; - const status = normalizeToNumber(row.status); const payStatus = normalizeToNumber(row.payStatus); - // 只有报名状态为已支付(1) 且 支付状态为已支付(1),才能取消/退款 - return !(status === 1 && payStatus === 1); + // 只有支付状态为已支付(1),才能取消/退款 + return !(payStatus === 1); }; // 格式化日期 diff --git a/src/views/user/registration/index.vue b/src/views/user/registration/index.vue index 5303609..f9599d6 100644 --- a/src/views/user/registration/index.vue +++ b/src/views/user/registration/index.vue @@ -300,6 +300,19 @@
+ + + + + { groupTeamLimitMap.value = {}; memberRegistrationLimitMap.value = {}; registrationLimitCheckResult.value = null; + validationLoading.value = false; + if (validationDebounceTimer) { + clearTimeout(validationDebounceTimer); + validationDebounceTimer = null; + } previewLoading.value = false; previewData.value = null; paymentMethod.value = 1; @@ -1172,6 +1192,66 @@ const validateMemberForGroup = async (member, group) => { return { isValid: true, ageValid: true, genderValid: true, registrationLimitValid: true, message: '' }; }; +// 实时验证所有已选择的队员 +const validateAllMembers = async () => { + if (!currentEventId.value || activeStep.value !== 1) { + return; + } + + validationLoading.value = true; + + try { + // 收集所有需要验证的队员和组别组合 + const validationTasks = []; + + for (const group of selectedGroupItems.value) { + const gId = group.id; + + // 检查是否为团队项目且启用了多团队模式 + const isMultiTeamMode = isTeamProject(group) && groupTeamsMap.value[gId] && + (groupTeamsMap.value[gId].length > 1 || getGroupMaxTeams(gId) > 1); + + let membersToValidate = []; + + if (isMultiTeamMode) { + // 多团队模式:收集所有团队的队员 + for (const team of groupTeamsMap.value[gId]) { + membersToValidate = membersToValidate.concat(team.members); + } + } else { + // 单团队模式 + membersToValidate = groupMemberMap.value[gId] || []; + } + + // 为每个队员创建验证任务 + for (const member of membersToValidate) { + validationTasks.push(validateMemberForGroup(member, group)); + } + } + + // 并行执行所有验证任务 + await Promise.all(validationTasks); + + // 同时检查报名限制 + await checkRegistrationLimit(); + } catch (error) { + console.error('批量验证队员失败:', error); + } finally { + validationLoading.value = false; + } +}; + +// 带防抖的实时验证 +const debounceValidateAllMembers = () => { + if (validationDebounceTimer) { + clearTimeout(validationDebounceTimer); + } + + validationDebounceTimer = setTimeout(() => { + validateAllMembers(); + }, 400); // 400毫秒防抖 +}; + // 获取队员验证结果 const getMemberValidationResult = (member, group) => { const key = `${group.id}_${member.userId ?? member.id}`; @@ -1237,24 +1317,14 @@ const assignMemberToActiveGroup = async (member) => { return; } - // 获取当前组别信息进行验证 - const group = selectedGroupItems.value.find(g => g.id === gId); - if (group) { - const validation = await validateMemberForGroup(member, group); - if (!validation.isValid) { - ElMessage.warning(validation.message); - return; - } - } - // 设置已使用的团队 if (!usedTeamId.value) { usedTeamId.value = member.teamId; } groupMemberMap.value[gId].push({ ...member, teamId: member.teamId }); - // 实时检查报名限制 - await checkRegistrationLimit(); + // 触发实时验证(包含报名限制检查) + debounceValidateAllMembers(); }; // 从组别中移除成员 @@ -1272,6 +1342,9 @@ const removeMemberFromGroup = (groupId, memberId) => { if (!hasMembers) { usedTeamId.value = null; } + + // 触发实时验证 + debounceValidateAllMembers(); }; // ==================== 多团队管理函数 ==================== @@ -1381,6 +1454,9 @@ const removeTeam = async (groupId, teamIndex) => { }); ElMessage.success('团队删除成功'); + + // 触发实时验证 + debounceValidateAllMembers(); } } catch { // 用户取消 @@ -1464,23 +1540,14 @@ const assignMemberToActiveTeam = async (member) => { return; } - // 获取当前组别信息进行验证(group变量已在函数开头声明) - if (group) { - const validation = await validateMemberForGroup(member, group); - if (!validation.isValid) { - ElMessage.warning(validation.message); - return; - } - } - // 设置已使用的团队 if (!usedTeamId.value) { usedTeamId.value = member.teamId; } team.members.push({ ...member, teamId: member.teamId }); - // 实时检查报名限制 - await checkRegistrationLimit(); + // 触发实时验证(包含报名限制检查) + debounceValidateAllMembers(); }; // 从团队中移除成员 @@ -1501,6 +1568,9 @@ const removeMemberFromTeam = (groupId, teamIndex, memberId) => { if (!hasMembers) { usedTeamId.value = null; } + + // 触发实时验证 + debounceValidateAllMembers(); }; // 验证优惠券 @@ -1650,6 +1720,10 @@ const handleStep1Next = async () => { setActiveTeam(group.id, 1); } }); + + // 进入步骤2时立即进行一次完整验证 + await nextTick(); + validateAllMembers(); }; const handleStep2Next = async () => { @@ -1960,6 +2034,13 @@ const handleCreateOrderAndPay = async () => { // 微信/支付宝支付,显示二维码 paymentData.value = data; + + // 检查二维码是否获取成功 + if (!data.qrcodeUrl) { + ElMessage.error('支付链接获取失败,请稍后重试'); + return; + } + await generateQRCode(data.qrcodeUrl); // 开始轮询支付状态