2025-12-09 11:10:36 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="contest-detail-page">
|
|
|
|
|
|
<a-spin :spinning="loading">
|
2026-01-08 09:17:46 +08:00
|
|
|
|
<!-- 顶部海报区域 -->
|
|
|
|
|
|
<div class="poster-section">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="poster-image"
|
|
|
|
|
|
:style="{
|
|
|
|
|
|
backgroundImage:
|
|
|
|
|
|
contest?.posterUrl || contest?.coverUrl
|
|
|
|
|
|
? `url(${contest.posterUrl || contest.coverUrl})`
|
|
|
|
|
|
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
|
|
|
|
}"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 返回按钮 -->
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
class="back-button"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
shape="circle"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
@click="$router.back()"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #icon>
|
|
|
|
|
|
<ArrowLeftOutlined />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 右侧报名信息卡片(桌面端) -->
|
|
|
|
|
|
<div class="registration-card desktop-only">
|
|
|
|
|
|
<div class="registration-info">
|
|
|
|
|
|
<div class="info-title">报名时间</div>
|
|
|
|
|
|
<div class="info-time">
|
|
|
|
|
|
{{ formatDate(contest?.registerStartTime) }} ~
|
|
|
|
|
|
{{ formatDate(contest?.registerEndTime) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="isRegistering" class="countdown">
|
|
|
|
|
|
<a-tag color="processing" class="countdown-tag">
|
|
|
|
|
|
距离报名截止还有 {{ daysRemaining }} 天
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else-if="isRegisterEnded" class="countdown">
|
|
|
|
|
|
<a-tag color="error" class="countdown-tag">报名已截止</a-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="countdown">
|
|
|
|
|
|
<a-tag color="default" class="countdown-tag">报名未开始</a-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="registration-action">
|
2025-12-09 11:10:36 +08:00
|
|
|
|
<a-button
|
2026-01-08 09:17:46 +08:00
|
|
|
|
v-if="isRegistering && !hasRegistered"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
block
|
|
|
|
|
|
@click="handleRegister"
|
2025-12-09 11:10:36 +08:00
|
|
|
|
>
|
2026-01-08 09:17:46 +08:00
|
|
|
|
立即报名
|
2025-12-09 11:10:36 +08:00
|
|
|
|
</a-button>
|
2026-01-08 09:17:46 +08:00
|
|
|
|
<a-button
|
|
|
|
|
|
v-else-if="hasRegistered"
|
|
|
|
|
|
type="default"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
block
|
|
|
|
|
|
@click="handleViewRegistration"
|
|
|
|
|
|
>
|
|
|
|
|
|
查看报名
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-button v-else type="default" size="large" block disabled>
|
|
|
|
|
|
查看报名
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-12-09 11:10:36 +08:00
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
<!-- 移动端报名信息卡片 -->
|
|
|
|
|
|
<div v-if="contest" class="registration-card mobile-only">
|
|
|
|
|
|
<div class="registration-info">
|
|
|
|
|
|
<div class="info-title">报名时间</div>
|
|
|
|
|
|
<div class="info-time">
|
|
|
|
|
|
{{ formatDate(contest?.registerStartTime) }} ~
|
|
|
|
|
|
{{ formatDate(contest?.registerEndTime) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="isRegistering" class="countdown">
|
|
|
|
|
|
<a-tag color="processing" class="countdown-tag">
|
|
|
|
|
|
距离报名截止还有 {{ daysRemaining }} 天
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else-if="isRegisterEnded" class="countdown">
|
|
|
|
|
|
<a-tag color="error" class="countdown-tag">报名已截止</a-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="countdown">
|
|
|
|
|
|
<a-tag color="default" class="countdown-tag">报名未开始</a-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="registration-action">
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
v-if="isRegistering && !hasRegistered"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
block
|
|
|
|
|
|
@click="handleRegister"
|
|
|
|
|
|
>
|
|
|
|
|
|
立即报名
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
v-else-if="hasRegistered"
|
|
|
|
|
|
type="default"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
block
|
|
|
|
|
|
@click="handleViewRegistration"
|
|
|
|
|
|
>
|
|
|
|
|
|
查看报名
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-button v-else type="default" size="large" block disabled>
|
|
|
|
|
|
查看报名
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-12-09 11:10:36 +08:00
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
<!-- 内容区域 -->
|
|
|
|
|
|
<div v-if="contest" class="content-section">
|
|
|
|
|
|
<a-tabs
|
|
|
|
|
|
v-model:activeKey="activeTab"
|
|
|
|
|
|
class="detail-tabs"
|
|
|
|
|
|
@change="handleTabChange"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 赛事信息 -->
|
|
|
|
|
|
<a-tab-pane key="info" tab="赛事信息">
|
|
|
|
|
|
<a-card>
|
|
|
|
|
|
<a-descriptions :column="2" bordered>
|
|
|
|
|
|
<a-descriptions-item label="比赛名称">
|
|
|
|
|
|
{{ contest.contestName }}
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item label="比赛类型">
|
|
|
|
|
|
<a-tag
|
|
|
|
|
|
:color="
|
|
|
|
|
|
contest.contestType === 'individual' ? 'blue' : 'green'
|
|
|
|
|
|
"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{
|
|
|
|
|
|
contest.contestType === "individual" ? "个人赛" : "团队赛"
|
|
|
|
|
|
}}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item label="发布状态">
|
|
|
|
|
|
<a-tag
|
|
|
|
|
|
:color="
|
|
|
|
|
|
contest.contestState === 'published'
|
|
|
|
|
|
? 'success'
|
|
|
|
|
|
: 'default'
|
|
|
|
|
|
"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{
|
|
|
|
|
|
contest.contestState === "published" ? "已发布" : "未发布"
|
|
|
|
|
|
}}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item label="赛事状态">
|
|
|
|
|
|
<a-tag
|
|
|
|
|
|
:color="
|
|
|
|
|
|
contest.status === 'ongoing' ? 'processing' : 'orange'
|
|
|
|
|
|
"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ contest.status === "ongoing" ? "进行中" : "已完结" }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item label="比赛时间" :span="2">
|
|
|
|
|
|
{{ formatDateTime(contest.startTime) }} -
|
|
|
|
|
|
{{ formatDateTime(contest.endTime) }}
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item
|
|
|
|
|
|
v-if="contest.address"
|
|
|
|
|
|
label="比赛地址"
|
|
|
|
|
|
:span="2"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ contest.address }}
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item
|
|
|
|
|
|
v-if="contest.content"
|
|
|
|
|
|
label="比赛详情"
|
|
|
|
|
|
:span="2"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div v-html="contest.content"></div>
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
</a-descriptions>
|
|
|
|
|
|
|
|
|
|
|
|
<a-divider orientation="left">联系信息</a-divider>
|
|
|
|
|
|
<a-descriptions :column="3" bordered>
|
|
|
|
|
|
<a-descriptions-item label="联系人">
|
|
|
|
|
|
{{ contest.contactName || "-" }}
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item label="联系电话">
|
|
|
|
|
|
{{ contest.contactPhone || "-" }}
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item label="联系二维码">
|
|
|
|
|
|
<a-image
|
|
|
|
|
|
v-if="contest.contactQrcode"
|
|
|
|
|
|
:src="contest.contactQrcode"
|
|
|
|
|
|
:width="80"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span v-else>-</span>
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
</a-descriptions>
|
|
|
|
|
|
|
|
|
|
|
|
<a-divider orientation="left">主办信息</a-divider>
|
|
|
|
|
|
<a-descriptions :column="1" bordered>
|
|
|
|
|
|
<a-descriptions-item label="主办方">
|
|
|
|
|
|
<template
|
|
|
|
|
|
v-if="contest.organizers && contest.organizers.length"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-tag v-for="org in contest.organizers" :key="org">{{
|
|
|
|
|
|
org
|
|
|
|
|
|
}}</a-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<span v-else>-</span>
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item label="协办方">
|
|
|
|
|
|
<template
|
|
|
|
|
|
v-if="contest.coOrganizers && contest.coOrganizers.length"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-tag v-for="org in contest.coOrganizers" :key="org">
|
|
|
|
|
|
{{ org }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<span v-else>-</span>
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item label="赞助商">
|
|
|
|
|
|
<template v-if="contest.sponsors && contest.sponsors.length">
|
|
|
|
|
|
<a-tag v-for="sp in contest.sponsors" :key="sp">{{
|
|
|
|
|
|
sp
|
|
|
|
|
|
}}</a-tag>
|
2025-12-09 11:10:36 +08:00
|
|
|
|
</template>
|
2026-01-08 09:17:46 +08:00
|
|
|
|
<span v-else>-</span>
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
</a-descriptions>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 报名配置 -->
|
|
|
|
|
|
<a-divider orientation="left">报名配置</a-divider>
|
|
|
|
|
|
<a-descriptions :column="2" bordered>
|
|
|
|
|
|
<a-descriptions-item label="报名时间" :span="2">
|
|
|
|
|
|
{{ formatDateTime(contest.registerStartTime) }} -
|
|
|
|
|
|
{{ formatDateTime(contest.registerEndTime) }}
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item label="需要审核">
|
|
|
|
|
|
<a-tag :color="contest.requireAudit ? 'orange' : 'green'">
|
|
|
|
|
|
{{ contest.requireAudit ? "需要审核" : "无需审核" }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item label="当前报名状态">
|
|
|
|
|
|
<a-tag :color="getRegisterStateColor()">
|
|
|
|
|
|
{{ getRegisterStateText() }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item
|
|
|
|
|
|
v-if="contest.allowedGrades && contest.allowedGrades.length"
|
|
|
|
|
|
label="允许年级"
|
|
|
|
|
|
:span="2"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-tag v-for="grade in contest.allowedGrades" :key="grade">
|
|
|
|
|
|
{{ grade }}年级
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item
|
|
|
|
|
|
v-if="contest.allowedClasses && contest.allowedClasses.length"
|
|
|
|
|
|
label="允许班级"
|
|
|
|
|
|
:span="2"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-tag v-for="cls in contest.allowedClasses" :key="cls">
|
|
|
|
|
|
{{ cls }}班
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item
|
|
|
|
|
|
v-if="contest.contestType === 'team'"
|
|
|
|
|
|
label="团队人数限制"
|
|
|
|
|
|
:span="2"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ contest.teamMinMembers || 1 }} -
|
|
|
|
|
|
{{ contest.teamMaxMembers || "不限" }} 人
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
</a-descriptions>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 作品配置 -->
|
|
|
|
|
|
<a-divider orientation="left">作品配置</a-divider>
|
|
|
|
|
|
<a-descriptions :column="2" bordered>
|
|
|
|
|
|
<a-descriptions-item label="作品提交时间" :span="2">
|
|
|
|
|
|
{{ formatDateTime(contest.submitStartTime) }} -
|
|
|
|
|
|
{{ formatDateTime(contest.submitEndTime) }}
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item label="提交规则">
|
|
|
|
|
|
<a-tag
|
|
|
|
|
|
:color="
|
|
|
|
|
|
contest.submitRule === 'resubmit' ? 'blue' : 'default'
|
|
|
|
|
|
"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{
|
|
|
|
|
|
contest.submitRule === "once"
|
|
|
|
|
|
? "单次提交"
|
|
|
|
|
|
: "允许重新提交"
|
|
|
|
|
|
}}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item label="当前提交状态">
|
|
|
|
|
|
<a-tag :color="getSubmitStateColor()">
|
|
|
|
|
|
{{ getSubmitStateText() }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item v-if="contest.workType" label="作品类型">
|
|
|
|
|
|
<a-tag>{{ getWorkTypeText(contest.workType) }}</a-tag>
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
<a-descriptions-item
|
|
|
|
|
|
v-if="contest.workRequirement"
|
|
|
|
|
|
label="作品要求"
|
|
|
|
|
|
:span="2"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div v-html="contest.workRequirement"></div>
|
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
|
</a-descriptions>
|
|
|
|
|
|
</a-card>
|
2025-12-09 11:10:36 +08:00
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
<!-- 通知公告 -->
|
|
|
|
|
|
<a-tab-pane key="notices" tab="通知公告">
|
|
|
|
|
|
<a-card>
|
|
|
|
|
|
<a-list
|
|
|
|
|
|
:data-source="notices"
|
|
|
|
|
|
:loading="noticesLoading"
|
|
|
|
|
|
item-layout="vertical"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #renderItem="{ item }">
|
|
|
|
|
|
<a-list-item>
|
|
|
|
|
|
<a-list-item-meta>
|
|
|
|
|
|
<template #title>
|
|
|
|
|
|
<a-space>
|
|
|
|
|
|
<span>{{ item.title }}</span>
|
|
|
|
|
|
<a-tag :color="getNoticeTypeColor(item.noticeType)">
|
|
|
|
|
|
{{ getNoticeTypeText(item.noticeType) }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
<a-tag v-if="item.priority > 0" color="red">
|
|
|
|
|
|
优先级: {{ item.priority }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template #description>
|
|
|
|
|
|
<div>{{ item.content }}</div>
|
|
|
|
|
|
<div style="margin-top: 8px; color: #999">
|
|
|
|
|
|
发布时间: {{ formatDateTime(item.publishTime) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-list-item-meta>
|
|
|
|
|
|
</a-list-item>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template #empty>
|
|
|
|
|
|
<a-empty description="暂无公告" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-list>
|
|
|
|
|
|
</a-card>
|
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 赛事结果 -->
|
|
|
|
|
|
<a-tab-pane key="results" tab="赛事结果">
|
|
|
|
|
|
<a-card>
|
|
|
|
|
|
<div v-if="contest.resultState === 'published'">
|
|
|
|
|
|
<a-spin :spinning="resultsLoading">
|
|
|
|
|
|
<a-table
|
|
|
|
|
|
:columns="resultColumns"
|
|
|
|
|
|
:data-source="results"
|
|
|
|
|
|
:pagination="resultsPagination"
|
|
|
|
|
|
row-key="id"
|
|
|
|
|
|
@change="handleResultsTableChange"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #bodyCell="{ column, record }">
|
|
|
|
|
|
<template v-if="column.key === 'rank'">
|
|
|
|
|
|
<a-tag :color="getRankColor(record.rank)">
|
|
|
|
|
|
{{ record.rank || "-" }}
|
2025-12-09 11:10:36 +08:00
|
|
|
|
</a-tag>
|
2026-01-08 09:17:46 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'author'">
|
|
|
|
|
|
{{
|
|
|
|
|
|
record.registration?.user?.nickname ||
|
|
|
|
|
|
record.registration?.team?.teamName ||
|
|
|
|
|
|
"-"
|
|
|
|
|
|
}}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'award'">
|
|
|
|
|
|
<a-tag
|
|
|
|
|
|
v-if="record.awardName"
|
|
|
|
|
|
:color="getAwardColor(record.awardName)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ record.awardName }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
<span v-else>-</span>
|
|
|
|
|
|
</template>
|
2025-12-09 11:10:36 +08:00
|
|
|
|
</template>
|
2026-01-08 09:17:46 +08:00
|
|
|
|
</a-table>
|
|
|
|
|
|
</a-spin>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-empty v-else description="结果尚未公布" />
|
|
|
|
|
|
</a-card>
|
2025-12-09 11:10:36 +08:00
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
</a-tabs>
|
2026-01-08 09:17:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-09 11:10:36 +08:00
|
|
|
|
<a-empty v-else description="比赛不存在" />
|
|
|
|
|
|
</a-spin>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-01-08 09:17:46 +08:00
|
|
|
|
import { ref, onMounted, computed } from "vue"
|
|
|
|
|
|
import { useRoute, useRouter } from "vue-router"
|
|
|
|
|
|
import { message } from "ant-design-vue"
|
|
|
|
|
|
import { ArrowLeftOutlined } from "@ant-design/icons-vue"
|
|
|
|
|
|
import dayjs from "dayjs"
|
|
|
|
|
|
import { useAuthStore } from "@/stores/auth"
|
|
|
|
|
|
import {
|
|
|
|
|
|
contestsApi,
|
|
|
|
|
|
noticesApi,
|
|
|
|
|
|
resultsApi,
|
|
|
|
|
|
registrationsApi,
|
|
|
|
|
|
type Contest,
|
|
|
|
|
|
type ContestNotice,
|
|
|
|
|
|
type ContestResult,
|
|
|
|
|
|
} from "@/api/contests"
|
2025-12-09 11:10:36 +08:00
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
const router = useRouter()
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const authStore = useAuthStore()
|
|
|
|
|
|
const tenantCode = route.params.tenantCode as string
|
2025-12-09 11:10:36 +08:00
|
|
|
|
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const noticesLoading = ref(false)
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const resultsLoading = ref(false)
|
2025-12-09 11:10:36 +08:00
|
|
|
|
const contest = ref<Contest | null>(null)
|
|
|
|
|
|
const notices = ref<ContestNotice[]>([])
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const results = ref<ContestResult[]>([])
|
|
|
|
|
|
const activeTab = ref("info")
|
|
|
|
|
|
const hasRegistered = ref(false)
|
|
|
|
|
|
const myRegistration = ref<any>(null)
|
2025-12-09 11:10:36 +08:00
|
|
|
|
|
|
|
|
|
|
const contestId = Number(route.params.id)
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const resultsPagination = ref({
|
|
|
|
|
|
current: 1,
|
|
|
|
|
|
pageSize: 20,
|
|
|
|
|
|
total: 0,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const resultColumns = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "排名",
|
|
|
|
|
|
key: "rank",
|
|
|
|
|
|
dataIndex: "rank",
|
|
|
|
|
|
width: 80,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "作品名称",
|
|
|
|
|
|
key: "title",
|
|
|
|
|
|
dataIndex: "title",
|
|
|
|
|
|
width: 200,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "作者",
|
|
|
|
|
|
key: "author",
|
|
|
|
|
|
width: 150,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "最终得分",
|
|
|
|
|
|
key: "finalScore",
|
|
|
|
|
|
dataIndex: "finalScore",
|
|
|
|
|
|
width: 120,
|
|
|
|
|
|
sorter: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "奖项",
|
|
|
|
|
|
key: "award",
|
|
|
|
|
|
width: 120,
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化日期
|
|
|
|
|
|
const formatDate = (dateStr?: string) => {
|
|
|
|
|
|
if (!dateStr) return "-"
|
|
|
|
|
|
return dayjs(dateStr).format("YYYY-MM-DD")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 11:10:36 +08:00
|
|
|
|
// 格式化日期时间
|
|
|
|
|
|
const formatDateTime = (dateStr?: string) => {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
if (!dateStr) return "-"
|
|
|
|
|
|
return dayjs(dateStr).format("YYYY-MM-DD HH:mm")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 判断是否在报名中
|
|
|
|
|
|
const isRegistering = computed(() => {
|
|
|
|
|
|
if (!contest.value) return false
|
|
|
|
|
|
const now = dayjs()
|
|
|
|
|
|
const start = dayjs(contest.value.registerStartTime)
|
|
|
|
|
|
const end = dayjs(contest.value.registerEndTime)
|
|
|
|
|
|
return now.isAfter(start) && now.isBefore(end)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 判断报名是否已结束
|
|
|
|
|
|
const isRegisterEnded = computed(() => {
|
|
|
|
|
|
if (!contest.value) return false
|
|
|
|
|
|
const now = dayjs()
|
|
|
|
|
|
const end = dayjs(contest.value.registerEndTime)
|
|
|
|
|
|
return now.isAfter(end)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 计算距离报名截止还有几天
|
|
|
|
|
|
const daysRemaining = computed(() => {
|
|
|
|
|
|
if (!contest.value || !isRegistering.value) return 0
|
|
|
|
|
|
const now = dayjs()
|
|
|
|
|
|
const end = dayjs(contest.value.registerEndTime)
|
|
|
|
|
|
const diff = end.diff(now, "day")
|
|
|
|
|
|
return diff > 0 ? diff : 0
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 获取作品类型文本
|
|
|
|
|
|
const getWorkTypeText = (type?: string) => {
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
|
case "image":
|
|
|
|
|
|
return "图片"
|
|
|
|
|
|
case "video":
|
|
|
|
|
|
return "视频"
|
|
|
|
|
|
case "document":
|
|
|
|
|
|
return "文档"
|
|
|
|
|
|
case "code":
|
|
|
|
|
|
return "代码"
|
|
|
|
|
|
case "other":
|
|
|
|
|
|
return "其他"
|
|
|
|
|
|
default:
|
|
|
|
|
|
return type || "-"
|
|
|
|
|
|
}
|
2025-12-09 11:10:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取公告类型颜色
|
|
|
|
|
|
const getNoticeTypeColor = (type?: string) => {
|
|
|
|
|
|
switch (type) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
case "urgent":
|
|
|
|
|
|
return "red"
|
|
|
|
|
|
case "system":
|
|
|
|
|
|
return "blue"
|
2025-12-09 11:10:36 +08:00
|
|
|
|
default:
|
2026-01-08 09:17:46 +08:00
|
|
|
|
return "default"
|
2025-12-09 11:10:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取公告类型文本
|
|
|
|
|
|
const getNoticeTypeText = (type?: string) => {
|
|
|
|
|
|
switch (type) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
case "urgent":
|
|
|
|
|
|
return "紧急通知"
|
|
|
|
|
|
case "system":
|
|
|
|
|
|
return "系统公告"
|
2025-12-09 11:10:36 +08:00
|
|
|
|
default:
|
2026-01-08 09:17:46 +08:00
|
|
|
|
return "普通公告"
|
2025-12-09 11:10:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
// 计算报名阶段状态
|
|
|
|
|
|
const getRegisterStateColor = () => {
|
|
|
|
|
|
if (!contest.value) return "default"
|
|
|
|
|
|
const now = dayjs()
|
|
|
|
|
|
const start = dayjs(contest.value.registerStartTime)
|
|
|
|
|
|
const end = dayjs(contest.value.registerEndTime)
|
|
|
|
|
|
if (now.isBefore(start)) return "default"
|
|
|
|
|
|
if (now.isAfter(end)) return "orange"
|
|
|
|
|
|
return "processing"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getRegisterStateText = () => {
|
|
|
|
|
|
if (!contest.value) return "-"
|
|
|
|
|
|
const now = dayjs()
|
|
|
|
|
|
const start = dayjs(contest.value.registerStartTime)
|
|
|
|
|
|
const end = dayjs(contest.value.registerEndTime)
|
|
|
|
|
|
if (now.isBefore(start)) return "未开始"
|
|
|
|
|
|
if (now.isAfter(end)) return "已结束"
|
|
|
|
|
|
return "进行中"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算作品提交阶段状态
|
|
|
|
|
|
const getSubmitStateColor = () => {
|
|
|
|
|
|
if (!contest.value) return "default"
|
|
|
|
|
|
const now = dayjs()
|
|
|
|
|
|
const start = dayjs(contest.value.submitStartTime)
|
|
|
|
|
|
const end = dayjs(contest.value.submitEndTime)
|
|
|
|
|
|
if (now.isBefore(start)) return "default"
|
|
|
|
|
|
if (now.isAfter(end)) return "orange"
|
|
|
|
|
|
return "processing"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getSubmitStateText = () => {
|
|
|
|
|
|
if (!contest.value) return "-"
|
|
|
|
|
|
const now = dayjs()
|
|
|
|
|
|
const start = dayjs(contest.value.submitStartTime)
|
|
|
|
|
|
const end = dayjs(contest.value.submitEndTime)
|
|
|
|
|
|
if (now.isBefore(start)) return "未开始"
|
|
|
|
|
|
if (now.isAfter(end)) return "已结束"
|
|
|
|
|
|
return "进行中"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取排名颜色
|
|
|
|
|
|
const getRankColor = (rank?: number) => {
|
|
|
|
|
|
if (!rank) return "default"
|
|
|
|
|
|
if (rank === 1) return "gold"
|
|
|
|
|
|
if (rank === 2) return "default"
|
|
|
|
|
|
if (rank === 3) return "orange"
|
|
|
|
|
|
return "blue"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取奖项颜色
|
|
|
|
|
|
const getAwardColor = (award?: string) => {
|
|
|
|
|
|
if (!award) return "default"
|
|
|
|
|
|
if (award.includes("一等奖") || award.includes("金奖")) return "gold"
|
|
|
|
|
|
if (award.includes("二等奖") || award.includes("银奖")) return "default"
|
|
|
|
|
|
if (award.includes("三等奖") || award.includes("铜奖")) return "orange"
|
|
|
|
|
|
return "blue"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 11:10:36 +08:00
|
|
|
|
// 加载比赛详情
|
|
|
|
|
|
const fetchContestDetail = async () => {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
contest.value = await contestsApi.getDetail(contestId)
|
2026-01-08 09:17:46 +08:00
|
|
|
|
// 检查是否已报名
|
|
|
|
|
|
await checkRegistration()
|
2025-12-09 11:10:36 +08:00
|
|
|
|
} catch (error: any) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
message.error(error?.response?.data?.message || "获取比赛详情失败")
|
2025-12-09 11:10:36 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
// 检查是否已报名
|
|
|
|
|
|
const checkRegistration = async () => {
|
|
|
|
|
|
if (!authStore.user) return
|
2025-12-09 11:10:36 +08:00
|
|
|
|
try {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const response = await registrationsApi.getList({
|
|
|
|
|
|
contestId,
|
|
|
|
|
|
userId: authStore.user.id,
|
|
|
|
|
|
page: 1,
|
|
|
|
|
|
pageSize: 1,
|
|
|
|
|
|
})
|
|
|
|
|
|
if (response.list && response.list.length > 0) {
|
|
|
|
|
|
hasRegistered.value = true
|
|
|
|
|
|
myRegistration.value = response.list[0]
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("检查报名状态失败:", error)
|
2025-12-09 11:10:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 加载公告列表
|
|
|
|
|
|
const fetchNotices = async () => {
|
|
|
|
|
|
noticesLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
notices.value = await noticesApi.getList(contestId)
|
|
|
|
|
|
} catch (error: any) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
message.error("获取公告列表失败")
|
2025-12-09 11:10:36 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
noticesLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
// 加载赛事结果
|
|
|
|
|
|
const fetchResults = async () => {
|
|
|
|
|
|
if (!contest.value || contest.value.resultState !== "published") return
|
|
|
|
|
|
resultsLoading.value = true
|
2025-12-09 11:10:36 +08:00
|
|
|
|
try {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const response = await resultsApi.getResults(
|
|
|
|
|
|
contestId,
|
|
|
|
|
|
resultsPagination.value.current,
|
|
|
|
|
|
resultsPagination.value.pageSize
|
|
|
|
|
|
)
|
|
|
|
|
|
results.value = response.list || []
|
|
|
|
|
|
resultsPagination.value.total = response.total || 0
|
2025-12-09 11:10:36 +08:00
|
|
|
|
} catch (error: any) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
message.error("获取赛事结果失败")
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
resultsLoading.value = false
|
2025-12-09 11:10:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
// 结果表格变化处理
|
|
|
|
|
|
const handleResultsTableChange = (pag: any) => {
|
|
|
|
|
|
resultsPagination.value.current = pag.current || 1
|
|
|
|
|
|
resultsPagination.value.pageSize = pag.pageSize || 20
|
|
|
|
|
|
fetchResults()
|
2025-12-09 11:10:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
// 立即报名
|
|
|
|
|
|
const handleRegister = async () => {
|
|
|
|
|
|
if (!authStore.user) {
|
|
|
|
|
|
message.warning("请先登录")
|
|
|
|
|
|
router.push(`/${tenantCode}/login`)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 11:10:36 +08:00
|
|
|
|
try {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
await registrationsApi.create({
|
|
|
|
|
|
contestId,
|
|
|
|
|
|
userId: authStore.user.id,
|
|
|
|
|
|
registrationType: contest.value?.contestType || "individual",
|
|
|
|
|
|
})
|
|
|
|
|
|
message.success("报名成功")
|
|
|
|
|
|
hasRegistered.value = true
|
|
|
|
|
|
await checkRegistration()
|
2025-12-09 11:10:36 +08:00
|
|
|
|
} catch (error: any) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
message.error(error?.response?.data?.message || "报名失败,请稍后重试")
|
2025-12-09 11:10:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
// 查看报名
|
|
|
|
|
|
const handleViewRegistration = () => {
|
|
|
|
|
|
if (myRegistration.value) {
|
|
|
|
|
|
router.push(
|
|
|
|
|
|
`/${tenantCode}/contests/registrations/${myRegistration.value.id}`
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
router.push(`/${tenantCode}/contests/registrations?contestId=${contestId}`)
|
2025-12-09 11:10:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
fetchContestDetail()
|
|
|
|
|
|
fetchNotices()
|
|
|
|
|
|
})
|
2026-01-08 09:17:46 +08:00
|
|
|
|
|
|
|
|
|
|
// 监听tab切换,切换到结果tab时加载结果
|
|
|
|
|
|
const handleTabChange = (key: string) => {
|
|
|
|
|
|
activeTab.value = key
|
|
|
|
|
|
if (key === "results" && contest.value?.resultState === "published") {
|
|
|
|
|
|
fetchResults()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-09 11:10:36 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
<style lang="scss" scoped>
|
2025-12-09 11:10:36 +08:00
|
|
|
|
.contest-detail-page {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
background-color: #f0f2f5;
|
|
|
|
|
|
|
|
|
|
|
|
.poster-section {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 400px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
|
|
.poster-image {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background-size: cover;
|
|
|
|
|
|
background-position: center;
|
|
|
|
|
|
background-repeat: no-repeat;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
|
|
|
|
.back-button {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 24px;
|
|
|
|
|
|
left: 24px;
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.registration-card {
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.95);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
|
|
|
|
|
|
|
|
|
&.desktop-only {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 24px;
|
|
|
|
|
|
right: 24px;
|
|
|
|
|
|
width: 320px;
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.mobile-only {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.registration-info {
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
|
|
|
|
|
.info-title {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #8c8c8c;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-time {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
color: #262626;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.countdown {
|
|
|
|
|
|
.countdown-tag {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.registration-action {
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-section {
|
|
|
|
|
|
max-width: 1200px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
|
|
|
|
|
|
.detail-tabs {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-09 11:10:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
// 响应式设计
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
.contest-detail-page {
|
|
|
|
|
|
.poster-section {
|
|
|
|
|
|
height: 300px;
|
|
|
|
|
|
|
|
|
|
|
|
.poster-image {
|
|
|
|
|
|
.back-button {
|
|
|
|
|
|
top: 16px;
|
|
|
|
|
|
left: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.registration-card.desktop-only {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.registration-card.mobile-only {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin: 16px;
|
|
|
|
|
|
width: calc(100% - 32px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-section {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
|
|
|
|
|
|
.detail-tabs {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|