From 150c7f26b981d0adf400d0c20614a4673e449dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E6=A2=93=E5=BD=AD?= Date: Thu, 2 Apr 2026 08:42:24 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B5=9B=E4=BA=8B=E8=AE=B0=E5=BD=95=E9=80=80?= =?UTF-8?q?=E6=AC=BE=EF=BC=8C=E8=B5=9B=E4=BA=8B=E6=8A=A5=E5=90=8D=E9=99=90?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/match/usereventrecord..js | 52 +++ src/api/match/usereventregistration.js | 70 +++ src/views/user/record/index.vue | 576 ++++++++++++++++++++++++- src/views/user/registration/index.vue | 276 +++++++++++- 4 files changed, 957 insertions(+), 17 deletions(-) diff --git a/src/api/match/usereventrecord..js b/src/api/match/usereventrecord..js index 189fb1c..215142d 100644 --- a/src/api/match/usereventrecord..js +++ b/src/api/match/usereventrecord..js @@ -176,4 +176,56 @@ export const usereventrecordMembersReplacePost = (registrationid, userid, data) */ export const usereventrecordCertificateGet = (registrationid) => { return http.get(`/usereventrecord/certificate/${registrationid}`); +}; + +/** + * 7. 获取取消报名预览 + * @param {number} registrationId - 报名记录ID (路径参数) + * @returns {Promise<{ + * registrationId: number, + * eventName: string, + * teamName: string, + * chargeMode: number, + * chargeModeText: string, + * originalAmount: number, + * couponDiscountAmount: number, + * actualPaidAmount: number, + * daysBeforeMatch: number, + * refundRuleDescription: string, + * deductPercentage: number, + * deductAmount: number, + * estimatedRefundAmount: number, + * cancelableMembers: Array<{ + * personId: number, + * name: string, + * canCancel: boolean, + * uncancelableReason: string, + * isLeader: boolean + * }>, + * isTeamProject: boolean, + * canPartialCancel: boolean, + * cancelReasons: Array + * }>} + */ +export const usereventrecordCancelPreviewGet = (registrationId) => { + return http.get(`/usereventrecord/cancel/preview/${registrationId}`); +}; + +/** + * 8. 执行取消报名 + * @param {Object} data - 取消报名数据 + * @param {number} data.registrationId - 报名ID + * @param {number} data.cancelType - 取消类型:0-全队取消,1-部分取消 + * @param {Array} [data.personIds] - 取消的队员ID列表(部分取消时必填) + * @param {string} data.cancelReason - 取消原因 + * @returns {Promise<{ + * success: boolean, + * message: string, + * refundAmount: number, + * refundNo: string, + * cancelTime: string + * }>} + */ +export const usereventrecordCancelExecutePost = (data) => { + return http.post('/usereventrecord/cancel/execute', data); }; \ No newline at end of file diff --git a/src/api/match/usereventregistration.js b/src/api/match/usereventregistration.js index 28fe76b..b95adaf 100644 --- a/src/api/match/usereventregistration.js +++ b/src/api/match/usereventregistration.js @@ -213,4 +213,74 @@ export const usereventregistrationOrderCreatePost = (data) => { */ export const usereventregistrationCouponValidatePost = (data) => { return http.post('/usereventregistration/coupon/validate', data); +}; + +/** +* 验证队员是否符合报名条件 +* @param {Object} data - 验证信息 +* @param {number} data.eventId - 赛事ID +* @param {number} data.memberId - 队员ID +* @param {number} data.groupId - 组别ID +* @returns {Promise<{ +* isValid: boolean, +* ageValid: boolean, +* genderValid: boolean, +* registrationLimitValid: boolean, +* message: string +* }>} +*/ +export const usereventregistrationMemberValidatePost = (data) => { + return http.post('/usereventregistration/member/validate', data); +}; + +/** +* 检查报名限制 +* @param {Object} data - 检查信息 +* @param {number} data.eventId - 赛事ID +* @param {Array} data.memberIds - 队员ID列表 +* @param {Array} data.groupIds - 组别ID列表 +* @returns {Promise<{ +* hasLimit: boolean, +* limitDetails: Array<{ +* memberId: number, +* memberName: string, +* limitType: string, +* limitMessage: string +* }> +* }>} +*/ +export const usereventregistrationRegistrationLimitCheckPost = (data) => { + return http.post('/usereventregistration/registration-limit/check', data); +}; + +/** +* 获取组别可报队伍数量配置 +* @param {number} eventId - 赛事ID +* @returns {Promise<{ +* groupId: number, +* maxTeams: number, +* currentTeams: number +* }>} +*/ +export const usereventregistrationGroupTeamLimitGet = (eventId) => { + return http.get(`/usereventregistration/group-team-limit/${eventId}`); +}; + +/** +* 检查项目完成人数 +* @param {Object} data - 检查信息 +* @param {number} data.eventId - 赛事ID +* @param {Array<{groupId: number, memberIds: Array}>} data.groupMembers - 组别队员分配 +* @returns {Promise<{ +* hasError: boolean, +* errors: Array<{ +* projectId: number, +* projectName: string, +* errorType: string, +* errorMessage: string +* }> +* }>} +*/ +export const usereventregistrationProjectCompletionCheckPost = (data) => { + return http.post('/usereventregistration/project-completion/check', data); }; \ No newline at end of file diff --git a/src/views/user/record/index.vue b/src/views/user/record/index.vue index 033d299..f38818c 100644 --- a/src/views/user/record/index.vue +++ b/src/views/user/record/index.vue @@ -25,11 +25,7 @@ \ No newline at end of file diff --git a/src/views/user/registration/index.vue b/src/views/user/registration/index.vue index 5c23d6a..6653c7f 100644 --- a/src/views/user/registration/index.vue +++ b/src/views/user/registration/index.vue @@ -179,6 +179,7 @@ v-for="m in (groupMemberMap[group.id] ?? [])" :key="m.userId ?? m.id" closable + :type="getMemberValidationResult(m, group).isValid ? '' : 'danger'" @close="removeMemberFromGroup(group.id, m.userId ?? m.id)" style="margin: 3px;" > @@ -186,6 +187,19 @@ 点击此卡片后,从右侧选择队员 + +
+
+ +
+
@@ -226,6 +240,58 @@ + + +
+ + + + + + + + + + + +
+ + + @@ -376,6 +442,10 @@ import { usereventregistrationPreviewGet, usereventregistrationOrderCreatePost, usereventregistrationCouponValidatePost, + usereventregistrationMemberValidatePost, + usereventregistrationRegistrationLimitCheckPost, + usereventregistrationGroupTeamLimitGet, + usereventregistrationProjectCompletionCheckPost, } from '/@/api/match/usereventregistration'; import { teamPageGet, teamDetailGet } from '/@/api/match/unitUser'; import QRCode from 'qrcodejs2-fixes'; @@ -646,6 +716,13 @@ const memberKeyword = ref(''); const memberLoading = ref(false); const selectedMemberIds = ref([]); +// 队员验证相关 +const memberValidationMap = ref({}); // { groupId_memberId: { isValid: boolean, ageValid: boolean, genderValid: boolean, message: string } } +const groupTeamLimitMap = ref({}); // { groupId: { maxTeams: number, currentTeams: number } } +const memberRegistrationLimitMap = ref({}); // { memberId: { isLimited: boolean, message: string } } +const projectCompletionCheckResult = ref(null); // 项目完成人数检查结果 +const registrationLimitCheckResult = ref(null); // 报名限制检查结果 + // Step2: 团队相关 const teamList = ref([]); const teamListLoading = ref(false); @@ -699,6 +776,11 @@ const clearRegisterState = () => { teamMemberLoading.value = false; activeAssignGroupId.value = null; groupMemberMap.value = {}; + memberValidationMap.value = {}; + groupTeamLimitMap.value = {}; + memberRegistrationLimitMap.value = {}; + projectCompletionCheckResult.value = null; + registrationLimitCheckResult.value = null; previewLoading.value = false; previewData.value = null; paymentMethod.value = 1; @@ -814,8 +896,81 @@ const isMemberAssignedToActive = (memberId) => { return list.some((m) => (m.userId ?? m.id) === memberId); }; +// ==================== 队员验证相关函数 ==================== + +// 调用后端API验证队员 +const validateMemberForGroup = async (member, group) => { + if (!currentEventId.value) { + return { isValid: true, ageValid: true, genderValid: true, registrationLimitValid: true, message: '' }; + } + + try { + const { succeeded, data } = await usereventregistrationMemberValidatePost({ + eventId: currentEventId.value, + memberId: member.userId ?? member.id, + groupId: group.id + }); + + if (succeeded && data) { + const key = `${group.id}_${member.userId ?? member.id}`; + memberValidationMap.value[key] = data; + return data; + } + } catch { + console.error('验证队员失败'); + } + + return { isValid: true, ageValid: true, genderValid: true, registrationLimitValid: true, message: '' }; +}; + +// 获取队员验证结果 +const getMemberValidationResult = (member, group) => { + const key = `${group.id}_${member.userId ?? member.id}`; + return memberValidationMap.value[key] || { isValid: true, ageValid: true, genderValid: true, registrationLimitValid: true, message: '' }; +}; + +// ==================== 验证相关计算属性 ==================== + +// 检查某个组别是否有验证错误 +const hasValidationErrorsForGroup = (group) => { + const members = groupMemberMap.value[group.id] || []; + return members.some(member => { + const result = getMemberValidationResult(member, group); + return !result.isValid; + }); +}; + +// 检查是否有任何验证错误 +const hasAnyValidationErrors = () => { + return selectedGroupItems.value.some(group => hasValidationErrorsForGroup(group)); +}; + +// 检查是否有任何报名限制 +const hasAnyRegistrationLimit = () => { + return registrationLimitCheckResult.value?.hasLimit ?? false; +}; + +// 检查是否有项目完成人数错误 +const hasProjectCompletionCountError = () => { + return projectCompletionCheckResult.value?.hasError ?? false; +}; + +// 判断组别是否已达到队伍数量限制 +const isGroupAtTeamLimit = (groupId) => { + const limit = groupTeamLimitMap.value[groupId]; + if (!limit) return false; + return limit.currentTeams >= limit.maxTeams; +}; + +// 是否显示队伍数量限制提示 +const showTeamLimitHint = computed(() => { + return selectedGroupItems.value.some(group => isGroupAtTeamLimit(group.id)); +}); + +// ==================== 队员分配函数 ==================== + // 将成员分配到当前激活的组别 -const assignMemberToActiveGroup = (member) => { +const assignMemberToActiveGroup = async (member) => { const gId = activeAssignGroupId.value; if (!gId) { ElMessage.warning('请先点击左侧的组别卡片'); @@ -829,7 +984,22 @@ const assignMemberToActiveGroup = (member) => { if (groupMemberMap.value[gId].some((m) => (m.userId ?? m.id) === memberId)) { 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; + } + + } + groupMemberMap.value[gId].push({ ...member }); + // 实时检查项目完成人数和报名限制 + await checkProjectCompletion(); + await checkRegistrationLimit(); }; // 从组别中移除成员 @@ -915,6 +1085,64 @@ const handlePrevious = () => { } }; +// 加载组别队伍限制配置 +const fetchGroupTeamLimit = async () => { + if (!currentEventId.value) return; + try { + const { succeeded, data } = await usereventregistrationGroupTeamLimitGet(currentEventId.value); + if (succeeded && data) { + const map = {}; + data.forEach(item => { + map[item.groupId] = { + maxTeams: item.maxTeams, + currentTeams: item.currentTeams + }; + }); + groupTeamLimitMap.value = map; + } + } catch { + console.error('加载队伍限制配置失败'); + } +}; + +// 检查项目完成人数 +const checkProjectCompletion = async () => { + if (!currentEventId.value) return; + const groupMembers = selectedGroupIds.value.map((gId) => ({ + groupId: gId, + memberIds: (groupMemberMap.value[gId] ?? []).map((m) => m.userId ?? m.id), + })); + try { + const { succeeded, data } = await usereventregistrationProjectCompletionCheckPost({ + eventId: currentEventId.value, + groupMembers + }); + if (succeeded) { + projectCompletionCheckResult.value = data; + } + } catch { + console.error('检查项目完成人数失败'); + } +}; + +// 检查报名限制 +const checkRegistrationLimit = async () => { + if (!currentEventId.value) return; + const allMemberIds = [...new Set(Object.values(groupMemberMap.value).flat().map(m => m.userId ?? m.id))]; + try { + const { succeeded, data } = await usereventregistrationRegistrationLimitCheckPost({ + eventId: currentEventId.value, + memberIds: allMemberIds, + groupIds: selectedGroupIds.value + }); + if (succeeded) { + registrationLimitCheckResult.value = data; + } + } catch { + console.error('检查报名限制失败'); + } +}; + const handleStep1Next = async () => { if (!(selectedGroupIds.value?.length ?? 0)) { ElMessage.warning('请先选择报名组别'); @@ -929,11 +1157,31 @@ const handleStep1Next = async () => { } // 加载团队列表 await fetchTeamList(); + // 加载队伍限制配置 + await fetchGroupTeamLimit(); }; const handleStep2Next = async () => { if (!currentEventId.value) return; + // 先检查项目完成人数和报名限制 + await checkProjectCompletion(); + await checkRegistrationLimit(); + + // 检查是否有项目完成人数错误 + if (hasProjectCompletionCountError()) { + const errors = projectCompletionCheckResult.value?.errors ?? []; + ElMessage.error(errors[0]?.errorMessage ?? '项目完成人数有错误,请检查'); + return; + } + + // 检查是否有报名限制 + if (hasAnyRegistrationLimit()) { + const details = registrationLimitCheckResult.value?.limitDetails ?? []; + ElMessage.warning(details[0]?.limitMessage ?? '有队员达到报名限制'); + return; + } + // 检查每个组别是否至少分配了一个队员 const emptyGroups = selectedGroupItems.value.filter((g) => !(groupMemberMap.value[g.id]?.length)); if (emptyGroups.length > 0) { @@ -1487,6 +1735,32 @@ const handleCreateOrderAndPay = async () => { color: #67c23a; } +/* 阶段二验证提示区域 */ +.validation-notes-section { + margin-top: 16px; + padding: 0 16px; +} + +.validation-error-section { + margin-top: 8px; +} + +.validation-error-item { + margin-bottom: 4px; +} + +/* 阶段二底部操作区域 */ +.step2-footer { + margin-top: 24px; + padding: 16px; + background: #f5f7fa; + border-top: 1px solid #e4e7ed; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 12px; +} + /* 支付二维码区域 */ .payment-qrcode-section { text-align: center;