2033 lines
54 KiB
Vue
2033 lines
54 KiB
Vue
<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="height: 60px; width: 80px"
|
||
: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="scope.row.status === 0 || scope.row.status === 'Enrolling' || scope.row.status === 'enrolling' ? 'success' : scope.row.status === 1 || scope.row.status === 'Ongoing' || scope.row.status === 'ongoing' ? 'warning' : 'danger'">
|
||
{{ scope.row.status === 0 || scope.row.status === 'Enrolling' || scope.row.status === 'enrolling' ? '报名中' : scope.row.status === 1 || scope.row.status === 'Ongoing' || scope.row.status === 'ongoing' ? '比赛中' : '已结束' }}
|
||
</el-tag>
|
||
</template>
|
||
|
||
<template #registrationStartTime="scope">
|
||
<div v-if="scope.row.registrationStartTime && scope.row.registrationEndTime">
|
||
<div>开始:{{ formatDateTime(scope.row.registrationStartTime) }}</div>
|
||
<div>截止:{{ formatDateTime(scope.row.registrationEndTime) }}</div>
|
||
</div>
|
||
<span v-else>-</span>
|
||
</template>
|
||
|
||
<template #matchStartTime="scope">
|
||
<div v-if="scope.row.matchStartTime && scope.row.matchEndTime">
|
||
<div>开始:{{ formatDateTime(scope.row.matchStartTime) }}</div>
|
||
<div>截止:{{ formatDateTime(scope.row.matchEndTime) }}</div>
|
||
</div>
|
||
<span v-else>-</span>
|
||
</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%" style="min-height: 600px;" @closed="clearDetailState">
|
||
<el-skeleton :rows="12" animated v-if="detailLoading" />
|
||
<template v-else>
|
||
<div v-if="formData">
|
||
<Step1BasicInfo v-model:modelValue="formData.basicInfo" :readonly="true" />
|
||
<div style="height:50px;"></div>
|
||
<Step2Units v-model:modelValue="formData.units" :readonly="true" />
|
||
<div style="height:50px;"></div>
|
||
<Step3ProjectConfig v-model:modelValue="formData.projectConfig" :readonly="true" />
|
||
<div style="height:50px;"></div>
|
||
<Step4FeeConfig v-model:modelValue="formData.feeConfig" :readonly="true" />
|
||
<div style="height:100px;"></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-alert
|
||
title="报名规则"
|
||
type="info"
|
||
:closable="false"
|
||
style="margin-bottom: 20px;"
|
||
show-icon
|
||
>
|
||
<template #default>
|
||
<ul style="margin: 0; padding-left: 20px;">
|
||
<li>一次报名操作仅允许选择并提交<strong>单个团队</strong>内的运动员</li>
|
||
<li>该团队只能报名参与<strong>一个项目</strong></li>
|
||
</ul>
|
||
</template>
|
||
</el-alert>
|
||
<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 />
|
||
<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.parentProjectTypeName ?? project.projectTypeName ?? '' }}{{ project.parentProjectTypeName || project.projectTypeName ? '-' + 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>
|
||
</div>
|
||
|
||
<!-- Step 2 -->
|
||
<div v-else-if="activeStep === 1">
|
||
<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.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>
|
||
</div>
|
||
<div 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;"
|
||
>
|
||
{{ m.name }}
|
||
</el-tag>
|
||
<span v-if="!(groupMemberMap[group.id]?.length)" class="assign-card-placeholder">点击此卡片后,从右侧选择队员</span>
|
||
</div>
|
||
<!-- 队员验证错误提示 -->
|
||
<div v-if="hasValidationErrorsForGroup(group)" class="validation-error-section">
|
||
<div class="validation-error-item" v-for="m in (groupMemberMap[group.id] ?? [])" :key="m.userId ?? m.id">
|
||
<el-alert
|
||
v-if="!getMemberValidationResult(m, group).isValid"
|
||
:title="getMemberValidationResult(m, group).message"
|
||
type="error"
|
||
:closable="false"
|
||
size="small"
|
||
style="margin-top: 4px;"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<el-empty v-if="!selectedGroupItems.length" description="请先在第一步选择报名组别" />
|
||
</div>
|
||
|
||
<!-- 右侧:团队选择器 + 成员列表 -->
|
||
<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>
|
||
|
||
<!-- 验证提示区域 -->
|
||
<div class="validation-notes-section">
|
||
<!-- 年龄性别不符合要求提示 -->
|
||
<el-alert
|
||
v-if="hasAnyValidationErrors()"
|
||
title="队员信息不符合组别要求"
|
||
type="error"
|
||
:closable="false"
|
||
style="margin-bottom: 12px;"
|
||
/>
|
||
|
||
<!-- 限报提示 -->
|
||
<el-alert
|
||
v-if="hasAnyRegistrationLimit()"
|
||
title="此队员已达到报名限制"
|
||
type="warning"
|
||
:closable="false"
|
||
style="margin-bottom: 12px;"
|
||
/>
|
||
|
||
<!-- 项目完成人数提示 -->
|
||
<el-alert
|
||
v-if="hasProjectCompletionCountError()"
|
||
title="项目完成人数错误"
|
||
type="error"
|
||
:closable="false"
|
||
style="margin-bottom: 12px;"
|
||
/>
|
||
|
||
<!-- 队伍数量限制提示 -->
|
||
<el-alert
|
||
v-if="showTeamLimitHint"
|
||
title="若管理后台配置一个组别只能限报2队,添加2队后,'添加'按钮隐藏"
|
||
type="info"
|
||
:closable="false"
|
||
style="margin-bottom: 12px;"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 底部操作区域 -->
|
||
<div class="step2-footer">
|
||
<el-button @click="handlePrevious">上一步</el-button>
|
||
<el-button
|
||
type="primary"
|
||
@click="handleStep2Next"
|
||
:loading="submitDraftLoading"
|
||
:disabled="hasAnyValidationErrors() || hasProjectCompletionCountError() || hasAnyRegistrationLimit()"
|
||
>
|
||
预览
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 3 -->
|
||
<div v-else-if="activeStep === 2">
|
||
<el-skeleton :rows="10" animated v-if="previewLoading" />
|
||
<template v-else>
|
||
<!-- 支付二维码显示区域 -->
|
||
<div v-if="paymentData" class="payment-qrcode-section">
|
||
<div class="qrcode-title">请使用微信扫码支付</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>
|
||
<div class="qrcode-tip">支付链接获取失败</div>
|
||
</div>
|
||
</div>
|
||
<div class="payment-amount">
|
||
<span class="amount-label">支付金额:</span>
|
||
<span class="amount-value">¥ {{ finalPaymentAmount }}</span>
|
||
</div>
|
||
<div class="payment-status" v-if="pollingStatus">
|
||
<el-icon class="is-loading"><Loading /></el-icon>
|
||
{{ pollingStatus }}
|
||
</div>
|
||
<div class="payment-actions">
|
||
<el-button @click="cancelPayment">取消支付</el-button>
|
||
<el-button type="primary" @click="refreshPayment" :loading="polling">刷新二维码</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 预览区域(未支付时) -->
|
||
<template v-else>
|
||
<!-- 根据原型图,将项目和队员整合在一个表格中 -->
|
||
<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" width="180" />
|
||
<el-table-column prop="groupName" label="组别" align="center" width="150" />
|
||
<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">
|
||
{{ member.name }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<!-- 优惠券输入区域 -->
|
||
<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-button>
|
||
<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.discountAmount }} 元</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部金额和操作区域 -->
|
||
<div class="preview-footer-section">
|
||
<div class="total-amount-display">
|
||
<span class="total-label">合计金额:</span>
|
||
<span v-if="!validatedCoupon" class="total-value">
|
||
¥ {{ previewData?.totalAmount ?? 0 }}
|
||
</span>
|
||
<span v-else class="total-value">
|
||
<del class="original-price">¥ {{ previewData?.totalAmount ?? 0 }}</del>
|
||
<span class="discounted-price">¥ {{ validatedCoupon.finalAmount }}</span>
|
||
</span>
|
||
</div>
|
||
<div class="payment-actions">
|
||
<el-button @click="handlePrevious">上一步</el-button>
|
||
<div class="payment-method-wrapper" v-if="!isFreePreview">
|
||
<el-select v-model="paymentMethod" placeholder="请选择支付方式" style="width: 200px;">
|
||
<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>
|
||
</div>
|
||
<el-button v-else type="primary" @click="handleCreateOrderAndPay" :loading="createOrderLoading" :disabled="!draftRegistrationId">
|
||
确认报名
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 免费项目提示 -->
|
||
<div v-if="isFreePreview" class="free-project-hint">
|
||
<el-tag type="success">免费项目</el-tag>
|
||
<span>无需支付,点击确认报名直接完成</span>
|
||
</div>
|
||
</template>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<span class="dialog-footer">
|
||
<el-button @click="isShowRegisterDialog = false">取消</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>
|
||
</span>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, nextTick, onUnmounted } from 'vue';
|
||
import { ElMessage } from 'element-plus';
|
||
import { Close, Picture, Loading, CircleCheck, ArrowRight } from '@element-plus/icons-vue';
|
||
import ProTable from '/@/components/ProTable/index.vue';
|
||
import { formatDate } from '/@/utils/formatTime';
|
||
import {
|
||
usereventregistrationListGet,
|
||
usereventregistrationDetailGet,
|
||
usereventregistrationMembersSearchGet,
|
||
usereventregistrationSubmitPost,
|
||
usereventregistrationPreviewGet,
|
||
usereventregistrationOrderCreatePost,
|
||
usereventregistrationCouponValidatePost,
|
||
usereventregistrationMemberValidatePost,
|
||
usereventregistrationRegistrationLimitCheckPost,
|
||
usereventregistrationGroupTeamLimitGet,
|
||
usereventregistrationProjectCompletionCheckPost,
|
||
usereventregistrationOrderStatusGet,
|
||
} from '/@/api/match/usereventregistration';
|
||
import { teamPageGet, teamDetailGet } from '/@/api/match/unitUser';
|
||
import QRCode from 'qrcodejs2-fixes';
|
||
import Step1BasicInfo from '/@/views/match/manage/Steps/Step1BasicInfo.vue';
|
||
import Step2Units from '/@/views/match/manage/Steps/Step2Units.vue';
|
||
import Step3ProjectConfig from '/@/views/match/manage/Steps/Step3ProjectConfig.vue';
|
||
import Step4FeeConfig from '/@/views/match/manage/Steps/Step4FeeConfig.vue';
|
||
|
||
const tableRef = ref();
|
||
const initParam = ref({ refresh: true });
|
||
|
||
const columns = ref([
|
||
{ type: 'index', label: '序号', align: 'center', width: 70, fixed: 'left' },
|
||
{
|
||
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: 320 },
|
||
{ prop: 'matchStartTime', label: '比赛起止时间', align: 'center', minWidth: 320 },
|
||
{ 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 formatTimeRange = (start, end) => {
|
||
if (!start && !end) return '-';
|
||
if (start && end) return `${formatDateTime(start)} - ${formatDateTime(end)}`;
|
||
return formatDateTime(start || end);
|
||
};
|
||
|
||
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 0:
|
||
return '只允许个人报名';
|
||
case 'OnlyTeam':
|
||
case 1:
|
||
return '只允许团队报名';
|
||
case 'Both':
|
||
case 2:
|
||
return '两者都有';
|
||
default:
|
||
return v ?? '-';
|
||
}
|
||
};
|
||
|
||
const chargeModeText = (row) => {
|
||
const v = row?.chargeMode ?? row?.feeConfig?.chargeMode ?? row?.feeMode;
|
||
if (!v) return '-';
|
||
// 后端枚举值映射,确保与后端完全匹配
|
||
switch (v) {
|
||
case 'Free':
|
||
case 0:
|
||
case 'free':
|
||
return '免费';
|
||
case 'FixedPrice':
|
||
case 1:
|
||
return '定价(赛事)';
|
||
case 'UnitPriceEvent':
|
||
case 2:
|
||
return '单价(赛事)';
|
||
case 'UnitPriceProject':
|
||
case 3:
|
||
return '单价(项目)';
|
||
default:
|
||
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) => {
|
||
// 用户端"去报名"只对报名中开放,同时需要用户通过商户认证
|
||
const s = row?.status ?? row?.eventStatus;
|
||
const isEnrolling = s === 'Enrolling' || s === 'enrolling' || s === 0 || s === '0';
|
||
const canRegister = row?.canRegister ?? row?.CanRegister ?? false;
|
||
return isEnrolling && canRegister;
|
||
};
|
||
|
||
// 展平项目和组别数据用于详情弹窗显示
|
||
const detailProjectsForDisplay = computed(() => {
|
||
const list = [];
|
||
(detailData.value?.projects ?? []).forEach((p) => {
|
||
(p.groups ?? []).forEach((g) => {
|
||
list.push({
|
||
projectName: p.name,
|
||
groupName: g.groupName,
|
||
fee: p.fee,
|
||
limitCount: p.limitCount,
|
||
completionCount: p.completionCount
|
||
});
|
||
});
|
||
// 如果没有组别,只显示项目
|
||
if (!(p.groups?.length)) {
|
||
list.push({
|
||
projectName: p.name,
|
||
groupName: '-',
|
||
fee: p.fee,
|
||
limitCount: p.limitCount,
|
||
completionCount: p.completionCount
|
||
});
|
||
}
|
||
});
|
||
return list;
|
||
});
|
||
|
||
// -------------------- 赛事详情 --------------------
|
||
const isShowDetailDialog = ref(false);
|
||
const detailLoading = ref(false);
|
||
const detailData = ref(null);
|
||
const formData = ref({
|
||
// 步骤1:基本信息
|
||
basicInfo: {
|
||
id: undefined,
|
||
eventName: '',
|
||
coverImage: '',
|
||
eventVideo: '',
|
||
eventType: '',
|
||
registrationType: '',
|
||
pointsRuleId: undefined,
|
||
registrationTime: [],
|
||
isNeedMaterial: false,
|
||
materialRemark: '',
|
||
materialUploadTime: [],
|
||
matchTime: [],
|
||
regionIds: [],
|
||
detailAddress: '',
|
||
eventIntroduction: '',
|
||
customConfigJson: '',
|
||
},
|
||
// 步骤2:参与单位
|
||
units: {
|
||
units: [],
|
||
},
|
||
// 步骤3:项目与限报配置
|
||
projectConfig: {
|
||
projects: [],
|
||
registrationLimit: {
|
||
individualProjectLimit: 1,
|
||
teamProjectLimit: 1,
|
||
},
|
||
},
|
||
// 步骤4:费用与账户配置
|
||
feeConfig: {
|
||
chargeMode: 'Free',
|
||
price: 0,
|
||
merchantId: undefined,
|
||
merchantName: '',
|
||
creditCode: '',
|
||
bankName: '',
|
||
accountNumber: '',
|
||
},
|
||
});
|
||
|
||
const clearDetailState = () => {
|
||
detailLoading.value = false;
|
||
detailData.value = null;
|
||
formData.value = {
|
||
basicInfo: {
|
||
id: undefined,
|
||
eventName: '',
|
||
coverImage: '',
|
||
eventVideo: '',
|
||
eventType: '',
|
||
registrationType: '',
|
||
pointsRuleId: undefined,
|
||
registrationTime: [],
|
||
isNeedMaterial: false,
|
||
materialRemark: '',
|
||
materialUploadTime: [],
|
||
matchTime: [],
|
||
regionIds: [],
|
||
detailAddress: '',
|
||
eventIntroduction: '',
|
||
customConfigJson: '',
|
||
},
|
||
units: { units: [] },
|
||
projectConfig: {
|
||
projects: [],
|
||
registrationLimit: { individualProjectLimit: 1, teamProjectLimit: 1 },
|
||
},
|
||
feeConfig: {
|
||
chargeMode: 'Free',
|
||
price: 0,
|
||
merchantId: undefined,
|
||
merchantName: '',
|
||
creditCode: '',
|
||
bankName: '',
|
||
accountNumber: '',
|
||
},
|
||
};
|
||
};
|
||
|
||
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;
|
||
if (data) {
|
||
// 映射数据到 formData 结构
|
||
formData.value.basicInfo = {
|
||
...formData.value.basicInfo,
|
||
id: data.id,
|
||
eventName: data.name || data.eventName || '',
|
||
coverImage: data.coverImage || '',
|
||
eventVideo: data.eventVideo || '',
|
||
eventType: data.type || data.eventType || '',
|
||
registrationType: data.registrationType || data.registrationMode || '',
|
||
pointsRuleId: data.pointsRuleId,
|
||
registrationTime: [data.registrationStartTime, data.registrationEndTime],
|
||
isNeedMaterial: !!data.isNeedMaterial,
|
||
materialRemark: data.materialRemark || '',
|
||
materialUploadTime: [data.materialUploadStartTime, data.materialUploadEndTime],
|
||
matchTime: [data.matchStartTime, data.matchEndTime],
|
||
regionIds: [data.provinceId, data.cityId, data.districtId].filter((x) => x !== undefined && x !== null),
|
||
detailAddress: data.detailAddress || '',
|
||
eventIntroduction: data.eventIntroduction || '',
|
||
customConfigJson: data.customConfigJson || '',
|
||
};
|
||
formData.value.units = { units: data.units || [] };
|
||
formData.value.projectConfig = {
|
||
projects: data.projects || [],
|
||
registrationLimit: data.registrationLimit || { individualProjectLimit: 1, teamProjectLimit: 1 },
|
||
};
|
||
formData.value.feeConfig = { ...formData.value.feeConfig, ...(data.feeConfig || {}) };
|
||
}
|
||
} 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([]);
|
||
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,
|
||
parentProjectTypeName: p.parentProjectTypeName,
|
||
projectTypeName: p.projectTypeName,
|
||
});
|
||
});
|
||
});
|
||
return list;
|
||
});
|
||
|
||
// 已选中的组别详情列表
|
||
const selectedGroupItems = computed(() => {
|
||
return allGroups.value
|
||
.filter((g) => selectedGroupIds.value.includes(g.id))
|
||
.map((g) => ({
|
||
...g,
|
||
dateRange: formatGroupDate(g),
|
||
}));
|
||
});
|
||
|
||
// 已选中的项目ID列表(去重)
|
||
const selectedProjectIds = computed(() => {
|
||
const ids = new Set();
|
||
selectedGroupItems.value.forEach((g) => {
|
||
if (g.projectId) {
|
||
ids.add(g.projectId);
|
||
}
|
||
});
|
||
return Array.from(ids);
|
||
});
|
||
|
||
// 判断是否为免费项目预览
|
||
const isFreePreview = computed(() => {
|
||
if (!previewData.value) {
|
||
return false;
|
||
}
|
||
const baseAmount = previewData.value?.totalAmount ?? 0;
|
||
const finalAmount = validatedCoupon.value?.finalAmount ?? baseAmount;
|
||
return finalAmount <= 0;
|
||
});
|
||
|
||
// 切换组别选中状态
|
||
const toggleGroupSelection = (id) => {
|
||
const idx = selectedGroupIds.value.indexOf(id);
|
||
if (idx >= 0) {
|
||
selectedGroupIds.value.splice(idx, 1);
|
||
} else {
|
||
// 检查是否已经选择了其他项目的组别
|
||
const groupToAdd = allGroups.value.find(g => g.id === id);
|
||
if (groupToAdd && selectedGroupIds.value.length > 0) {
|
||
const existingProjectId = allGroups.value.find(g => g.id === selectedGroupIds.value[0])?.projectId;
|
||
if (groupToAdd.projectId !== existingProjectId) {
|
||
ElMessage.warning('一次报名只能选择一个项目,请先取消已选择的项目');
|
||
return;
|
||
}
|
||
}
|
||
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);
|
||
};
|
||
|
||
const members = ref([]);
|
||
const memberKeyword = ref('');
|
||
const memberLoading = ref(false);
|
||
const selectedMemberIds = ref([]);
|
||
|
||
// 队员验证相关
|
||
const memberValidationMap = ref({}); // { groupId_memberId: { isValid: boolean, ageValid: boolean, genderValid: boolean, message: string } }
|
||
const groupTeamLimitMap = ref({}); // { groupId: { maxTeams: number, currentTeams: number } }
|
||
const memberRegistrationLimitMap = ref({}); // { memberId: { isLimited: boolean, message: string } }
|
||
const projectCompletionCheckResult = ref(null); // 项目完成人数检查结果
|
||
const registrationLimitCheckResult = ref(null); // 报名限制检查结果
|
||
|
||
// Step2: 团队相关
|
||
const teamList = ref([]);
|
||
const teamListLoading = ref(false);
|
||
const selectedTeamId = ref(null);
|
||
const teamMembers = ref([]);
|
||
const teamMemberLoading = ref(false);
|
||
const activeAssignGroupId = ref(null); // 当前正在分配队员的组别ID
|
||
const groupMemberMap = ref({}); // { groupId: [member, ...] }
|
||
const usedTeamId = ref(null); // 本次报名已使用的团队ID
|
||
|
||
const previewLoading = ref(false);
|
||
const previewData = ref(null);
|
||
const paymentMethod = ref(1);
|
||
const draftRegistrationId = ref(null);
|
||
|
||
// 支付相关
|
||
const paymentData = ref(null);
|
||
const qrcodeRef = ref(null);
|
||
const polling = ref(false);
|
||
const pollingStatus = ref('');
|
||
let pollingTimer = null;
|
||
let qrcodeInstance = null;
|
||
|
||
// 计算最终支付金额
|
||
const finalPaymentAmount = computed(() => {
|
||
if (!previewData.value) return 0;
|
||
const baseAmount = previewData.value.totalAmount ?? 0;
|
||
return validatedCoupon.value?.finalAmount ?? baseAmount;
|
||
});
|
||
|
||
// 优惠券相关
|
||
const couponCode = ref('');
|
||
const couponValidationLoading = ref(false);
|
||
const validatedCoupon = ref(null);
|
||
const showCouponSection = ref(true);
|
||
|
||
const clearRegisterState = () => {
|
||
activeStep.value = 0;
|
||
registerLoading.value = false;
|
||
submitDraftLoading.value = false;
|
||
createOrderLoading.value = false;
|
||
currentEventId.value = null;
|
||
projects.value = [];
|
||
selectedGroupIds.value = [];
|
||
members.value = [];
|
||
memberKeyword.value = '';
|
||
memberLoading.value = false;
|
||
selectedMemberIds.value = [];
|
||
teamList.value = [];
|
||
selectedTeamId.value = null;
|
||
teamMembers.value = [];
|
||
teamMemberLoading.value = false;
|
||
activeAssignGroupId.value = null;
|
||
groupMemberMap.value = {};
|
||
usedTeamId.value = null;
|
||
memberValidationMap.value = {};
|
||
groupTeamLimitMap.value = {};
|
||
memberRegistrationLimitMap.value = {};
|
||
projectCompletionCheckResult.value = null;
|
||
registrationLimitCheckResult.value = null;
|
||
previewLoading.value = false;
|
||
previewData.value = null;
|
||
paymentMethod.value = 1;
|
||
draftRegistrationId.value = null;
|
||
// 支付相关
|
||
paymentData.value = null;
|
||
polling.value = false;
|
||
pollingStatus.value = '';
|
||
clearQRCode();
|
||
stopPolling();
|
||
// 优惠券相关
|
||
couponCode.value = '';
|
||
couponValidationLoading.value = false;
|
||
validatedCoupon.value = null;
|
||
showCouponSection.value = true;
|
||
};
|
||
|
||
const currentEventId = ref(null);
|
||
|
||
const resetStep2AndBeyond = () => {
|
||
members.value = [];
|
||
selectedMemberIds.value = [];
|
||
memberKeyword.value = '';
|
||
selectedTeamId.value = null;
|
||
teamMembers.value = [];
|
||
teamMemberLoading.value = false;
|
||
activeAssignGroupId.value = null;
|
||
groupMemberMap.value = {};
|
||
usedTeamId.value = null;
|
||
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 {
|
||
const { succeeded, data } = await usereventregistrationDetailGet(eventId);
|
||
if (!succeeded) {
|
||
ElMessage.error(data ?? '项目加载失败');
|
||
return;
|
||
}
|
||
// 保存完整的详情数据,用于项目类型显示等
|
||
detailData.value = data;
|
||
projects.value = Array.isArray(data?.projects) ? data.projects : [];
|
||
} 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;
|
||
};
|
||
|
||
// 加载团队列表
|
||
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;
|
||
|
||
// 检查是否已经分配了其他团队的队员
|
||
if (usedTeamId.value && usedTeamId.value !== teamId) {
|
||
ElMessage.warning('本次报名已选择了其他团队的队员,请先移除已分配的队员');
|
||
// 恢复到之前选择的团队
|
||
selectedTeamId.value = usedTeamId.value;
|
||
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 ?? '',
|
||
teamId: teamId, // 添加团队ID
|
||
}));
|
||
}
|
||
} 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);
|
||
};
|
||
|
||
// ==================== 队员验证相关函数 ====================
|
||
|
||
// 调用后端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 = async (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;
|
||
}
|
||
|
||
// 检查队员是否来自已选择的团队
|
||
if (usedTeamId.value && member.teamId !== usedTeamId.value) {
|
||
ElMessage.warning('本次报名只能选择同一个团队的队员');
|
||
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;
|
||
}
|
||
}
|
||
|
||
// 设置已使用的团队
|
||
if (!usedTeamId.value) {
|
||
usedTeamId.value = member.teamId;
|
||
}
|
||
|
||
groupMemberMap.value[gId].push({ ...member, teamId: member.teamId });
|
||
// 实时检查项目完成人数和报名限制
|
||
await checkProjectCompletion();
|
||
await checkRegistrationLimit();
|
||
};
|
||
|
||
// 从组别中移除成员
|
||
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 => {
|
||
if (members.length > 0) {
|
||
hasMembers = true;
|
||
}
|
||
});
|
||
if (!hasMembers) {
|
||
usedTeamId.value = null;
|
||
}
|
||
};
|
||
|
||
// 验证优惠券
|
||
const validateCoupon = async () => {
|
||
if (!couponCode.value?.trim()) {
|
||
ElMessage.warning('请输入优惠券码');
|
||
return;
|
||
}
|
||
if (!currentEventId.value) return;
|
||
if (!previewData.value?.totalAmount) {
|
||
ElMessage.warning('请先完成报名信息填写');
|
||
return;
|
||
}
|
||
|
||
couponValidationLoading.value = true;
|
||
try {
|
||
const { succeeded, data } = await usereventregistrationCouponValidatePost({
|
||
couponCode: couponCode.value.trim(),
|
||
eventId: currentEventId.value,
|
||
originalAmount: previewData.value.totalAmount
|
||
});
|
||
if (!succeeded) {
|
||
ElMessage.error(data?.invalidReason ?? '优惠券验证失败');
|
||
validatedCoupon.value = null;
|
||
return;
|
||
}
|
||
if (!data.isValid) {
|
||
ElMessage.error(data.invalidReason ?? '优惠券无效');
|
||
validatedCoupon.value = null;
|
||
return;
|
||
}
|
||
validatedCoupon.value = data;
|
||
ElMessage.success('优惠券验证成功');
|
||
} catch {
|
||
ElMessage.error('优惠券验证失败');
|
||
validatedCoupon.value = null;
|
||
} finally {
|
||
couponValidationLoading.value = false;
|
||
}
|
||
};
|
||
|
||
// 清除优惠券
|
||
const clearCoupon = () => {
|
||
couponCode.value = '';
|
||
validatedCoupon.value = null;
|
||
};
|
||
|
||
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 fetchGroupTeamLimit = async () => {
|
||
if (!currentEventId.value) return;
|
||
try {
|
||
const { succeeded, data } = await usereventregistrationGroupTeamLimitGet(currentEventId.value);
|
||
if (succeeded && data) {
|
||
const map = {};
|
||
data.forEach(item => {
|
||
map[item.groupId] = {
|
||
maxTeams: item.maxTeams,
|
||
currentTeams: item.currentTeams
|
||
};
|
||
});
|
||
groupTeamLimitMap.value = map;
|
||
}
|
||
} catch {
|
||
console.error('加载队伍限制配置失败');
|
||
}
|
||
};
|
||
|
||
// 检查项目完成人数
|
||
const checkProjectCompletion = async () => {
|
||
if (!currentEventId.value) return;
|
||
const groupMembers = selectedGroupIds.value.map((gId) => ({
|
||
groupId: gId,
|
||
memberIds: (groupMemberMap.value[gId] ?? []).map((m) => m.userId ?? m.id),
|
||
}));
|
||
try {
|
||
const { succeeded, data } = await usereventregistrationProjectCompletionCheckPost({
|
||
eventId: currentEventId.value,
|
||
groupMembers
|
||
});
|
||
if (succeeded) {
|
||
projectCompletionCheckResult.value = data;
|
||
}
|
||
} catch {
|
||
console.error('检查项目完成人数失败');
|
||
}
|
||
};
|
||
|
||
// 检查报名限制
|
||
const checkRegistrationLimit = async () => {
|
||
if (!currentEventId.value) return;
|
||
const allMemberIds = [...new Set(Object.values(groupMemberMap.value).flat().map(m => m.userId ?? m.id))];
|
||
try {
|
||
const { succeeded, data } = await usereventregistrationRegistrationLimitCheckPost({
|
||
eventId: currentEventId.value,
|
||
memberIds: allMemberIds,
|
||
groupIds: selectedGroupIds.value
|
||
});
|
||
if (succeeded) {
|
||
registrationLimitCheckResult.value = data;
|
||
}
|
||
} catch {
|
||
console.error('检查报名限制失败');
|
||
}
|
||
};
|
||
|
||
const handleStep1Next = async () => {
|
||
if (!(selectedGroupIds.value?.length ?? 0)) {
|
||
ElMessage.warning('请先选择报名组别');
|
||
return;
|
||
}
|
||
|
||
activeStep.value = 1;
|
||
resetStep2AndBeyond();
|
||
// 默认激活第一个组别
|
||
if (selectedGroupItems.value.length > 0) {
|
||
activeAssignGroupId.value = selectedGroupItems.value[0].id;
|
||
}
|
||
// 加载团队列表
|
||
await fetchTeamList();
|
||
// 加载队伍限制配置
|
||
await fetchGroupTeamLimit();
|
||
};
|
||
|
||
const handleStep2Next = async () => {
|
||
if (!currentEventId.value) return;
|
||
|
||
// 先检查项目完成人数和报名限制
|
||
await checkProjectCompletion();
|
||
await checkRegistrationLimit();
|
||
|
||
// 检查是否有项目完成人数错误
|
||
if (hasProjectCompletionCountError()) {
|
||
const errors = projectCompletionCheckResult.value?.errors ?? [];
|
||
ElMessage.error(errors[0]?.errorMessage ?? '项目完成人数有错误,请检查');
|
||
return;
|
||
}
|
||
|
||
// 检查是否有报名限制
|
||
if (hasAnyRegistrationLimit()) {
|
||
const details = registrationLimitCheckResult.value?.limitDetails ?? [];
|
||
ElMessage.warning(details[0]?.limitMessage ?? '有队员达到报名限制');
|
||
return;
|
||
}
|
||
|
||
// 检查每个组别是否至少分配了一个队员
|
||
const emptyGroups = selectedGroupItems.value.filter((g) => !(groupMemberMap.value[g.id]?.length));
|
||
if (emptyGroups.length > 0) {
|
||
ElMessage.warning(`请为所有组别分配队员,"${emptyGroups[0].groupName ?? emptyGroups[0].name}"尚未分配`);
|
||
return;
|
||
}
|
||
|
||
// 构造按组别分配的数据
|
||
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))];
|
||
|
||
submitDraftLoading.value = true;
|
||
try {
|
||
const payload = {
|
||
eventId: currentEventId.value,
|
||
projectIds: selectedProjectIds.value,
|
||
groupIds: selectedGroupIds.value,
|
||
groupMembers,
|
||
memberIds: allMemberIds,
|
||
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 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,
|
||
width: 220,
|
||
height: 220,
|
||
colorDark: '#000000',
|
||
colorLight: '#ffffff',
|
||
correctLevel: QRCode.CorrectLevel.H
|
||
});
|
||
} catch (error) {
|
||
console.error('生成二维码失败:', error);
|
||
}
|
||
};
|
||
|
||
// 清理二维码
|
||
const clearQRCode = () => {
|
||
if (qrcodeInstance) {
|
||
qrcodeInstance.clear();
|
||
qrcodeInstance = null;
|
||
}
|
||
};
|
||
|
||
// 组件卸载时清理
|
||
onUnmounted(() => {
|
||
clearQRCode();
|
||
stopPolling();
|
||
});
|
||
|
||
// 开始支付状态轮询
|
||
const startPolling = (orderNo) => {
|
||
polling.value = true;
|
||
pollingStatus.value = '等待支付...';
|
||
|
||
pollingTimer = setInterval(async () => {
|
||
try {
|
||
const { succeeded, data } = await usereventregistrationOrderStatusGet(orderNo);
|
||
if (succeeded && data === 1) {
|
||
stopPolling();
|
||
onPaymentSuccess();
|
||
}
|
||
} catch (error) {
|
||
console.error('查询支付状态失败:', error);
|
||
}
|
||
}, 3000);
|
||
};
|
||
|
||
// 停止轮询
|
||
const stopPolling = () => {
|
||
polling.value = false;
|
||
pollingStatus.value = '';
|
||
if (pollingTimer) {
|
||
clearInterval(pollingTimer);
|
||
pollingTimer = null;
|
||
}
|
||
};
|
||
|
||
// 支付成功处理
|
||
const onPaymentSuccess = () => {
|
||
pollingStatus.value = '支付成功!';
|
||
ElMessage.success('支付成功!报名已完成');
|
||
initParam.value.refresh = !initParam.value.refresh;
|
||
setTimeout(() => {
|
||
isShowRegisterDialog.value = false;
|
||
}, 2000);
|
||
};
|
||
|
||
// 取消支付
|
||
const cancelPayment = () => {
|
||
stopPolling();
|
||
clearQRCode();
|
||
paymentData.value = null;
|
||
};
|
||
|
||
// 刷新二维码(重新创建订单)
|
||
const refreshPayment = async () => {
|
||
stopPolling();
|
||
await handleCreateOrderAndPay();
|
||
};
|
||
|
||
const handleCreateOrderAndPay = async () => {
|
||
if (!draftRegistrationId.value) return;
|
||
createOrderLoading.value = true;
|
||
try {
|
||
const payload = {
|
||
registrationId: draftRegistrationId.value,
|
||
paymentMethod: paymentMethod.value,
|
||
// 如果使用了优惠券,传递优惠券ID
|
||
couponId: validatedCoupon.value?.couponId
|
||
};
|
||
const { succeeded, data } = await usereventregistrationOrderCreatePost(payload);
|
||
if (!succeeded) {
|
||
ElMessage.error(data ?? '创建支付订单失败');
|
||
return;
|
||
}
|
||
|
||
// 检查是否为免费项目
|
||
if (data?.isFree) {
|
||
ElMessage.success('报名成功!');
|
||
initParam.value.refresh = !initParam.value.refresh;
|
||
isShowRegisterDialog.value = false;
|
||
return;
|
||
}
|
||
|
||
// 付费项目,显示二维码
|
||
paymentData.value = data;
|
||
await generateQRCode(data.qrcodeUrl);
|
||
|
||
// 开始轮询支付状态
|
||
if (data.orderNo) {
|
||
startPolling(data.orderNo);
|
||
}
|
||
} 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);
|
||
}
|
||
|
||
/* ---- 项目卡片 ---- */
|
||
.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;
|
||
}
|
||
}
|
||
|
||
/* 优惠券相关样式 */
|
||
.coupon-section {
|
||
margin-top: 20px;
|
||
padding: 16px;
|
||
border: 1px solid #d0e4f7;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.coupon-input-wrapper {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.coupon-info {
|
||
margin-top: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.coupon-discount {
|
||
color: #f56c6c;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.coupon-preview-section {
|
||
margin-top: 14px;
|
||
padding: 12px;
|
||
background: #f0f9eb;
|
||
border: 1px solid #e1f3d8;
|
||
border-radius: 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.preview-footer-section {
|
||
margin-top: 24px;
|
||
padding: 20px;
|
||
background: linear-gradient(90deg, #409eff, #66b1ff);
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex-wrap: wrap;
|
||
gap: 16px;
|
||
}
|
||
|
||
.total-amount-display {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 10px;
|
||
color: #fff;
|
||
}
|
||
|
||
.total-label {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.total-value {
|
||
font-size: 32px;
|
||
font-weight: bold;
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 12px;
|
||
}
|
||
|
||
.original-price {
|
||
font-size: 20px;
|
||
color: rgba(255, 255, 255, 0.7);
|
||
text-decoration: line-through;
|
||
}
|
||
|
||
.discounted-price {
|
||
color: #fff;
|
||
}
|
||
|
||
.payment-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.payment-method-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.free-project-hint {
|
||
margin-top: 16px;
|
||
padding: 12px 16px;
|
||
background: #f0f9eb;
|
||
border: 1px dashed #67c23a;
|
||
border-radius: 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
color: #67c23a;
|
||
}
|
||
|
||
/* 阶段二验证提示区域 */
|
||
.validation-notes-section {
|
||
margin-top: 16px;
|
||
padding: 0 16px;
|
||
}
|
||
|
||
.validation-error-section {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.validation-error-item {
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
/* 阶段二底部操作区域 */
|
||
.step2-footer {
|
||
margin-top: 24px;
|
||
padding: 16px;
|
||
background: #f5f7fa;
|
||
border-top: 1px solid #e4e7ed;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
}
|
||
|
||
/* 支付二维码区域 */
|
||
.payment-qrcode-section {
|
||
text-align: center;
|
||
padding: 20px;
|
||
}
|
||
|
||
.qrcode-title {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.qrcode-wrapper {
|
||
display: flex;
|
||
justify-content: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.qrcode-container {
|
||
width: 260px;
|
||
height: 260px;
|
||
border: 2px solid #e0e0e0;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #fff;
|
||
}
|
||
|
||
.qrcode-image {
|
||
width: 240px;
|
||
height: 240px;
|
||
}
|
||
|
||
.qrcode-placeholder {
|
||
width: 260px;
|
||
height: 260px;
|
||
border: 2px dashed #e0e0e0;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #fafafa;
|
||
}
|
||
|
||
.qrcode-tip {
|
||
margin-top: 12px;
|
||
color: #909399;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.payment-amount {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.amount-label {
|
||
font-size: 16px;
|
||
color: #666;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.amount-value {
|
||
font-size: 32px;
|
||
font-weight: bold;
|
||
color: #f56c6c;
|
||
}
|
||
|
||
.payment-status {
|
||
margin-bottom: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
color: #409eff;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* 优惠券区域 */
|
||
.coupon-section {
|
||
margin-top: 20px;
|
||
padding: 16px;
|
||
background: #f5f7fa;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.coupon-input-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.coupon-result {
|
||
margin-top: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 12px;
|
||
border-radius: 6px;
|
||
|
||
&.success {
|
||
background: #f0f9eb;
|
||
color: #67c23a;
|
||
}
|
||
}
|
||
</style> |