赛事记录取消报名

This commit is contained in:
马梓彭 2026-04-28 08:58:13 +08:00
parent bbab43e8fa
commit 696746c4da
2 changed files with 476 additions and 88 deletions

View File

@ -1363,9 +1363,8 @@ const calculateProjectDuration = (item) => {
let error = null;
if (isTeamProject(item.projectType)) {
// 团队项目:计算队伍数(这里暂时用人数模拟,实际应该统计团队数)
// 由于团队数需要从后端获取,这里先使用人数模拟
const teamCount = totalParticipants > 0 ? Math.ceil(totalParticipants / 5) : 1; // 假设每个团队5人
// 团队项目:使用后端返回的团队总数
const teamCount = item.totalTeams > 0 ? item.totalTeams : 1;
totalRounds = Math.ceil(teamCount / venueCount);
} else {
// 个人项目

View File

@ -433,53 +433,113 @@
</template>
</el-alert>
<!-- 项目和队员列表 -->
<!-- 项目和队员列表 - 使用新的数据结构 -->
<div class="project-list">
<!-- 使用真实数据渲染项目 -->
<div
v-for="project in cancelPreviewData?.projects || []"
:key="project.projectId"
class="project-card">
<div class="project-header">
<span class="project-tag">({{ project.projectType }}/{{ project.personCount }}) {{ project.projectName }}</span>
<el-icon
v-if="project.projectType === '团队'"
class="remove-team-icon"
:class="{ 'team-removed': teamRemoved }"
@click="toggleTeam()">
<Close v-if="!teamRemoved" /><CircleCheck v-else />
</el-icon>
</div>
<div class="member-group">
<div
v-for="member in cancelPreviewData?.members || []"
:key="member.memberId"
:class="[
'member-tag',
{
'member-selected': selectedMembers.includes(member.memberId),
'team-removed': project.projectType === '团队' && teamRemoved,
'disabled': project.projectType === '团队'
}
]"
@click="toggleMember(member.memberId)">
{{ member.name }}
<el-tag
v-if="member.isLeader"
size="small"
type="success"
style="margin-left: 4px; font-size: 10px;">
队长
</el-tag>
<el-icon
v-if="project.projectType === '个人' && selectedMembers.includes(member.memberId)"
class="remove-icon"
@click.stop="removeMember(member.memberId)">
<Close />
</el-icon>
<!-- 使用新的 projectDetails 数据渲染 -->
<template v-if="cancelPreviewData?.projectDetails && cancelPreviewData.projectDetails.length > 0">
<div
v-for="project in cancelPreviewData.projectDetails"
:key="project.projectId"
class="project-card">
<!-- 按组别显示,每个组别直接显示合并的标题 -->
<div v-for="group in project.groups" :key="group.registrationItemId" class="group-container">
<div class="project-header">
<span class="project-tag">{{ formatCancelPreviewItemTitle(project, group) }}</span>
</div>
<!-- 每个团队独立显示 -->
<div v-for="(team, loopTeamIndex) in group.teams" :key="'team-' + team.registrationItemId + '-' + team.teamIndex" class="team-container">
<div class="team-header" v-if="project.projectType === '团队'">
<span class="team-label">团队 {{ loopTeamIndex + 1 }}</span>
<el-icon
class="remove-team-icon"
:class="{ 'team-removed': isTeamRemoved(team.registrationItemId, team.teamIndex) }"
@click="toggleTeam(team.registrationItemId, team.teamIndex)">
<Close v-if="!isTeamRemoved(team.registrationItemId, team.teamIndex)" /><CircleCheck v-else />
</el-icon>
</div>
<div class="member-group">
<div
v-for="member in team.members"
:key="member.memberId"
:class="[
'member-tag',
{
'member-selected': selectedMembers.includes(member.memberId),
'team-removed': isTeamRemoved(team.registrationItemId, team.teamIndex),
'disabled': project.projectType === '团队'
}
]"
@click="toggleMember(member.memberId)">
{{ member.name }}
<el-tag
v-if="member.isLeader"
size="small"
type="success"
style="margin-left: 4px; font-size: 10px;">
队长
</el-tag>
<el-icon
v-if="project.projectType === '个人' && selectedMembers.includes(member.memberId)"
class="remove-icon"
@click.stop="removeMember(member.memberId)">
<Close />
</el-icon>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- 向后兼容:使用旧的 projects 和 members 数据 -->
<template v-else>
<div
v-for="project in cancelPreviewData?.projects || []"
:key="project.projectId"
class="project-card">
<div class="project-header">
<span class="project-tag">({{ project.projectType }}/{{ project.personCount }}) {{ project.projectName }}</span>
<el-icon
v-if="project.projectType === '团队'"
class="remove-team-icon"
:class="{ 'team-removed': teamRemoved }"
@click="toggleTeam()">
<Close v-if="!teamRemoved" /><CircleCheck v-else />
</el-icon>
</div>
<div class="member-group">
<div
v-for="member in cancelPreviewData?.members || []"
:key="member.memberId"
:class="[
'member-tag',
{
'member-selected': selectedMembers.includes(member.memberId),
'team-removed': project.projectType === '团队' && teamRemoved,
'disabled': project.projectType === '团队'
}
]"
@click="toggleMember(member.memberId)">
{{ member.name }}
<el-tag
v-if="member.isLeader"
size="small"
type="success"
style="margin-left: 4px; font-size: 10px;">
队长
</el-tag>
<el-icon
v-if="project.projectType === '个人' && selectedMembers.includes(member.memberId)"
class="remove-icon"
@click.stop="removeMember(member.memberId)">
<Close />
</el-icon>
</div>
</div>
</div>
</template>
</div>
<!-- 验证提示 -->
@ -807,22 +867,41 @@ const formatRefundRules = (rules) => {
// 格式化项目标题
const formatItemTitle = (item) => {
const projectType = item.projectTypeName || '';
// 使用报名项目人数作为完成人数显示
const completionCount = item.personCount ?? 0;
const projectName = item.projectName || '';
const groupName = item.groupName || '';
const dateRange = formatGroupDate(item);
const genderReq = formatGenderRequirement(item.genderRequirement);
let title = `(${projectType}/${completionCount}) ${projectName}-${groupName}`;
if (dateRange) {
title += `/${dateRange}`;
}
if (genderReq) {
title += `/${genderReq}`;
}
return title;
const projectType = item.projectTypeName || '';
// 使用报名项目人数作为完成人数显示
const completionCount = item.personCount ?? 0;
const projectName = item.projectName || '';
const groupName = item.groupName || '';
const dateRange = formatGroupDate(item);
const genderReq = formatGenderRequirement(item.genderRequirement);
let title = `(${projectType}/${completionCount}) ${projectName}-${groupName}`;
if (dateRange) {
title += `/${dateRange}`;
}
if (genderReq) {
title += `/${genderReq}`;
}
return title;
};
// 格式化取消报名预览中的项目组别标题
const formatCancelPreviewItemTitle = (project, group) => {
const projectType = group.projectTypeName || '';
const completionCount = group.personCount ?? 0;
const projectName = project.projectName || '';
const groupName = group.groupName || '';
const dateRange = formatGroupDate(group);
const genderReq = formatGenderRequirement(group.genderRequirement);
let title = `(${projectType}/${completionCount}) ${projectName}-${groupName}`;
if (dateRange) {
title += `/${dateRange}`;
}
if (genderReq) {
title += `/${genderReq}`;
}
return title;
};
const onOpenMenu = async (row) => {
@ -1153,8 +1232,8 @@ const mockMembers3 = ref([
]);
// 新的响应式变量
const selectedMembers = ref([]);
const teamRemoved = ref(false);
const selectedMembers = ref([]); // 选择的队员ID
const removedTeams = ref(new Set()); // 存储被移除的团队(格式:registrationItemId_teamIndex
const cancelAll = ref(false);
const hasValidationError = ref(false);
const hasLimitError = ref(false);
@ -1169,12 +1248,107 @@ const mockMembers3 = ref([
const totalRefundAmount = computed(() => {
// 免费项目退款金额为0
if (isFreeProject.value) {
if (isFreeProject.value || cancelPreviewData.value?.chargeMode === 0) {
return 0;
}
// 使用后端返回的可退款金额
return cancelPreviewData.value?.refundableAmount ?? 0;
// 如果取消全部,直接返回可退款金额
if (cancelAll.value) {
return cancelPreviewData.value?.refundableAmount ?? 0;
}
// 根据收费模式动态计算退款金额
const chargeMode = cancelPreviewData.value?.chargeMode;
const refundableAmount = cancelPreviewData.value?.refundableAmount ?? 0;
const actualPaidAmount = cancelPreviewData.value?.actualPaidAmount ?? 0;
// 计算退款比例
let refundRatio = 1;
if (actualPaidAmount > 0 && refundableAmount >= 0) {
refundRatio = refundableAmount / actualPaidAmount;
}
let calculatedAmount = 0;
switch (chargeMode) {
case 1: // 定价(赛事)
// 定价赛事:个人取消不退款,团队取消按比例退款
// 检查是否有团队被取消
const hasRemovedTeam = removedTeams.value.size > 0;
if (hasRemovedTeam) {
calculatedAmount = refundableAmount;
}
break;
case 2: // 单价(赛事)
// 单价赛事:个人取消按比例退款,团队取消按比例退款
// 统计取消的人数
let cancelCount = 0;
// 统计团队被取消的团队中的队员数量
for (const project of cancelPreviewData.value?.projectDetails || []) {
for (const group of project.groups || []) {
for (const team of group.teams || []) {
const teamKey = `${team.registrationItemId}_${team.teamIndex}`;
if (removedTeams.value.has(teamKey)) {
cancelCount += (team.members?.length || 0);
} else {
// 统计该团队中被单独取消的队员
for (const member of team.members || []) {
if (selectedMembers.value.includes(member.memberId)) {
cancelCount++;
}
}
}
}
}
}
// 统计总人数
let totalCount = 0;
for (const member of cancelPreviewData.value?.members || []) {
totalCount++;
}
// 计算单价
const unitPriceEvent = totalCount > 0 ? actualPaidAmount / totalCount : 0;
calculatedAmount = unitPriceEvent * cancelCount * refundRatio;
break;
case 3: // 单价(项目)
// 单价项目:个人取消按比例退款,团队取消按比例退款
let projectCancelAmount = 0;
for (const project of cancelPreviewData.value?.projectDetails || []) {
for (const group of project.groups || []) {
const unitPrice = group.unitPrice || 0;
for (const team of group.teams || []) {
const teamKey = `${team.registrationItemId}_${team.teamIndex}`;
if (removedTeams.value.has(teamKey)) {
// 团队取消:整个团队都取消
projectCancelAmount += unitPrice * (team.members?.length || 0);
} else {
// 统计该团队中被单独取消的队员
for (const member of team.members || []) {
if (selectedMembers.value.includes(member.memberId)) {
projectCancelAmount += unitPrice;
}
}
}
}
}
}
calculatedAmount = projectCancelAmount * refundRatio;
break;
default:
calculatedAmount = 0;
}
return Math.max(0, calculatedAmount);
});
const toggleMember = (memberId) => {
@ -1183,12 +1357,40 @@ const toggleMember = (memberId) => {
return;
}
// 团队项目队员不允许手动选择
const teamMemberIds = (cancelPreviewData.value?.projects || [])
.filter(p => p.projectType === '团队')
.flatMap(p => (cancelPreviewData.value?.members || []).map(m => m.memberId));
// 检查该队员是否属于团队项目
let isTeamMember = false;
if (teamMemberIds.includes(memberId)) {
// 检查新的数据结构
if (cancelPreviewData.value?.projectDetails && cancelPreviewData.value.projectDetails.length > 0) {
for (const project of cancelPreviewData.value.projectDetails) {
if (project.projectType === '团队') {
for (const group of project.groups || []) {
for (const team of group.teams || []) {
for (const member of team.members || []) {
if (member.memberId === memberId) {
isTeamMember = true;
break;
}
}
if (isTeamMember) break;
}
if (isTeamMember) break;
}
if (isTeamMember) break;
}
}
} else {
// 检查旧的数据结构
const teamMemberIds = (cancelPreviewData.value?.projects || [])
.filter(p => p.projectType === '团队')
.flatMap(p => (cancelPreviewData.value?.members || []).map(m => m.memberId));
if (teamMemberIds.includes(memberId)) {
isTeamMember = true;
}
}
if (isTeamMember) {
return;
}
@ -1212,27 +1414,135 @@ const removeMember = (memberId) => {
}
};
const toggleTeam = () => {
teamRemoved.value = !teamRemoved.value;
if (teamRemoved.value) {
// 团队被移除时,清空该团队的队员选择
const teamMemberIds = (cancelPreviewData.value?.members || [])
.filter(m => !m.isLeader)
.map(m => m.memberId);
selectedMembers.value = selectedMembers.value.filter(id => !teamMemberIds.includes(id));
// 团队级别的移除功能
const isTeamRemoved = (registrationItemId, teamIndex) => {
const teamKey = `${registrationItemId}_${teamIndex}`;
return removedTeams.value.has(teamKey);
};
const toggleTeam = (registrationItemId, teamIndex) => {
const teamKey = `${registrationItemId}_${teamIndex}`;
if (removedTeams.value.has(teamKey)) {
removedTeams.value.delete(teamKey);
} else {
removedTeams.value.add(teamKey);
// 移除该团队下所有队员的选择
for (const project of cancelPreviewData.value?.projectDetails || []) {
for (const group of project.groups || []) {
for (const team of group.teams || []) {
if (team.registrationItemId === registrationItemId && team.teamIndex === teamIndex) {
const teamMemberIds = (team.members || [])
.filter(m => !m.isLeader)
.map(m => m.memberId);
selectedMembers.value = selectedMembers.value.filter(id => !teamMemberIds.includes(id));
}
}
}
}
}
};
// 项目级别的团队移除功能(兼容旧逻辑)
const isTeamRemovedForProject = (projectId) => {
// 检查该项目下是否有任何团队被移除
for (const project of cancelPreviewData.value?.projectDetails || []) {
if (project.projectId === projectId) {
for (const group of project.groups || []) {
for (const team of group.teams || []) {
const teamKey = `${team.registrationItemId}_${team.teamIndex}`;
if (removedTeams.value.has(teamKey)) {
return true;
}
}
}
}
}
return false;
};
const toggleTeamForProject = (projectId) => {
// 切换该项目下所有团队的移除状态
let hasAnyRemoved = false;
for (const project of cancelPreviewData.value?.projectDetails || []) {
if (project.projectId === projectId) {
for (const group of project.groups || []) {
for (const team of group.teams || []) {
const teamKey = `${team.registrationItemId}_${team.teamIndex}`;
if (removedTeams.value.has(teamKey)) {
hasAnyRemoved = true;
break;
}
}
}
}
}
if (hasAnyRemoved) {
// 如果有任何团队被移除,则移除所有团队
for (const project of cancelPreviewData.value?.projectDetails || []) {
if (project.projectId === projectId) {
for (const group of project.groups || []) {
for (const team of group.teams || []) {
const teamKey = `${team.registrationItemId}_${team.teamIndex}`;
removedTeams.value.delete(teamKey);
}
}
}
}
} else {
// 如果没有团队被移除,则添加所有团队
for (const project of cancelPreviewData.value?.projectDetails || []) {
if (project.projectId === projectId) {
for (const group of project.groups || []) {
for (const team of group.teams || []) {
const teamKey = `${team.registrationItemId}_${team.teamIndex}`;
removedTeams.value.add(teamKey);
}
}
}
}
// 移除该项目下所有队员的选择
for (const project of cancelPreviewData.value?.projectDetails || []) {
if (project.projectId === projectId) {
const teamMemberIds = [];
for (const group of project.groups || []) {
for (const team of group.teams || []) {
for (const member of team.members || []) {
if (!member.isLeader) {
teamMemberIds.push(member.memberId);
}
}
}
}
selectedMembers.value = selectedMembers.value.filter(id => !teamMemberIds.includes(id));
}
}
}
};
// 监听取消全部的变化
watch(cancelAll, (newVal) => {
if (newVal) {
// 取消全部:选中所有队员,并取消团队
// 取消全部:选中所有队员,并取消所有团队
selectedMembers.value = (cancelPreviewData.value?.members || []).map(m => m.memberId);
teamRemoved.value = true;
// 处理新数据结构中的所有团队
removedTeams.value.clear();
for (const project of cancelPreviewData.value?.projectDetails || []) {
for (const group of project.groups || []) {
for (const team of group.teams || []) {
const teamKey = `${team.registrationItemId}_${team.teamIndex}`;
removedTeams.value.add(teamKey);
}
}
}
} else {
// 取消取消全部:清空选择
selectedMembers.value = [];
teamRemoved.value = false;
removedTeams.value.clear();
}
});
@ -1244,7 +1554,7 @@ const openCancelDialog = async (row) => {
cancelPreviewData.value = null;
cancelResult.value = null;
selectedMembers.value = [];
teamRemoved.value = false;
removedTeams.value.clear();
cancelAll.value = false;
hasValidationError.value = false;
hasLimitError.value = false;
@ -1294,16 +1604,39 @@ const submitCancel = async () => {
}
}
// 收集所有要取消的人员ID
const allCancelMemberIds = new Set(selectedMembers.value);
// 添加被取消团队中的所有队员
for (const project of cancelPreviewData.value?.projectDetails || []) {
for (const group of project.groups || []) {
for (const team of group.teams || []) {
const teamKey = `${team.registrationItemId}_${team.teamIndex}`;
if (removedTeams.value.has(teamKey)) {
for (const member of team.members || []) {
allCancelMemberIds.add(member.memberId);
}
}
}
}
}
const finalPersonIds = Array.from(allCancelMemberIds);
cancelSubmitting.value = true;
try {
const payload = {
registrationId: cancelPreviewData.value.registrationId,
cancelType: cancelAll.value ? 0 : 1,
cancelReason: '个人原因'
cancelReason: '个人原因',
// 添加取消的团队
cancelTeams: Array.from(removedTeams.value),
// 添加计算后的退款金额
calculatedRefundAmount: totalRefundAmount.value
};
if (!cancelAll.value) {
payload.personIds = selectedMembers.value;
payload.personIds = finalPersonIds;
}
// 如果是线下支付且已支付,添加收款人信息
@ -1361,7 +1694,7 @@ const onCancelDialogClosed = () => {
cancelResult.value = null;
// 按顺序重置,避免 watch 触发问题
selectedMembers.value = [];
teamRemoved.value = false;
removedTeams.value.clear();
cancelAll.value = false;
hasValidationError.value = false;
hasLimitError.value = false;
@ -1926,6 +2259,62 @@ const submitChangeMembers = async () => {
font-weight: 500;
}
// 组别容器
.group-container {
margin-bottom: 12px;
}
.group-header {
display: inline-block;
padding: 4px 10px;
background: #f0f7ff;
color: #409eff;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
margin-bottom: 8px;
}
// 团队容器
.team-container {
margin-left: 16px;
margin-bottom: 8px;
}
.team-name {
font-size: 13px;
color: var(--el-text-color-secondary);
margin-bottom: 6px;
padding-left: 4px;
}
.team-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #e4e7ed;
}
.team-label {
font-size: 13px;
font-weight: 500;
color: #606266;
}
.team-container {
margin-bottom: 12px;
padding: 12px;
background: #f5f7fa;
border-radius: 8px;
border: 1px dashed #dcdfe6;
&:last-child {
margin-bottom: 0;
}
}
.remove-team-icon {
cursor: pointer;
color: var(--el-text-color-secondary);