退款页面优化

This commit is contained in:
马梓彭 2026-04-30 09:29:05 +08:00
parent 696746c4da
commit 95146c207b
2 changed files with 252 additions and 145 deletions

View File

@ -307,5 +307,23 @@ export const usereventrecordMemberValidatePost = (data) => {
* }>}
*/
export const usereventrecordRegistrationLimitCheckPost = (data) => {
return http.post('/usereventrecord/registration-limit/check', data);
return http.post("usereventrecord/registration-limit/check", data);
}
/**
* 计算退款金额
* @param {Object} data - 计算退款金额请求
* @param {number} data.RegistrationId - 报名ID
* @param {number} data.CancelType - 取消类型(0-全部取消,1-部分取消)
* @param {Array<number>} [data.PersonIds] - 要取消的队员ID列表(部分取消时使用)
* @param {Array<string>} [data.CancelTeams] - 要取消的团队列表(团队项目部分取消时使用)
* @returns {Promise<{
* RefundAmount: number,
* RemainingRefundableAmount: number,
* RefundedAmount: number,
* ActualPaidAmount: number
* }>}
*/
export const usereventrecordCalculateRefund = (data) => {
return http.post("usereventrecord/cancel/calculate-refund", data);
};

View File

@ -468,10 +468,14 @@
{
'member-selected': selectedMembers.includes(member.memberId),
'team-removed': isTeamRemoved(team.registrationItemId, team.teamIndex),
'disabled': project.projectType === '团队'
'disabled': project.projectType === '团队',
'personal-selected': project.projectType === '个人' && selectedMembers.includes(member.memberId)
}
]"
@click="toggleMember(member.memberId)">
<!-- 个人项目取消遮罩 -->
<div v-if="project.projectType === '个人' && selectedMembers.includes(member.memberId)" class="member-overlay"></div>
{{ member.name }}
<el-tag
v-if="member.isLeader"
@ -480,12 +484,22 @@
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 v-if="project.projectType === '个人'" class="personal-remove-container">
<el-icon
v-if="selectedMembers.includes(member.memberId)"
class="personal-remove-icon"
@click.stop="toggleMember(member.memberId)">
<CircleCheck />
</el-icon>
<el-icon
v-else
class="personal-remove-icon"
@click.stop="toggleMember(member.memberId)">
<Close />
</el-icon>
</div>
</div>
</div>
</div>
@ -518,10 +532,14 @@
{
'member-selected': selectedMembers.includes(member.memberId),
'team-removed': project.projectType === '团队' && teamRemoved,
'disabled': project.projectType === '团队'
'disabled': project.projectType === '团队',
'personal-selected': project.projectType === '个人' && selectedMembers.includes(member.memberId)
}
]"
@click="toggleMember(member.memberId)">
<!-- 个人项目取消遮罩 -->
<div v-if="project.projectType === '个人' && selectedMembers.includes(member.memberId)" class="member-overlay"></div>
{{ member.name }}
<el-tag
v-if="member.isLeader"
@ -530,12 +548,22 @@
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 v-if="project.projectType === '个人'" class="personal-remove-container">
<el-icon
v-if="selectedMembers.includes(member.memberId)"
class="personal-remove-icon"
@click.stop="toggleMember(member.memberId)">
<CircleCheck />
</el-icon>
<el-icon
v-else
class="personal-remove-icon"
@click.stop="toggleMember(member.memberId)">
<Close />
</el-icon>
</div>
</div>
</div>
</div>
@ -555,13 +583,31 @@
</el-alert>
</div>
<!-- 退款信息显示(通用) -->
<div class="refund-info-display" style="background: #f5f7fa; padding: 15px; border-radius: 6px; margin-bottom: 16px;">
<div class="refund-info-item" style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span>实际支付:</span>
<span>¥{{ formatAmount(refundCalculationResult?.actualPaidAmount) }}</span>
</div>
<div v-if="refundCalculationResult?.refundedAmount > 0" class="refund-info-item" style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span>已退款:</span>
<span style="color: #f56c6c;">¥{{ formatAmount(refundCalculationResult?.refundedAmount) }}</span>
</div>
<div class="refund-info-item" style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span>剩余可退款:</span>
<span style="color: #e6a23c; font-weight: bold;">¥{{ formatAmount(refundCalculationResult?.refundableAmount) }}</span>
</div>
<div class="refund-info-item" style="display: flex; justify-content: space-between; padding-top: 8px; border-top: 1px solid #e4e7ed;">
<span style="font-weight: bold;">本次退款:</span>
<span v-if="calculatingRefund" style="font-weight: bold; color: #909399;">计算中...</span>
<span v-else style="font-weight: bold; color: #1fae66;">¥{{ formatAmount(totalRefundAmount) }}</span>
</div>
</div>
<!-- 线下支付退款表单(仅在需要时显示) -->
<template v-if="cancelPreviewData?.needPayeeInfo">
<el-divider content-position="left">退款信息</el-divider>
<el-form :model="refundForm" label-width="120px" style="max-width: 500px;">
<el-form-item label="退款金额">
<span style="font-size: 18px; font-weight: bold; color: #1fae66;">¥{{ totalRefundAmount.toFixed(2) }}</span>
</el-form-item>
<el-divider content-position="left">退款账户信息</el-divider>
<el-form :model="refundForm" label-width="140px" style="max-width: 550px;">
<el-form-item label="收款人姓名" required>
<el-input v-model="refundForm.payeeName" placeholder="请输入收款人姓名" />
</el-form-item>
@ -584,7 +630,7 @@
<template #extra>
<div v-if="cancelResult?.success">
<p v-if="cancelResult?.refundAmount > 0">
退款金额:<span style="font-weight: bold; color: #1fae66;">¥{{ cancelResult?.refundAmount?.toFixed(2) }}</span>
退款金额:<span style="font-weight: bold; color: #1fae66;">¥{{ formatAmount(cancelResult?.refundAmount) }}</span>
</p>
<p v-if="cancelResult?.refundNo">
退款单号:{{ cancelResult?.refundNo }}
@ -603,7 +649,7 @@
</div>
<div class="footer-right">
<span class="refund-label">退款金额</span>
<span class="refund-amount">¥{{ totalRefundAmount.toFixed(2) }}</span>
<span class="refund-amount">¥{{ formatAmount(totalRefundAmount) }}</span>
<el-button @click="isShowCancelDialog = false" class="footer-btn">上一步</el-button>
<el-button
type="danger"
@ -648,6 +694,7 @@ import {
usereventrecordUpdateMembersPost,
usereventrecordMemberValidatePost,
usereventrecordRegistrationLimitCheckPost,
usereventrecordCalculateRefund,
} from '/@/api/match/usereventrecord';
import { teamPageGet, teamDetailGet } from '/@/api/match/unitUser';
@ -1232,126 +1279,88 @@ const mockMembers3 = ref([
]);
// 新的响应式变量
const selectedMembers = ref([]); // 选择的队员ID
const removedTeams = ref(new Set()); // 存储被移除的团队(格式:registrationItemId_teamIndex
const cancelAll = ref(false);
const hasValidationError = ref(false);
const hasLimitError = ref(false);
const hasCountError = ref(false);
const isFreeProject = ref(false); // 控制是否为免费项目
// 线下支付收款人信息
const refundForm = reactive({
payeeName: '',
payeeBankName: '',
payeeBankAccount: ''
});
const totalRefundAmount = computed(() => {
// 免费项目退款金额为0
if (isFreeProject.value || cancelPreviewData.value?.chargeMode === 0) {
return 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 selectedMembers = ref([]); // 选择的队员ID
const removedTeams = ref(new Set()); // 存储被移除的团队(格式:registrationItemId_teamIndex
const cancelAll = ref(false);
const hasValidationError = ref(false);
const hasLimitError = ref(false);
const hasCountError = ref(false);
const isFreeProject = ref(false); // 控制是否为免费项目
// 线下支付收款人信息
const refundForm = reactive({
payeeName: '',
payeeBankName: '',
payeeBankAccount: ''
});
const toggleMember = (memberId) => {
// 安全的金额格式化函数
const formatAmount = (amount) => {
if (amount === null || amount === undefined || isNaN(amount)) {
return '0.00';
}
return Number(amount).toFixed(2);
};
// 后端返回的退款金额数据
const refundCalculationResult = ref({
refundAmount: 0,
refundableAmount: 0,
refundedAmount: 0,
actualPaidAmount: 0
});
const calculatingRefund = ref(false);
// 调用后端计算退款金额的函数
const calculateRefundAmount = async () => {
if (!cancelPreviewData.value?.registrationId) {
return;
}
// 免费项目不计算
if (isFreeProject.value || cancelPreviewData.value?.chargeMode === 0) {
refundCalculationResult.value = {
refundAmount: 0,
refundableAmount: cancelPreviewData.value?.refundableAmount ?? 0,
refundedAmount: cancelPreviewData.value?.refundedAmount ?? 0,
actualPaidAmount: cancelPreviewData.value?.actualPaidAmount ?? 0
};
return;
}
calculatingRefund.value = true;
try {
const payload = {
RegistrationId: cancelPreviewData.value.registrationId,
CancelType: cancelAll.value ? 0 : 1,
PersonIds: cancelAll.value ? undefined : [...selectedMembers.value],
CancelTeams: cancelAll.value ? undefined : [...removedTeams.value]
};
const { succeeded, data } = await usereventrecordCalculateRefund(payload);
if (succeeded && data) {
refundCalculationResult.value = {
// 这里只更新本次退款金额
refundAmount: data.refundAmount ?? 0,
// 剩余可退款、已退款、实际支付都来自预览数据,保持一致
refundableAmount: cancelPreviewData.value?.refundableAmount ?? 0,
refundedAmount: cancelPreviewData.value?.refundedAmount ?? 0,
actualPaidAmount: cancelPreviewData.value?.actualPaidAmount ?? 0
};
}
} catch (e) {
console.error('计算退款金额失败:', e);
} finally {
calculatingRefund.value = false;
}
};
const totalRefundAmount = computed(() => {
return refundCalculationResult.value?.refundAmount ?? 0;
});
const toggleMember = async (memberId) => {
// 如果取消全部被选中,则不允许手动修改
if (cancelAll.value) {
return;
@ -1365,8 +1374,7 @@ const toggleMember = (memberId) => {
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 || []) {
for (const team of group.teams || []) { for (const member of team.members || []) {
if (member.memberId === memberId) {
isTeamMember = true;
break;
@ -1400,6 +1408,9 @@ const toggleMember = (memberId) => {
} else {
selectedMembers.value.push(memberId);
}
// 重新计算退款金额
await calculateRefundAmount();
};
const removeMember = (memberId) => {
@ -1420,7 +1431,7 @@ const isTeamRemoved = (registrationItemId, teamIndex) => {
return removedTeams.value.has(teamKey);
};
const toggleTeam = (registrationItemId, teamIndex) => {
const toggleTeam = async (registrationItemId, teamIndex) => {
const teamKey = `${registrationItemId}_${teamIndex}`;
if (removedTeams.value.has(teamKey)) {
@ -1442,6 +1453,9 @@ const toggleTeam = (registrationItemId, teamIndex) => {
}
}
}
// 重新计算退款金额
await calculateRefundAmount();
};
// 项目级别的团队移除功能(兼容旧逻辑)
@ -1524,7 +1538,7 @@ const toggleTeamForProject = (projectId) => {
};
// 监听取消全部的变化
watch(cancelAll, (newVal) => {
watch(cancelAll, async (newVal) => {
if (newVal) {
// 取消全部:选中所有队员,并取消所有团队
selectedMembers.value = (cancelPreviewData.value?.members || []).map(m => m.memberId);
@ -1544,6 +1558,9 @@ watch(cancelAll, (newVal) => {
selectedMembers.value = [];
removedTeams.value.clear();
}
// 重新计算退款金额
await calculateRefundAmount();
});
const openCancelDialog = async (row) => {
@ -1572,6 +1589,16 @@ const openCancelDialog = async (row) => {
cancelPreviewData.value = data;
// 根据后端返回的数据设置是否为免费项目
isFreeProject.value = data?.chargeMode === 0 || data?.actualPaidAmount <= 0;
// 初始化退款计算结果 - 先使用preview数据
refundCalculationResult.value = {
// 初始状态下没有选择取消项,所以本次退款金额为0
refundAmount: 0,
// 剩余可退款金额直接使用preview返回的数据
refundableAmount: data?.refundableAmount ?? 0,
refundedAmount: data?.refundedAmount ?? 0,
actualPaidAmount: data?.actualPaidAmount ?? 0
};
} else {
ElMessage.error('获取取消报名预览失败');
isShowCancelDialog.value = false;
@ -1632,7 +1659,7 @@ const submitCancel = async () => {
// 添加取消的团队
cancelTeams: Array.from(removedTeams.value),
// 添加计算后的退款金额
calculatedRefundAmount: totalRefundAmount.value
calculatedRefundAmount: totalRefundAmount
};
if (!cancelAll.value) {
@ -2389,6 +2416,68 @@ const submitChangeMembers = async () => {
top: -4px;
}
// 个人项目取消相关样式
.member-tag {
&.personal-selected {
background: white;
position: relative;
overflow: visible; // 保持可见,让图标在容器外显示
}
// 选中状态下的图标样式
&.personal-selected .personal-remove-icon {
color: #1fae66;
border-color: #1fae66;
background: white;
&:hover {
color: #1fae66;
background: rgba(31, 174, 102, 0.1);
}
}
}
.member-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(144, 147, 153, 0.6);
pointer-events: none;
z-index: 1;
border-radius: 20px;
}
.personal-remove-container {
position: absolute;
right: -8px;
top: -8px;
z-index: 2;
}
.personal-remove-icon {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: white;
color: var(--el-text-color-secondary);
border: 2px solid var(--el-border-color-lighter);
border-radius: 50%;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
&:hover {
color: #f56c6c;
border-color: #f56c6c;
background: rgba(245, 108, 108, 0.1);
}
}
// 验证提示
.validation-hints {
margin-bottom: 16px;