Files
hfb_sys/frontend/src/features/admin/views/AdminAnnouncementsView.vue
T
ymlandClaude Opus 4.8 ec9baada67 统一管理后台分页样式
- 新建 AdminTablePagination 组件,统一分页 UI
- 改造 6 个前端页面使用统一分页组件:
  - 管理员管理
  - 用户管理
  - 申诉管理
  - 审计日志
  - 公告管理
  - 钱包流水
- 订单管理前后端完整改造:
  - 后端:添加 PaginatedResult 结构,支持 page/page_size 参数
  - 前端:API 支持分页参数,页面使用统一分页组件
- 所有列表页分页样式统一为商品管理的 .table-pagination 设计

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 00:13:59 +08:00

370 lines
11 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,
deleteAnnouncement,
type CreateAnnouncementRequest,
type UpdateAnnouncementRequest,
} from '@/features/admin/api/adminAnnouncements'
import type { Announcement } from '@/features/announcement'
import { formatDateTime } from '@/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 (err) {
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 (err) {
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 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()
}
async function handleSizeChange() {
currentPage.value = 1
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 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>