Files
hfb_sys/frontend/src/features/admin/views/AdminAnnouncementsView.vue
T

460 lines
12 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 { onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Bell,
Delete,
Document,
Edit,
Plus,
QuestionFilled,
Refresh,
Top,
Warning,
} from '@element-plus/icons-vue'
import {
fetchAdminAnnouncements,
createAnnouncement,
updateAnnouncement,
publishAnnouncement,
archiveAnnouncement,
unarchiveAnnouncement,
deleteAnnouncement,
type CreateAnnouncementRequest,
type UpdateAnnouncementRequest,
} from '@/features/admin/api/adminAnnouncements'
import type { Announcement } from '@/features/announcement'
import { formatDateTime } from '@/shared/utils/time'
import AnnouncementDialog from '../components/AnnouncementDialog.vue'
import AdminTablePagination from '../components/AdminTablePagination.vue'
const loading = ref(false)
const announcements = ref<Announcement[]>([])
const currentPage = ref(1)
const currentPageSize = ref(20)
const total = ref(0)
const statusFilter = ref('')
const categoryFilter = ref('')
const dialogVisible = ref(false)
const dialogMode = ref<'create' | 'edit'>('create')
const currentAnnouncement = ref<Announcement | null>(null)
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' },
]
const statusOptions = [
{ value: '', label: '全部状态' },
{ value: 'draft', label: '草稿' },
{ value: 'published', label: '已发布' },
{ value: 'archived', label: '已归档' },
]
onMounted(loadAnnouncements)
async function loadAnnouncements() {
loading.value = true
try {
const result = await fetchAdminAnnouncements({
status: statusFilter.value || undefined,
category: categoryFilter.value || undefined,
page: currentPage.value,
page_size: currentPageSize.value,
})
announcements.value = result.items
total.value = result.total
} catch {
ElMessage.error('加载公告列表失败')
} finally {
loading.value = false
}
}
function handleCreate() {
dialogMode.value = 'create'
currentAnnouncement.value = null
dialogVisible.value = true
}
function handleEdit(item: Announcement) {
dialogMode.value = 'edit'
currentAnnouncement.value = item
dialogVisible.value = true
}
async function handleSave(data: CreateAnnouncementRequest | UpdateAnnouncementRequest) {
try {
if (dialogMode.value === 'create') {
await createAnnouncement(data as CreateAnnouncementRequest)
ElMessage.success('创建成功')
} else if (currentAnnouncement.value) {
await updateAnnouncement(currentAnnouncement.value.id, data as UpdateAnnouncementRequest)
ElMessage.success('更新成功')
}
dialogVisible.value = false
loadAnnouncements()
} catch {
ElMessage.error('保存失败')
}
}
async function handlePublish(item: Announcement) {
try {
await ElMessageBox.confirm('确认要发布该公告吗?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
await publishAnnouncement(item.id)
ElMessage.success('发布成功')
loadAnnouncements()
} catch (err) {
if (err !== 'cancel') {
ElMessage.error('发布失败')
}
}
}
async function handleArchive(item: Announcement) {
try {
await ElMessageBox.confirm('确认要归档该公告吗?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
await archiveAnnouncement(item.id)
ElMessage.success('归档成功')
loadAnnouncements()
} catch (err) {
if (err !== 'cancel') {
ElMessage.error('归档失败')
}
}
}
async function handleUnarchive(item: Announcement) {
try {
await ElMessageBox.confirm('确认要取消归档该公告吗?取消后公告将恢复发布。', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
await unarchiveAnnouncement(item.id)
ElMessage.success('取消归档成功')
loadAnnouncements()
} catch (err) {
if (err !== 'cancel') {
ElMessage.error('取消归档失败')
}
}
}
async function handleDelete(item: Announcement) {
try {
await ElMessageBox.confirm('确认要删除该公告吗?此操作不可恢复。', '警告', {
confirmButtonText: '确认删除',
cancelButtonText: '取消',
type: 'error',
})
await deleteAnnouncement(item.id)
ElMessage.success('删除成功')
loadAnnouncements()
} catch (err) {
if (err !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
function handleFilterChange() {
currentPage.value = 1
loadAnnouncements()
}
async function handlePageChange() {
await loadAnnouncements()
}
function getCategoryLabel(category: string) {
return categories.find(c => c.value === category)?.label || category
}
function getCategoryColor(category: string) {
return categories.find(c => c.value === category)?.color || '#64748b'
}
function getStatusLabel(status: string) {
return statusOptions.find(s => s.value === status)?.label || status
}
function getStatusType(status: string): '' | 'success' | 'info' | 'warning' {
const map: Record<string, '' | 'success' | 'info' | 'warning'> = {
draft: 'info',
published: 'success',
archived: 'warning',
}
return map[status] || ''
}
</script>
<template>
<section class="page">
<div class="page-header-row">
<div class="page-header">
<p class="eyebrow">公告管理</p>
<h1>公告中心</h1>
<p>管理平台公告,包括通知、教程、规则和常见问题</p>
</div>
<div class="header-actions">
<el-button @click="loadAnnouncements" :loading="loading">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
<el-button type="primary" @click="handleCreate">
<el-icon><Plus /></el-icon>
新建公告
</el-button>
</div>
</div>
<div class="filter-bar">
<el-select
v-model="statusFilter"
placeholder="筛选状态"
clearable
@change="handleFilterChange"
style="width: 150px"
>
<el-option
v-for="item in statusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-select
v-model="categoryFilter"
placeholder="筛选分类"
clearable
@change="handleFilterChange"
style="width: 150px"
>
<el-option value="" label="全部分类" />
<el-option
v-for="cat in categories"
:key="cat.value"
:label="cat.label"
:value="cat.value"
/>
</el-select>
</div>
<div class="data-table-card">
<el-table :data="announcements" v-loading="loading" style="width: 100%">
<el-table-column label="ID" prop="id" width="80" />
<el-table-column label="标题" prop="title" min-width="200">
<template #default="{ row }">
<div class="title-cell">
<span>{{ row.title }}</span>
<div class="title-badges">
<el-tag v-if="row.is_pinned" type="warning" size="small" effect="plain">
<el-icon><Top /></el-icon>
置顶
</el-tag>
<el-tag v-if="row.is_important" type="danger" size="small" effect="plain"
>重要</el-tag
>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="分类" width="120">
<template #default="{ row }">
<el-tag
:style="{
borderColor: getCategoryColor(row.category),
color: getCategoryColor(row.category),
}"
effect="plain"
size="small"
>
{{ getCategoryLabel(row.category) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" effect="light" size="small">
{{ getStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="优先级" prop="priority" width="100" align="center" />
<el-table-column label="浏览次数" prop="view_count" width="100" align="center" />
<el-table-column label="发布时间" width="180">
<template #default="{ row }">
{{ row.published_at ? formatDateTime(row.published_at) : '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="handleEdit(row)"
text
style="color: #1d6fd6; font-weight: 700"
>
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button
v-if="row.status === 'draft'"
type="success"
size="small"
@click="handlePublish(row)"
text
style="color: #4caf50; font-weight: 700"
>
发布
</el-button>
<el-button
v-if="row.status === 'published'"
type="warning"
size="small"
@click="handleArchive(row)"
text
style="color: #d97706; font-weight: 700"
>
归档
</el-button>
<el-button
v-if="row.status === 'archived'"
type="warning"
size="small"
@click="handleUnarchive(row)"
text
style="color: #d97706; font-weight: 700"
>
取消归档
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(row)"
text
style="color: #dc2626; font-weight: 700"
>
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<AdminTablePagination
v-if="total > 0"
v-model:current-page="currentPage"
v-model:page-size="currentPageSize"
:total="total"
:loading="loading"
@page-change="handlePageChange"
/>
</div>
<AnnouncementDialog
v-model:visible="dialogVisible"
:mode="dialogMode"
:announcement="currentAnnouncement"
@save="handleSave"
/>
</section>
</template>
<style scoped>
.page-header-row {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 24px;
margin-bottom: 24px;
}
.header-actions {
display: flex;
gap: 12px;
}
.filter-bar {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.data-table-card {
padding: 20px;
border: 1px solid #e6eaf2;
border-radius: 12px;
background: #ffffff;
box-shadow: 0 12px 30px rgba(17, 24, 39, 0.05);
}
.title-cell {
display: flex;
align-items: center;
gap: 8px;
}
.title-badges {
display: flex;
gap: 4px;
}
.pagination-wrap {
display: flex;
justify-content: center;
margin-top: 20px;
}
/* 按钮样式 - 使用 :deep() 穿透 */
:deep(.el-button.is-text.el-button--primary) {
color: #409eff !important;
font-weight: 600;
}
:deep(.el-button.is-text.el-button--primary:hover) {
color: #66b1ff !important;
}
:deep(.el-button.is-text.el-button--success) {
color: #67c23a !important;
font-weight: 600;
}
:deep(.el-button.is-text.el-button--success:hover) {
color: #85ce61 !important;
}
:deep(.el-button.is-text.el-button--warning) {
color: #e6a23c !important;
font-weight: 600;
}
:deep(.el-button.is-text.el-button--warning:hover) {
color: #ebb563 !important;
}
:deep(.el-button.is-text.el-button--danger) {
color: #f56c6c !important;
font-weight: 600;
}
:deep(.el-button.is-text.el-button--danger:hover) {
color: #f78989 !important;
}
</style>