feat: 新增公告管理

This commit is contained in:
ccnetcore
2026-01-24 22:08:54 +08:00
parent 1ada6360d4
commit 2845f03250
13 changed files with 902 additions and 8 deletions

View File

@@ -1,5 +1,14 @@
import type { AnnouncementLogDto } from './types';
import { get } from '@/utils/request';
import type {
AnnouncementLogDto,
AnnouncementDto,
AnnouncementCreateInput,
AnnouncementUpdateInput,
AnnouncementGetListInput,
PagedResultDto,
} from './types';
import { del, get, post, put } from '@/utils/request';
// ==================== 前端首页用 ====================
/**
* 获取系统公告和活动数据
@@ -9,4 +18,49 @@ import { get } from '@/utils/request';
export function getSystemAnnouncements() {
return get<AnnouncementLogDto[]>('/announcement').json();
}
// ==================== 后台管理用 ====================
// 获取公告列表
export function getList(params?: AnnouncementGetListInput) {
const queryParams = new URLSearchParams();
if (params?.searchKey) {
queryParams.append('SearchKey', params.searchKey);
}
if (params?.skipCount !== undefined) {
queryParams.append('SkipCount', params.skipCount.toString());
}
if (params?.maxResultCount !== undefined) {
queryParams.append('MaxResultCount', params.maxResultCount.toString());
}
if (params?.type !== undefined) {
queryParams.append('Type', params.type.toString());
}
const queryString = queryParams.toString();
const url = queryString ? `/announcement/list?${queryString}` : '/announcement/list';
return get<PagedResultDto<AnnouncementDto>>(url).json();
}
// 根据ID获取公告
export function getById(id: string) {
return get<AnnouncementDto>(`/announcement/${id}`).json();
}
// 创建公告
export function create(data: AnnouncementCreateInput) {
return post<AnnouncementDto>('/announcement', data).json();
}
// 更新公告
export function update(data: AnnouncementUpdateInput) {
return put<AnnouncementDto>('/announcement', data).json();
}
// 删除公告
export function deleteById(id: string) {
return del(`/announcement/${id}`).json();
}
export * from './types';

View File

@@ -1,4 +1,10 @@
// 公告类型(对应后端 AnnouncementTypeEnum
// 公告类型枚举(对应后端 AnnouncementTypeEnum
export enum AnnouncementTypeEnum {
Activity = 1,
System = 2,
}
// 公告类型(兼容旧代码)
export type AnnouncementType = 'Activity' | 'System'
// 公告DTO对应后端 AnnouncementLogDto
@@ -16,3 +22,58 @@ export interface AnnouncementLogDto {
/** 公告类型(系统、活动) */
type: AnnouncementType
}
// ==================== 后台管理用 DTO ====================
// 公告 DTO后台管理列表
export interface AnnouncementDto {
id: string;
title: string;
content: string[];
remark?: string;
imageUrl?: string;
startTime: string;
endTime?: string;
type: AnnouncementTypeEnum;
url?: string;
creationTime: string;
}
// 创建公告输入
export interface AnnouncementCreateInput {
title: string;
content: string[];
remark?: string;
imageUrl?: string;
startTime: string;
endTime?: string;
type: AnnouncementTypeEnum;
url?: string;
}
// 更新公告输入
export interface AnnouncementUpdateInput {
id: string;
title: string;
content: string[];
remark?: string;
imageUrl?: string;
startTime: string;
endTime?: string;
type: AnnouncementTypeEnum;
url?: string;
}
// 获取公告列表输入
export interface AnnouncementGetListInput {
searchKey?: string;
skipCount?: number;
maxResultCount?: number;
type?: AnnouncementTypeEnum;
}
// 分页结果
export interface PagedResultDto<T> {
items: T[];
totalCount: number;
}

View File

@@ -30,6 +30,11 @@ export const PAGE_PERMISSIONS: PermissionConfig[] = [
allowedUsers: ['cc', 'Guo'],
description: '系统统计页面 - 仅限cc和Guo用户访问',
},
{
path: '/console/announcement',
allowedUsers: ['cc', 'Guo'],
description: '公告管理页面 - 仅限cc和Guo用户访问',
},
// 可以在这里继续添加其他需要权限控制的页面
// {
// path: '/console/admin',

View File

@@ -0,0 +1,404 @@
<script setup lang="ts">
import type { AnnouncementDto } from '@/api/announcement/types';
import { Delete, Edit, Plus, Refresh } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { onMounted, ref, watch } from 'vue';
import {
create,
deleteById,
getList,
update,
} from '@/api/announcement';
import { AnnouncementTypeEnum } from '@/api/announcement/types';
// ==================== Tab 切换 ====================
const activeTab = ref<'activity' | 'system'>('system');
// Tab 切换时重新加载数据
function handleTabChange() {
currentPage.value = 1;
fetchList();
}
// 获取当前 Tab 对应的类型枚举值
function getCurrentTypeEnum(): AnnouncementTypeEnum {
return activeTab.value === 'activity' ? AnnouncementTypeEnum.Activity : AnnouncementTypeEnum.System;
}
// ==================== 公告列表管理 ====================
const announcementList = ref<AnnouncementDto[]>([]);
const loading = ref(false);
const searchKey = ref('');
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
// 公告对话框
const dialogVisible = ref(false);
const dialogTitle = ref('');
const form = ref<Partial<AnnouncementDto>>({});
// 获取公告列表
async function fetchList() {
loading.value = true;
try {
const res = await getList({
searchKey: searchKey.value,
skipCount: (currentPage.value - 1) * pageSize.value,
maxResultCount: pageSize.value,
type: getCurrentTypeEnum(),
});
announcementList.value = res.data.items;
total.value = res.data.totalCount;
}
catch (error: any) {
ElMessage.error(error.message || '获取公告列表失败');
}
finally {
loading.value = false;
}
}
// 打开对话框
function openDialog(type: 'create' | 'edit', row?: AnnouncementDto) {
dialogTitle.value = type === 'create' ? '创建公告' : '编辑公告';
if (type === 'create') {
form.value = {
title: '',
content: [''],
remark: '',
imageUrl: '',
startTime: new Date().toISOString().slice(0, 19),
endTime: '',
type: getCurrentTypeEnum(),
url: '',
};
}
else {
form.value = {
...row,
startTime: row.startTime ? new Date(row.startTime).toISOString().slice(0, 19) : '',
endTime: row.endTime ? new Date(row.endTime).toISOString().slice(0, 19) : '',
};
}
dialogVisible.value = true;
}
// 添加内容项
function addContentItem() {
if (form.value.content && form.value.content.length < 10) {
form.value.content.push('');
}
else {
ElMessage.warning('最多只能添加10条内容');
}
}
// 删除内容项
function removeContentItem(index: number) {
if (form.value.content && form.value.content.length > 1) {
form.value.content.splice(index, 1);
}
else {
ElMessage.warning('至少需要保留一条内容');
}
}
// 保存
async function save() {
if (!form.value.title || !form.value.content || form.value.content.some(c => !c)) {
ElMessage.warning('请填写标题和所有内容项');
return;
}
try {
const data = {
...form.value,
content: form.value.content?.filter(c => c.trim()) || [],
remark: form.value.remark || null,
imageUrl: form.value.imageUrl || null,
endTime: form.value.endTime || null,
url: form.value.url || null,
};
if (form.value.id) {
await update(data as any);
ElMessage.success('更新成功');
}
else {
await create(data as any);
ElMessage.success('创建成功');
}
dialogVisible.value = false;
fetchList();
}
catch (error: any) {
ElMessage.error(error.message || '保存失败');
}
}
// 删除
async function handleDelete(row: AnnouncementDto) {
try {
await ElMessageBox.confirm('确定要删除该公告吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await deleteById(row.id);
ElMessage.success('删除成功');
fetchList();
}
catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.message || '删除失败');
}
}
}
// 分页改变
function handleCurrentChange(page: number) {
currentPage.value = page;
fetchList();
}
function handleSizeChange(size: number) {
pageSize.value = size;
currentPage.value = 1;
fetchList();
}
// 初始化
onMounted(() => {
fetchList();
});
</script>
<template>
<div class="announcement-management">
<div class="management-container">
<!-- Tab 切换 -->
<el-tabs v-model="activeTab" class="announcement-tabs" @tab-change="handleTabChange">
<el-tab-pane label="系统公告" name="system" />
<el-tab-pane label="活动公告" name="activity" />
</el-tabs>
<!-- 顶部操作栏 -->
<div class="action-bar">
<el-input
v-model="searchKey"
placeholder="搜索标题或备注"
clearable
style="width: 250px; margin-right: 10px"
@keyup.enter="fetchList"
>
<template #append>
<el-button :icon="Refresh" @click="fetchList" />
</template>
</el-input>
<el-button type="primary" :icon="Plus" @click="openDialog('create')">
新建公告
</el-button>
</div>
<!-- 表格 -->
<div class="table-wrapper">
<el-table
v-loading="loading"
:data="announcementList"
border
stripe
style="width: 100%"
height="100%"
>
<el-table-column prop="title" label="标题" min-width="180" show-overflow-tooltip />
<el-table-column prop="content" label="内容预览" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
{{ row.content?.join(' / ') || '-' }}
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ row.remark || '-' }}
</template>
</el-table-column>
<el-table-column prop="startTime" label="开始时间" width="160" />
<el-table-column prop="endTime" label="结束时间" width="160">
<template #default="{ row }">
{{ row.endTime || '-' }}
</template>
</el-table-column>
<el-table-column prop="creationTime" label="创建时间" width="160" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openDialog('edit', row)">
编辑
</el-button>
<el-button link type="danger" size="small" @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
:hide-on-single-page="false"
background
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
<!-- 编辑对话框 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px">
<el-form :model="form" label-width="100px">
<el-form-item label="标题" required>
<el-input v-model="form.title" placeholder="请输入公告标题" maxlength="200" show-word-limit />
</el-form-item>
<el-form-item label="内容" required>
<div style="width: 100%">
<div
v-for="(item, index) in form.content"
:key="index"
style="display: flex; gap: 8px; margin-bottom: 8px"
>
<el-input
v-model="form.content![index]"
placeholder="请输入内容"
maxlength="500"
show-word-limit
/>
<el-button
v-if="form.content && form.content.length > 1"
type="danger"
:icon="Delete"
circle
size="small"
@click="removeContentItem(index)"
/>
</div>
<el-button
v-if="form.content && form.content.length < 10"
type="primary"
:icon="Plus"
size="small"
@click="addContentItem"
>
添加内容项
</el-button>
</div>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="form.remark"
type="textarea"
placeholder="请输入备注(可选)"
maxlength="500"
show-word-limit
:rows="2"
/>
</el-form-item>
<el-form-item label="图片URL">
<el-input v-model="form.imageUrl" placeholder="请输入图片URL可选" maxlength="500" />
</el-form-item>
<el-form-item label="跳转链接">
<el-input v-model="form.url" placeholder="请输入跳转链接(可选)" maxlength="500" />
</el-form-item>
<el-form-item label="开始时间" required>
<el-date-picker
v-model="form.startTime"
type="datetime"
placeholder="选择开始时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DDTHH:mm:ss"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="结束时间">
<el-date-picker
v-model="form.endTime"
type="datetime"
placeholder="选择结束时间(可选)"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DDTHH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">
取消
</el-button>
<el-button type="primary" @click="save">
保存
</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped lang="scss">
.announcement-management {
height: 100vh;
padding: 16px;
box-sizing: border-box;
background: #f5f7fa;
.management-container {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
overflow: hidden;
}
.announcement-tabs {
padding: 0 16px;
flex-shrink: 0;
:deep(.el-tabs__header) {
margin-bottom: 0;
}
}
.action-bar {
padding: 16px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.table-wrapper {
flex: 1;
overflow: hidden;
padding: 16px 16px 0 16px;
min-height: 0;
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
padding: 16px;
flex-shrink: 0;
border-top: 1px solid #eee;
}
}
</style>

View File

@@ -21,6 +21,7 @@ const userName = userStore.userInfo?.user?.userName;
const hasChannelPermission = checkPagePermission('/console/channel', userName);
const hasSystemStatisticsPermission = checkPagePermission('/console/system-statistics', userName);
const hasAnnouncementPermission = checkPagePermission('/console/announcement', userName);
// 菜单项配置
@@ -47,6 +48,10 @@ if (hasSystemStatisticsPermission) {
navItems.push({ name: 'system-statistics', label: '系统统计', icon: 'DataAnalysis', path: '/console/system-statistics' });
}
if (hasAnnouncementPermission) {
navItems.push({ name: 'announcement', label: '公告管理', icon: 'Bell', path: '/console/announcement' });
}
// 当前激活的菜单
const activeNav = computed(() => {
const path = route.path;

View File

@@ -231,6 +231,14 @@ export const layoutRouter: RouteRecordRaw[] = [
title: '意心Ai-系统统计',
},
},
{
path: 'announcement',
name: 'consoleAnnouncement',
component: () => import('@/pages/console/announcement/index.vue'),
meta: {
title: '意心Ai-公告管理',
},
},
],
},
],