赛事流程优化
This commit is contained in:
parent
0d98b72ab5
commit
8ecd8ffe45
@ -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<number>} 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);
|
||||
};
|
||||
|
||||
|
||||
@ -75,11 +75,11 @@
|
||||
<span>拖动项目卡片可调整顺序,按最终顺序导出</span>
|
||||
</div>
|
||||
<div ref="arrangementContainerRef" class="arrangement-cards-wrapper">
|
||||
<div class="arrangement-card" v-for="(item, index) in eventlist" :key="index">
|
||||
<div class="arrangement-card" v-for="(item, index) in eventlist" :key="item._tempId">
|
||||
<div class="card-left">
|
||||
<div class="project-name">{{ item.projectName }}</div>
|
||||
<div class="project-type">类型: {{ item.projectType }}</div>
|
||||
<div class="project-duration">时长: {{ formatDuration(item.durationSeconds) }}</div>
|
||||
<div class="project-duration">时长: {{ formatDurationChinese(item.durationSeconds) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-middle">
|
||||
@ -106,7 +106,7 @@
|
||||
</div>
|
||||
<div class="duration-box">
|
||||
<div class="box-label">项目所需时长</div>
|
||||
<div class="duration-value">{{ formatDuration(item.totalDurationSeconds || 0) }}</div>
|
||||
<div class="duration-value">{{ formatDurationSymbol(item.totalDurationSeconds || 0) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -114,7 +114,6 @@
|
||||
|
||||
<div class="action-bar">
|
||||
<el-button type="default" @click="previewArrangement">预览</el-button>
|
||||
<el-button type="primary" @click="onSave">保存修改</el-button>
|
||||
<el-button type="primary" @click="getArr">导出</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@ -264,7 +263,7 @@
|
||||
:rowspan="getTimeRowspan(index)"
|
||||
class="time-cell"
|
||||
>
|
||||
{{ formatTime(item.timeRange) }}
|
||||
{{ item.projectTotalDuration }}
|
||||
</td>
|
||||
<td class="project-cell">{{ item.projectName }}</td>
|
||||
<td class="group-cell">{{ item.groupName }}</td>
|
||||
@ -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);
|
||||
|
||||
@ -2,9 +2,7 @@
|
||||
<div class="system-menu-container layout-padding">
|
||||
<ProTable ref="tableRef" :request-api="usereventrecordListGet" :columns="columns" :pagination="true"
|
||||
:tool-button="false" :init-param="initParam" row-key="registrationId">
|
||||
<template #status="scope">
|
||||
<el-tag :type="tagTypeForStatus(scope.row.status)">{{ tagTextForStatus(scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
|
||||
<template #payStatus="scope">
|
||||
<el-tag :type="tagTypeForPayStatus(scope.row.payStatus)">{{ tagTextForPayStatus(scope.row.payStatus) }}</el-tag>
|
||||
</template>
|
||||
@ -81,9 +79,7 @@
|
||||
<el-descriptions :column="2" border size="small" v-if="currentRow">
|
||||
<el-descriptions-item label="赛事名称">{{ currentRow.eventName ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="参赛队伍">{{ currentRow.teamName ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报名状态">
|
||||
<el-tag :type="tagTypeForStatus(currentRow.status)">{{ tagTextForStatus(currentRow.status) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="支付状态">
|
||||
<el-tag :type="tagTypeForPayStatus(currentRow.payStatus)">{{ tagTextForPayStatus(currentRow.payStatus) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
@ -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);
|
||||
};
|
||||
|
||||
// 格式化日期
|
||||
|
||||
@ -300,6 +300,19 @@
|
||||
|
||||
<!-- 验证提示区域 -->
|
||||
<div class="validation-notes-section">
|
||||
<!-- 验证加载中提示 -->
|
||||
<el-alert
|
||||
v-if="validationLoading"
|
||||
title="正在验证队员信息..."
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 12px;"
|
||||
>
|
||||
<template #icon>
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<!-- 年龄性别不符合要求提示 -->
|
||||
<el-alert
|
||||
v-if="hasAnyValidationErrors()"
|
||||
@ -929,6 +942,8 @@ const memberValidationMap = ref({}); // { groupId_memberId: { isValid: boolean,
|
||||
const groupTeamLimitMap = ref({}); // { groupId: { maxTeams: number, currentTeams: number } }
|
||||
const memberRegistrationLimitMap = ref({}); // { memberId: { isLimited: boolean, message: string } }
|
||||
const registrationLimitCheckResult = ref(null); // 报名限制检查结果
|
||||
const validationLoading = ref(false); // 验证加载状态
|
||||
let validationDebounceTimer = null; // 防抖定时器
|
||||
|
||||
// Step2: 团队相关
|
||||
const teamList = ref([]);
|
||||
@ -997,6 +1012,11 @@ const clearRegisterState = () => {
|
||||
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);
|
||||
|
||||
// 开始轮询支付状态
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user