library-picturebook-activity/frontend/src/views/contests/Detail.vue

340 lines
12 KiB
Vue
Raw Normal View History

2025-12-09 11:10:36 +08:00
<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>