2026-03-23 18:01:01 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="system-menu-container layout-padding">
|
|
|
|
|
|
<ProTable ref="tableRef" :request-api="usereventregistrationListGet" :columns="columns" :pagination="true"
|
|
|
|
|
|
:tool-button="false" :init-param="initParam" row-key="id">
|
|
|
|
|
|
<template #coverImage="scope">
|
|
|
|
|
|
<el-image v-if="scope.row?.coverImage" style="width: 70px; height: 46px;"
|
|
|
|
|
|
:src="scope.row.coverImage" fit="cover" />
|
|
|
|
|
|
<span v-else>-</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template #type="scope">
|
|
|
|
|
|
<el-tag :type="eventTypeTagType(scope.row)">{{ eventTypeText(scope.row) }}</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template #registrationType="scope">
|
|
|
|
|
|
{{ registrationTypeText(scope.row) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template #chargeMode="scope">
|
|
|
|
|
|
{{ chargeModeText(scope.row) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template #status="scope">
|
|
|
|
|
|
<el-tag :type="eventStatusTagType(scope.row.status)">{{ eventStatusText(scope.row.status) }}</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template #registrationStartTime="scope">
|
|
|
|
|
|
{{ formatRange(scope.row.registrationStartTime, scope.row.registrationEndTime) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template #matchStartTime="scope">
|
|
|
|
|
|
{{ formatRange(scope.row.matchStartTime, scope.row.matchEndTime) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template #action="scope">
|
|
|
|
|
|
<el-space wrap>
|
|
|
|
|
|
<el-button v-if="isEventEnrolling(scope.row)" size="small" type="primary" text
|
|
|
|
|
|
@click="openRegistrationDialog(scope.row)">去报名</el-button>
|
|
|
|
|
|
<el-button v-else size="small" text disabled>不可报名</el-button>
|
|
|
|
|
|
|
|
|
|
|
|
<el-button size="small" text type="primary" @click="openDetailDialog(scope.row)">查看详情</el-button>
|
|
|
|
|
|
</el-space>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</ProTable>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 赛事详情弹窗 -->
|
|
|
|
|
|
<el-dialog title="赛事详情" v-model="isShowDetailDialog" width="80%" @closed="clearDetailState">
|
|
|
|
|
|
<el-skeleton :rows="12" animated v-if="detailLoading" />
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<div v-if="detailData" style="display: flex; gap: 18px; align-items: flex-start;">
|
|
|
|
|
|
<el-image v-if="detailData?.coverImage" style="width: 180px; height: 120px; flex: 0 0 auto;"
|
|
|
|
|
|
:src="detailData.coverImage" fit="cover" />
|
|
|
|
|
|
<el-empty v-else description="暂无封面" style="width: 180px; height: 120px;" />
|
|
|
|
|
|
|
|
|
|
|
|
<div style="flex: 1;">
|
|
|
|
|
|
<el-descriptions :column="2" border size="small" v-if="detailData">
|
|
|
|
|
|
<el-descriptions-item label="赛事名称">{{ detailData?.name ?? '-' }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="赛事类型">
|
|
|
|
|
|
{{ eventTypeText(detailData) }}
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="报名方式">
|
|
|
|
|
|
{{ registrationTypeText(detailData) }}
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="收费模式">{{ chargeModeText(detailData) }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="赛事状态">
|
|
|
|
|
|
<el-tag :type="eventStatusTagType(detailData?.status)">{{ eventStatusText(detailData?.status) }}</el-tag>
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="报名起止时间">
|
|
|
|
|
|
{{ formatRange(detailData?.registrationStartTime, detailData?.registrationEndTime) }}
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="比赛起止时间">
|
|
|
|
|
|
{{ formatRange(detailData?.matchStartTime, detailData?.matchEndTime) }}
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="积分发放规则">
|
|
|
|
|
|
{{ detailData?.pointsRuleName ?? '-' }}
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="detail-section-title">项目列表</div>
|
|
|
|
|
|
<el-table :data="detailData?.projects ?? []" border size="small" style="margin-top: 10px;">
|
|
|
|
|
|
<el-table-column prop="name" label="项目" align="center" show-overflow-tooltip />
|
|
|
|
|
|
<el-table-column prop="groupName" label="组别" align="center" />
|
|
|
|
|
|
<el-table-column prop="fee" label="费用(元)" align="center" width="120" />
|
|
|
|
|
|
<el-table-column prop="limitCount" label="名额" align="center" width="120" />
|
|
|
|
|
|
<el-table-column prop="registeredCount" label="已报名" align="center" width="120" />
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-empty v-else description="暂无详情数据" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<span class="dialog-footer">
|
|
|
|
|
|
<el-button @click="isShowDetailDialog = false">关闭</el-button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 赛事报名三步弹窗 -->
|
|
|
|
|
|
<el-dialog title="赛事报名" v-model="isShowRegisterDialog" width="900px" :close-on-click-modal="false"
|
|
|
|
|
|
@closed="clearRegisterState">
|
|
|
|
|
|
<el-steps :active="activeStep" finish-status="success" align-center>
|
|
|
|
|
|
<el-step title="选择报名项目" />
|
|
|
|
|
|
<el-step title="分配队员" />
|
|
|
|
|
|
<el-step title="预览支付" />
|
|
|
|
|
|
</el-steps>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="register-step-content">
|
|
|
|
|
|
<!-- Step 1 -->
|
|
|
|
|
|
<div v-if="activeStep === 0">
|
|
|
|
|
|
<el-empty v-if="!registerLoading && !(projects?.length ?? 0)" description="暂无报名项目" />
|
|
|
|
|
|
<el-skeleton v-else-if="registerLoading" :rows="8" animated />
|
2026-03-30 02:42:26 +08:00
|
|
|
|
<template v-else>
|
|
|
|
|
|
<!-- 项目卡片列表 -->
|
|
|
|
|
|
<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.teamSize > 1 ? '团队' : '个人' }}-{{ project.name }}</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"
|
|
|
|
|
|
:class="{ 'group-tag-selected': selectedGroupIds.includes(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-gender">{{ formatGender(g.gender) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-empty v-if="!(project.groups?.length)" description="暂无组别" :image-size="40" style="padding: 8px 0;" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 已选报名项目汇总 -->
|
|
|
|
|
|
<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"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ item.groupName ?? item.name }}
|
|
|
|
|
|
<template v-if="item.dateRange"> · {{ item.dateRange }}</template>
|
|
|
|
|
|
· {{ formatGender(item.gender) }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-03-23 18:01:01 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Step 2 -->
|
|
|
|
|
|
<div v-else-if="activeStep === 1">
|
2026-03-30 02:42:26 +08:00
|
|
|
|
<div class="step2-layout">
|
|
|
|
|
|
<!-- 左侧:已选组别,每个组别可分配队员 -->
|
|
|
|
|
|
<div class="step2-left">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="assign-card"
|
|
|
|
|
|
v-for="(group, gIdx) in selectedGroupItems"
|
|
|
|
|
|
:key="group.id"
|
|
|
|
|
|
:class="{ 'assign-card-active': activeAssignGroupId === group.id }"
|
|
|
|
|
|
@click="activeAssignGroupId = group.id"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="assign-card-header">
|
|
|
|
|
|
<span>
|
|
|
|
|
|
({{ 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>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="assign-card-members">
|
|
|
|
|
|
<el-tag
|
|
|
|
|
|
v-for="m in (groupMemberMap[group.id] ?? [])"
|
|
|
|
|
|
:key="m.userId ?? m.id"
|
|
|
|
|
|
closable
|
|
|
|
|
|
@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>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-empty v-if="!selectedGroupItems.length" description="请先在第一步选择报名组别" />
|
|
|
|
|
|
</div>
|
2026-03-23 18:01:01 +08:00
|
|
|
|
|
2026-03-30 02:42:26 +08:00
|
|
|
|
<!-- 右侧:团队选择器 + 成员列表 -->
|
|
|
|
|
|
<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>
|
|
|
|
|
|
</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" />
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<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="assignMemberToActiveGroup(m)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ m.name }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-23 18:01:01 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Step 3 -->
|
|
|
|
|
|
<div v-else-if="activeStep === 2">
|
|
|
|
|
|
<el-skeleton :rows="10" animated v-if="previewLoading" />
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<el-descriptions :column="2" border size="small" v-if="previewData">
|
|
|
|
|
|
<el-descriptions-item label="赛事">{{ previewData?.eventName ?? '-' }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="合计金额">
|
|
|
|
|
|
{{ previewData?.totalAmount ?? 0 }} 元
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="detail-section-title">已选择的项目</div>
|
|
|
|
|
|
<el-table :data="previewData?.projects ?? []" border size="small" style="margin-top: 10px;">
|
|
|
|
|
|
<el-table-column prop="name" label="项目" align="center" />
|
|
|
|
|
|
<el-table-column prop="groupName" label="组别" align="center" />
|
|
|
|
|
|
<el-table-column prop="fee" label="费用(元)" align="center" width="120" />
|
|
|
|
|
|
<el-table-column prop="limitCount" label="名额" align="center" width="120" />
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="detail-section-title">已选择的队员</div>
|
|
|
|
|
|
<el-table :data="previewData?.members ?? []" border size="small" style="margin-top: 10px;">
|
|
|
|
|
|
<el-table-column prop="name" label="姓名" align="center" />
|
|
|
|
|
|
<el-table-column prop="mobile" label="手机号" align="center" />
|
|
|
|
|
|
<el-table-column prop="idCard" label="身份证" align="center" show-overflow-tooltip />
|
|
|
|
|
|
<el-table-column prop="gender" label="性别" align="center" />
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="payment-panel">
|
|
|
|
|
|
<div class="payment-panel-title">支付方式</div>
|
|
|
|
|
|
<el-select v-model="paymentMethod" placeholder="请选择支付方式" style="width: 240px;">
|
|
|
|
|
|
<el-option :value="1" label="微信支付" />
|
|
|
|
|
|
<el-option :value="2" label="支付宝支付" />
|
|
|
|
|
|
<el-option :value="3" label="银行转账" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<span class="dialog-footer">
|
|
|
|
|
|
<el-button @click="isShowRegisterDialog = false">取消</el-button>
|
|
|
|
|
|
|
|
|
|
|
|
<el-button v-if="activeStep === 1" @click="handlePrevious" :disabled="registerLoading">上一步</el-button>
|
|
|
|
|
|
<el-button v-if="activeStep === 2" @click="handlePrevious">上一步</el-button>
|
|
|
|
|
|
|
|
|
|
|
|
<el-button v-if="activeStep === 0" type="primary" @click="handleStep1Next"
|
|
|
|
|
|
:loading="registerLoading">下一步</el-button>
|
|
|
|
|
|
<el-button v-else-if="activeStep === 1" type="primary" @click="handleStep2Next"
|
|
|
|
|
|
:loading="submitDraftLoading">下一步</el-button>
|
|
|
|
|
|
<el-button v-else-if="activeStep === 2" type="primary" @click="handleCreateOrderAndPay"
|
|
|
|
|
|
:loading="createOrderLoading" :disabled="!draftRegistrationId">去支付</el-button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-03-30 02:42:26 +08:00
|
|
|
|
import { ref, computed } from 'vue';
|
2026-03-23 18:01:01 +08:00
|
|
|
|
import { ElMessage } from 'element-plus';
|
2026-03-30 02:42:26 +08:00
|
|
|
|
import { Close } from '@element-plus/icons-vue';
|
2026-03-23 18:01:01 +08:00
|
|
|
|
import ProTable from '/@/components/ProTable/index.vue';
|
|
|
|
|
|
import { formatDate } from '/@/utils/formatTime';
|
|
|
|
|
|
import {
|
|
|
|
|
|
usereventregistrationListGet,
|
|
|
|
|
|
usereventregistrationDetailGet,
|
|
|
|
|
|
usereventregistrationMembersSearchGet,
|
|
|
|
|
|
usereventregistrationSubmitPost,
|
|
|
|
|
|
usereventregistrationPreviewGet,
|
|
|
|
|
|
usereventregistrationOrderCreatePost,
|
|
|
|
|
|
} from '/@/api/match/usereventregistration';
|
2026-03-30 02:42:26 +08:00
|
|
|
|
import { teamPageGet, teamDetailGet } from '/@/api/match/unitUser';
|
2026-03-23 18:01:01 +08:00
|
|
|
|
|
|
|
|
|
|
const tableRef = ref();
|
|
|
|
|
|
const initParam = ref({ refresh: true });
|
|
|
|
|
|
|
|
|
|
|
|
const columns = ref([
|
2026-03-30 20:14:20 +08:00
|
|
|
|
{ type: 'index', label: '序号', align: 'center', width: 70, fixed: 'left' },
|
2026-03-23 18:01:01 +08:00
|
|
|
|
{
|
|
|
|
|
|
prop: 'name',
|
|
|
|
|
|
label: '赛事名称',
|
|
|
|
|
|
search: { el: 'input', key: 'Keyword' },
|
|
|
|
|
|
fixed: 'left',
|
|
|
|
|
|
minWidth: 170,
|
|
|
|
|
|
},
|
|
|
|
|
|
{ prop: 'coverImage', label: '封面', align: 'center', width: 110 },
|
|
|
|
|
|
{ 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 },
|
|
|
|
|
|
{ prop: 'registrationStartTime', label: '报名起止时间', align: 'center', minWidth: 220 },
|
|
|
|
|
|
{ prop: 'matchStartTime', label: '比赛起止时间', align: 'center', minWidth: 220 },
|
|
|
|
|
|
{ prop: 'action', label: '操作', align: 'center', fixed: 'right', width: 210 },
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------- 字段展示(列表) --------------------
|
|
|
|
|
|
const safeString = (v) => (v === null || v === undefined ? '' : String(v));
|
|
|
|
|
|
|
|
|
|
|
|
const formatDateTime = (v) => {
|
|
|
|
|
|
if (!v) return '-';
|
|
|
|
|
|
try {
|
|
|
|
|
|
return formatDate(new Date(v), 'YYYY-mm-dd HH:MM:SS');
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return safeString(v);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const formatRange = (start, end) => {
|
|
|
|
|
|
if (!start && !end) return '-';
|
|
|
|
|
|
if (start && end) return `${formatDateTime(start)} 至 ${formatDateTime(end)}`;
|
|
|
|
|
|
return formatDateTime(start || end);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const eventTypeText = (row) => {
|
|
|
|
|
|
const t = row?.type ?? row?.eventType;
|
|
|
|
|
|
if (t === 'Online' || t === 0) return '线上';
|
|
|
|
|
|
if (t === 'Offline' || t === 1) return '线下';
|
|
|
|
|
|
return t ?? '-';
|
|
|
|
|
|
};
|
|
|
|
|
|
const eventTypeTagType = (row) => (eventTypeText(row) === '线上' ? 'primary' : 'info');
|
|
|
|
|
|
|
|
|
|
|
|
const registrationTypeText = (row) => {
|
|
|
|
|
|
const v = row?.registrationType ?? row?.registrationMode ?? row?.registrationTypeName;
|
|
|
|
|
|
if (!v) return '-';
|
|
|
|
|
|
switch (v) {
|
|
|
|
|
|
case 'OnlyIndividual':
|
|
|
|
|
|
case 1:
|
|
|
|
|
|
return '仅个人报名';
|
|
|
|
|
|
case 'OnlyTeam':
|
|
|
|
|
|
case 2:
|
|
|
|
|
|
return '仅团队报名';
|
|
|
|
|
|
case 'Both':
|
|
|
|
|
|
case 3:
|
|
|
|
|
|
return '个人&团队';
|
|
|
|
|
|
default:
|
|
|
|
|
|
return v ?? '-';
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const chargeModeText = (row) => {
|
|
|
|
|
|
const v = row?.chargeMode ?? row?.feeConfig?.chargeMode ?? row?.feeMode;
|
|
|
|
|
|
if (!v) return '-';
|
|
|
|
|
|
// 后端可能返回 Free/Paid 等枚举,这里做一个常见值映射
|
|
|
|
|
|
if (v === 'Free' || v === 0 || v === 'free') return '免费';
|
|
|
|
|
|
if (v === 'Paid' || v === 1 || v === 'paid') return '收费';
|
|
|
|
|
|
return v ?? '-';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const eventStatusText = (status) => {
|
|
|
|
|
|
if (!status && status !== 0) return '-';
|
|
|
|
|
|
// 兼容:string 枚举 + 数字枚举
|
|
|
|
|
|
const s = status;
|
|
|
|
|
|
switch (s) {
|
|
|
|
|
|
case 'Enrolling':
|
|
|
|
|
|
case 'enrolling':
|
|
|
|
|
|
case 0:
|
|
|
|
|
|
return '报名中';
|
|
|
|
|
|
case 'Ongoing':
|
|
|
|
|
|
case 'ongoing':
|
|
|
|
|
|
case 1:
|
|
|
|
|
|
return '比赛中';
|
|
|
|
|
|
case 'Ended':
|
|
|
|
|
|
case 'ended':
|
|
|
|
|
|
case 2:
|
|
|
|
|
|
return '已结束';
|
|
|
|
|
|
default:
|
|
|
|
|
|
return safeString(status);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const eventStatusTagType = (status) => {
|
|
|
|
|
|
const text = eventStatusText(status);
|
|
|
|
|
|
if (text === '报名中') return 'warning';
|
|
|
|
|
|
if (text === '比赛中') return 'primary';
|
|
|
|
|
|
if (text === '已结束') return 'info';
|
|
|
|
|
|
return 'info';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const isEventEnrolling = (row) => {
|
2026-03-30 20:14:20 +08:00
|
|
|
|
// 用户端"去报名"只对报名中开放,同时需要用户通过商户认证
|
2026-03-23 18:01:01 +08:00
|
|
|
|
const s = row?.status ?? row?.eventStatus;
|
2026-03-30 20:14:20 +08:00
|
|
|
|
const isEnrolling = s === 'Enrolling' || s === 'enrolling' || s === 0 || s === '0';
|
|
|
|
|
|
const canRegister = row?.canRegister ?? row?.CanRegister ?? false;
|
|
|
|
|
|
return isEnrolling && canRegister;
|
2026-03-23 18:01:01 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------- 赛事详情 --------------------
|
|
|
|
|
|
const isShowDetailDialog = ref(false);
|
|
|
|
|
|
const detailLoading = ref(false);
|
|
|
|
|
|
const detailData = ref(null);
|
|
|
|
|
|
|
|
|
|
|
|
const clearDetailState = () => {
|
|
|
|
|
|
detailLoading.value = false;
|
|
|
|
|
|
detailData.value = null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const openDetailDialog = async (row) => {
|
|
|
|
|
|
const eventId = row?.id;
|
|
|
|
|
|
if (!eventId) return;
|
|
|
|
|
|
isShowDetailDialog.value = true;
|
|
|
|
|
|
detailLoading.value = true;
|
|
|
|
|
|
detailData.value = null;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { succeeded, data } = await usereventregistrationDetailGet(eventId);
|
|
|
|
|
|
if (!succeeded) {
|
|
|
|
|
|
ElMessage.error(data ?? '赛事详情加载失败');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
detailData.value = data ?? null;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
ElMessage.error('赛事详情加载失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
detailLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------- 三步报名 --------------------
|
|
|
|
|
|
const isShowRegisterDialog = ref(false);
|
|
|
|
|
|
const activeStep = ref(0);
|
|
|
|
|
|
const registerLoading = ref(false);
|
|
|
|
|
|
const submitDraftLoading = ref(false);
|
|
|
|
|
|
const createOrderLoading = ref(false);
|
|
|
|
|
|
|
|
|
|
|
|
const projects = ref([]);
|
2026-03-30 02:42:26 +08:00
|
|
|
|
const selectedGroupIds = ref([]);
|
|
|
|
|
|
|
|
|
|
|
|
// 从所有 projects 的 groups 中提取所有组别的扁平列表
|
|
|
|
|
|
const allGroups = computed(() => {
|
|
|
|
|
|
const list = [];
|
|
|
|
|
|
(projects.value ?? []).forEach((p) => {
|
|
|
|
|
|
(p.groups ?? []).forEach((g) => {
|
|
|
|
|
|
list.push({
|
|
|
|
|
|
...g,
|
|
|
|
|
|
projectId: p.id,
|
|
|
|
|
|
projectName: p.name,
|
|
|
|
|
|
projectTeamSize: p.teamSize ?? 1,
|
|
|
|
|
|
projectCompletionCount: p.completionCount ?? 0,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
return list;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 已选中的组别详情列表
|
|
|
|
|
|
const selectedGroupItems = computed(() => {
|
|
|
|
|
|
return allGroups.value
|
|
|
|
|
|
.filter((g) => selectedGroupIds.value.includes(g.id))
|
|
|
|
|
|
.map((g) => ({
|
|
|
|
|
|
...g,
|
|
|
|
|
|
dateRange: formatGroupDate(g),
|
|
|
|
|
|
}));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 切换组别选中状态
|
|
|
|
|
|
const toggleGroupSelection = (id) => {
|
|
|
|
|
|
const idx = selectedGroupIds.value.indexOf(id);
|
|
|
|
|
|
if (idx >= 0) {
|
|
|
|
|
|
selectedGroupIds.value.splice(idx, 1);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
selectedGroupIds.value.push(id);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化性别限制
|
|
|
|
|
|
const formatGender = (gender) => {
|
|
|
|
|
|
const v = Number(gender);
|
|
|
|
|
|
if (v === 0) return '男';
|
|
|
|
|
|
if (v === 1) return '女';
|
|
|
|
|
|
return '不限';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化组别的年龄段(出生日期范围)
|
|
|
|
|
|
const formatGroupDate = (item) => {
|
|
|
|
|
|
const start = item?.birthDateStart;
|
|
|
|
|
|
const end = item?.birthDateEnd;
|
|
|
|
|
|
if (!start && !end) return '';
|
|
|
|
|
|
const fmt = (v) => {
|
|
|
|
|
|
if (!v) return '';
|
|
|
|
|
|
try { return formatDate(new Date(v), 'YYYY-mm-dd'); } catch { return String(v); }
|
|
|
|
|
|
};
|
|
|
|
|
|
if (start && end) return `${fmt(start)} ~ ${fmt(end)}`;
|
|
|
|
|
|
return fmt(start || end);
|
|
|
|
|
|
};
|
2026-03-23 18:01:01 +08:00
|
|
|
|
|
|
|
|
|
|
const members = ref([]);
|
|
|
|
|
|
const memberKeyword = ref('');
|
|
|
|
|
|
const memberLoading = ref(false);
|
|
|
|
|
|
const selectedMemberIds = ref([]);
|
|
|
|
|
|
|
2026-03-30 02:42:26 +08:00
|
|
|
|
// Step2: 团队相关
|
|
|
|
|
|
const teamList = ref([]);
|
|
|
|
|
|
const teamListLoading = ref(false);
|
|
|
|
|
|
const selectedTeamId = ref(null);
|
|
|
|
|
|
const teamMembers = ref([]);
|
|
|
|
|
|
const teamMemberLoading = ref(false);
|
|
|
|
|
|
const activeAssignGroupId = ref(null); // 当前正在分配队员的组别ID
|
|
|
|
|
|
const groupMemberMap = ref({}); // { groupId: [member, ...] }
|
|
|
|
|
|
|
2026-03-23 18:01:01 +08:00
|
|
|
|
const previewLoading = ref(false);
|
|
|
|
|
|
const previewData = ref(null);
|
|
|
|
|
|
const paymentMethod = ref(1);
|
|
|
|
|
|
const draftRegistrationId = ref(null);
|
|
|
|
|
|
|
|
|
|
|
|
const clearRegisterState = () => {
|
|
|
|
|
|
activeStep.value = 0;
|
|
|
|
|
|
registerLoading.value = false;
|
|
|
|
|
|
submitDraftLoading.value = false;
|
|
|
|
|
|
createOrderLoading.value = false;
|
|
|
|
|
|
currentEventId.value = null;
|
|
|
|
|
|
projects.value = [];
|
2026-03-30 02:42:26 +08:00
|
|
|
|
selectedGroupIds.value = [];
|
2026-03-23 18:01:01 +08:00
|
|
|
|
members.value = [];
|
|
|
|
|
|
memberKeyword.value = '';
|
|
|
|
|
|
memberLoading.value = false;
|
|
|
|
|
|
selectedMemberIds.value = [];
|
2026-03-30 02:42:26 +08:00
|
|
|
|
teamList.value = [];
|
|
|
|
|
|
selectedTeamId.value = null;
|
|
|
|
|
|
teamMembers.value = [];
|
|
|
|
|
|
teamMemberLoading.value = false;
|
|
|
|
|
|
activeAssignGroupId.value = null;
|
|
|
|
|
|
groupMemberMap.value = {};
|
2026-03-23 18:01:01 +08:00
|
|
|
|
previewLoading.value = false;
|
|
|
|
|
|
previewData.value = null;
|
|
|
|
|
|
paymentMethod.value = 1;
|
|
|
|
|
|
draftRegistrationId.value = null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const currentEventId = ref(null);
|
|
|
|
|
|
|
|
|
|
|
|
const resetStep2AndBeyond = () => {
|
|
|
|
|
|
members.value = [];
|
|
|
|
|
|
selectedMemberIds.value = [];
|
|
|
|
|
|
memberKeyword.value = '';
|
2026-03-30 02:42:26 +08:00
|
|
|
|
selectedTeamId.value = null;
|
|
|
|
|
|
teamMembers.value = [];
|
|
|
|
|
|
teamMemberLoading.value = false;
|
|
|
|
|
|
activeAssignGroupId.value = null;
|
|
|
|
|
|
groupMemberMap.value = {};
|
2026-03-23 18:01:01 +08:00
|
|
|
|
previewData.value = null;
|
|
|
|
|
|
previewLoading.value = false;
|
|
|
|
|
|
draftRegistrationId.value = null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const openRegistrationDialog = async (row) => {
|
|
|
|
|
|
const eventId = row?.id;
|
|
|
|
|
|
if (!eventId) return;
|
|
|
|
|
|
|
|
|
|
|
|
clearRegisterState();
|
|
|
|
|
|
currentEventId.value = eventId;
|
|
|
|
|
|
isShowRegisterDialog.value = true;
|
|
|
|
|
|
|
|
|
|
|
|
activeStep.value = 0;
|
|
|
|
|
|
registerLoading.value = true;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-30 02:42:26 +08:00
|
|
|
|
const { succeeded, data } = await usereventregistrationDetailGet(eventId);
|
2026-03-23 18:01:01 +08:00
|
|
|
|
if (!succeeded) {
|
|
|
|
|
|
ElMessage.error(data ?? '项目加载失败');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-30 02:42:26 +08:00
|
|
|
|
projects.value = Array.isArray(data?.projects) ? data.projects : [];
|
2026-03-23 18:01:01 +08:00
|
|
|
|
} catch {
|
|
|
|
|
|
ElMessage.error('项目加载失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
registerLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const isMemberSelectable = (row) => row?.isEligible !== false;
|
|
|
|
|
|
|
|
|
|
|
|
const onMemberSelectionChange = (rows) => {
|
|
|
|
|
|
const list = (rows ?? []).map((r) => r?.userId ?? r?.id).filter((id) => id !== undefined && id !== null);
|
|
|
|
|
|
selectedMemberIds.value = list;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-30 02:42:26 +08:00
|
|
|
|
// 加载团队列表
|
|
|
|
|
|
const fetchTeamList = async () => {
|
|
|
|
|
|
teamListLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { succeeded, data } = await teamPageGet({ PageNo: 1, PageSize: 200 });
|
|
|
|
|
|
if (succeeded && data?.rows) {
|
|
|
|
|
|
teamList.value = data.rows;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
ElMessage.error('团队列表加载失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
teamListLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 切换团队时加载成员
|
|
|
|
|
|
const onTeamChange = async (teamId) => {
|
|
|
|
|
|
teamMembers.value = [];
|
|
|
|
|
|
if (!teamId) return;
|
|
|
|
|
|
teamMemberLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { succeeded, data } = await teamDetailGet(teamId);
|
|
|
|
|
|
if (succeeded && data?.members) {
|
|
|
|
|
|
// 只取 personType 为 1 的运动员
|
|
|
|
|
|
teamMembers.value = data.members
|
|
|
|
|
|
.filter((m) => m.personType === 1)
|
|
|
|
|
|
.map((m) => ({
|
|
|
|
|
|
userId: m.userId ?? m.id,
|
|
|
|
|
|
name: m.name,
|
|
|
|
|
|
mobile: m.mobile ?? '',
|
|
|
|
|
|
gender: m.gender ?? '',
|
|
|
|
|
|
role: m.role ?? '',
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
ElMessage.error('团队成员加载失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
teamMemberLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 判断某成员是否已分配到当前激活的组别
|
|
|
|
|
|
const isMemberAssignedToActive = (memberId) => {
|
|
|
|
|
|
if (!activeAssignGroupId.value) return false;
|
|
|
|
|
|
const list = groupMemberMap.value[activeAssignGroupId.value] ?? [];
|
|
|
|
|
|
return list.some((m) => (m.userId ?? m.id) === memberId);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 将成员分配到当前激活的组别
|
|
|
|
|
|
const assignMemberToActiveGroup = (member) => {
|
|
|
|
|
|
const gId = activeAssignGroupId.value;
|
|
|
|
|
|
if (!gId) {
|
|
|
|
|
|
ElMessage.warning('请先点击左侧的组别卡片');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const memberId = member.userId ?? member.id;
|
|
|
|
|
|
if (!groupMemberMap.value[gId]) {
|
|
|
|
|
|
groupMemberMap.value[gId] = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
// 已存在则不重复添加
|
|
|
|
|
|
if (groupMemberMap.value[gId].some((m) => (m.userId ?? m.id) === memberId)) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
groupMemberMap.value[gId].push({ ...member });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 从组别中移除成员
|
|
|
|
|
|
const removeMemberFromGroup = (groupId, memberId) => {
|
|
|
|
|
|
if (!groupMemberMap.value[groupId]) return;
|
|
|
|
|
|
groupMemberMap.value[groupId] = groupMemberMap.value[groupId].filter((m) => (m.userId ?? m.id) !== memberId);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-23 18:01:01 +08:00
|
|
|
|
const fetchMembers = async () => {
|
|
|
|
|
|
if (!currentEventId.value) return;
|
|
|
|
|
|
memberLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { succeeded, data } = await usereventregistrationMembersSearchGet({ keyword: memberKeyword.value });
|
|
|
|
|
|
if (!succeeded) {
|
|
|
|
|
|
ElMessage.error(data ?? '队员加载失败');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
members.value = Array.isArray(data) ? data : [];
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
ElMessage.error('队员加载失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
memberLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handlePrevious = () => {
|
|
|
|
|
|
if (activeStep.value === 2) {
|
|
|
|
|
|
activeStep.value = 1;
|
|
|
|
|
|
previewData.value = null;
|
|
|
|
|
|
previewLoading.value = false;
|
|
|
|
|
|
draftRegistrationId.value = null;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (activeStep.value === 1) {
|
|
|
|
|
|
activeStep.value = 0;
|
|
|
|
|
|
resetStep2AndBeyond();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleStep1Next = async () => {
|
2026-03-30 02:42:26 +08:00
|
|
|
|
if (!(selectedGroupIds.value?.length ?? 0)) {
|
|
|
|
|
|
ElMessage.warning('请先选择报名组别');
|
2026-03-23 18:01:01 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
activeStep.value = 1;
|
|
|
|
|
|
resetStep2AndBeyond();
|
2026-03-30 02:42:26 +08:00
|
|
|
|
// 默认激活第一个组别
|
|
|
|
|
|
if (selectedGroupItems.value.length > 0) {
|
|
|
|
|
|
activeAssignGroupId.value = selectedGroupItems.value[0].id;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 加载团队列表
|
|
|
|
|
|
await fetchTeamList();
|
2026-03-23 18:01:01 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleStep2Next = async () => {
|
|
|
|
|
|
if (!currentEventId.value) return;
|
2026-03-30 02:42:26 +08:00
|
|
|
|
|
|
|
|
|
|
// 检查每个组别是否至少分配了一个队员
|
|
|
|
|
|
const emptyGroups = selectedGroupItems.value.filter((g) => !(groupMemberMap.value[g.id]?.length));
|
|
|
|
|
|
if (emptyGroups.length > 0) {
|
|
|
|
|
|
ElMessage.warning(`请为所有组别分配队员,"${emptyGroups[0].groupName ?? emptyGroups[0].name}"尚未分配`);
|
2026-03-23 18:01:01 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-30 02:42:26 +08:00
|
|
|
|
// 构造按组别分配的数据
|
|
|
|
|
|
const groupMembers = selectedGroupIds.value.map((gId) => ({
|
|
|
|
|
|
groupId: gId,
|
|
|
|
|
|
memberIds: (groupMemberMap.value[gId] ?? []).map((m) => m.userId ?? m.id),
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// 收集所有不重复的 memberIds
|
|
|
|
|
|
const allMemberIds = [...new Set(groupMembers.flatMap((g) => g.memberIds))];
|
|
|
|
|
|
|
2026-03-23 18:01:01 +08:00
|
|
|
|
submitDraftLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload = {
|
|
|
|
|
|
eventId: currentEventId.value,
|
2026-03-30 02:42:26 +08:00
|
|
|
|
groupIds: selectedGroupIds.value,
|
|
|
|
|
|
groupMembers,
|
|
|
|
|
|
memberIds: allMemberIds,
|
2026-03-23 18:01:01 +08:00
|
|
|
|
isDraft: true,
|
|
|
|
|
|
};
|
|
|
|
|
|
const { succeeded, data } = await usereventregistrationSubmitPost(payload);
|
|
|
|
|
|
if (!succeeded) {
|
|
|
|
|
|
ElMessage.error(data ?? '报名提交失败');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
draftRegistrationId.value = data;
|
|
|
|
|
|
|
|
|
|
|
|
activeStep.value = 2;
|
|
|
|
|
|
previewLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { succeeded: pOk, data: pData } = await usereventregistrationPreviewGet(data);
|
|
|
|
|
|
if (!pOk) {
|
|
|
|
|
|
ElMessage.error(pData ?? '预览支付加载失败');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
previewData.value = pData ?? null;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
previewLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
ElMessage.error('报名提交失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
submitDraftLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleCreateOrderAndPay = async () => {
|
|
|
|
|
|
if (!draftRegistrationId.value) return;
|
|
|
|
|
|
createOrderLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload = {
|
|
|
|
|
|
registrationId: draftRegistrationId.value,
|
|
|
|
|
|
paymentMethod: paymentMethod.value,
|
|
|
|
|
|
};
|
|
|
|
|
|
const { succeeded, data } = await usereventregistrationOrderCreatePost(payload);
|
|
|
|
|
|
if (!succeeded) {
|
|
|
|
|
|
ElMessage.error(data ?? '创建支付订单失败');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (data?.payUrl) {
|
|
|
|
|
|
window.open(data.payUrl, '_blank');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.warning('未获取到支付链接');
|
|
|
|
|
|
}
|
|
|
|
|
|
// 触发列表刷新(支付成功后后端状态会变更)
|
|
|
|
|
|
initParam.value.refresh = !initParam.value.refresh;
|
|
|
|
|
|
isShowRegisterDialog.value = false;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
ElMessage.error('创建支付订单失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
createOrderLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
.dialog-footer {
|
|
|
|
|
|
margin-top: 80px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.register-step-content {
|
|
|
|
|
|
min-height: 380px;
|
|
|
|
|
|
padding: 20px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.search-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
margin: 6px 0 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-section-title {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
margin-top: 14px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.payment-panel {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
padding: 14px;
|
|
|
|
|
|
border: 1px solid var(--el-border-color);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.payment-panel-title {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
}
|
2026-03-30 02:42:26 +08:00
|
|
|
|
|
|
|
|
|
|
/* ---- 项目卡片 ---- */
|
|
|
|
|
|
.project-card {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
border: 1px solid #d0e4f7;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
|
|
|
|
|
&-left {
|
|
|
|
|
|
flex: 0 0 150px;
|
|
|
|
|
|
background: linear-gradient(135deg, #56b4f9, #3d9be9);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
padding: 14px 12px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&-name {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&-right {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
align-content: flex-start;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.group-tag {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 8px 14px;
|
|
|
|
|
|
border: 1px solid #d0e4f7;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
border-color: #56b4f9;
|
|
|
|
|
|
background: #f0f7ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&-selected {
|
|
|
|
|
|
border-color: #409eff;
|
|
|
|
|
|
background: #ecf5ff;
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
box-shadow: 0 0 0 1px #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&-name {
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&-date, &-gender {
|
|
|
|
|
|
color: #888;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&-selected &-date,
|
|
|
|
|
|
&-selected &-gender {
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ---- 已选报名项目汇总 ---- */
|
|
|
|
|
|
.selected-summary {
|
|
|
|
|
|
margin-top: 18px;
|
|
|
|
|
|
border: 1px solid #d0e4f7;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
|
|
&-header {
|
|
|
|
|
|
background: linear-gradient(135deg, #56b4f9, #3d9be9);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 10px 0;
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&-list {
|
|
|
|
|
|
padding: 14px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
min-height: 50px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ---- Step2 分配队员 ---- */
|
|
|
|
|
|
.step2-layout {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
min-height: 400px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step2-left {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
max-height: 520px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step2-right {
|
|
|
|
|
|
flex: 0 0 280px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.assign-card {
|
|
|
|
|
|
border: 1px solid #d0e4f7;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
margin-bottom: 14px;
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&-header {
|
|
|
|
|
|
background: linear-gradient(135deg, #56b4f9, #3d9be9);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
padding: 10px 14px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&-close {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
opacity: 0.8;
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&-members {
|
|
|
|
|
|
padding: 10px 14px;
|
|
|
|
|
|
min-height: 46px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&-placeholder {
|
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.team-selector {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.team-members-panel {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
border: 1px solid #d0e4f7;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
max-height: 460px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
align-content: flex-start;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.team-member-tag {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
min-width: 100px;
|
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
|
border: 1px solid #d0e4f7;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
border-color: #56b4f9;
|
|
|
|
|
|
background: #f0f7ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&-assigned {
|
|
|
|
|
|
border-color: #409eff;
|
|
|
|
|
|
background: #ecf5ff;
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-23 18:01:01 +08:00
|
|
|
|
</style>
|