340 lines
12 KiB
Vue
340 lines
12 KiB
Vue
|
|
<template>
|
||
|
|
<div class="contest-detail-page">
|
||
|
|
<a-spin :spinning="loading">
|
||
|
|
<a-card v-if="contest">
|
||
|
|
<template #title>
|
||
|
|
<a-space>
|
||
|
|
<span>{{ contest.contestName }}</span>
|
||
|
|
<a-tag :color="contest.contestState === 'published' ? 'success' : 'default'">
|
||
|
|
{{ contest.contestState === 'published' ? '已发布' : '未发布' }}
|
||
|
|
</a-tag>
|
||
|
|
<a-tag :color="contest.contestType === 'individual' ? 'blue' : 'green'">
|
||
|
|
{{ contest.contestType === 'individual' ? '个人赛' : '团队赛' }}
|
||
|
|
</a-tag>
|
||
|
|
</a-space>
|
||
|
|
</template>
|
||
|
|
<template #extra>
|
||
|
|
<a-space>
|
||
|
|
<a-button v-permission="'contest:update'" @click="handleEdit">编辑</a-button>
|
||
|
|
<a-button
|
||
|
|
v-permission="'contest:publish'"
|
||
|
|
:type="contest.contestState === 'published' ? 'default' : 'primary'"
|
||
|
|
@click="handlePublish"
|
||
|
|
>
|
||
|
|
{{ contest.contestState === 'published' ? '撤回' : '发布' }}
|
||
|
|
</a-button>
|
||
|
|
<a-button @click="$router.back()">返回</a-button>
|
||
|
|
</a-space>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<a-tabs v-model:activeKey="activeTab">
|
||
|
|
<!-- 基本信息 -->
|
||
|
|
<a-tab-pane key="info" tab="基本信息">
|
||
|
|
<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="提交规则">
|
||
|
|
{{ contest.submitRule === 'once' ? '单次提交' : '允许重新提交' }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="比赛时间" :span="2">
|
||
|
|
{{ formatDateTime(contest.startTime) }} - {{ formatDateTime(contest.endTime) }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="报名时间" :span="2">
|
||
|
|
{{ formatDateTime(contest.registerStartTime) }} - {{ formatDateTime(contest.registerEndTime) }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="作品提交时间" :span="2">
|
||
|
|
{{ formatDateTime(contest.submitStartTime) }} - {{ formatDateTime(contest.submitEndTime) }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item label="评审时间" :span="2">
|
||
|
|
{{ formatDateTime(contest.reviewStartTime) }} - {{ formatDateTime(contest.reviewEndTime) }}
|
||
|
|
</a-descriptions-item>
|
||
|
|
<a-descriptions-item v-if="contest.resultPublishTime" label="结果发布时间" :span="2">
|
||
|
|
{{ formatDateTime(contest.resultPublishTime) }}
|
||
|
|
</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-tab-pane>
|
||
|
|
|
||
|
|
<!-- 统计信息 -->
|
||
|
|
<a-tab-pane key="statistics" tab="统计信息">
|
||
|
|
<a-row :gutter="16">
|
||
|
|
<a-col :span="6">
|
||
|
|
<a-statistic title="报名人数" :value="contest._count?.registrations || 0" />
|
||
|
|
</a-col>
|
||
|
|
<a-col :span="6">
|
||
|
|
<a-statistic title="作品数量" :value="contest._count?.works || 0" />
|
||
|
|
</a-col>
|
||
|
|
<a-col :span="6">
|
||
|
|
<a-statistic title="团队数量" :value="contest._count?.teams || 0" />
|
||
|
|
</a-col>
|
||
|
|
<a-col :span="6">
|
||
|
|
<a-statistic title="评委数量" :value="contest._count?.judges || 0" />
|
||
|
|
</a-col>
|
||
|
|
</a-row>
|
||
|
|
</a-tab-pane>
|
||
|
|
|
||
|
|
<!-- 附件 -->
|
||
|
|
<a-tab-pane key="attachments" tab="附件">
|
||
|
|
<a-button v-permission="'contest:update'" type="primary" @click="handleAddAttachment" style="margin-bottom: 16px">
|
||
|
|
添加附件
|
||
|
|
</a-button>
|
||
|
|
<a-list
|
||
|
|
:data-source="attachments"
|
||
|
|
:loading="attachmentsLoading"
|
||
|
|
>
|
||
|
|
<template #renderItem="{ item }">
|
||
|
|
<a-list-item>
|
||
|
|
<a-list-item-meta>
|
||
|
|
<template #title>
|
||
|
|
<a :href="item.fileUrl" target="_blank">{{ item.fileName }}</a>
|
||
|
|
</template>
|
||
|
|
<template #description>
|
||
|
|
<span>大小: {{ item.size || '0' }} | 类型: {{ item.fileType || '-' }}</span>
|
||
|
|
</template>
|
||
|
|
</a-list-item-meta>
|
||
|
|
<template #actions>
|
||
|
|
<a-button
|
||
|
|
v-permission="'contest:update'"
|
||
|
|
type="link"
|
||
|
|
danger
|
||
|
|
size="small"
|
||
|
|
@click="handleDeleteAttachment(item.id)"
|
||
|
|
>
|
||
|
|
删除
|
||
|
|
</a-button>
|
||
|
|
</template>
|
||
|
|
</a-list-item>
|
||
|
|
</template>
|
||
|
|
</a-list>
|
||
|
|
</a-tab-pane>
|
||
|
|
|
||
|
|
<!-- 公告 -->
|
||
|
|
<a-tab-pane key="notices" tab="公告">
|
||
|
|
<a-button v-permission="'notice:create'" type="primary" @click="handleAddNotice" style="margin-bottom: 16px">
|
||
|
|
发布公告
|
||
|
|
</a-button>
|
||
|
|
<a-list
|
||
|
|
:data-source="notices"
|
||
|
|
:loading="noticesLoading"
|
||
|
|
>
|
||
|
|
<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>
|
||
|
|
<template #actions>
|
||
|
|
<a-button
|
||
|
|
v-permission="'notice:update'"
|
||
|
|
type="link"
|
||
|
|
size="small"
|
||
|
|
@click="handleEditNotice(item)"
|
||
|
|
>
|
||
|
|
编辑
|
||
|
|
</a-button>
|
||
|
|
<a-button
|
||
|
|
v-permission="'notice:delete'"
|
||
|
|
type="link"
|
||
|
|
danger
|
||
|
|
size="small"
|
||
|
|
@click="handleDeleteNotice(item.id)"
|
||
|
|
>
|
||
|
|
删除
|
||
|
|
</a-button>
|
||
|
|
</template>
|
||
|
|
</a-list-item>
|
||
|
|
</template>
|
||
|
|
</a-list>
|
||
|
|
</a-tab-pane>
|
||
|
|
</a-tabs>
|
||
|
|
</a-card>
|
||
|
|
<a-empty v-else description="比赛不存在" />
|
||
|
|
</a-spin>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
import { ref, onMounted } from 'vue'
|
||
|
|
import { useRoute, useRouter } from 'vue-router'
|
||
|
|
import { message } from 'ant-design-vue'
|
||
|
|
import dayjs from 'dayjs'
|
||
|
|
import { contestsApi, attachmentsApi, noticesApi, type Contest, type ContestAttachment, type ContestNotice } from '@/api/contests'
|
||
|
|
|
||
|
|
const route = useRoute()
|
||
|
|
const router = useRouter()
|
||
|
|
|
||
|
|
const loading = ref(false)
|
||
|
|
const attachmentsLoading = ref(false)
|
||
|
|
const noticesLoading = ref(false)
|
||
|
|
const contest = ref<Contest | null>(null)
|
||
|
|
const attachments = ref<ContestAttachment[]>([])
|
||
|
|
const notices = ref<ContestNotice[]>([])
|
||
|
|
const activeTab = ref('info')
|
||
|
|
|
||
|
|
const contestId = Number(route.params.id)
|
||
|
|
|
||
|
|
// 格式化日期时间
|
||
|
|
const formatDateTime = (dateStr?: string) => {
|
||
|
|
if (!dateStr) return '-'
|
||
|
|
return dayjs(dateStr).format('YYYY-MM-DD HH:mm')
|
||
|
|
}
|
||
|
|
|
||
|
|
// 获取公告类型颜色
|
||
|
|
const getNoticeTypeColor = (type?: string) => {
|
||
|
|
switch (type) {
|
||
|
|
case 'urgent':
|
||
|
|
return 'red'
|
||
|
|
case 'system':
|
||
|
|
return 'blue'
|
||
|
|
default:
|
||
|
|
return 'default'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 获取公告类型文本
|
||
|
|
const getNoticeTypeText = (type?: string) => {
|
||
|
|
switch (type) {
|
||
|
|
case 'urgent':
|
||
|
|
return '紧急通知'
|
||
|
|
case 'system':
|
||
|
|
return '系统公告'
|
||
|
|
default:
|
||
|
|
return '普通公告'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 加载比赛详情
|
||
|
|
const fetchContestDetail = async () => {
|
||
|
|
loading.value = true
|
||
|
|
try {
|
||
|
|
contest.value = await contestsApi.getDetail(contestId)
|
||
|
|
} catch (error: any) {
|
||
|
|
message.error(error?.response?.data?.message || '获取比赛详情失败')
|
||
|
|
} finally {
|
||
|
|
loading.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 加载附件列表
|
||
|
|
const fetchAttachments = async () => {
|
||
|
|
attachmentsLoading.value = true
|
||
|
|
try {
|
||
|
|
attachments.value = await attachmentsApi.getList(contestId)
|
||
|
|
} catch (error: any) {
|
||
|
|
message.error('获取附件列表失败')
|
||
|
|
} finally {
|
||
|
|
attachmentsLoading.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 加载公告列表
|
||
|
|
const fetchNotices = async () => {
|
||
|
|
noticesLoading.value = true
|
||
|
|
try {
|
||
|
|
notices.value = await noticesApi.getList(contestId)
|
||
|
|
} catch (error: any) {
|
||
|
|
message.error('获取公告列表失败')
|
||
|
|
} finally {
|
||
|
|
noticesLoading.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 编辑
|
||
|
|
const handleEdit = () => {
|
||
|
|
router.push(`/contests/${contestId}/edit`)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 发布/撤回
|
||
|
|
const handlePublish = async () => {
|
||
|
|
if (!contest.value) return
|
||
|
|
try {
|
||
|
|
const newState = contest.value.contestState === 'published' ? 'unpublished' : 'published'
|
||
|
|
await contestsApi.publish(contestId, newState)
|
||
|
|
message.success(newState === 'published' ? '发布成功' : '撤回成功')
|
||
|
|
fetchContestDetail()
|
||
|
|
} catch (error: any) {
|
||
|
|
message.error(error?.response?.data?.message || '操作失败')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 添加附件
|
||
|
|
const handleAddAttachment = () => {
|
||
|
|
// TODO: 实现添加附件功能
|
||
|
|
message.info('添加附件功能待实现')
|
||
|
|
}
|
||
|
|
|
||
|
|
// 删除附件
|
||
|
|
const handleDeleteAttachment = async (id: number) => {
|
||
|
|
try {
|
||
|
|
await attachmentsApi.delete(id)
|
||
|
|
message.success('删除成功')
|
||
|
|
fetchAttachments()
|
||
|
|
} catch (error: any) {
|
||
|
|
message.error('删除失败')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 添加公告
|
||
|
|
const handleAddNotice = () => {
|
||
|
|
// TODO: 实现添加公告功能
|
||
|
|
message.info('添加公告功能待实现')
|
||
|
|
}
|
||
|
|
|
||
|
|
// 编辑公告
|
||
|
|
const handleEditNotice = (notice: ContestNotice) => {
|
||
|
|
// TODO: 实现编辑公告功能
|
||
|
|
message.info('编辑公告功能待实现')
|
||
|
|
}
|
||
|
|
|
||
|
|
// 删除公告
|
||
|
|
const handleDeleteNotice = async (id: number) => {
|
||
|
|
try {
|
||
|
|
await noticesApi.delete(id)
|
||
|
|
message.success('删除成功')
|
||
|
|
fetchNotices()
|
||
|
|
} catch (error: any) {
|
||
|
|
message.error('删除失败')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
onMounted(() => {
|
||
|
|
fetchContestDetail()
|
||
|
|
fetchAttachments()
|
||
|
|
fetchNotices()
|
||
|
|
})
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
.contest-detail-page {
|
||
|
|
padding: 24px;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
|