This commit is contained in:
马梓彭 2026-04-17 12:48:38 +08:00
commit 4e851c9e36

View File

@ -3,15 +3,8 @@
<ProTable ref="tableRef" :request-api="usereventregistrationListGet" :columns="columns" :pagination="true"
:tool-button="false" :init-param="initParam" row-key="id">
<template #coverImage="scope">
<PreviewImage
v-if="scope.row?.coverImage"
:src="scope.row.coverImage"
width="80px"
height="60px"
fit="cover"
radius="4px"
:show-zoom-icon="true"
/>
<PreviewImage v-if="scope.row?.coverImage" :src="scope.row.coverImage" width="80px" height="60px"
fit="cover" radius="4px" :show-zoom-icon="true" />
<span v-else>-</span>
</template>
@ -61,7 +54,8 @@
</ProTable>
<!-- 赛事详情弹窗 -->
<el-dialog title="赛事详情" v-model="isShowDetailDialog" width="80%" style="min-height: 600px;" @closed="clearDetailState">
<el-dialog title="赛事详情" v-model="isShowDetailDialog" width="80%" style="min-height: 600px;"
@closed="clearDetailState">
<el-skeleton :rows="12" animated v-if="detailLoading" />
<template v-else>
<div v-if="formData">
@ -103,22 +97,21 @@
<div class="project-card" v-for="project in projects" :key="project.id">
<div class="project-card-left">
<div class="project-card-name">名称:{{ project.name }}</div>
<div class="project-card-type">类型:{{ project.parentProjectTypeName ?? project.projectTypeName ?? '' }}</div>
<div class="project-card-type">类型:{{ project.parentProjectTypeName ??
project.projectTypeName ?? '' }}</div>
<div class="project-card-count">完成人数:{{ project.completionCount ?? 0 }}</div>
</div>
<div class="project-card-right">
<div
v-for="g in (project.groups ?? [])"
:key="g.id"
class="group-tag"
<div v-for="g in (project.groups ?? [])" :key="g.id" class="group-tag"
:class="{ 'group-tag-selected': selectedGroupIds.includes(g.id) }"
@click="toggleGroupSelection(g.id)"
>
@click="toggleGroupSelection(g.id)">
<span class="group-tag-name">{{ g.groupName ?? g.name }}</span>
<span class="group-tag-date" v-if="g.birthDateStart || g.birthDateEnd">{{ formatGroupDate(g) }}</span>
<span class="group-tag-date" v-if="g.birthDateStart || g.birthDateEnd">{{
formatGroupDate(g) }}</span>
<span class="group-tag-gender">{{ formatGender(g.gender) }}</span>
</div>
<el-empty v-if="!(project.groups?.length)" description="暂无组别" :image-size="40" style="padding: 8px 0;" />
<el-empty v-if="!(project.groups?.length)" description="暂无组别" :image-size="40"
style="padding: 8px 0;" />
</div>
</div>
@ -126,14 +119,8 @@
<div class="selected-summary" v-if="selectedGroupItems.length > 0">
<div class="selected-summary-header">报名项目</div>
<div class="selected-summary-list">
<el-tag
v-for="item in selectedGroupItems"
:key="item.id"
closable
@close="toggleGroupSelection(item.id)"
style="margin: 4px;"
size="large"
>
<el-tag v-for="item in selectedGroupItems" :key="item.id" closable
@close="toggleGroupSelection(item.id)" style="margin: 4px;" size="large">
{{ item.groupName ?? item.name }}
<template v-if="item.dateRange"> · {{ item.dateRange }}</template>
· {{ formatGender(item.gender) }}
@ -144,11 +131,7 @@
<!-- 底部操作区域 -->
<div class="step-footer" style="display: flex; justify-content: flex-end; margin-top: 20px;">
<el-button @click="isShowRegisterDialog = false" style="margin-right: 10px;">取消</el-button>
<el-button
type="primary"
@click="handleStep1Next"
:loading="registerLoading"
>
<el-button type="primary" @click="handleStep1Next" :loading="registerLoading">
下一步
</el-button>
</div>
@ -160,101 +143,84 @@
<div class="step2-layout">
<!-- 左侧:已选组别,每个组别可分配队员 -->
<div class="step2-left">
<div
class="assign-card"
v-for="(group, gIdx) in selectedGroupItems"
:key="group.id"
<div class="assign-card" v-for="(group, gIdx) in selectedGroupItems" :key="group.id"
:class="{ 'assign-card-active': activeAssignGroupId === group.id }"
@click="activeAssignGroupId = group.id"
>
@click="activeAssignGroupId = group.id">
<div class="assign-card-header">
<span>
({{ group.parentProjectTypeName ?? group.projectTypeName ?? (group.projectTeamSize > 1 ? '团队' : '个人') }}/{{ group.projectCompletionCount ?? 0 }})
{{ group.projectName }}-{{ group.groupName ?? group.name }}/{{ formatGroupDate(group) }}/{{ formatGender(group.gender) }}
({{ group.parentProjectTypeName ?? group.projectTypeName ??
(group.projectTeamSize > 1 ?
'团队' : '个人') }}/{{ group.projectCompletionCount ?? 0 }})
{{ group.projectName }}-{{ group.groupName ?? group.name }}/{{
formatGroupDate(group) }}/{{
formatGender(group.gender) }}
</span>
<el-icon class="assign-card-close" @click.stop="toggleGroupSelection(group.id)"><Close /></el-icon>
<el-icon class="assign-card-close" @click.stop="toggleGroupSelection(group.id)">
<Close />
</el-icon>
</div>
<!-- 团队数量信息和添加团队按钮 -->
<div class="team-info-section" v-if="isTeamProject(group) && getGroupMaxTeams(group.id) > 1">
<div class="team-info-section"
v-if="isTeamProject(group) && getGroupMaxTeams(group.id) > 1">
<div class="team-count-info">
已添加 {{ getGroupTeams(group.id).length }} / {{ getGroupMaxTeams(group.id) }} 队
</div>
<el-button
v-if="getGroupTeams(group.id).length < getGroupMaxTeams(group.id)"
type="primary"
size="small"
text
@click.stop="addTeam(group.id)"
>
<el-icon><Plus /></el-icon>
<el-button v-if="getGroupTeams(group.id).length < getGroupMaxTeams(group.id)"
type="primary" size="small" text @click.stop="addTeam(group.id)">
<el-icon>
<Plus />
</el-icon>
添加团队
</el-button>
</div>
<!-- 团队列表 -->
<div v-if="isTeamProject(group) && getGroupMaxTeams(group.id) > 1" class="team-section">
<div
v-for="team in getGroupTeams(group.id)"
:key="team.teamIndex"
class="team-card"
<div v-for="team in getGroupTeams(group.id)" :key="team.teamIndex" class="team-card"
:class="{ 'team-card-active': activeTeamKey === `${group.id}_${team.teamIndex}` }"
@click.stop="setActiveTeam(group.id, team.teamIndex)"
>
:data-team-key="`${group.id}_${team.teamIndex}`"
@click.stop="setActiveTeam(group.id, team.teamIndex)">
<div class="team-card-header">
<span class="team-name">团队{{ getTeamNumber(team.teamIndex) }}</span>
<el-button
v-if="getGroupTeams(group.id).length > 1"
type="danger"
size="small"
text
@click.stop="removeTeam(group.id, team.teamIndex)"
>
<el-icon><Delete /></el-icon>
<el-button v-if="getGroupTeams(group.id).length > 1" type="danger"
size="small" text @click.stop="removeTeam(group.id, team.teamIndex)">
<el-icon>
<Delete />
</el-icon>
</el-button>
</div>
<div class="team-members">
<el-tag
v-for="m in team.members"
:key="m.userId ?? m.id"
closable
<el-tag v-for="m in team.members" :key="m.userId ?? m.id" closable
:type="getMemberValidationResult(m, group).isValid ? '' : 'danger'"
@close.stop="removeMemberFromTeam(group.id, team.teamIndex, m.userId ?? m.id)"
style="margin: 3px;"
>
style="margin: 3px;">
{{ m.name }}
</el-tag>
<span v-if="!team.members.length" class="team-placeholder">选择此团队后添加队员</span>
</div>
</div>
</div>
<!-- 单团队模式(向后兼容) -->
<div v-else class="assign-card-members">
<el-tag
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;"
>
<el-tag 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;">
{{ m.name }}
</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 v-if="hasValidationErrorsForGroup(group)" class="validation-error-section">
<div class="validation-error-item" v-for="m in (isTeamProject(group) && getGroupMaxTeams(group.id) > 1 ? getGroupTeams(group.id).flatMap(t => t.members) : (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 class="validation-error-item"
v-for="m in (isTeamProject(group) && getGroupMaxTeams(group.id) > 1 ? getGroupTeams(group.id).flatMap(t => t.members) : (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>
@ -264,33 +230,19 @@
<!-- 右侧:团队选择器 + 成员列表 -->
<div class="step2-right">
<div class="team-selector">
<el-select
v-model="selectedTeamId"
placeholder="请选择团队"
style="width: 100%;"
@change="onTeamChange"
:loading="teamListLoading"
filterable
>
<el-option
v-for="t in teamList"
:key="t.id"
:label="t.name"
:value="t.id"
/>
<el-select v-model="selectedTeamId" placeholder="请选择团队" style="width: 100%;"
@change="onTeamChange" :loading="teamListLoading" filterable>
<el-option v-for="t in teamList" :key="t.id" :label="t.name" :value="t.id" />
</el-select>
</div>
<div class="team-members-panel">
<el-skeleton v-if="teamMemberLoading" :rows="6" animated />
<el-empty v-else-if="!teamMembers.length" :description="selectedTeamId ? '该团队暂无运动员' : '请先选择团队'" :image-size="60" />
<el-empty v-else-if="!teamMembers.length"
:description="selectedTeamId ? '该团队暂无运动员' : '请先选择团队'" :image-size="60" />
<template v-else>
<div
v-for="m in teamMembers"
:key="m.userId ?? m.id"
class="team-member-tag"
<div v-for="m in teamMembers" :key="m.userId ?? m.id" class="team-member-tag"
:class="{ 'team-member-tag-assigned': isMemberAssignedToActive(m.userId ?? m.id) }"
@click="assignMemberToActiveTeam(m)"
>
@click="assignMemberToActiveTeam(m)">
{{ m.name }}
</div>
</template>
@ -301,48 +253,29 @@
<!-- 验证提示区域 -->
<div class="validation-notes-section">
<!-- 年龄性别不符合要求提示 -->
<el-alert
v-if="hasAnyValidationErrors()"
title="队员信息不符合组别要求"
type="error"
:closable="false"
style="margin-bottom: 12px;"
/>
<el-alert v-if="hasAnyValidationErrors()" title="队员信息不符合组别要求" type="error" :closable="false"
style="margin-bottom: 12px;" />
<!-- 限报提示 -->
<template v-if="hasAnyRegistrationLimit()">
<el-alert
v-for="(detail, index) in (registrationLimitCheckResult?.limitDetails ?? [])"
:key="index"
:title="detail?.limitMessage ?? '报名限制'"
type="warning"
:closable="false"
style="margin-bottom: 12px;"
/>
<el-alert v-for="(detail, index) in (registrationLimitCheckResult?.limitDetails ?? [])"
:key="index" :title="detail?.limitMessage ?? '报名限制'" type="warning" :closable="false"
style="margin-bottom: 12px;" />
</template>
<!-- 队伍数量限制提示 -->
<el-alert
v-if="showTeamLimitHint"
title="若管理后台配置一个组别只能限报2队,添加2队后,'添加'按钮隐藏"
type="info"
: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="step-footer" style="display: flex; justify-content: flex-end; margin-top: 20px;">
<el-button @click="isShowRegisterDialog = false" style="margin-right: 10px;">取消</el-button>
<el-button @click="handlePrevious" style="margin-right: 10px;">上一步</el-button>
<el-button
type="primary"
@click="handleStep2Next"
:loading="submitDraftLoading"
:disabled="hasAnyValidationErrors() || hasAnyRegistrationLimit()"
>
<el-button type="primary" @click="handleStep2Next" :loading="submitDraftLoading"
:disabled="hasAnyValidationErrors() || hasAnyRegistrationLimit()">
下一步
</el-button>
</div>
@ -378,13 +311,15 @@
</div>
<!-- 支付二维码显示区域(微信/支付宝) -->
<div v-else-if="paymentData" class="payment-qrcode-section">
<div class="qrcode-title">请使用{{ paymentMethod === 2 ? '支付宝' : '微信'}}扫码支付</div>
<div class="qrcode-title">请使用{{ paymentMethod === 2 ? '支付宝' : '微信' }}扫码支付</div>
<div class="qrcode-wrapper">
<div v-if="paymentData.qrcodeUrl" class="qrcode-container">
<div ref="qrcodeRef"></div>
</div>
<div v-else class="qrcode-placeholder">
<el-icon :size="60" color="#909399"><Picture /></el-icon>
<el-icon :size="60" color="#909399">
<Picture />
</el-icon>
<div class="qrcode-tip">支付链接获取失败</div>
</div>
</div>
@ -393,7 +328,9 @@
<span class="amount-value">¥ {{ finalPaymentAmount }}</span>
</div>
<div class="payment-status" v-if="pollingStatus">
<el-icon class="is-loading"><Loading /></el-icon>
<el-icon class="is-loading">
<Loading />
</el-icon>
{{ pollingStatus }}
</div>
</div>
@ -408,7 +345,8 @@
<el-table-column prop="projectTypeName" label="类型" align="center" width="180" />
<el-table-column label="参赛队员" align="center" min-width="200">
<template #default="scope">
<el-tag size="small" style="margin: 2px;" v-for="member in scope.row?.members" :key="member.userId">
<el-tag size="small" style="margin: 2px;" v-for="member in scope.row?.members"
:key="member.userId">
{{ member.name }}
</el-tag>
</template>
@ -419,32 +357,23 @@
<div class="coupon-section">
<div class="detail-section-title">优惠券</div>
<div class="coupon-input-wrapper">
<el-input
v-model="couponCode"
placeholder="请输入优惠券兑换码"
:disabled="!!validatedCoupon"
style="flex: 1; margin-right: 10px;"
clearable
/>
<el-button
v-if="!validatedCoupon"
type="primary"
:loading="couponValidationLoading"
@click="validateCoupon"
>
<el-input v-model="couponCode" placeholder="请输入优惠券兑换码" :disabled="!!validatedCoupon"
style="flex: 1; margin-right: 10px;" clearable />
<el-button v-if="!validatedCoupon" type="primary" :loading="couponValidationLoading"
@click="validateCoupon">
使用优惠券
</el-button>
<el-button
v-else
type="danger"
@click="clearCoupon"
>
<el-button v-else type="danger" @click="clearCoupon">
清除优惠券
</el-button>
</div>
<div v-if="validatedCoupon" class="coupon-result success">
<el-icon style="color: #67c23a;"><CircleCheck /></el-icon>
<span>{{ validatedCoupon.couponName }},{{validatedCoupon.discountType == 0 ? `优惠价${validatedCoupon.discountValue / 10}折` : `优惠 ${validatedCoupon.discountAmount}元`}}</span>
<el-icon style="color: #67c23a;">
<CircleCheck />
</el-icon>
<span>{{ validatedCoupon.couponName }},{{ validatedCoupon.discountType == 0 ?
`优惠价${validatedCoupon.discountValue / 10}折` : `优惠
${validatedCoupon.discountAmount}元`}}</span>
</div>
</div>
@ -462,16 +391,19 @@
</div>
<div class="payment-actions">
<div class="payment-method-wrapper" v-if="!isFreePreview">
<el-select v-model="paymentMethod" placeholder="请选择支付方式" style="width: 200px; margin-right: 10px;">
<el-select v-model="paymentMethod" placeholder="请选择支付方式"
style="width: 200px; margin-right: 10px;">
<el-option :value="1" label="微信支付" />
<el-option :value="2" label="支付宝支付" />
<el-option :value="3" label="银行转账" />
</el-select>
<el-button type="primary" @click="handleCreateOrderAndPay" :loading="createOrderLoading" :disabled="!draftRegistrationId">
<el-button type="primary" @click="handleCreateOrderAndPay"
:loading="createOrderLoading" :disabled="!draftRegistrationId">
确认支付
</el-button>
</div>
<el-button v-else type="primary" @click="handleCreateOrderAndPay" :loading="createOrderLoading" :disabled="!draftRegistrationId">
<el-button v-else type="primary" @click="handleCreateOrderAndPay"
:loading="createOrderLoading" :disabled="!draftRegistrationId">
确认报名
</el-button>
</div>
@ -488,8 +420,10 @@
<!-- 底部操作区域 -->
<div class="step-footer step-footer--payment">
<el-button @click="isShowRegisterDialog = false">取消</el-button>
<el-button v-if="paymentData" type="primary" @click="refreshPayment" :loading="polling">刷新二维码</el-button>
<el-button v-if="paymentData && paymentMethod === 3" type="primary" @click="confirmBankTransfer">确认已转账</el-button>
<el-button v-if="paymentData" type="primary" @click="refreshPayment"
:loading="polling">刷新二维码</el-button>
<el-button v-if="paymentData && paymentMethod === 3" type="primary"
@click="confirmBankTransfer">确认已转账</el-button>
<el-button @click="handlePrevious">上一步</el-button>
</div>
</div>
@ -543,13 +477,15 @@ const columns = ref([
{ prop: 'type', label: '赛事类型', align: 'center', width: 120 },
{ prop: 'registrationType', label: '报名方式', align: 'center', width: 120 },
{ prop: 'chargeMode', label: '收费模式', align: 'center', width: 120 },
{ prop: 'status', label: '赛事状态', align: 'center', width: 120 , enum: [
{
prop: 'status', label: '赛事状态', align: 'center', width: 120, enum: [
{ label: '未开始', value: 0 },
{ label: '报名中', value: 1 },
{ label: '报名结束', value: 2 },
{ label: '比赛中', value: 3 },
{ label: '已结束', value: 4 },
],},
],
},
{ prop: 'registrationStartTime', label: '报名起止时间', align: 'center', minWidth: 320 },
{ prop: 'matchStartTime', label: '比赛起止时间', align: 'center', minWidth: 320 },
{ prop: 'action', label: '操作', align: 'center', fixed: 'right', width: 210 },
@ -905,8 +841,8 @@ const formatGender = (gender) => {
if (v === 0) return '男';
if (v === 1) return '女';
if (v === 2) return '保密';
if (v === 3) return '不限';
return '不限';
if (v === 3) return '性别不限';
return '性别不限';
};
// 格式化组别的年龄段(出生日期范围)
@ -1066,7 +1002,7 @@ const openRegistrationDialog = async (row) => {
ElMessage.error(data ?? '项目加载失败');
return;
}
// 检查报名类型,如果是只允许个人报名,则显示提示并阻止继续
const registrationType = data?.registrationType;
if (registrationType === 'OnlyIndividual') {
@ -1080,7 +1016,7 @@ const openRegistrationDialog = async (row) => {
);
return;
}
// 保存完整的详情数据,用于项目类型显示等
detailData.value = data;
projects.value = Array.isArray(data?.projects) ? data.projects : [];
@ -1119,7 +1055,7 @@ const fetchTeamList = async () => {
const onTeamChange = async (teamId) => {
teamMembers.value = [];
if (!teamId) return;
// 检查是否已经分配了其他团队的队员
if (usedTeamId.value && usedTeamId.value !== teamId) {
ElMessage.warning('本次报名已选择了其他团队的队员,请先移除已分配的队员');
@ -1127,7 +1063,7 @@ const onTeamChange = async (teamId) => {
selectedTeamId.value = usedTeamId.value;
return;
}
teamMemberLoading.value = true;
try {
const { succeeded, data } = await teamDetailGet(teamId);
@ -1154,7 +1090,20 @@ const onTeamChange = async (teamId) => {
// 判断某成员是否已分配到当前激活的组别
const isMemberAssignedToActive = (memberId) => {
if (!activeAssignGroupId.value) return false;
const list = groupMemberMap.value[activeAssignGroupId.value] ?? [];
const gId = activeAssignGroupId.value;
// 多团队模式:检查当前激活的 team-card 里是否有该成员
if (activeTeamKey.value) {
const [activeGroupId, activeTeamIdx] = activeTeamKey.value.split('_').map(Number);
if (activeGroupId === gId) {
const teams = groupTeamsMap.value[gId] ?? [];
const team = teams.find(t => t.teamIndex === activeTeamIdx);
if (team) {
return team.members.some(m => (m.userId ?? m.id) === memberId);
}
}
}
// 单团队模式降级
const list = groupMemberMap.value[gId] ?? [];
return list.some((m) => (m.userId ?? m.id) === memberId);
};
@ -1200,13 +1149,13 @@ const validateAllMembers = async () => {
for (const group of selectedGroupItems.value) {
const gId = group.id;
// 检查是否为团队项目且启用了多团队模式
const isMultiTeamMode = isTeamProject(group) && groupTeamsMap.value[gId] &&
const isMultiTeamMode = isTeamProject(group) && groupTeamsMap.value[gId] &&
(groupTeamsMap.value[gId].length > 1 || getGroupMaxTeams(gId) > 1);
let membersToValidate = [];
if (isMultiTeamMode) {
// 多团队模式:收集所有团队的队员
for (const team of groupTeamsMap.value[gId]) {
@ -1330,7 +1279,7 @@ const assignMemberToActiveGroup = async (member) => {
const removeMemberFromGroup = (groupId, memberId) => {
if (!groupMemberMap.value[groupId]) return;
groupMemberMap.value[groupId] = groupMemberMap.value[groupId].filter((m) => (m.userId ?? m.id) !== memberId);
// 检查是否还有队员,如果没有则重置已使用的团队
let hasMembers = false;
Object.values(groupMemberMap.value).forEach(members => {
@ -1341,7 +1290,7 @@ const removeMemberFromGroup = (groupId, memberId) => {
if (!hasMembers) {
usedTeamId.value = null;
}
// 触发实时验证
debounceValidateAllMembers();
};
@ -1364,18 +1313,18 @@ const isTeamProject = (group) => {
// 1006: 团队花样
return group.projectType === 1005 || group.projectType === 1006;
}
// 备用判断:使用 projectTypeName 或 parentProjectTypeName
if (group?.projectTypeName || group?.parentProjectTypeName) {
const typeName = group.projectTypeName || group.parentProjectTypeName;
return typeName && typeName.includes('团队');
}
// 最后的备用判断:使用 projectTeamSize
if (group?.projectTeamSize) {
return group.projectTeamSize > 1;
}
return false;
};
@ -1407,18 +1356,18 @@ const addTeam = (groupId) => {
initGroupTeams(groupId);
const teams = groupTeamsMap.value[groupId];
const maxTeams = getGroupMaxTeams(groupId);
if (teams.length >= maxTeams) {
ElMessage.warning(`已达到该项目组别限报${maxTeams}队的上限`);
return;
}
const newTeamIndex = teams.length + 1;
teams.push({
teamIndex: newTeamIndex,
members: []
});
ElMessage.success(`已添加团队${getTeamNumber(newTeamIndex)}`);
};
@ -1426,12 +1375,12 @@ const addTeam = (groupId) => {
const removeTeam = async (groupId, teamIndex) => {
initGroupTeams(groupId);
const teams = groupTeamsMap.value[groupId];
if (teams.length <= 1) {
ElMessage.warning('至少需要保留一个团队');
return;
}
try {
await ElMessageBox.confirm(
`确定要删除团队${getTeamNumber(teamIndex)}吗?该团队的队员也会被移除。`,
@ -1442,16 +1391,16 @@ const removeTeam = async (groupId, teamIndex) => {
type: 'warning'
}
);
const teamIdx = teams.findIndex(t => t.teamIndex === teamIndex);
if (teamIdx > -1) {
teams.splice(teamIdx, 1);
// 重新编号团队索引
teams.forEach((team, idx) => {
team.teamIndex = idx + 1;
});
ElMessage.success('团队删除成功');
}
} catch {
@ -1465,24 +1414,29 @@ const getActiveTeamMembers = (groupId) => {
if (!groupTeamsMap.value[groupId] || groupTeamsMap.value[groupId].length === 0) {
return groupMemberMap.value[groupId] ?? [];
}
// 多团队模式:找到当前激活的团队
if (!activeTeamKey.value) {
return [];
}
const [activeGroupId, activeTeamIdx] = activeTeamKey.value.split('_').map(Number);
if (activeGroupId !== groupId) {
return [];
}
const team = groupTeamsMap.value[groupId].find(t => t.teamIndex === activeTeamIdx);
return team?.members ?? [];
};
// 设置当前激活的团队
const setActiveTeam = (groupId, teamIndex) => {
activeAssignGroupId.value = groupId;
activeTeamKey.value = `${groupId}_${teamIndex}`;
nextTick(() => {
const el = document.querySelector(`.team-card[data-team-key="${groupId}_${teamIndex}"]`);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
});
};
// 将成员分配到当前激活的团队
@ -1492,55 +1446,55 @@ const assignMemberToActiveTeam = async (member) => {
ElMessage.warning('请先点击左侧的组别卡片');
return;
}
initGroupTeams(gId);
// 获取当前组别信息
const group = selectedGroupItems.value.find(g => g.id === gId);
// 检查是否为团队项目且启用了多团队模式
const isMultiTeamMode = group && isTeamProject(group) &&
const isMultiTeamMode = group && isTeamProject(group) &&
(groupTeamsMap.value[gId].length > 1 || getGroupMaxTeams(gId) > 1);
if (!isMultiTeamMode) {
// 单团队模式,使用原来的逻辑
await assignMemberToActiveGroup(member);
return;
}
// 多团队模式
if (!activeTeamKey.value) {
ElMessage.warning('请先选择要分配队员的团队');
return;
}
const [activeGroupId, activeTeamIdx] = activeTeamKey.value.split('_').map(Number);
if (activeGroupId !== gId) {
ElMessage.warning('请先选择对应组别的团队');
return;
}
const team = groupTeamsMap.value[gId].find(t => t.teamIndex === activeTeamIdx);
if (!team) {
return;
}
const memberId = member.userId ?? member.id;
// 已存在则不重复添加
if (team.members.some((m) => (m.userId ?? m.id) === memberId)) {
return;
}
// 检查队员是否来自已选择的团队
if (usedTeamId.value && member.teamId !== usedTeamId.value) {
ElMessage.warning('本次报名只能选择同一个团队的队员');
return;
}
// 设置已使用的团队
if (!usedTeamId.value) {
usedTeamId.value = member.teamId;
}
team.members.push({ ...member, teamId: member.teamId });
// 触发实时验证(包含报名限制检查)
debounceValidateAllMembers();
@ -1551,9 +1505,9 @@ const removeMemberFromTeam = (groupId, teamIndex, memberId) => {
initGroupTeams(groupId);
const team = groupTeamsMap.value[groupId].find(t => t.teamIndex === teamIndex);
if (!team) return;
team.members = team.members.filter((m) => (m.userId ?? m.id) !== memberId);
// 检查是否还有队员
let hasMembers = false;
groupTeamsMap.value[groupId].forEach(t => {
@ -1564,7 +1518,7 @@ const removeMemberFromTeam = (groupId, teamIndex, memberId) => {
if (!hasMembers) {
usedTeamId.value = null;
}
// 触发实时验证
debounceValidateAllMembers();
};
@ -1679,29 +1633,29 @@ const checkRegistrationLimit = async () => {
// 构造按组别分配的数据(与提交报名时保持一致)
const groupMembers = [];
const allMemberIdsSet = new Set();
for (const gId of selectedGroupIds.value) {
// 找到当前组别信息
const group = selectedGroupItems.value.find(g => g.id === gId);
// 检查是否为团队项目且启用了多团队模式
const isMultiTeamMode = group && isTeamProject(group) && groupTeamsMap.value[gId] &&
const isMultiTeamMode = group && isTeamProject(group) && groupTeamsMap.value[gId] &&
(groupTeamsMap.value[gId].length > 1 || getGroupMaxTeams(gId) > 1);
if (isMultiTeamMode) {
// 多团队模式
const teams = groupTeamsMap.value[gId];
const groupTeamsData = [];
for (const team of teams) {
const memberIds = team.members.map(m => m.userId ?? m.id);
memberIds.forEach(id => allMemberIdsSet.add(id));
groupTeamsData.push({
teamIndex: team.teamIndex,
memberIds: memberIds
});
}
groupMembers.push({
groupId: gId,
memberIds: groupTeamsData.flatMap(t => t.memberIds), // 保持向后兼容
@ -1711,16 +1665,16 @@ const checkRegistrationLimit = async () => {
// 单团队模式(保持向后兼容)
const memberIds = (groupMemberMap.value[gId] ?? []).map((m) => m.userId ?? m.id);
memberIds.forEach(id => allMemberIdsSet.add(id));
groupMembers.push({
groupId: gId,
memberIds: memberIds
});
}
}
const allMemberIds = [...allMemberIdsSet];
try {
const { succeeded, data } = await usereventregistrationRegistrationLimitCheckPost({
eventId: currentEventId.value,
@ -1783,7 +1737,7 @@ const handleStep1Next = async () => {
await fetchTeamList();
// 加载队伍限制配置
await fetchGroupTeamLimit();
// 初始化每个组别的团队数据
await nextTick();
selectedGroupItems.value.forEach(group => {
@ -1813,7 +1767,7 @@ const handleStep2Next = async () => {
for (const g of selectedGroupItems.value) {
const gId = g.id;
let hasMembers = false;
// 检查是否为团队项目且启用了多团队模式
if (isTeamProject(g) && groupTeamsMap.value[gId] && groupTeamsMap.value[gId].length > 0) {
for (const team of groupTeamsMap.value[gId]) {
@ -1826,12 +1780,12 @@ const handleStep2Next = async () => {
// 单团队模式
hasMembers = !!(groupMemberMap.value[gId]?.length);
}
if (!hasMembers) {
emptyGroups.push(g);
}
}
if (emptyGroups.length > 0) {
ElMessage.warning(`请为所有组别分配队员,"${emptyGroups[0].groupName ?? emptyGroups[0].name}"尚未分配`);
return;
@ -1848,35 +1802,35 @@ const handleStep2Next = async () => {
const groupMembers = [];
const allTeams = []; // 用于收集所有团队数据
const allMemberIdsSet = new Set();
for (const gId of selectedGroupIds.value) {
// 找到当前组别信息
const group = selectedGroupItems.value.find(g => g.id === gId);
// 检查是否为团队项目且启用了多团队模式
const isMultiTeamMode = group && isTeamProject(group) && groupTeamsMap.value[gId] &&
const isMultiTeamMode = group && isTeamProject(group) && groupTeamsMap.value[gId] &&
(groupTeamsMap.value[gId].length > 1 || getGroupMaxTeams(gId) > 1);
if (isMultiTeamMode) {
// 多团队模式
const teams = groupTeamsMap.value[gId];
const groupTeamsData = [];
for (const team of teams) {
const memberIds = team.members.map(m => m.userId ?? m.id);
memberIds.forEach(id => allMemberIdsSet.add(id));
groupTeamsData.push({
teamIndex: team.teamIndex,
memberIds: memberIds
});
}
groupMembers.push({
groupId: gId,
memberIds: groupTeamsData.flatMap(t => t.memberIds), // 保持向后兼容
teams: groupTeamsData
});
allTeams.push(...groupTeamsData.map(t => ({
groupId: gId,
...t
@ -1885,14 +1839,14 @@ const handleStep2Next = async () => {
// 单团队模式(保持向后兼容)
const memberIds = (groupMemberMap.value[gId] ?? []).map((m) => m.userId ?? m.id);
memberIds.forEach(id => allMemberIdsSet.add(id));
groupMembers.push({
groupId: gId,
memberIds: memberIds
});
}
}
const allMemberIds = [...allMemberIdsSet];
submitDraftLoading.value = true;
@ -1936,21 +1890,21 @@ const handleStep2Next = async () => {
const generateQRCode = async (qrcodeUrl) => {
try {
await nextTick();
// 清理之前的二维码
if (qrcodeInstance) {
qrcodeInstance.clear();
qrcodeInstance = null;
}
if (!qrcodeRef.value) {
console.error('二维码容器未找到');
return;
}
// 清空容器
qrcodeRef.value.innerHTML = '';
// 创建新的二维码
qrcodeInstance = new QRCode(qrcodeRef.value, {
text: qrcodeUrl,
@ -1983,7 +1937,7 @@ onUnmounted(() => {
const startPolling = (orderNo) => {
polling.value = true;
pollingStatus.value = '等待支付...';
pollingTimer = setInterval(async () => {
try {
const { succeeded, data } = await usereventregistrationOrderStatusGet(orderNo);
@ -2103,15 +2057,15 @@ const handleCreateOrderAndPay = async () => {
// 微信/支付宝支付,显示二维码
paymentData.value = data;
// 检查二维码是否获取成功
if (!data.qrcodeUrl) {
ElMessage.error('支付链接获取失败,请稍后重试');
return;
}
await generateQRCode(data.qrcodeUrl);
// 开始轮询支付状态
if (data.orderNo) {
startPolling(data.orderNo);
@ -2228,7 +2182,8 @@ const handleCreateOrderAndPay = async () => {
font-weight: 500;
}
&-date, &-gender {
&-date,
&-gender {
color: #888;
font-size: 12px;
}
@ -2299,7 +2254,9 @@ const handleCreateOrderAndPay = async () => {
&-active {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
border-left: 8px solid #56b4f9;
box-shadow: 0 4px 8px rgba(64, 158, 255, 0.35);
// background: #f0f7ff;
}
&-header {
@ -2317,6 +2274,7 @@ const handleCreateOrderAndPay = async () => {
cursor: pointer;
font-size: 16px;
opacity: 0.8;
&:hover {
opacity: 1;
}
@ -2369,11 +2327,11 @@ const handleCreateOrderAndPay = async () => {
cursor: pointer;
transition: all 0.2s;
overflow: hidden;
&:hover {
border-color: #56b4f9;
}
&-active {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);