更新 AdminLayout 以移除不必要的菜单项,添加公告管理功能,并调整路由权限检查逻辑以支持新功能。

This commit is contained in:
zyh
2025-08-27 17:36:19 +08:00
parent 1652e5199f
commit abde5d1f9d
6 changed files with 514 additions and 59 deletions

View 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
View 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')
}

View File

@@ -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() {

View File

@@ -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()

View File

@@ -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()] || []

View 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>