Files
hfb_sys/frontend/src/features/announcement/views/AnnouncementDetailView.vue
T
2026-06-05 21:49:33 +08:00

356 lines
7.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ArrowLeft, Bell, Calendar, Document, QuestionFilled, View, Warning } from '@element-plus/icons-vue'
import { fetchAnnouncementDetail, type Announcement } from '@/features/announcement'
import { formatDateTime } from '@/utils/time'
import { marked } from 'marked'
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const announcement = ref<Announcement | null>(null)
// 配置 marked 选项
marked.setOptions({
breaks: true, // 支持换行
gfm: true, // GitHub Flavored Markdown
})
// 计算属性:将 Markdown 转换为 HTML
const renderedContent = computed(() => {
if (!announcement.value?.content) return ''
return marked.parse(announcement.value.content)
})
const categories = [
{ value: 'notice', label: '通知公告', icon: Bell, color: '#3b82f6' },
{ value: 'tutorial', label: '使用教程', icon: Document, color: '#10b981' },
{ value: 'rule', label: '规则说明', icon: Warning, color: '#f59e0b' },
{ value: 'faq', label: '常见问题', icon: QuestionFilled, color: '#8b5cf6' },
]
onMounted(loadAnnouncement)
async function loadAnnouncement() {
const id = Number(route.params.id)
if (!id) {
router.push('/announcements')
return
}
loading.value = true
try {
announcement.value = await fetchAnnouncementDetail(id)
} catch (err) {
console.error('加载公告失败', err)
router.push('/announcements')
} finally {
loading.value = false
}
}
function goBack() {
router.push('/announcements')
}
function getCategoryInfo(category: string) {
return categories.find(c => c.value === category) || categories[0]
}
function getCategoryLabel(category: string) {
const cat = categories.find(c => c.value === category)
return cat ? cat.label : category
}
</script>
<template>
<section class="page announcement-detail-page" v-loading="loading">
<div class="detail-header">
<button class="back-button" @click="goBack">
<el-icon><ArrowLeft /></el-icon>
<span>返回列表</span>
</button>
</div>
<div v-if="announcement" class="announcement-detail-card">
<div class="detail-meta">
<el-tag
:type="announcement.category === 'notice' ? 'primary' : 'info'"
effect="plain"
size="small"
class="category-tag"
:style="{
borderColor: getCategoryInfo(announcement.category)?.color || '#94a3b8',
color: getCategoryInfo(announcement.category)?.color || '#94a3b8'
}"
>
<el-icon><component :is="getCategoryInfo(announcement.category)?.icon || Bell" /></el-icon>
{{ getCategoryLabel(announcement.category) }}
</el-tag>
<el-tag v-if="announcement.is_pinned" type="warning" effect="plain" size="small">置顶</el-tag>
<el-tag v-if="announcement.is_important" type="danger" effect="plain" size="small">重要</el-tag>
</div>
<h1 class="detail-title">{{ announcement.title }}</h1>
<div class="detail-info">
<span class="info-item">
<el-icon><Calendar /></el-icon>
发布时间{{ formatDateTime(announcement.published_at || announcement.created_at) }}
</span>
<span class="info-item">
<el-icon><View /></el-icon>
浏览次数{{ announcement.view_count }}
</span>
</div>
<div class="detail-divider"></div>
<div class="detail-content" v-html="renderedContent"></div>
</div>
</section>
</template>
<style scoped>
.announcement-detail-page {
display: grid;
gap: 20px;
}
.detail-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.back-button {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 18px;
border: 1px solid #e2e8f0;
border-radius: 10px;
background: #ffffff;
color: #64748b;
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
}
.back-button:hover {
border-color: #3b82f6;
background: #eff6ff;
color: #3b82f6;
}
.announcement-detail-card {
padding: 40px;
border: 1px solid #e6eaf2;
border-radius: 12px;
background: #ffffff;
box-shadow: 0 12px 30px rgba(17, 24, 39, 0.05);
}
.detail-meta {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 20px;
}
.category-tag {
display: flex;
align-items: center;
gap: 4px;
}
.detail-title {
margin: 0 0 20px;
color: #111827;
font-size: 28px;
font-weight: 800;
line-height: 1.3;
}
.detail-info {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 24px;
margin-bottom: 24px;
}
.info-item {
display: flex;
align-items: center;
gap: 6px;
color: #64748b;
font-size: 14px;
}
.info-item .el-icon {
font-size: 16px;
}
.detail-divider {
height: 1px;
margin-bottom: 32px;
background: #e6eaf2;
}
.detail-content {
max-width: 100%;
color: #334155;
font-size: 16px;
line-height: 1.8;
word-wrap: break-word;
overflow-wrap: break-word;
}
.detail-content :deep(h1),
.detail-content :deep(h2),
.detail-content :deep(h3) {
margin: 28px 0 16px;
color: #111827;
font-weight: 700;
}
.detail-content :deep(h1) {
font-size: 24px;
padding-bottom: 12px;
border-bottom: 2px solid #e6eaf2;
}
.detail-content :deep(h2) {
font-size: 20px;
padding-bottom: 8px;
border-bottom: 1px solid #f1f5f9;
}
.detail-content :deep(h3) {
font-size: 18px;
}
.detail-content :deep(p) {
margin: 16px 0;
line-height: 1.8;
}
.detail-content :deep(strong) {
color: #111827;
font-weight: 700;
}
.detail-content :deep(em) {
font-style: italic;
color: #475569;
}
.detail-content :deep(ul),
.detail-content :deep(ol) {
margin: 16px 0;
padding-left: 24px;
}
.detail-content :deep(li) {
margin: 8px 0;
line-height: 1.7;
}
.detail-content :deep(code) {
padding: 2px 6px;
border-radius: 4px;
background: #f1f5f9;
color: #e11d48;
font-family: 'SF Mono', 'Menlo', 'Consolas', monospace;
font-size: 14px;
}
.detail-content :deep(pre) {
margin: 20px 0;
padding: 16px;
border-radius: 8px;
background: #1e293b;
color: #e2e8f0;
font-family: 'SF Mono', 'Menlo', 'Consolas', monospace;
font-size: 14px;
line-height: 1.6;
overflow-x: auto;
}
.detail-content :deep(pre code) {
padding: 0;
background: transparent;
color: inherit;
}
.detail-content :deep(blockquote) {
margin: 20px 0;
padding: 12px 20px;
border-left: 4px solid #3b82f6;
background: #eff6ff;
color: #1e40af;
}
.detail-content :deep(a) {
color: #3b82f6;
text-decoration: none;
transition: color 0.2s;
}
.detail-content :deep(a:hover) {
color: #2563eb;
text-decoration: underline;
}
.detail-content :deep(img) {
max-width: 100%;
height: auto;
margin: 20px 0;
border-radius: 8px;
}
.detail-content :deep(table) {
width: 100%;
max-width: 100%;
border-collapse: collapse;
margin: 20px 0;
overflow-x: auto;
display: block;
}
.detail-content :deep(table th),
.detail-content :deep(table td) {
padding: 12px;
border: 1px solid #e6eaf2;
text-align: left;
}
.detail-content :deep(table th) {
background: #f8fafc;
font-weight: 700;
}
@media (max-width: 720px) {
.announcement-detail-card {
padding: 24px 20px;
}
.detail-title {
font-size: 22px;
}
.detail-content {
font-size: 15px;
}
.detail-info {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
}
</style>