This commit is contained in:
2025-12-06 13:53:58 +08:00
parent 069ce39ca1
commit 6269482a43
12 changed files with 1684 additions and 34 deletions

View File

@@ -0,0 +1,167 @@
<template>
<el-dialog
v-model="dialogVisible"
title="公告详情"
width="70%"
:close-on-click-modal="false"
@open="handleDialogOpen"
v-loading="loading"
>
<div v-if="announcement" class="space-y-6">
<!-- 基本信息 -->
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="text-lg font-medium text-gray-900 mb-4">基本信息</h3>
<el-descriptions :column="2" border>
<el-descriptions-item label="公告标题">
<span class="font-medium">{{ announcement.title }}</span>
</el-descriptions-item>
<el-descriptions-item label="公告状态">
<el-tag :type="getStatusType(announcement.status, announcement.scheduled_at)">
{{ getStatusText(announcement.status, announcement.scheduled_at) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDate(announcement.created_at) }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ formatDate(announcement.updated_at) }}
</el-descriptions-item>
<el-descriptions-item v-if="announcement.scheduled_at" label="定时发布时间">
{{ formatDate(announcement.scheduled_at) }}
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 公告内容 -->
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="text-lg font-medium text-gray-900 mb-4">公告内容</h3>
<div class="prose max-w-none" v-html="announcement.content"></div>
</div>
</div>
</el-dialog>
</template>
<script setup>
import { announcementApi } from '@/api';
import { ElMessage } from 'element-plus';
import { computed, ref } from 'vue';
// Props
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
announcement: {
type: Object,
default: null
}
})
// Emits
const emit = defineEmits(['update:modelValue'])
// 响应式数据
const loading = ref(false)
const announcementDetail = ref(null)
// 计算属性
const dialogVisible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const announcement = computed(() => announcementDetail.value || props.announcement)
// 获取公告详情
const fetchAnnouncementDetail = async (announcementId) => {
if (!announcementId) return
loading.value = true
try {
const response = await announcementApi.getAnnouncementDetail(announcementId)
announcementDetail.value = response.data
} catch (error) {
ElMessage.error('获取公告详情失败')
console.error('获取公告详情失败:', error)
} finally {
loading.value = false
}
}
// 对话框打开时获取详情
const handleDialogOpen = () => {
if (props.announcement?.id && !announcementDetail.value) {
fetchAnnouncementDetail(props.announcement.id)
}
}
// 状态类型映射
const getStatusType = (status, scheduledAt) => {
if (status === 'draft' && scheduledAt) {
return 'warning'
}
const statusMap = {
draft: 'info',
published: 'success',
archived: 'warning'
}
return statusMap[status] || 'info'
}
// 状态文本映射
const getStatusText = (status, scheduledAt) => {
if (status === 'draft' && scheduledAt) {
return '定时发布'
}
const statusMap = {
draft: '草稿',
published: '已发布',
archived: '已归档'
}
return statusMap[status] || status
}
// 格式化日期
const formatDate = (dateString) => {
if (!dateString) return '-'
return new Date(dateString).toLocaleString('zh-CN')
}
</script>
<style scoped>
:deep(.prose) {
color: #374151;
line-height: 1.75;
}
:deep(.prose p) {
margin-bottom: 1rem;
}
:deep(.prose h1),
:deep(.prose h2),
:deep(.prose h3) {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
font-weight: 600;
}
:deep(.prose ul),
:deep(.prose ol) {
margin-left: 1.5rem;
margin-bottom: 1rem;
}
:deep(.prose img) {
max-width: 100%;
height: auto;
border-radius: 0.5rem;
}
</style>

View File

@@ -0,0 +1,212 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑公告' : '新增公告'"
width="80%"
:close-on-click-modal="false"
@open="handleDialogOpen"
v-loading="loading"
@close="handleClose"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" class="space-y-6">
<!-- 基本信息 -->
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="text-lg font-medium text-gray-900 mb-4">基本信息</h3>
<el-form-item label="公告标题" prop="title">
<el-input v-model="form.title" placeholder="请输入公告标题" maxlength="200" show-word-limit />
</el-form-item>
</div>
<!-- 公告内容 -->
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="text-lg font-medium text-gray-900 mb-4">公告内容</h3>
<el-form-item label="公告内容" prop="content">
<Editor style="width: 100%;" v-model="form.content" :init="editorInit"
tinymceScriptSrc="https://cdn.jsdelivr.net/npm/tinymce@7.9.1/tinymce.min.js" />
</el-form-item>
</div>
</el-form>
<template #footer>
<div class="flex justify-end space-x-3">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="loading" @click="handleSubmit">
{{ isEdit ? '更新' : '创建' }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { announcementApi } from '@/api';
import Editor from '@tinymce/tinymce-vue';
import { ElMessage } from 'element-plus';
import { computed, reactive, ref, watch } from 'vue';
// Props
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
announcement: {
type: Object,
default: null
}
})
// Emits
const emit = defineEmits(['update:modelValue', 'success'])
// 响应式数据
const formRef = ref(null)
const loading = ref(false)
const announcementDetail = ref(null)
// 表单数据
const form = reactive({
title: '',
content: ''
})
// 表单验证规则
const rules = {
title: [
{ required: true, message: '请输入公告标题', trigger: 'blur' },
{ min: 1, max: 200, message: '标题长度在 1 到 200 个字符', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入公告内容', trigger: 'blur' }
]
}
// TinyMCE 配置(简化版)
const editorInit = {
menubar: false,
statusbar: false,
placeholder: '开始编写公告内容...',
theme: 'silver',
license_key: 'gpl',
promotion: false,
branding: false,
toolbar: [
'undo redo | bold italic underline | forecolor backcolor | alignleft aligncenter alignright | bullist numlist | link image | code'
],
plugins: [
'anchor', 'autolink', 'charmap', 'codesample', 'emoticons', 'image', 'link',
'lists', 'media', 'searchreplace', 'table', 'visualblocks', 'wordcount'
],
height: 400
}
// 计算属性
const dialogVisible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const isEdit = computed(() => !!props.announcement)
// 获取公告详情
const fetchAnnouncementDetail = async (announcementId) => {
if (!announcementId) return
loading.value = true
try {
const response = await announcementApi.getAnnouncementDetail(announcementId)
announcementDetail.value = response.data
fillFormWithDetail(announcementDetail.value)
} catch (error) {
ElMessage.error('获取公告详情失败')
console.error('获取公告详情失败:', error)
} finally {
loading.value = false
}
}
// 使用详情数据填充表单
const fillFormWithDetail = (detail) => {
if (!detail) return
Object.assign(form, {
title: detail.title || '',
content: detail.content || ''
})
}
// 对话框打开时获取详情
const handleDialogOpen = () => {
if (props.announcement?.id && isEdit.value) {
fetchAnnouncementDetail(props.announcement.id)
}
}
// 监听公告数据变化,初始化表单
watch(() => props.announcement, (newAnnouncement) => {
if (newAnnouncement && isEdit.value) {
if (announcementDetail.value) {
fillFormWithDetail(announcementDetail.value)
} else {
Object.assign(form, {
title: newAnnouncement.title || '',
content: newAnnouncement.content || ''
})
}
} else {
resetForm()
}
}, { immediate: true })
// 重置表单
function resetForm() {
Object.assign(form, {
title: '',
content: ''
})
if (formRef.value) {
formRef.value.clearValidate()
}
}
// 关闭对话框
const handleClose = () => {
dialogVisible.value = false
resetForm()
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
loading.value = true
if (isEdit.value) {
await announcementApi.updateAnnouncement(props.announcement.id, form)
ElMessage.success('公告更新成功')
} else {
await announcementApi.createAnnouncement(form)
ElMessage.success('公告创建成功')
}
emit('success')
handleClose()
} catch (error) {
if (error.message) {
ElMessage.error(error.message)
} else {
ElMessage.error(isEdit.value ? '更新公告失败' : '创建公告失败')
}
console.error('提交表单失败:', error)
} finally {
loading.value = false
}
}
</script>

View File

@@ -0,0 +1,85 @@
<template>
<div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-4">
<div class="rounded-md bg-gray-50 p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-6 h-6 bg-blue-50 rounded-md flex items-center justify-center">
<el-icon class="text-blue-600 text-sm"><MegaphoneIcon /></el-icon>
</div>
</div>
<div class="ml-3">
<p class="text-xs text-gray-500">总公告数</p>
<p class="text-xl font-semibold text-gray-900">{{ stats.total_announcements || 0 }}</p>
</div>
</div>
</div>
<div class="rounded-md bg-gray-50 p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-6 h-6 bg-green-50 rounded-md flex items-center justify-center">
<el-icon class="text-green-600 text-sm"><CheckCircleIcon /></el-icon>
</div>
</div>
<div class="ml-3">
<p class="text-xs text-gray-500">已发布</p>
<p class="text-xl font-semibold text-gray-900">{{ stats.published_announcements || 0 }}</p>
</div>
</div>
</div>
<div class="rounded-md bg-gray-50 p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-6 h-6 bg-yellow-50 rounded-md flex items-center justify-center">
<el-icon class="text-yellow-600 text-sm"><ClockIcon /></el-icon>
</div>
</div>
<div class="ml-3">
<p class="text-xs text-gray-500">草稿</p>
<p class="text-xl font-semibold text-gray-900">{{ stats.draft_announcements || 0 }}</p>
</div>
</div>
</div>
<div class="rounded-md bg-gray-50 p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-6 h-6 bg-orange-50 rounded-md flex items-center justify-center">
<el-icon class="text-orange-600 text-sm"><ArchiveBoxIcon /></el-icon>
</div>
</div>
<div class="ml-3">
<p class="text-xs text-gray-500">已归档</p>
<p class="text-xl font-semibold text-gray-900">{{ stats.archived_announcements || 0 }}</p>
</div>
</div>
</div>
<div class="rounded-md bg-gray-50 p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-6 h-6 bg-purple-50 rounded-md flex items-center justify-center">
<el-icon class="text-purple-600 text-sm"><ClockIcon /></el-icon>
</div>
</div>
<div class="ml-3">
<p class="text-xs text-gray-500">定时发布</p>
<p class="text-xl font-semibold text-gray-900">{{ stats.scheduled_announcements || 0 }}</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ArchiveBoxIcon, CheckCircleIcon, ClockIcon, MegaphoneIcon } from '@heroicons/vue/24/outline';
defineProps({
stats: {
type: Object,
default: () => ({})
}
})
</script>

View File

@@ -0,0 +1,186 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="announcement?.scheduled_at ? '修改定时发布时间' : '定时发布公告'"
width="500px"
:close-on-click-modal="false"
@close="handleClose"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="120px"
>
<el-form-item label="公告标题">
<div class="text-gray-600">{{ announcement?.title }}</div>
</el-form-item>
<el-form-item label="定时发布日期" prop="scheduled_date">
<el-date-picker
v-model="form.scheduled_date"
type="date"
placeholder="选择定时发布日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled-date="disabledDate"
class="w-full"
/>
</el-form-item>
<el-form-item label="定时发布时间" prop="scheduled_time">
<el-time-picker
v-model="form.scheduled_time"
placeholder="选择定时发布时间"
format="HH:mm:ss"
value-format="HH:mm:ss"
:disabled="!form.scheduled_date"
class="w-full"
/>
</el-form-item>
<el-form-item label="提示信息">
<div class="text-sm text-gray-500">
<p> 定时发布日期不能早于今天</p>
<p> 设置后公告将保持草稿状态到指定时间自动发布</p>
<p> 可以随时取消定时发布重新设置</p>
</div>
</el-form-item>
</el-form>
<template #footer>
<div class="flex justify-end space-x-3">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="loading" @click="handleSubmit">
{{ announcement?.scheduled_at ? '确认修改' : '确认设置' }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { announcementApi } from '@/api'
import { ElMessage } from 'element-plus'
import { computed, reactive, ref, watch } from 'vue'
// Props
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
announcement: {
type: Object,
default: null
}
})
// Emits
const emit = defineEmits(['update:modelValue', 'success'])
// 响应式数据
const loading = ref(false)
const formRef = ref()
// 表单数据
const form = reactive({
scheduled_date: '',
scheduled_time: ''
})
// 表单验证规则
const rules = {
scheduled_date: [
{ required: true, message: '请选择定时发布日期', trigger: 'change' }
],
scheduled_time: [
{ required: true, message: '请选择定时发布时间', trigger: 'change' }
]
}
// 计算属性
const dialogVisible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
// 禁用过去的日期
const disabledDate = (time) => {
const today = new Date()
today.setHours(0, 0, 0, 0)
return time.getTime() < today.getTime()
}
// 监听对话框显示状态
watch(() => props.modelValue, (visible) => {
if (visible && props.announcement) {
if (props.announcement.scheduled_at) {
const scheduledDate = new Date(props.announcement.scheduled_at)
const year = scheduledDate.getFullYear()
const month = String(scheduledDate.getMonth() + 1).padStart(2, '0')
const day = String(scheduledDate.getDate()).padStart(2, '0')
form.scheduled_date = `${year}-${month}-${day}`
const hours = String(scheduledDate.getHours()).padStart(2, '0')
const minutes = String(scheduledDate.getMinutes()).padStart(2, '0')
const seconds = String(scheduledDate.getSeconds()).padStart(2, '0')
form.scheduled_time = `${hours}:${minutes}:${seconds}`
} else {
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
form.scheduled_date = `${year}-${month}-${day}`
const defaultTime = new Date()
defaultTime.setHours(defaultTime.getHours() + 1)
const hours = String(defaultTime.getHours()).padStart(2, '0')
const minutes = String(defaultTime.getMinutes()).padStart(2, '0')
const seconds = String(defaultTime.getSeconds()).padStart(2, '0')
form.scheduled_time = `${hours}:${minutes}:${seconds}`
}
}
})
// 处理关闭
const handleClose = () => {
dialogVisible.value = false
form.scheduled_date = ''
form.scheduled_time = ''
}
// 处理提交
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
} catch (error) {
return
}
loading.value = true
try {
if (props.announcement.scheduled_at) {
await announcementApi.updateSchedulePublishAnnouncement(props.announcement.id, {
scheduled_time: `${form.scheduled_date} ${form.scheduled_time}`
})
} else {
await announcementApi.schedulePublishAnnouncement(props.announcement.id, {
scheduled_time: `${form.scheduled_date} ${form.scheduled_time}`
})
}
ElMessage.success(props.announcement.scheduled_at ? '定时发布时间修改成功' : '定时发布设置成功')
emit('success')
handleClose()
} catch (error) {
ElMessage.error(error.message || '设置定时发布失败')
} finally {
loading.value = false
}
}
</script>

View File

@@ -0,0 +1,545 @@
<template>
<ListPageLayout
title="公告管理"
subtitle="管理系统中的所有公告内容"
>
<template #actions>
<el-button type="primary" @click="handleCreateAnnouncement">
<el-icon class="mr-1"><PlusIcon /></el-icon>
新增公告
</el-button>
</template>
<template #filters>
<FilterSection>
<FilterItem label="公告状态">
<el-select
v-model="filters.status"
placeholder="选择状态"
clearable
@change="handleFilterChange"
class="w-full"
>
<el-option label="草稿" value="draft" />
<el-option label="已发布" value="published" />
<el-option label="已归档" value="archived" />
</el-select>
</FilterItem>
<FilterItem label="标题关键词">
<el-input
v-model="filters.title"
placeholder="输入公告标题关键词"
clearable
@input="handleSearch"
class="w-full"
>
<template #prefix>
<el-icon><MagnifyingGlassIcon /></el-icon>
</template>
</el-input>
</FilterItem>
<template #stats>
共找到 {{ total }} 条公告
</template>
<template #buttons>
<el-button @click="resetFilters">重置筛选</el-button>
<el-button type="primary" @click="loadAnnouncements">应用筛选</el-button>
</template>
</FilterSection>
</template>
<template #table>
<!-- 统计卡片 -->
<div class="mb-6">
<AnnouncementStats :stats="stats" />
</div>
<!-- 公告列表表格 -->
<el-table
v-loading="loading"
:data="announcements"
stripe
class="w-full"
>
<el-table-column prop="title" label="公告标题" min-width="200">
<template #default="{ row }">
<span class="font-medium text-blue-600 cursor-pointer hover:text-blue-800" @click="handleViewAnnouncement(row)">
{{ row.title }}
</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="120">
<template #default="{ row }">
<div class="flex flex-col gap-1">
<el-tag
:type="getStatusType(row.status, row.scheduled_at)"
size="small"
>
{{ getStatusText(row.status, row.scheduled_at) }}
</el-tag>
<div v-if="row.scheduled_at" class="text-xs text-gray-500">
定时: {{ formatDate(row.scheduled_at) }}
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="180">
<template #default="{ row }">
<span class="text-gray-600">{{ formatDate(row.created_at) }}</span>
</template>
</el-table-column>
<el-table-column prop="updated_at" label="更新时间" width="180">
<template #default="{ row }">
<span class="text-gray-600">{{ formatDate(row.updated_at) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" min-width="200" fixed="right">
<template #default="{ row }">
<div class="flex gap-2">
<!-- 主要操作按钮 -->
<el-button
v-if="row.status === 'draft'"
type="success"
size="small"
@click="handlePublishAnnouncement(row)"
>
发布
</el-button>
<el-button
v-if="row.status === 'published'"
type="warning"
size="small"
@click="handleWithdrawAnnouncement(row)"
>
撤回
</el-button>
<el-button
v-if="row.status === 'published'"
type="info"
size="small"
@click="handleArchiveAnnouncement(row)"
>
归档
</el-button>
<el-button
type="primary"
size="small"
@click="handleEditAnnouncement(row)"
>
编辑
</el-button>
<!-- 更多操作下拉菜单 -->
<el-dropdown @command="(command) => handleMoreAction(command, row)">
<el-button size="small" type="info">
更多<el-icon class="el-icon--right"><ChevronDownIcon /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<!-- 定时发布相关操作 -->
<el-dropdown-item
v-if="row.status === 'draft' && !row.scheduled_at"
command="schedule-publish"
>
<div class="flex items-center gap-2">
<ClockIcon class="w-4 h-4" />
<span>定时发布</span>
</div>
</el-dropdown-item>
<el-dropdown-item
v-if="row.status === 'draft' && row.scheduled_at"
command="schedule-publish"
>
<div class="flex items-center gap-2">
<ClockIcon class="w-4 h-4" />
<span>修改时间</span>
</div>
</el-dropdown-item>
<el-dropdown-item
v-if="row.status === 'draft' && row.scheduled_at"
command="cancel-schedule"
divided
>
<div class="flex items-center gap-2">
<XMarkIcon class="w-4 h-4" />
<span>取消定时</span>
</div>
</el-dropdown-item>
<!-- 查看操作 -->
<el-dropdown-item command="view">
<div class="flex items-center gap-2">
<EyeIcon class="w-4 h-4" />
<span>查看</span>
</div>
</el-dropdown-item>
<!-- 删除操作 -->
<el-dropdown-item command="delete" divided>
<div class="flex items-center gap-2">
<TrashIcon class="w-4 h-4" />
<span>删除</span>
</div>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
</el-table-column>
</el-table>
</template>
<template #pagination>
<el-pagination
v-if="total > 0"
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</template>
<template #extra>
<!-- 公告编辑对话框 -->
<AnnouncementEditDialog
v-model="showEditDialog"
:announcement="currentAnnouncement"
@success="handleEditSuccess"
/>
<!-- 公告详情对话框 -->
<AnnouncementDetailDialog
v-model="showDetailDialog"
:announcement="currentAnnouncement"
/>
<!-- 定时发布对话框 -->
<SchedulePublishDialog
v-model="showScheduleDialog"
:announcement="currentAnnouncement"
@success="handleScheduleSuccess"
/>
</template>
</ListPageLayout>
</template>
<script setup>
import { announcementApi } from '@/api'
import FilterItem from '@/components/common/FilterItem.vue'
import FilterSection from '@/components/common/FilterSection.vue'
import ListPageLayout from '@/components/common/ListPageLayout.vue'
import { ChevronDownIcon, ClockIcon, EyeIcon, MagnifyingGlassIcon, PlusIcon, TrashIcon, XMarkIcon } from '@heroicons/vue/24/outline'
import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, reactive, ref } from 'vue'
import AnnouncementDetailDialog from './components/AnnouncementDetailDialog.vue'
import AnnouncementEditDialog from './components/AnnouncementEditDialog.vue'
import AnnouncementStats from './components/AnnouncementStats.vue'
import SchedulePublishDialog from './components/SchedulePublishDialog.vue'
// 响应式数据
const loading = ref(false)
const announcements = ref([])
const total = ref(0)
const stats = ref({})
// 筛选器
const filters = reactive({
status: '',
title: ''
})
// 分页
const pagination = reactive({
page: 1,
pageSize: 10
})
// 搜索防抖
let searchTimer = null
// 对话框控制
const showEditDialog = ref(false)
const showDetailDialog = ref(false)
const showScheduleDialog = ref(false)
const currentAnnouncement = ref(null)
// 获取公告列表
const loadAnnouncements = async () => {
loading.value = true
try {
const params = {
page: pagination.page,
page_size: pagination.pageSize,
...filters
}
const response = await announcementApi.getAnnouncementsForAdmin(params)
announcements.value = response.data.items || []
total.value = response.data.total || 0
} catch (error) {
console.error('获取公告列表失败:', error)
ElMessage.error('获取公告列表失败')
} finally {
loading.value = false
}
}
// 获取统计数据
const loadStats = async () => {
try {
const response = await announcementApi.getAnnouncementStats()
stats.value = response.data || {}
} catch (error) {
console.error('获取统计数据失败:', error)
}
}
// 筛选器变化处理
const handleFilterChange = () => {
pagination.page = 1
loadAnnouncements()
}
// 搜索处理
const handleSearch = () => {
if (searchTimer) {
clearTimeout(searchTimer)
}
searchTimer = setTimeout(() => {
pagination.page = 1
loadAnnouncements()
}, 500)
}
// 重置筛选器
const resetFilters = () => {
Object.keys(filters).forEach(key => {
filters[key] = ''
})
pagination.page = 1
loadAnnouncements()
}
// 分页处理
const handleSizeChange = (size) => {
pagination.pageSize = size
pagination.page = 1
loadAnnouncements()
}
const handleCurrentChange = (page) => {
pagination.page = page
loadAnnouncements()
}
// 新增公告
const handleCreateAnnouncement = () => {
currentAnnouncement.value = null
showEditDialog.value = true
}
// 编辑公告
const handleEditAnnouncement = (announcement) => {
currentAnnouncement.value = { id: announcement.id }
showEditDialog.value = true
}
// 查看公告详情
const handleViewAnnouncement = (announcement) => {
currentAnnouncement.value = { id: announcement.id }
showDetailDialog.value = true
}
// 发布公告
const handlePublishAnnouncement = async (announcement) => {
try {
await ElMessageBox.confirm('确定要发布这条公告吗?', '确认发布', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await announcementApi.publishAnnouncement(announcement.id)
ElMessage.success('公告发布成功')
loadAnnouncements()
loadStats()
} catch (error) {
if (error !== 'cancel') {
console.error('发布公告失败:', error)
ElMessage.error(error.message || '发布公告失败')
}
}
}
// 撤回公告
const handleWithdrawAnnouncement = async (announcement) => {
try {
await ElMessageBox.confirm('确定要撤回这条公告吗?', '确认撤回', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await announcementApi.withdrawAnnouncement(announcement.id)
ElMessage.success('公告撤回成功')
loadAnnouncements()
loadStats()
} catch (error) {
if (error !== 'cancel') {
console.error('撤回公告失败:', error)
ElMessage.error(error.message || '撤回公告失败')
}
}
}
// 归档公告
const handleArchiveAnnouncement = async (announcement) => {
try {
await ElMessageBox.confirm('确定要归档这条公告吗?', '确认归档', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await announcementApi.archiveAnnouncement(announcement.id)
ElMessage.success('公告归档成功')
loadAnnouncements()
loadStats()
} catch (error) {
if (error !== 'cancel') {
console.error('归档公告失败:', error)
ElMessage.error(error.message || '归档公告失败')
}
}
}
// 删除公告
const handleDeleteAnnouncement = async (announcement) => {
try {
await ElMessageBox.confirm('确定要删除这条公告吗?删除后无法恢复!', '确认删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await announcementApi.deleteAnnouncement(announcement.id)
ElMessage.success('公告删除成功')
loadAnnouncements()
loadStats()
} catch (error) {
if (error !== 'cancel') {
console.error('删除公告失败:', error)
ElMessage.error(error.message || '删除公告失败')
}
}
}
// 编辑成功回调
const handleEditSuccess = () => {
loadAnnouncements()
loadStats()
}
// 定时发布公告
const handleSchedulePublish = (announcement) => {
currentAnnouncement.value = announcement
showScheduleDialog.value = true
}
// 定时发布成功回调
const handleScheduleSuccess = () => {
loadAnnouncements()
loadStats()
}
// 取消定时发布
const handleCancelSchedule = async (announcement) => {
try {
await ElMessageBox.confirm('确定要取消这条公告的定时发布吗?', '确认取消', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await announcementApi.cancelSchedulePublishAnnouncement(announcement.id)
ElMessage.success('取消定时发布成功')
loadAnnouncements()
loadStats()
} catch (error) {
if (error !== 'cancel') {
console.error('取消定时发布失败:', error)
ElMessage.error(error.message || '取消定时发布失败')
}
}
}
// 处理更多操作
const handleMoreAction = (command, announcement) => {
switch (command) {
case 'schedule-publish':
handleSchedulePublish(announcement)
break
case 'cancel-schedule':
handleCancelSchedule(announcement)
break
case 'view':
handleViewAnnouncement(announcement)
break
case 'delete':
handleDeleteAnnouncement(announcement)
break
default:
console.warn('未知的操作命令:', command)
}
}
// 状态类型映射
const getStatusType = (status, scheduledAt) => {
if (status === 'draft' && scheduledAt) {
return 'warning'
}
const statusMap = {
draft: 'info',
published: 'success',
archived: 'warning'
}
return statusMap[status] || 'info'
}
// 状态文本映射
const getStatusText = (status, scheduledAt) => {
if (status === 'draft' && scheduledAt) {
return '定时发布'
}
const statusMap = {
draft: '草稿',
published: '已发布',
archived: '已归档'
}
return statusMap[status] || status
}
// 格式化日期
const formatDate = (dateString) => {
if (!dateString) return '-'
return new Date(dateString).toLocaleString('zh-CN')
}
// 页面初始化
onMounted(() => {
loadAnnouncements()
loadStats()
})
</script>