更新 AdminLayout 以移除不必要的菜单项,添加公告管理功能,并调整路由权限检查逻辑以支持新功能。
This commit is contained in:
117
docs/公告管理使用说明.md
Normal file
117
docs/公告管理使用说明.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# 公告管理使用说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
公告管理模块提供了完整的公告信息管理功能,包括公告的创建、编辑、删除、启用/禁用等操作。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 公告列表查看(支持分页)
|
||||
- ✅ 按标题/内容关键词搜索
|
||||
- ✅ 按启用状态筛选
|
||||
- ✅ 创建新公告
|
||||
- ✅ 编辑现有公告
|
||||
- ✅ 删除公告
|
||||
- ✅ 一键启用/禁用公告
|
||||
- ✅ 支持跳转链接设置
|
||||
- ✅ 权限控制(管理员和代理商不同权限)
|
||||
|
||||
## 权限说明
|
||||
|
||||
### 管理员权限
|
||||
- 查看公告列表
|
||||
- 创建新公告
|
||||
- 编辑现有公告
|
||||
- 删除公告
|
||||
- 启用/禁用公告
|
||||
|
||||
### 代理商权限
|
||||
- 查看公告列表(只读)
|
||||
|
||||
## 使用步骤
|
||||
|
||||
### 1. 访问公告管理
|
||||
- 登录管理后台
|
||||
- 在左侧导航菜单中点击"公告管理"
|
||||
|
||||
### 2. 查看公告列表
|
||||
- 公告列表显示所有公告信息
|
||||
- 支持按关键词搜索(标题或内容)
|
||||
- 支持按启用状态筛选
|
||||
- 支持分页浏览
|
||||
|
||||
### 3. 创建新公告
|
||||
1. 点击"新增公告"按钮
|
||||
2. 填写公告信息:
|
||||
- **标题**:必填,公告标题
|
||||
- **内容**:必填,公告详细内容
|
||||
- **跳转链接**:可选,点击公告后跳转的URL
|
||||
- **状态**:选择是否启用
|
||||
3. 点击"保存"完成创建
|
||||
|
||||
### 4. 编辑公告
|
||||
1. 在公告列表中找到要编辑的公告
|
||||
2. 点击"编辑"按钮
|
||||
3. 修改公告信息
|
||||
4. 点击"保存"完成修改
|
||||
|
||||
### 5. 删除公告
|
||||
1. 在公告列表中找到要删除的公告
|
||||
2. 点击"删除"按钮
|
||||
3. 确认删除操作
|
||||
|
||||
### 6. 启用/禁用公告
|
||||
- 在公告列表中,直接点击状态开关即可快速启用或禁用公告
|
||||
- 只有启用的公告才会对用户可见
|
||||
|
||||
## 字段说明
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 标题 | 文本 | 是 | 公告标题,用于列表显示 |
|
||||
| 内容 | 文本 | 是 | 公告详细内容 |
|
||||
| 跳转链接 | URL | 否 | 点击公告后跳转的链接地址 |
|
||||
| 状态 | 布尔 | 是 | 是否启用,只有启用的公告用户才能看到 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **权限控制**:只有具备相应权限的用户才能进行相应操作
|
||||
2. **数据验证**:标题和内容为必填字段
|
||||
3. **URL验证**:跳转链接必须是有效的URL格式(以http://或https://开头)
|
||||
4. **状态控制**:只有启用的公告才会在前端显示给用户
|
||||
5. **操作记录**:所有操作都会记录创建时间和更新时间
|
||||
|
||||
## API接口
|
||||
|
||||
公告管理使用以下API接口:
|
||||
|
||||
- `GET /api/admin/announcement/list` - 获取公告列表
|
||||
- `POST /api/admin/announcement` - 创建公告
|
||||
- `PUT /api/admin/announcement/{id}` - 更新公告
|
||||
- `DELETE /api/admin/announcement/{id}` - 删除公告
|
||||
- `PUT /api/admin/announcement/{id}/enabled` - 更新启用状态
|
||||
- `GET /api/admin/announcement/enabled` - 获取启用的公告
|
||||
|
||||
详细的API文档请参考项目根目录的接口文档。
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **无法访问公告管理页面**
|
||||
- 检查用户是否有相应权限
|
||||
- 确认已正确登录
|
||||
|
||||
2. **创建公告失败**
|
||||
- 检查标题和内容是否已填写
|
||||
- 检查跳转链接格式是否正确
|
||||
|
||||
3. **状态切换失败**
|
||||
- 检查网络连接
|
||||
- 确认用户有编辑权限
|
||||
|
||||
4. **搜索无结果**
|
||||
- 检查搜索关键词是否正确
|
||||
- 尝试重置搜索条件
|
||||
|
||||
如有其他问题,请联系系统管理员。
|
||||
42
src/api/announcement.js
Normal file
42
src/api/announcement.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import http from '../plugins/http'
|
||||
|
||||
/**
|
||||
* 公告管理API
|
||||
*/
|
||||
|
||||
// 获取公告列表(分页)
|
||||
export function getAnnouncementList(params) {
|
||||
return http.get('/api/admin/announcement/list', { params })
|
||||
}
|
||||
|
||||
// 获取公告详情
|
||||
export function getAnnouncementDetail(id) {
|
||||
return http.get(`/api/admin/announcement/${id}`)
|
||||
}
|
||||
|
||||
// 创建公告
|
||||
export function createAnnouncement(data) {
|
||||
return http.post('/api/admin/announcement', data)
|
||||
}
|
||||
|
||||
// 更新公告
|
||||
export function updateAnnouncement(id, data) {
|
||||
return http.put(`/api/admin/announcement/${id}`, data)
|
||||
}
|
||||
|
||||
// 删除公告
|
||||
export function deleteAnnouncement(id) {
|
||||
return http.delete(`/api/admin/announcement/${id}`)
|
||||
}
|
||||
|
||||
// 更新公告启用状态
|
||||
export function updateAnnouncementStatus(id, enabled) {
|
||||
return http.put(`/api/admin/announcement/${id}/enabled`, null, {
|
||||
params: { enabled }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取启用的公告
|
||||
export function getEnabledAnnouncements() {
|
||||
return http.get('/api/admin/announcement/enabled')
|
||||
}
|
||||
@@ -14,42 +14,28 @@
|
||||
text-color="#bfcbd9"
|
||||
active-text-color="#fff"
|
||||
>
|
||||
<el-menu-item index="Dashboard" :route="{ name: 'Dashboard' }">
|
||||
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg></i>
|
||||
<span>仪表盘</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item v-if="canAccessUsers" index="Users" :route="{ name: 'Users' }">
|
||||
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 12c2.7 0 5-2.3 5-5s-2.3-5-5-5s-5 2.3-5 5s2.3 5 5 5m0 2c-3.3 0-10 1.7-10 5v3h20v-3c0-3.3-6.7-5-10-5Z"/></svg></i>
|
||||
<span>用户管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="canAccessGames" index="Games" :route="{ name: 'Games' }">
|
||||
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M6 8h12v8H6z"/></svg></i>
|
||||
<span>游戏管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="canAccessOrders" index="Orders" :route="{ name: 'Orders' }">
|
||||
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M3 6h18v2H3V6m0 5h18v2H3v-2m0 5h18v2H3v-2Z"/></svg></i>
|
||||
<span>订单管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="canAccessReports" index="Reports" :route="{ name: 'Reports' }">
|
||||
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M3 3h2v18H3V3m4 8h2v10H7V11m4-6h2v16h-2V5m4 10h2v6h-2v-6m4-3h2v9h-2v-9Z"/></svg></i>
|
||||
<span>报表分析</span>
|
||||
</el-menu-item>
|
||||
|
||||
|
||||
|
||||
<el-menu-item v-if="canAccessLinks" index="Links" :route="{ name: 'Links' }">
|
||||
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M10.59 13.41c.41.39.41 1.03 0 1.42c-.39.39-1.03.39-1.42 0a5.003 5.003 0 0 1 0-7.07l3.54-3.54a5.003 5.003 0 0 1 7.07 0a5.003 5.003 0 0 1 0 7.07l-1.49 1.49c.01-.82-.12-1.64-.4-2.42l.47-.48a2.982 2.982 0 0 0 0-4.24a2.982 2.982 0 0 0-4.24 0l-3.53 3.53a2.982 2.982 0 0 0 0 4.24zm2.82-4.24c.39-.39 1.03-.39 1.42 0a5.003 5.003 0 0 1 0 7.07l-3.54 3.54a5.003 5.003 0 0 1-7.07 0a5.003 5.003 0 0 1 0-7.07l1.49-1.49c-.01.82.12 1.64.4 2.43l-.47.47a2.982 2.982 0 0 0 0 4.24a2.982 2.982 0 0 0 4.24 0l3.53-3.53a2.982 2.982 0 0 0 0-4.24a.973.973 0 0 1 0-1.42z"/></svg></i>
|
||||
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M10.59 13.41c.41.39.41 1.03 0 1.42c-.39.39-1.03.39-1.42 0a5.003 5.003 0 0 1 0-7.07l3.54-3.54a5.003 5.003 0 0 1 7.07 0a5.003 5.003 0 0 1 0 7.07l-1.49 1.49c.01-.82-.12-1.64-.4-2.42l.47-.48a2.982 2.982 0 0 0 0-4.24a2.982 2.982 0 0 0-4.24 0l-3.53 3.53a2.982 2.982 0 0 0 0-4.24zm2.82-4.24c.39-.39 1.03-.39 1.42 0a5.003 5.003 0 0 1 0 7.07l-3.54 3.54a5.003 5.003 0 0 1-7.07 0a5.003 5.003 0 0 1 0-7.07l1.49-1.49c-.01.82.12 1.64.4 2.43l-.47.47a2.982 2.982 0 0 0 0 4.24a2.982 2.982 0 0 0 4.24 0l3.53-3.53a2.982 2.982 0 0 0 0-4.24a.973.973 0 0 1 0-1.42z"/></svg></i>
|
||||
<span>链接管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="canAccessAnnouncements" index="Announcements" :route="{ name: 'Announcements' }">
|
||||
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></i>
|
||||
<span>公告管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="canAccessSettings" index="Settings" :route="{ name: 'Settings' }">
|
||||
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="m12 8l-2 4h4l-2 4"/></svg></i>
|
||||
<span>系统设置</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="ErrorTest" :route="{ name: 'ErrorTest' }">
|
||||
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg></i>
|
||||
<span>错误测试</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="PermissionTest" :route="{ name: 'PermissionTest' }">
|
||||
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm-2 16l-4-4 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg></i>
|
||||
<span>权限测试</span>
|
||||
</el-menu-item>
|
||||
|
||||
|
||||
</el-menu>
|
||||
</aside>
|
||||
<section class="main">
|
||||
@@ -92,10 +78,8 @@ const currentUser = computed(() => getCurrentUser())
|
||||
|
||||
// 权限检查
|
||||
const canAccessUsers = computed(() => canAccessRoute('Users'))
|
||||
const canAccessGames = computed(() => canAccessRoute('Games'))
|
||||
const canAccessOrders = computed(() => canAccessRoute('Orders'))
|
||||
const canAccessReports = computed(() => canAccessRoute('Reports'))
|
||||
const canAccessLinks = computed(() => canAccessRoute('Links'))
|
||||
const canAccessAnnouncements = computed(() => canAccessRoute('Announcements'))
|
||||
const canAccessSettings = computed(() => canAccessRoute('Settings'))
|
||||
|
||||
function onProfile() {
|
||||
|
||||
@@ -5,15 +5,10 @@ import { canAccessRoute } from '@/utils/permission'
|
||||
const AdminLayout = () => import('@/layouts/AdminLayout.vue')
|
||||
const Login = () => import('@/views/Login.vue')
|
||||
|
||||
const Dashboard = () => import('@/views/Dashboard.vue')
|
||||
const UserList = () => import('@/views/users/UserList.vue')
|
||||
const GameList = () => import('@/views/games/GameList.vue')
|
||||
const OrderList = () => import('@/views/orders/OrderList.vue')
|
||||
const ReportAnalysis = () => import('@/views/reports/ReportAnalysis.vue')
|
||||
const Settings = () => import('@/views/settings/Settings.vue')
|
||||
const LinkGenerate = () => import('@/views/links/LinkGenerate.vue')
|
||||
const ErrorTest = () => import('@/views/ErrorTest.vue')
|
||||
const PermissionTest = () => import('@/views/PermissionTest.vue')
|
||||
const AnnouncementList = () => import('@/views/announcements/AnnouncementList.vue')
|
||||
const Play = () => import('@/views/Play.vue')
|
||||
const NotFound = () => import('@/views/NotFound.vue')
|
||||
|
||||
@@ -24,15 +19,11 @@ export const routes = [
|
||||
path: '/',
|
||||
component: AdminLayout,
|
||||
children: [
|
||||
{ path: '', name: 'Dashboard', component: Dashboard, meta: { title: '仪表盘' } },
|
||||
{ path: '', redirect: '/users' },
|
||||
{ path: 'users', name: 'Users', component: UserList, meta: { title: '用户管理' } },
|
||||
{ path: 'games', name: 'Games', component: GameList, meta: { title: '游戏管理' } },
|
||||
{ path: 'orders', name: 'Orders', component: OrderList, meta: { title: '订单管理' } },
|
||||
{ path: 'reports', name: 'Reports', component: ReportAnalysis, meta: { title: '报表分析' } },
|
||||
{ path: 'settings', name: 'Settings', component: Settings, meta: { title: '系统设置' } },
|
||||
{ path: 'links', name: 'Links', component: LinkGenerate, meta: { title: '链接管理' } },
|
||||
{ path: 'error-test', name: 'ErrorTest', component: ErrorTest, meta: { title: '错误处理测试' } },
|
||||
{ path: 'permission-test', name: 'PermissionTest', component: PermissionTest, meta: { title: '权限测试' } },
|
||||
{ path: 'announcements', name: 'Announcements', component: AnnouncementList, meta: { title: '公告管理' } },
|
||||
],
|
||||
},
|
||||
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound, meta: { public: true, title: '未找到' } },
|
||||
@@ -51,7 +42,7 @@ router.beforeEach((to, from, next) => {
|
||||
|
||||
// 检查路由权限
|
||||
if (to.name && !canAccessRoute(to.name)) {
|
||||
return next({ name: 'Dashboard' }) // 无权限时跳转到仪表盘
|
||||
return next({ name: 'Users' }) // 无权限时跳转到用户管理
|
||||
}
|
||||
|
||||
next()
|
||||
|
||||
@@ -36,6 +36,13 @@ export const PERMISSIONS = {
|
||||
// 二维码权限
|
||||
QR_GENERATE: 'qr:generate',
|
||||
QR_VIEW: 'qr:view',
|
||||
|
||||
// 公告管理权限
|
||||
ANNOUNCEMENT_MANAGE: 'announcement:manage',
|
||||
ANNOUNCEMENT_CREATE: 'announcement:create',
|
||||
ANNOUNCEMENT_UPDATE: 'announcement:update',
|
||||
ANNOUNCEMENT_DELETE: 'announcement:delete',
|
||||
ANNOUNCEMENT_VIEW: 'announcement:view',
|
||||
}
|
||||
|
||||
// 角色权限映射
|
||||
@@ -47,14 +54,6 @@ export const ROLE_PERMISSIONS = {
|
||||
PERMISSIONS.USER_UPDATE,
|
||||
PERMISSIONS.USER_DELETE,
|
||||
PERMISSIONS.USER_VIEW,
|
||||
PERMISSIONS.GAME_MANAGE,
|
||||
PERMISSIONS.GAME_CREATE,
|
||||
PERMISSIONS.GAME_UPDATE,
|
||||
PERMISSIONS.GAME_DELETE,
|
||||
PERMISSIONS.GAME_VIEW,
|
||||
PERMISSIONS.ORDER_MANAGE,
|
||||
PERMISSIONS.ORDER_VIEW,
|
||||
PERMISSIONS.REPORT_VIEW,
|
||||
PERMISSIONS.SETTING_MANAGE,
|
||||
PERMISSIONS.LINK_MANAGE,
|
||||
PERMISSIONS.LINK_CREATE,
|
||||
@@ -63,28 +62,26 @@ export const ROLE_PERMISSIONS = {
|
||||
PERMISSIONS.LINK_VIEW,
|
||||
PERMISSIONS.QR_GENERATE,
|
||||
PERMISSIONS.QR_VIEW,
|
||||
PERMISSIONS.ANNOUNCEMENT_MANAGE,
|
||||
PERMISSIONS.ANNOUNCEMENT_CREATE,
|
||||
PERMISSIONS.ANNOUNCEMENT_UPDATE,
|
||||
PERMISSIONS.ANNOUNCEMENT_DELETE,
|
||||
PERMISSIONS.ANNOUNCEMENT_VIEW,
|
||||
],
|
||||
AGENT: [
|
||||
// 代理商只有查看权限,没有管理权限
|
||||
PERMISSIONS.GAME_VIEW,
|
||||
PERMISSIONS.ORDER_VIEW,
|
||||
PERMISSIONS.REPORT_VIEW,
|
||||
PERMISSIONS.LINK_VIEW,
|
||||
PERMISSIONS.QR_VIEW,
|
||||
PERMISSIONS.ANNOUNCEMENT_VIEW,
|
||||
]
|
||||
}
|
||||
|
||||
// 路由权限映射
|
||||
export const ROUTE_PERMISSIONS = {
|
||||
'Dashboard': [], // 仪表盘所有用户都可以访问
|
||||
'Users': [PERMISSIONS.USER_VIEW],
|
||||
'Games': [PERMISSIONS.GAME_VIEW],
|
||||
'Orders': [PERMISSIONS.ORDER_VIEW],
|
||||
'Reports': [PERMISSIONS.REPORT_VIEW],
|
||||
'Settings': [PERMISSIONS.SETTING_MANAGE],
|
||||
'Links': [PERMISSIONS.LINK_VIEW],
|
||||
'ErrorTest': [], // 错误测试页面所有用户都可以访问
|
||||
'PermissionTest': [], // 权限测试页面所有用户都可以访问
|
||||
'Announcements': [PERMISSIONS.ANNOUNCEMENT_VIEW],
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
@@ -153,7 +150,7 @@ export function getAccessibleRoutes() {
|
||||
|
||||
// 管理员可以访问所有路由
|
||||
if (isAdmin()) {
|
||||
return ['Dashboard', 'Users', 'Games', 'Orders', 'Reports', 'Settings', 'Links', 'ErrorTest', 'PermissionTest']
|
||||
return ['Dashboard', 'Users', 'Games', 'Orders', 'Reports', 'Settings', 'Links', 'Announcements', 'ErrorTest', 'PermissionTest']
|
||||
}
|
||||
|
||||
const userPermissions = ROLE_PERMISSIONS[userType?.toUpperCase()] || []
|
||||
|
||||
324
src/views/announcements/AnnouncementList.vue
Normal file
324
src/views/announcements/AnnouncementList.vue
Normal file
@@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<div class="announcements-page">
|
||||
<!-- 权限检查 -->
|
||||
<div v-if="!canViewAnnouncements" class="permission-denied">
|
||||
<el-result
|
||||
icon="warning"
|
||||
title="权限不足"
|
||||
sub-title="您没有访问公告管理的权限,请联系管理员"
|
||||
>
|
||||
<template #extra>
|
||||
<el-button type="primary" @click="$router.push({ name: 'Dashboard' })">返回首页</el-button>
|
||||
</template>
|
||||
</el-result>
|
||||
</div>
|
||||
|
||||
<!-- 公告管理内容 -->
|
||||
<div v-else>
|
||||
<el-card class="mb16" shadow="never">
|
||||
<el-form :inline="true" :model="query" @submit.prevent>
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model.trim="query.keyword" placeholder="标题或内容" clearable style="width: 220px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="query.enabled" clearable placeholder="全部" style="width: 140px">
|
||||
<el-option :value="true" label="启用" />
|
||||
<el-option :value="false" label="禁用" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" @click="onSearch">查询</el-button>
|
||||
<el-button @click="onReset">重置</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="canCreateAnnouncement">
|
||||
<el-button type="success" @click="openCreate">新增公告</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<el-table :data="list" v-loading="loading" border stripe>
|
||||
<el-table-column type="index" label="#" width="60" />
|
||||
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="content" label="内容" min-width="300" show-overflow-tooltip />
|
||||
<el-table-column prop="jumpUrl" label="跳转链接" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.jumpUrl">{{ row.jumpUrl }}</span>
|
||||
<span v-else class="text-gray">无</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
:model-value="row.enabled"
|
||||
@change="(v) => onToggle(row, v)"
|
||||
:disabled="!canEditAnnouncement"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" min-width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updatedAt" label="更新时间" min-width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.updatedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="canEditAnnouncement" size="small" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button v-if="canDeleteAnnouncement" size="small" type="danger" @click="onRemove(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pager">
|
||||
<el-pagination
|
||||
background
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
:page-size="query.size"
|
||||
:current-page="query.page"
|
||||
:page-sizes="[10,20,50,100]"
|
||||
@size-change="(s) => { query.size = s; query.page = 1; load(); }"
|
||||
@current-change="(p) => { query.page = p; load(); }"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 编辑/新增对话框 -->
|
||||
<el-dialog v-model="visible" :title="isEdit ? '编辑公告' : '新增公告'" width="600px">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="88px">
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model.trim="form.title" placeholder="请输入公告标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="内容" prop="content">
|
||||
<el-input
|
||||
v-model.trim="form.content"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入公告内容"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="跳转链接" prop="jumpUrl">
|
||||
<el-input
|
||||
v-model.trim="form.jumpUrl"
|
||||
placeholder="可选,点击公告后跳转的链接"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="enabled">
|
||||
<el-switch v-model="form.enabled" />
|
||||
<span class="ml8 text-gray">启用后用户可见</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="onSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
getAnnouncementList,
|
||||
createAnnouncement,
|
||||
updateAnnouncement,
|
||||
deleteAnnouncement,
|
||||
updateAnnouncementStatus
|
||||
} from '@/api/announcement'
|
||||
import { showErrorMessage, showSuccessMessage } from '@/utils/error'
|
||||
import { hasPermission, PERMISSIONS } from '@/utils/permission'
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
|
||||
const query = reactive({
|
||||
page: 1,
|
||||
size: 10,
|
||||
keyword: '',
|
||||
enabled: undefined,
|
||||
})
|
||||
|
||||
// 权限检查
|
||||
const canViewAnnouncements = computed(() => hasPermission(PERMISSIONS.ANNOUNCEMENT_VIEW))
|
||||
const canCreateAnnouncement = computed(() => hasPermission(PERMISSIONS.ANNOUNCEMENT_CREATE))
|
||||
const canEditAnnouncement = computed(() => hasPermission(PERMISSIONS.ANNOUNCEMENT_UPDATE))
|
||||
const canDeleteAnnouncement = computed(() => hasPermission(PERMISSIONS.ANNOUNCEMENT_DELETE))
|
||||
|
||||
// 格式化日期时间
|
||||
function formatDateTime(dateStr) {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
function unwrap(res) {
|
||||
const d = res?.data
|
||||
if (d && typeof d === 'object' && 'code' in d) return d.code === 0 ? d.data : d
|
||||
return d
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { ...query }
|
||||
// 过滤空值
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] === '' || params[key] === undefined) {
|
||||
delete params[key]
|
||||
}
|
||||
})
|
||||
|
||||
const res = await getAnnouncementList(params)
|
||||
const data = unwrap(res) || {}
|
||||
list.value = data.items || []
|
||||
total.value = data.total || 0
|
||||
} catch (e) {
|
||||
showErrorMessage(e, '加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
query.page = 1
|
||||
load()
|
||||
}
|
||||
|
||||
function onReset() {
|
||||
query.keyword = ''
|
||||
query.enabled = undefined
|
||||
query.page = 1
|
||||
query.size = 10
|
||||
load()
|
||||
}
|
||||
|
||||
async function onToggle(row, val) {
|
||||
try {
|
||||
await updateAnnouncementStatus(row.id, val)
|
||||
row.enabled = val
|
||||
showSuccessMessage('状态已更新')
|
||||
} catch (e) {
|
||||
showErrorMessage(e, '更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function onRemove(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除公告「${row.title}」吗?`, '提示', { type: 'warning' })
|
||||
await deleteAnnouncement(row.id)
|
||||
showSuccessMessage('删除成功')
|
||||
load()
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') showErrorMessage(e, '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const visible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const saving = ref(false)
|
||||
const formRef = ref()
|
||||
const form = reactive({
|
||||
id: undefined,
|
||||
title: '',
|
||||
content: '',
|
||||
jumpUrl: '',
|
||||
enabled: true
|
||||
})
|
||||
|
||||
const rules = {
|
||||
title: [{ required: true, message: '请输入公告标题', trigger: 'blur' }],
|
||||
content: [{ required: true, message: '请输入公告内容', trigger: 'blur' }],
|
||||
jumpUrl: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (!value) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
// 简单的URL格式验证
|
||||
const urlPattern = /^https?:\/\/.+/
|
||||
if (!urlPattern.test(value)) {
|
||||
callback(new Error('请输入有效的URL地址(以http://或https://开头)'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
isEdit.value = false
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
title: '',
|
||||
content: '',
|
||||
jumpUrl: '',
|
||||
enabled: true
|
||||
})
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
isEdit.value = true
|
||||
Object.assign(form, { ...row })
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
function doSubmit() {
|
||||
const payload = {
|
||||
title: form.title,
|
||||
content: form.content,
|
||||
jumpUrl: form.jumpUrl || null,
|
||||
enabled: form.enabled
|
||||
}
|
||||
|
||||
return isEdit.value
|
||||
? updateAnnouncement(form.id, payload)
|
||||
: createAnnouncement(payload)
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
saving.value = true
|
||||
try {
|
||||
await doSubmit()
|
||||
showSuccessMessage('保存成功')
|
||||
visible.value = false
|
||||
load()
|
||||
} catch (e) {
|
||||
showErrorMessage(e, '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mb16 { margin-bottom: 16px; }
|
||||
.ml8 { margin-left: 8px; }
|
||||
.text-gray { color: #999; }
|
||||
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user