feat: 新增公告管理
This commit is contained in:
404
Yi.Ai.Vue3/src/pages/console/announcement/index.vue
Normal file
404
Yi.Ai.Vue3/src/pages/console/announcement/index.vue
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user