赛事报名和赛事记录页面

This commit is contained in:
马梓彭 2026-04-06 09:21:16 +08:00
parent c4d302f0ca
commit aee2f29e05
4 changed files with 193 additions and 65 deletions

View File

@ -292,4 +292,27 @@ export const usereventregistrationProjectCompletionCheckPost = (data) => {
*/
export const usereventregistrationOrderStatusGet = (orderNo) => {
return http.get(`/usereventregistration/order/status/${orderNo}`);
};
/**
* 费用计算预览接口
* @param {Object} data - 计算预览信息
* @param {number} data.eventId - 赛事ID
* @param {Array<{groupId: number, memberIds: Array<number>}>} [data.groupMembers] - 组别队员分配
* @param {Array<number>} [data.memberIds] - 队员ID列表(当无组别时使用)
* @returns {Promise<{
* chargeMode: string,
* totalAmount: number,
* totalPersonCount: number,
* feeDetails: Array<{
* itemName: string,
* unitPrice: number,
* quantity: number,
* subtotal: number,
* remark: string
* }>
* }>}
*/
export const usereventregistrationFeePreviewPost = (data) => {
return http.post('/usereventregistration/fee/preview', data);
};

View File

@ -94,7 +94,9 @@
preserve-expanded-content @selectionChange="selection">
<template #tools>
<el-button v-auth="'event:add'" type="primary" :loading="importingScore" @click="inScore"> 导入成绩 </el-button>
<el-button v-auth="'event:add'" type="primary" @click="downloadTemplate"> 下载模板 </el-button>
<el-button v-auth="'event:add'" type="primary" @click="allSure"> 发放证书 </el-button>
<el-button v-auth="'event:add'" type="primary" @click="exportCertificate"> 导出证书 </el-button>
</template>
<template #index="scope">
{{ scope.$index + 1 }}
@ -155,7 +157,7 @@ import { ArrowLeft, ArrowRight, Check, CircleCheckFilled, Refresh } from '@eleme
import { eventPost, eventPut, eventDetailGet } from '/@/api/match/event';
import { eventarrangementListGet, eventarrangementPut, eventarrangementExportPost } from '/@/api/match/eventarrangement';
import { eventregistrationPagePost, eventregistrationBatchauditPost, eventregistrationMaterialurlsPost } from '/@/api/match/eventregistration';
import { eventscorePageathletePost, eventscorePagecoachleaderPost,eventscoreImportscorePost } from '/@/api/match/eventscore';
import { eventscorePageathletePost, eventscorePagecoachleaderPost,eventscoreImportscorePost, eventscoreDownloadtemplateGet, eventscoreExportcertificatePost } from '/@/api/match/eventscore';
import Step1BasicInfo from './Steps/Step1BasicInfo.vue';
import Step2Units from './Steps/Step2Units.vue';
import Step3ProjectConfig from './Steps/Step3ProjectConfig.vue';
@ -549,6 +551,50 @@ const onScoreFileChange = async (event) => {
}
};
const downloadTemplate = async () => {
try {
const data = await eventscoreDownloadtemplateGet();
const blob = new Blob([data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
downloadByBlob(blob, '成绩导入模板.xlsx');
ElMessage.success('模板下载成功');
} catch (error) {
console.error('下载模板失败:', error);
ElMessage.error('下载模板失败');
}
};
const exportCertificate = async () => {
try {
const { succeeded, data, errors } = await eventscoreExportcertificatePost({
eventId: initParam.value.id
});
if (!succeeded) {
ElMessage.error(errors || '导出证书失败');
return;
}
if (!data) {
ElMessage.warning('未获取到导出文件');
return;
}
const byteCharacters = atob(data);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
downloadByBlob(blob, '证书导出.xlsx');
ElMessage.success('导出成功');
} catch (error) {
console.error('导出证书失败:', error);
ElMessage.error('导出证书失败');
}
};
// 暴露变量
defineExpose({
openDialog,

View File

@ -1,11 +1,7 @@
<template>
<div class="system-menu-container layout-padding">
<ProTable ref="tableRef" :request-api="usereventrecordListGet" :columns="columns" :pagination="true"
:tool-button="false" :initParam="initParam" row-key="registrationId">
<template #index="scope">
{{ scope.$index + 1 }}
</template>
:tool-button="false" :init-param="initParam" row-key="registrationId">
<template #status="scope">
<el-tag :type="tagTypeForStatus(scope.row.status)">{{ tagTextForStatus(scope.row.status) }}</el-tag>
</template>
@ -21,6 +17,38 @@
<template #hasVoucher="scope">
<el-tag :type="scope.row.hasVoucher ? 'success' : 'info'">{{ scope.row.hasVoucher ? '已上传' : '未上传' }}</el-tag>
</template>
<template #coverImage="scope">
<el-image v-if="scope.row.coverImage" :src="scope.row.coverImage" style="height: 60px; width: 80px" fit="cover" />
<span v-else>-</span>
</template>
<template #eventStatus="scope">
<el-tag :type="scope.row.eventStatus === 0 ? 'success' : scope.row.eventStatus === 1 ? 'warning' : 'danger'">
{{ scope.row.eventStatus === 0 ? '报名中' : scope.row.eventStatus === 1 ? '比赛中' : '已结束' }}
</el-tag>
</template>
<template #registrationStartTime="scope">
<div v-if="scope.row.registrationStartTime && scope.row.registrationEndTime">
<div>开始:{{ scope.row.registrationStartTime }}</div>
<div>截止:{{ scope.row.registrationEndTime }}</div>
</div>
<span v-else>-</span>
</template>
<template #materialDeadline="scope">
<span>{{ scope.row.materialDeadline ?? '-' }}</span>
</template>
<template #matchStartTime="scope">
<div v-if="scope.row.matchStartTime && scope.row.matchEndTime">
<div>开始:{{ scope.row.matchStartTime }}</div>
<div>截止:{{ scope.row.matchEndTime }}</div>
</div>
<span v-else>-</span>
</template>
<template #eventType="scope">
<span>{{ getEventTypeText(scope.row.eventType) }}</span>
</template>
<template #chargeMode="scope">
<span>{{ getChargeModeText(scope.row.chargeMode) }}</span>
</template>
<template #action="scope">
<el-space wrap>
@ -345,32 +373,42 @@ const tableRef = ref();
const initParam = ref({ refresh: true });
const columns = ref([
{ prop: 'index', label: '序号', align: 'center' },
{ prop: 'eventName', label: '赛事名称', search: { el: 'input', key: 'Keyword' } },
{ prop: 'teamName', label: '参赛队伍' },
{ type: 'index', label: '序号', align: 'center', width: 70, fixed: 'left' },
{
prop: 'status',
label: '报名状态',
search: {
el: 'select',
key: 'Status',
},
prop: 'eventName',
label: '赛事名称',
search: { el: 'input', key: 'Keyword' },
fixed: 'left',
minWidth: 170,
},
{ prop: 'coverImage', label: '封面', align: 'center', width: 110 },
{
prop: 'eventType',
label: '赛事类型',
align: 'center',
width: 120,
enum: [
{ label: '全部', value: '' },
{ label: '待支付', value: 0 },
{ label: '已支付', value: 1 },
{ label: '已取消', value: 2 },
{ label: '已退款', value: 3 },
{ label: '已完成', value: 4 },
{ label: '线上', value: 0 },
{ label: '线下', value: 1 },
],
},
{ prop: 'payStatus', label: '支付状态' },
{ prop: 'certStatus', label: '证书状态' },
{ prop: 'hasMaterial', label: '素材', align: 'center' },
{ prop: 'hasVoucher', label: '凭证', align: 'center' },
{ prop: 'memberCount', label: '参赛人数', align: 'center' },
{ prop: 'expireTime', label: '截止时间', align: 'center' },
{ prop: 'action', label: '操作', align: 'center', fixed: 'right', width: 150 },
{
prop: 'chargeMode',
label: '收费模式',
align: 'center',
width: 120,
enum: [
{ label: '免费', value: 0 },
{ label: '定价(赛事)', value: 1 },
{ label: '单价(赛事)', value: 2 },
{ label: '单价(项目)', value: 3 },
],
},
{ prop: 'eventStatus', label: '赛事状态', align: 'center', width: 120 },
{ prop: 'registrationStartTime', label: '报名起止时间', align: 'center', minWidth: 320 },
{ prop: 'materialDeadline', label: '上传素材截止时间', align: 'center', minWidth: 180 },
{ prop: 'matchStartTime', label: '比赛起止时间', align: 'center', minWidth: 320 },
{ prop: 'action', label: '操作', align: 'center', fixed: 'right', width: 210 },
]);
const isShowDialog = ref(false);
@ -411,6 +449,21 @@ const tagTextForPayStatus = (v) => payTagMap[normalizeToNumber(v)]?.text ?? (v ?
const tagTypeForCertStatus = (v) => certTagMap[normalizeToNumber(v)]?.type ?? 'info';
const tagTextForCertStatus = (v) => certTagMap[normalizeToNumber(v)]?.text ?? (v ?? '-');
const eventTypeMap = {
0: '线上',
1: '线下',
};
const chargeModeMap = {
0: '免费',
1: '定价(赛事)',
2: '单价(赛事)',
3: '单价(项目)',
};
const getEventTypeText = (v) => eventTypeMap[normalizeToNumber(v)] ?? (v ?? '-');
const getChargeModeText = (v) => chargeModeMap[normalizeToNumber(v)] ?? (v ?? '-');
const disableCancel = computed(() => {
if (!currentRow.value) return true;
const status = normalizeToNumber(currentRow.value.status);

View File

@ -3,7 +3,7 @@
<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;"
<el-image v-if="scope.row?.coverImage" style="height: 60px; width: 80px"
:src="scope.row.coverImage" fit="cover" />
<span v-else>-</span>
</template>
@ -21,39 +21,25 @@
</template>
<template #status="scope">
<el-tag :type="eventStatusTagType(scope.row.status)">{{ eventStatusText(scope.row.status) }}</el-tag>
<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 class="time-range-display">
<div class="time-item">
<span class="time-label">开始</span>
<span class="time-value">{{ formatDateTime(scope.row.registrationStartTime) }}</span>
</div>
<div class="time-separator">
<el-icon><ArrowRight /></el-icon>
</div>
<div class="time-item">
<span class="time-label">结束</span>
<span class="time-value">{{ formatDateTime(scope.row.registrationEndTime) }}</span>
</div>
<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 class="time-range-display">
<div class="time-item">
<span class="time-label">开始</span>
<span class="time-value">{{ formatDateTime(scope.row.matchStartTime) }}</span>
</div>
<div class="time-separator">
<el-icon><ArrowRight /></el-icon>
</div>
<div class="time-item">
<span class="time-label">结束</span>
<span class="time-value">{{ formatDateTime(scope.row.matchEndTime) }}</span>
</div>
<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">
@ -283,7 +269,7 @@
<!-- 队伍数量限制提示 -->
<el-alert
v-if="showTeamLimitHint"
title="若管理后台配置一个组别只能限报2队,添加2队后,"添加"按钮隐藏"
title="若管理后台配置一个组别只能限报2队,添加2队后,'添加'按钮隐藏"
type="info"
:closable="false"
style="margin-bottom: 12px;"
@ -499,6 +485,12 @@ const formatDateTime = (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)}`;
@ -518,14 +510,14 @@ const registrationTypeText = (row) => {
if (!v) return '-';
switch (v) {
case 'OnlyIndividual':
case 1:
return '仅个人报名';
case 0:
return '只允许个人报名';
case 'OnlyTeam':
case 2:
return '仅团队报名';
case 1:
return '只允许团队报名';
case 'Both':
case 3:
return '个人&团队';
case 2:
return '两者都有';
default:
return v ?? '-';
}
@ -534,10 +526,24 @@ const registrationTypeText = (row) => {
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 ?? '-';
// 后端枚举值映射,确保与后端完全匹配
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) => {