赛事记录退款,赛事报名限制
This commit is contained in:
parent
c9e80c3a87
commit
150c7f26b9
@ -177,3 +177,55 @@ export const usereventrecordMembersReplacePost = (registrationid, userid, data)
|
|||||||
export const usereventrecordCertificateGet = (registrationid) => {
|
export const usereventrecordCertificateGet = (registrationid) => {
|
||||||
return http.get(`/usereventrecord/certificate/${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);
|
||||||
|
};
|
||||||
@ -214,3 +214,73 @@ export const usereventregistrationOrderCreatePost = (data) => {
|
|||||||
export const usereventregistrationCouponValidatePost = (data) => {
|
export const usereventregistrationCouponValidatePost = (data) => {
|
||||||
return http.post('/usereventregistration/coupon/validate', 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">
|
<template #action="scope">
|
||||||
<el-space wrap>
|
<el-space wrap>
|
||||||
<template v-if="!isCancelDisabledByRow(scope.row)">
|
<template v-if="!isCancelDisabledByRow(scope.row)">
|
||||||
<el-popconfirm title="确认取消报名吗?" @confirm="onCancelRegistration(scope.row.registrationId)">
|
<el-button type="danger" size="small" text @click="openCancelDialog(scope.row)">取消报名</el-button>
|
||||||
<template #reference>
|
|
||||||
<el-button type="danger" size="small" text>取消报名</el-button>
|
|
||||||
</template>
|
|
||||||
</el-popconfirm>
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<el-button type="danger" size="small" disabled text>取消报名</el-button>
|
<el-button type="danger" size="small" disabled text>取消报名</el-button>
|
||||||
@ -78,11 +74,7 @@
|
|||||||
|
|
||||||
<div style="margin: 14px 0;">
|
<div style="margin: 14px 0;">
|
||||||
<el-space wrap>
|
<el-space wrap>
|
||||||
<el-popconfirm title="确认取消报名吗?" @confirm="onCancelRegistration(currentRow?.registrationId)" v-if="currentRow">
|
<el-button type="danger" size="small" :disabled="disableCancel" @click="openCancelDialog(currentRow)" v-if="currentRow">取消报名</el-button>
|
||||||
<template #reference>
|
|
||||||
<el-button type="danger" size="small" :disabled="disableCancel">取消报名</el-button>
|
|
||||||
</template>
|
|
||||||
</el-popconfirm>
|
|
||||||
<el-button type="primary" size="small" @click="openMaterialDialog(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 type="primary" size="small" @click="openVoucherDialog(currentRow)" v-if="currentRow">上传凭证</el-button>
|
||||||
<el-button size="small" @click="openCertificateDialog" v-if="currentRow">查看证书</el-button>
|
<el-button size="small" @click="openCertificateDialog" v-if="currentRow">查看证书</el-button>
|
||||||
@ -198,13 +190,147 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, reactive, ref } from 'vue';
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
import { ElMessage } from 'element-plus';
|
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 ProTable from '/@/components/ProTable/index.vue';
|
||||||
import {
|
import {
|
||||||
usereventrecordListGet,
|
usereventrecordListGet,
|
||||||
@ -213,6 +339,8 @@ import {
|
|||||||
usereventrecordVoucherPost,
|
usereventrecordVoucherPost,
|
||||||
usereventrecordMembersGet,
|
usereventrecordMembersGet,
|
||||||
usereventrecordCertificateGet,
|
usereventrecordCertificateGet,
|
||||||
|
usereventrecordCancelPreviewGet,
|
||||||
|
usereventrecordCancelExecutePost,
|
||||||
} from '/@/api/match/usereventrecord';
|
} from '/@/api/match/usereventrecord';
|
||||||
|
|
||||||
const tableRef = ref();
|
const tableRef = ref();
|
||||||
@ -287,14 +415,18 @@ const tagTextForCertStatus = (v) => certTagMap[normalizeToNumber(v)]?.text ?? (v
|
|||||||
|
|
||||||
const disableCancel = computed(() => {
|
const disableCancel = computed(() => {
|
||||||
if (!currentRow.value) return true;
|
if (!currentRow.value) return true;
|
||||||
const st = normalizeToNumber(currentRow.value.status);
|
const status = normalizeToNumber(currentRow.value.status);
|
||||||
return st === 2 || st === 3 || st === 4;
|
const payStatus = normalizeToNumber(currentRow.value.payStatus);
|
||||||
|
// 只有报名状态为已支付(1) 且 支付状态为已支付(1),才能取消/退款
|
||||||
|
return !(status === 1 && payStatus === 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
const isCancelDisabledByRow = (row) => {
|
const isCancelDisabledByRow = (row) => {
|
||||||
if (!row) return true;
|
if (!row) return true;
|
||||||
const st = normalizeToNumber(row.status);
|
const status = normalizeToNumber(row.status);
|
||||||
return st === 2 || st === 3 || st === 4;
|
const payStatus = normalizeToNumber(row.payStatus);
|
||||||
|
// 只有报名状态为已支付(1) 且 支付状态为已支付(1),才能取消/退款
|
||||||
|
return !(status === 1 && payStatus === 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onOpenMenu = async (row) => {
|
const onOpenMenu = async (row) => {
|
||||||
@ -502,6 +634,232 @@ const openCertificateDialog = async () => {
|
|||||||
certLoading.value = false;
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@ -514,4 +872,190 @@ const openCertificateDialog = async () => {
|
|||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
color: var(--el-text-color-primary);
|
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>
|
</style>
|
||||||
@ -179,6 +179,7 @@
|
|||||||
v-for="m in (groupMemberMap[group.id] ?? [])"
|
v-for="m in (groupMemberMap[group.id] ?? [])"
|
||||||
:key="m.userId ?? m.id"
|
:key="m.userId ?? m.id"
|
||||||
closable
|
closable
|
||||||
|
:type="getMemberValidationResult(m, group).isValid ? '' : 'danger'"
|
||||||
@close="removeMemberFromGroup(group.id, m.userId ?? m.id)"
|
@close="removeMemberFromGroup(group.id, m.userId ?? m.id)"
|
||||||
style="margin: 3px;"
|
style="margin: 3px;"
|
||||||
>
|
>
|
||||||
@ -186,6 +187,19 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
<span v-if="!(groupMemberMap[group.id]?.length)" class="assign-card-placeholder">点击此卡片后,从右侧选择队员</span>
|
<span v-if="!(groupMemberMap[group.id]?.length)" class="assign-card-placeholder">点击此卡片后,从右侧选择队员</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<el-empty v-if="!selectedGroupItems.length" description="请先在第一步选择报名组别" />
|
<el-empty v-if="!selectedGroupItems.length" description="请先在第一步选择报名组别" />
|
||||||
</div>
|
</div>
|
||||||
@ -226,6 +240,58 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Step 3 -->
|
<!-- Step 3 -->
|
||||||
@ -376,6 +442,10 @@ import {
|
|||||||
usereventregistrationPreviewGet,
|
usereventregistrationPreviewGet,
|
||||||
usereventregistrationOrderCreatePost,
|
usereventregistrationOrderCreatePost,
|
||||||
usereventregistrationCouponValidatePost,
|
usereventregistrationCouponValidatePost,
|
||||||
|
usereventregistrationMemberValidatePost,
|
||||||
|
usereventregistrationRegistrationLimitCheckPost,
|
||||||
|
usereventregistrationGroupTeamLimitGet,
|
||||||
|
usereventregistrationProjectCompletionCheckPost,
|
||||||
} from '/@/api/match/usereventregistration';
|
} from '/@/api/match/usereventregistration';
|
||||||
import { teamPageGet, teamDetailGet } from '/@/api/match/unitUser';
|
import { teamPageGet, teamDetailGet } from '/@/api/match/unitUser';
|
||||||
import QRCode from 'qrcodejs2-fixes';
|
import QRCode from 'qrcodejs2-fixes';
|
||||||
@ -646,6 +716,13 @@ const memberKeyword = ref('');
|
|||||||
const memberLoading = ref(false);
|
const memberLoading = ref(false);
|
||||||
const selectedMemberIds = ref([]);
|
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: 团队相关
|
// Step2: 团队相关
|
||||||
const teamList = ref([]);
|
const teamList = ref([]);
|
||||||
const teamListLoading = ref(false);
|
const teamListLoading = ref(false);
|
||||||
@ -699,6 +776,11 @@ const clearRegisterState = () => {
|
|||||||
teamMemberLoading.value = false;
|
teamMemberLoading.value = false;
|
||||||
activeAssignGroupId.value = null;
|
activeAssignGroupId.value = null;
|
||||||
groupMemberMap.value = {};
|
groupMemberMap.value = {};
|
||||||
|
memberValidationMap.value = {};
|
||||||
|
groupTeamLimitMap.value = {};
|
||||||
|
memberRegistrationLimitMap.value = {};
|
||||||
|
projectCompletionCheckResult.value = null;
|
||||||
|
registrationLimitCheckResult.value = null;
|
||||||
previewLoading.value = false;
|
previewLoading.value = false;
|
||||||
previewData.value = null;
|
previewData.value = null;
|
||||||
paymentMethod.value = 1;
|
paymentMethod.value = 1;
|
||||||
@ -814,8 +896,81 @@ const isMemberAssignedToActive = (memberId) => {
|
|||||||
return list.some((m) => (m.userId ?? m.id) === 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;
|
const gId = activeAssignGroupId.value;
|
||||||
if (!gId) {
|
if (!gId) {
|
||||||
ElMessage.warning('请先点击左侧的组别卡片');
|
ElMessage.warning('请先点击左侧的组别卡片');
|
||||||
@ -829,7 +984,22 @@ const assignMemberToActiveGroup = (member) => {
|
|||||||
if (groupMemberMap.value[gId].some((m) => (m.userId ?? m.id) === memberId)) {
|
if (groupMemberMap.value[gId].some((m) => (m.userId ?? m.id) === memberId)) {
|
||||||
return;
|
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 });
|
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 () => {
|
const handleStep1Next = async () => {
|
||||||
if (!(selectedGroupIds.value?.length ?? 0)) {
|
if (!(selectedGroupIds.value?.length ?? 0)) {
|
||||||
ElMessage.warning('请先选择报名组别');
|
ElMessage.warning('请先选择报名组别');
|
||||||
@ -929,11 +1157,31 @@ const handleStep1Next = async () => {
|
|||||||
}
|
}
|
||||||
// 加载团队列表
|
// 加载团队列表
|
||||||
await fetchTeamList();
|
await fetchTeamList();
|
||||||
|
// 加载队伍限制配置
|
||||||
|
await fetchGroupTeamLimit();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStep2Next = async () => {
|
const handleStep2Next = async () => {
|
||||||
if (!currentEventId.value) return;
|
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));
|
const emptyGroups = selectedGroupItems.value.filter((g) => !(groupMemberMap.value[g.id]?.length));
|
||||||
if (emptyGroups.length > 0) {
|
if (emptyGroups.length > 0) {
|
||||||
@ -1487,6 +1735,32 @@ const handleCreateOrderAndPay = async () => {
|
|||||||
color: #67c23a;
|
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 {
|
.payment-qrcode-section {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user