Compare commits

...

2 Commits

3 changed files with 701 additions and 1 deletions

View File

@@ -89,6 +89,11 @@
<template #title>提现管理</template>
</el-menu-item>
<el-menu-item v-if="userStore.isAdmin" index="/announcements">
<el-icon><Bell /></el-icon>
<template #title>通知公告</template>
</el-menu-item>
<!-- <el-menu-item v-if="userStore.isAdmin" index="/database-monitor">
<el-icon><Monitor /></el-icon>
<template #title>数据库监控</template>
@@ -258,7 +263,8 @@ import {
DataAnalysis,
Monitor,
Connection,
CreditCard
CreditCard,
Bell
} from '@element-plus/icons-vue'
const route = useRoute()

View File

@@ -192,6 +192,16 @@ const routes = [
requiresAdmin: true
}
},
{
path: 'announcements',
name: 'Announcements',
component: () => import('@/views/Announcements.vue'),
meta: {
title: '通知公告 - 炬融圈',
icon: 'Bell',
requiresAdmin: true
}
},
// {
// path: 'database-monitor',
// name: 'DatabaseMonitor',

684
src/views/Announcements.vue Normal file
View File

@@ -0,0 +1,684 @@
<template>
<div class="announcements-page">
<div class="page-header">
<h1 class="page-title">通知公告管理</h1>
<p class="page-subtitle">管理系统中的所有通知公告</p>
</div>
<!-- 搜索和操作栏 -->
<el-card class="search-card" shadow="never">
<el-row :gutter="20" class="search-row">
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-input
v-model="searchForm.keyword"
placeholder="搜索公告标题或内容"
:prefix-icon="Search"
clearable
@keyup.enter="handleSearch"
/>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-select
v-model="searchForm.status"
placeholder="选择状态"
clearable
style="width: 100%"
>
<el-option label="全部" value="" />
<el-option label="草稿" value="draft" />
<el-option label="已发布" value="published" />
<el-option label="已归档" value="archived" />
</el-select>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-select
v-model="searchForm.type"
placeholder="选择类型"
clearable
style="width: 100%"
>
<el-option label="全部" value="" />
<el-option label="系统通知" value="system" />
<el-option label="维护公告" value="maintenance" />
<el-option label="活动推广" value="promotion" />
<el-option label="重要警告" value="warning" />
</el-select>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" class="button-group">
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-col>
</el-row>
</el-card>
<!-- 公告列表 -->
<el-card class="table-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">公告列表 ({{ pagination.total }})</span>
<el-button type="primary" @click="showCreateDialog">
<el-icon><Plus /></el-icon>
新建公告
</el-button>
</div>
</template>
<el-table
v-loading="loading"
:data="announcements"
stripe
style="width: 100%"
@sort-change="handleSortChange"
>
<el-table-column prop="id" label="ID" width="80" sortable="custom" />
<el-table-column prop="title" label="标题" min-width="200">
<template #default="{ row }">
<div class="title-cell">
<el-tag v-if="row.is_pinned" type="danger" size="small" class="pin-tag">
置顶
</el-tag>
<span class="title-text">{{ row.title }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="type" label="类型" width="120">
<template #default="{ row }">
<el-tag :type="getTypeTagType(row.type)" size="small">
{{ getTypeLabel(row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="priority" label="优先级" width="100">
<template #default="{ row }">
<el-tag :type="getPriorityTagType(row.priority)" size="small">
{{ getPriorityLabel(row.priority) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.status)" size="small">
{{ getStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="creator_name" label="创建者" width="120" />
<el-table-column prop="publish_time" label="发布时间" width="180">
<template #default="{ row }">
{{ row.publish_time ? formatDateTime(row.publish_time) : '-' }}
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="180" sortable="custom">
<template #default="{ row }">
{{ formatDateTime(row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="showEditDialog(row)">
编辑
</el-button>
<el-button
v-if="row.status === 'draft'"
type="success"
size="small"
@click="publishAnnouncement(row)"
>
发布
</el-button>
<el-button type="danger" size="small" @click="deleteAnnouncement(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.limit"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 创建/编辑公告对话框 -->
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑公告' : '新建公告'"
width="800px"
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="form"
:rules="formRules"
label-width="100px"
>
<el-form-item label="公告标题" prop="title">
<el-input v-model="form.title" placeholder="请输入公告标题" maxlength="255" show-word-limit />
</el-form-item>
<el-form-item label="公告内容" prop="content">
<el-input
v-model="form.content"
type="textarea"
:rows="6"
placeholder="请输入公告内容"
maxlength="2000"
show-word-limit
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="公告类型" prop="type">
<el-select v-model="form.type" placeholder="选择公告类型" style="width: 100%">
<el-option label="系统通知" value="system" />
<el-option label="维护公告" value="maintenance" />
<el-option label="活动推广" value="promotion" />
<el-option label="重要警告" value="warning" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="优先级" prop="priority">
<el-select v-model="form.priority" placeholder="选择优先级" style="width: 100%">
<el-option label="低" value="low" />
<el-option label="中" value="medium" />
<el-option label="高" value="high" />
<el-option label="紧急" value="urgent" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="选择状态" style="width: 100%">
<el-option label="草稿" value="draft" />
<el-option label="发布" value="published" />
<el-option label="归档" value="archived" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否置顶">
<el-switch v-model="form.is_pinned" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="发布时间">
<el-date-picker
v-model="form.publish_time"
type="datetime"
placeholder="选择发布时间"
style="width: 100%"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="过期时间">
<el-date-picker
v-model="form.expire_time"
type="datetime"
placeholder="选择过期时间"
style="width: 100%"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">
{{ isEdit ? '更新' : '创建' }}
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/user'
import api from '@/utils/api'
const userStore = useUserStore()
// 响应式数据
const loading = ref(false)
const submitting = ref(false)
const dialogVisible = ref(false)
const isEdit = ref(false)
const formRef = ref()
// 公告列表
const announcements = ref([])
// 搜索表单
const searchForm = reactive({
keyword: '',
status: '',
type: ''
})
// 分页
const pagination = reactive({
page: 1,
limit: 10,
total: 0
})
// 表单数据
const form = reactive({
id: null,
title: '',
content: '',
type: 'system',
priority: 'medium',
status: 'draft',
is_pinned: false,
publish_time: '',
expire_time: ''
})
// 表单验证规则
const formRules = {
title: [
{ required: true, message: '请输入公告标题', trigger: 'blur' },
{ min: 1, max: 255, message: '标题长度在 1 到 255 个字符', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入公告内容', trigger: 'blur' },
{ min: 1, max: 2000, message: '内容长度在 1 到 2000 个字符', trigger: 'blur' }
],
type: [
{ required: true, message: '请选择公告类型', trigger: 'change' }
],
priority: [
{ required: true, message: '请选择优先级', trigger: 'change' }
],
status: [
{ required: true, message: '请选择状态', trigger: 'change' }
]
}
// 计算属性
const totalPages = computed(() => Math.ceil(pagination.total / pagination.limit))
// 方法
const fetchAnnouncements = async () => {
try {
loading.value = true
const params = {
page: pagination.page,
limit: pagination.limit,
...searchForm
}
const response = await api.get('/announcements', { params })
if (response.data.success) {
announcements.value = response.data.data.announcements
pagination.total = response.data.data.total
}
} catch (error) {
console.error('获取公告列表失败:', error)
ElMessage.error('获取公告列表失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.page = 1
fetchAnnouncements()
}
const handleReset = () => {
Object.assign(searchForm, {
keyword: '',
status: '',
type: ''
})
pagination.page = 1
fetchAnnouncements()
}
const handleSizeChange = (size) => {
pagination.limit = size
pagination.page = 1
fetchAnnouncements()
}
const handleCurrentChange = (page) => {
pagination.page = page
fetchAnnouncements()
}
const handleSortChange = ({ prop, order }) => {
// 实现排序逻辑
fetchAnnouncements()
}
const showCreateDialog = () => {
isEdit.value = false
resetForm()
dialogVisible.value = true
}
const showEditDialog = (row) => {
isEdit.value = true
Object.assign(form, {
id: row.id,
title: row.title,
content: row.content,
type: row.type,
priority: row.priority,
status: row.status,
is_pinned: row.is_pinned,
publish_time: row.publish_time,
expire_time: row.expire_time
})
dialogVisible.value = true
}
const resetForm = () => {
Object.assign(form, {
id: null,
title: '',
content: '',
type: 'system',
priority: 'medium',
status: 'draft',
is_pinned: false,
publish_time: '',
expire_time: ''
})
if (formRef.value) {
formRef.value.resetFields()
}
}
const handleSubmit = async () => {
try {
await formRef.value.validate()
submitting.value = true
const data = { ...form }
delete data.id
let response
if (isEdit.value) {
response = await api.put(`/announcements/${form.id}`, data)
} else {
response = await api.post('/announcements', data)
}
if (response.data.success) {
ElMessage.success(isEdit.value ? '公告更新成功' : '公告创建成功')
dialogVisible.value = false
fetchAnnouncements()
}
} catch (error) {
console.error('提交失败:', error)
ElMessage.error(isEdit.value ? '公告更新失败' : '公告创建失败')
} finally {
submitting.value = false
}
}
const publishAnnouncement = async (row) => {
try {
await ElMessageBox.confirm('确定要发布这个公告吗?', '确认发布', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const response = await api.put(`/announcements/${row.id}`, {
status: 'published',
publish_time: new Date().toISOString().slice(0, 19).replace('T', ' ')
})
if (response.data.success) {
ElMessage.success('公告发布成功')
fetchAnnouncements()
}
} catch (error) {
if (error !== 'cancel') {
console.error('发布公告失败:', error)
ElMessage.error('发布公告失败')
}
}
}
const deleteAnnouncement = async (row) => {
try {
await ElMessageBox.confirm('确定要删除这个公告吗?删除后无法恢复。', '确认删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const response = await api.delete(`/announcements/${row.id}`)
if (response.data.success) {
ElMessage.success('公告删除成功')
fetchAnnouncements()
}
} catch (error) {
if (error !== 'cancel') {
console.error('删除公告失败:', error)
ElMessage.error('删除公告失败')
}
}
}
// 辅助方法
const getTypeLabel = (type) => {
const labels = {
system: '系统通知',
maintenance: '维护公告',
promotion: '活动推广',
warning: '重要警告'
}
return labels[type] || type
}
const getTypeTagType = (type) => {
const types = {
system: 'info',
maintenance: 'warning',
promotion: 'success',
warning: 'danger'
}
return types[type] || 'info'
}
const getPriorityLabel = (priority) => {
const labels = {
low: '低',
medium: '中',
high: '高',
urgent: '紧急'
}
return labels[priority] || priority
}
const getPriorityTagType = (priority) => {
const types = {
low: 'info',
medium: '',
high: 'warning',
urgent: 'danger'
}
return types[priority] || ''
}
const getStatusLabel = (status) => {
const labels = {
draft: '草稿',
published: '已发布',
archived: '已归档'
}
return labels[status] || status
}
const getStatusTagType = (status) => {
const types = {
draft: 'info',
published: 'success',
archived: 'warning'
}
return types[status] || 'info'
}
const formatDateTime = (dateTime) => {
if (!dateTime) return '-'
return new Date(dateTime).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
// 生命周期
onMounted(() => {
fetchAnnouncements()
})
</script>
<style scoped>
.announcements-page {
padding: 20px;
}
.page-header {
margin-bottom: 20px;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #303133;
margin: 0 0 8px 0;
}
.page-subtitle {
font-size: 14px;
color: #909399;
margin: 0;
}
.search-card {
margin-bottom: 20px;
}
.search-row {
align-items: flex-end;
}
.button-group {
display: flex;
gap: 10px;
}
.table-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.title-cell {
display: flex;
align-items: center;
gap: 8px;
}
.pin-tag {
flex-shrink: 0;
}
.title-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pagination-wrapper {
display: flex;
justify-content: center;
margin-top: 20px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.announcements-page {
padding: 10px;
}
.search-row .el-col {
margin-bottom: 10px;
}
.button-group {
width: 100%;
}
.button-group .el-button {
flex: 1;
}
}
</style>