赛事记录退款,赛事报名限制
This commit is contained in:
parent
c9e80c3a87
commit
150c7f26b9
@ -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<string>
|
||||
* }>}
|
||||
*/
|
||||
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<number>} [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);
|
||||
};
|
||||
@ -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<number>} data.memberIds - 队员ID列表
|
||||
* @param {Array<number>} 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<number>}>} 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);
|
||||
};
|
||||
@ -25,11 +25,7 @@
|
||||
<template #action="scope">
|
||||
<el-space wrap>
|
||||
<template v-if="!isCancelDisabledByRow(scope.row)">
|
||||
<el-popconfirm title="确认取消报名吗?" @confirm="onCancelRegistration(scope.row.registrationId)">
|
||||
<template #reference>
|
||||
<el-button type="danger" size="small" text>取消报名</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
<el-button type="danger" size="small" text @click="openCancelDialog(scope.row)">取消报名</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button type="danger" size="small" disabled text>取消报名</el-button>
|
||||
@ -78,11 +74,7 @@
|
||||
|
||||
<div style="margin: 14px 0;">
|
||||
<el-space wrap>
|
||||
<el-popconfirm title="确认取消报名吗?" @confirm="onCancelRegistration(currentRow?.registrationId)" v-if="currentRow">
|
||||
<template #reference>
|
||||
<el-button type="danger" size="small" :disabled="disableCancel">取消报名</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
<el-button type="danger" size="small" :disabled="disableCancel" @click="openCancelDialog(currentRow)" v-if="currentRow">取消报名</el-button>
|
||||
<el-button type="primary" size="small" @click="openMaterialDialog(currentRow)" v-if="currentRow">上传素材</el-button>
|
||||
<el-button type="primary" size="small" @click="openVoucherDialog(currentRow)" v-if="currentRow">上传凭证</el-button>
|
||||
<el-button size="small" @click="openCertificateDialog" v-if="currentRow">查看证书</el-button>
|
||||
@ -198,13 +190,147 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 取消报名对话框 -->
|
||||
<el-dialog title="取消报名" v-model="isShowCancelDialog" width="900px" :close-on-click-modal="false" @closed="onCancelDialogClosed">
|
||||
<el-skeleton :rows="10" animated v-if="cancelLoading" />
|
||||
<template v-else>
|
||||
<!-- 预览阶段 -->
|
||||
<template v-if="!isCancelConfirmed">
|
||||
|
||||
<!-- 退款规则提示 -->
|
||||
<el-alert title="注意" type="warning" :closable="false" show-icon style="margin-bottom: 16px;">
|
||||
<template #default>
|
||||
<div>
|
||||
<p>• 退款规则:开赛前7天扣除金额5%,3-7天扣除金额10%,3天内扣除金额20%</p>
|
||||
<p>• 退款路径:原路退款,请注意查收</p>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<!-- 项目和队员列表 -->
|
||||
<div class="project-list">
|
||||
<!-- 示例项目1 - 个人项目 -->
|
||||
<div class="project-card">
|
||||
<div class="project-header">
|
||||
<span class="project-tag">(个人/1) 30秒单摇-少年儿童组/18-20/男</span>
|
||||
</div>
|
||||
<div class="member-group">
|
||||
<div
|
||||
v-for="member in mockMembers1"
|
||||
:key="member.id"
|
||||
:class="['member-tag', { 'member-selected': selectedMembers.includes(member.id) }]"
|
||||
@click="toggleMember(member.id)">
|
||||
{{ member.name }}
|
||||
<el-icon v-if="selectedMembers.includes(member.id)" class="remove-icon" @click.stop="removeMember(member.id)">
|
||||
<Close />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 示例项目2 - 团队项目 -->
|
||||
<div class="project-card">
|
||||
<div class="project-header">
|
||||
<span class="project-tag">(团队/3) 交互绳-少年儿童组/10-24/女</span>
|
||||
<el-icon
|
||||
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 mockMembers2"
|
||||
:key="member.id"
|
||||
:class="['member-tag', { 'team-removed': teamRemoved, 'disabled': true }]">
|
||||
{{ member.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-group" v-if="mockMembers2.length > 2">
|
||||
<div
|
||||
v-for="member in mockMembers3"
|
||||
:key="member.id"
|
||||
:class="['member-tag', { 'team-removed': teamRemoved, 'disabled': true }]">
|
||||
{{ member.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 验证提示 -->
|
||||
<div class="validation-hints">
|
||||
<el-alert v-if="hasValidationError" type="error" :closable="false" show-icon style="margin-bottom: 8px;">
|
||||
队员信息不符合组别要求
|
||||
</el-alert>
|
||||
<el-alert v-if="hasLimitError" type="warning" :closable="false" show-icon style="margin-bottom: 8px;">
|
||||
此队员已达到报名限制
|
||||
</el-alert>
|
||||
<el-alert v-if="hasCountError" type="error" :closable="false" show-icon>
|
||||
项目完成人数错误
|
||||
</el-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 确认阶段 -->
|
||||
<template v-else>
|
||||
<el-result
|
||||
:icon="cancelResult?.success ? 'success' : 'error'"
|
||||
:title="cancelResult?.success ? '取消报名成功' : '取消报名失败'"
|
||||
:sub-title="cancelResult?.message">
|
||||
<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>
|
||||
</p>
|
||||
<p v-if="cancelResult?.refundNo">
|
||||
退款单号:{{ cancelResult?.refundNo }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</el-result>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<template #footer v-if="!isCancelConfirmed">
|
||||
<div class="footer-bar">
|
||||
<div class="footer-left">
|
||||
<el-checkbox v-model="cancelAll">取消全部</el-checkbox>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<span class="refund-label">退款金额</span>
|
||||
<span class="refund-amount">¥{{ totalRefundAmount.toFixed(2) }}</span>
|
||||
<el-button @click="isShowCancelDialog = false" class="footer-btn">上一步</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
:loading="cancelSubmitting"
|
||||
@click="submitCancel"
|
||||
class="footer-btn primary">
|
||||
确认取消
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-note">
|
||||
<el-link type="danger" :underline="false">取消报名后优惠券不予退还</el-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer v-else>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="closeCancelDialog" v-if="cancelResult?.success">关闭</el-button>
|
||||
<el-button @click="resetCancelConfirm" v-else>重试</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { UploadFilled } from '@element-plus/icons-vue';
|
||||
import { UploadFilled, ArrowRight, Close, CircleCheck } from '@element-plus/icons-vue';
|
||||
import ProTable from '/@/components/ProTable/index.vue';
|
||||
import {
|
||||
usereventrecordListGet,
|
||||
@ -213,6 +339,8 @@ import {
|
||||
usereventrecordVoucherPost,
|
||||
usereventrecordMembersGet,
|
||||
usereventrecordCertificateGet,
|
||||
usereventrecordCancelPreviewGet,
|
||||
usereventrecordCancelExecutePost,
|
||||
} from '/@/api/match/usereventrecord';
|
||||
|
||||
const tableRef = ref();
|
||||
@ -287,14 +415,18 @@ const tagTextForCertStatus = (v) => certTagMap[normalizeToNumber(v)]?.text ?? (v
|
||||
|
||||
const disableCancel = computed(() => {
|
||||
if (!currentRow.value) return true;
|
||||
const st = normalizeToNumber(currentRow.value.status);
|
||||
return st === 2 || st === 3 || st === 4;
|
||||
const status = normalizeToNumber(currentRow.value.status);
|
||||
const payStatus = normalizeToNumber(currentRow.value.payStatus);
|
||||
// 只有报名状态为已支付(1) 且 支付状态为已支付(1),才能取消/退款
|
||||
return !(status === 1 && payStatus === 1);
|
||||
});
|
||||
|
||||
const isCancelDisabledByRow = (row) => {
|
||||
if (!row) return true;
|
||||
const st = normalizeToNumber(row.status);
|
||||
return st === 2 || st === 3 || st === 4;
|
||||
const status = normalizeToNumber(row.status);
|
||||
const payStatus = normalizeToNumber(row.payStatus);
|
||||
// 只有报名状态为已支付(1) 且 支付状态为已支付(1),才能取消/退款
|
||||
return !(status === 1 && payStatus === 1);
|
||||
};
|
||||
|
||||
const onOpenMenu = async (row) => {
|
||||
@ -502,6 +634,232 @@ const openCertificateDialog = async () => {
|
||||
certLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 取消报名
|
||||
const isShowCancelDialog = ref(false);
|
||||
const cancelLoading = ref(false);
|
||||
const cancelSubmitting = ref(false);
|
||||
const isCancelConfirmed = ref(false);
|
||||
const cancelPreviewData = ref(null);
|
||||
const cancelResult = ref(null);
|
||||
|
||||
// 示例数据(实际应从后端获取)
|
||||
const mockMembers1 = ref([
|
||||
{ id: 1, name: '崔一' },
|
||||
{ id: 2, name: '崔一' }
|
||||
]);
|
||||
|
||||
const mockMembers2 = ref([
|
||||
{ id: 3, name: '崔一' },
|
||||
{ id: 4, name: '崔三' }
|
||||
]);
|
||||
|
||||
const mockMembers3 = ref([
|
||||
{ id: 5, name: '崔一' },
|
||||
{ id: 6, name: '崔三' }
|
||||
]);
|
||||
|
||||
// 新的响应式变量
|
||||
const selectedMembers = ref([]);
|
||||
const teamRemoved = ref(false);
|
||||
const cancelAll = ref(false);
|
||||
const hasValidationError = ref(false);
|
||||
const hasLimitError = ref(false);
|
||||
const hasCountError = ref(false);
|
||||
const isFreeProject = ref(false); // 控制是否为免费项目
|
||||
|
||||
// 每个项目的状态(根据项目标签判断类型)
|
||||
const isProjectTeamType = (projectTag) => {
|
||||
// 判断是否为团队项目:标签包含"团队"
|
||||
return projectTag && projectTag.includes('团队');
|
||||
};
|
||||
|
||||
const totalRefundAmount = computed(() => {
|
||||
// 免费项目退款金额为0
|
||||
if (isFreeProject.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 示例:每个队员100元
|
||||
const memberPrice = 100;
|
||||
let total = 0;
|
||||
|
||||
if (cancelAll.value) {
|
||||
// 取消全部:所有个人项目队员 + 整个团队
|
||||
// 个人项目队员
|
||||
total += mockMembers1.value.length * memberPrice;
|
||||
// 团队项目(整个团队)
|
||||
total += (mockMembers2.value.length + mockMembers3.value.length) * memberPrice;
|
||||
} else {
|
||||
// 个人项目:按选中的队员计算(只计算个人项目的队员,不包括团队项目)
|
||||
// 个人项目队员ID:1, 2
|
||||
const personalMemberIds = [1, 2];
|
||||
const selectedPersonalMembers = selectedMembers.value.filter(id => personalMemberIds.includes(id));
|
||||
total += selectedPersonalMembers.length * memberPrice;
|
||||
|
||||
// 团队项目:如果团队被取消,则计算整个团队
|
||||
if (teamRemoved.value) {
|
||||
total += (mockMembers2.value.length + mockMembers3.value.length) * memberPrice;
|
||||
}
|
||||
}
|
||||
|
||||
// 边界条件:确保金额不小于0
|
||||
return Math.max(0, total);
|
||||
});
|
||||
|
||||
const toggleMember = (memberId) => {
|
||||
// 如果取消全部被选中,则不允许手动修改
|
||||
if (cancelAll.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 团队项目队员不允许手动选择
|
||||
const teamMemberIds = [3, 4, 5, 6];
|
||||
if (teamMemberIds.includes(memberId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = selectedMembers.value.indexOf(memberId);
|
||||
if (index > -1) {
|
||||
selectedMembers.value.splice(index, 1);
|
||||
} else {
|
||||
selectedMembers.value.push(memberId);
|
||||
}
|
||||
};
|
||||
|
||||
const removeMember = (memberId) => {
|
||||
// 如果取消全部被选中,则不允许手动修改
|
||||
if (cancelAll.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = selectedMembers.value.indexOf(memberId);
|
||||
if (index > -1) {
|
||||
selectedMembers.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleTeam = () => {
|
||||
teamRemoved.value = !teamRemoved.value;
|
||||
if (teamRemoved.value) {
|
||||
// 团队被移除时,清空该团队的队员选择
|
||||
const teamMemberIds = [...mockMembers2.value, ...mockMembers3.value].map(m => m.id);
|
||||
selectedMembers.value = selectedMembers.value.filter(id => !teamMemberIds.includes(id));
|
||||
}
|
||||
};
|
||||
|
||||
// 监听取消全部的变化
|
||||
watch(cancelAll, (newVal) => {
|
||||
if (newVal) {
|
||||
// 取消全部:选中所有个人项目队员,并取消团队
|
||||
selectedMembers.value = [...mockMembers1.value.map(m => m.id)];
|
||||
teamRemoved.value = true;
|
||||
} else {
|
||||
// 取消取消全部:清空选择
|
||||
selectedMembers.value = [];
|
||||
teamRemoved.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
const openCancelDialog = async (row) => {
|
||||
if (!row?.registrationId) return;
|
||||
isShowCancelDialog.value = true;
|
||||
isCancelConfirmed.value = false;
|
||||
cancelLoading.value = true;
|
||||
cancelPreviewData.value = null;
|
||||
cancelResult.value = null;
|
||||
selectedMembers.value = [];
|
||||
teamRemoved.value = false;
|
||||
cancelAll.value = false;
|
||||
hasValidationError.value = false;
|
||||
hasLimitError.value = false;
|
||||
hasCountError.value = false;
|
||||
isFreeProject.value = false; // 默认为收费项目
|
||||
|
||||
try {
|
||||
const { succeeded, data } = await usereventrecordCancelPreviewGet(row.registrationId);
|
||||
if (succeeded) {
|
||||
cancelPreviewData.value = data;
|
||||
// 根据后端返回的数据设置是否为免费项目
|
||||
isFreeProject.value = data?.chargeMode === 0 || data?.actualPaidAmount <= 0;
|
||||
} else {
|
||||
ElMessage.error(data ?? '获取取消报名预览失败');
|
||||
isShowCancelDialog.value = false;
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('获取取消报名预览失败');
|
||||
isShowCancelDialog.value = false;
|
||||
} finally {
|
||||
cancelLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const submitCancel = async () => {
|
||||
if (!cancelPreviewData.value?.registrationId) return;
|
||||
|
||||
cancelSubmitting.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
registrationId: cancelPreviewData.value.registrationId,
|
||||
cancelType: cancelAll.value ? 0 : 1,
|
||||
cancelReason: '个人原因'
|
||||
};
|
||||
|
||||
if (!cancelAll.value) {
|
||||
payload.personIds = selectedMembers.value;
|
||||
}
|
||||
|
||||
const { succeeded, data } = await usereventrecordCancelExecutePost(payload);
|
||||
isCancelConfirmed.value = true;
|
||||
cancelResult.value = {
|
||||
success: succeeded,
|
||||
message: data?.message || (succeeded ? '操作成功' : '操作失败'),
|
||||
refundAmount: data?.refundAmount || totalRefundAmount.value,
|
||||
refundNo: data?.refundNo || null,
|
||||
cancelTime: data?.cancelTime || new Date().toISOString()
|
||||
};
|
||||
|
||||
if (succeeded) {
|
||||
initParam.value.refresh = !initParam.value.refresh;
|
||||
isShowDialog.value = false;
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('取消报名失败');
|
||||
isCancelConfirmed.value = true;
|
||||
cancelResult.value = {
|
||||
success: false,
|
||||
message: '取消报名失败',
|
||||
refundAmount: 0,
|
||||
refundNo: null,
|
||||
cancelTime: new Date().toISOString()
|
||||
};
|
||||
} finally {
|
||||
cancelSubmitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const resetCancelConfirm = () => {
|
||||
isCancelConfirmed.value = false;
|
||||
cancelResult.value = null;
|
||||
};
|
||||
|
||||
const closeCancelDialog = () => {
|
||||
isShowCancelDialog.value = false;
|
||||
};
|
||||
|
||||
const onCancelDialogClosed = () => {
|
||||
isCancelConfirmed.value = false;
|
||||
cancelPreviewData.value = null;
|
||||
cancelResult.value = null;
|
||||
// 按顺序重置,避免 watch 触发问题
|
||||
selectedMembers.value = [];
|
||||
teamRemoved.value = false;
|
||||
cancelAll.value = false;
|
||||
hasValidationError.value = false;
|
||||
hasLimitError.value = false;
|
||||
hasCountError.value = false;
|
||||
isFreeProject.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -514,4 +872,190 @@ const openCertificateDialog = async () => {
|
||||
margin-top: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
// 项目列表
|
||||
.project-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.project-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.project-tag {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background: linear-gradient(90deg, #409eff, #66b1ff);
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.remove-team-icon {
|
||||
cursor: pointer;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 20px;
|
||||
transition: all 0.3s;
|
||||
padding: 4px;
|
||||
|
||||
&:hover {
|
||||
color: #f56c6c;
|
||||
background: rgba(245, 108, 108, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&.team-removed {
|
||||
color: #1fae66;
|
||||
}
|
||||
}
|
||||
|
||||
.member-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.member-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 16px;
|
||||
background: white;
|
||||
border: 2px solid #409eff;
|
||||
border-radius: 20px;
|
||||
color: #409eff;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: rgba(64, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
&.member-selected {
|
||||
background: rgba(64, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
&.team-removed {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.remove-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #f56c6c;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
top: -4px;
|
||||
}
|
||||
|
||||
// 验证提示
|
||||
.validation-hints {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
// 底部操作栏
|
||||
.footer-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
background: linear-gradient(90deg, #409eff, #66b1ff);
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
:deep(.el-checkbox) {
|
||||
.el-checkbox__label {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.refund-label {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.refund-amount {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.footer-btn {
|
||||
background: white;
|
||||
color: #409eff;
|
||||
border: none;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background: #409eff;
|
||||
color: white;
|
||||
border: 1px solid white;
|
||||
|
||||
&:hover {
|
||||
background: #66b1ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-note {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
|
||||
:deep(.el-link) {
|
||||
&.el-link--danger {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -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 @@
|
||||
</el-tag>
|
||||
<span v-if="!(groupMemberMap[group.id]?.length)" class="assign-card-placeholder">点击此卡片后,从右侧选择队员</span>
|
||||
</div>
|
||||
<!-- 队员验证错误提示 -->
|
||||
<div v-if="hasValidationErrorsForGroup(group)" class="validation-error-section">
|
||||
<div class="validation-error-item" v-for="m in (groupMemberMap[group.id] ?? [])" :key="m.userId ?? m.id">
|
||||
<el-alert
|
||||
v-if="!getMemberValidationResult(m, group).isValid"
|
||||
:title="getMemberValidationResult(m, group).message"
|
||||
type="error"
|
||||
:closable="false"
|
||||
size="small"
|
||||
style="margin-top: 4px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="!selectedGroupItems.length" description="请先在第一步选择报名组别" />
|
||||
</div>
|
||||
@ -226,6 +240,58 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 验证提示区域 -->
|
||||
<div class="validation-notes-section">
|
||||
<!-- 年龄性别不符合要求提示 -->
|
||||
<el-alert
|
||||
v-if="hasAnyValidationErrors()"
|
||||
title="队员信息不符合组别要求"
|
||||
type="error"
|
||||
:closable="false"
|
||||
style="margin-bottom: 12px;"
|
||||
/>
|
||||
|
||||
<!-- 限报提示 -->
|
||||
<el-alert
|
||||
v-if="hasAnyRegistrationLimit()"
|
||||
title="此队员已达到报名限制"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
style="margin-bottom: 12px;"
|
||||
/>
|
||||
|
||||
<!-- 项目完成人数提示 -->
|
||||
<el-alert
|
||||
v-if="hasProjectCompletionCountError()"
|
||||
title="项目完成人数错误"
|
||||
type="error"
|
||||
:closable="false"
|
||||
style="margin-bottom: 12px;"
|
||||
/>
|
||||
|
||||
<!-- 队伍数量限制提示 -->
|
||||
<el-alert
|
||||
v-if="showTeamLimitHint"
|
||||
title="若管理后台配置一个组别只能限报2队,添加2队后,“添加”按钮隐藏"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 12px;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作区域 -->
|
||||
<div class="step2-footer">
|
||||
<el-button @click="handlePrevious">上一步</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleStep2Next"
|
||||
:loading="submitDraftLoading"
|
||||
:disabled="hasAnyValidationErrors() || hasProjectCompletionCountError() || hasAnyRegistrationLimit()"
|
||||
>
|
||||
预览
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3 -->
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user