feat: 新增系统站内信页面与接口联调

This commit is contained in:
sunlei 2026-06-10 13:21:39 +08:00
parent f52bd3fcec
commit 81af5b5bdf
9 changed files with 662 additions and 0 deletions

View File

@ -69,6 +69,10 @@ const SUPPORTED_ADMIN_MENU_NAMES = new Set([
'SystemMenuCreate',
'SystemMenuDelete',
'SystemMenuEdit',
'SystemNotice',
'SystemNoticeCreate',
'SystemNoticeDelete',
'SystemNoticeEdit',
'SystemRole',
'SystemRoleCreate',
'SystemRoleDelete',

View File

@ -3,4 +3,5 @@ export * from './dict';
export * from './log';
export * from './menu';
export * from './role';
export * from './notice';
export * from './user';

View File

@ -0,0 +1,104 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace SystemNoticeApi {
export interface NoticeItem {
[key: string]: any;
content: string;
createTime?: string;
createdBy?: string;
id: string;
isDeleted: boolean;
isTop: boolean;
level: number;
notifyUsers?: string;
status: 0 | 1;
summary?: string;
title: string;
updateTime?: string;
}
export interface NoticeInput {
content: string;
id?: string;
isTop?: boolean;
level?: number;
notifyUsers?: string;
status?: 0 | 1;
summary?: string;
title: string;
}
export interface NoticeQuery {
[key: string]: any;
isTop?: boolean | number | string;
keyword?: string;
level?: number | string;
notifyUsers?: string;
page?: number;
pageNo?: number;
pageSize?: number;
status?: 0 | 1 | number | string;
}
export interface PageResult<T> {
items: T[];
total: number;
}
}
async function getNoticeList(params: Recordable<any>) {
return requestClient.get<SystemNoticeApi.PageResult<SystemNoticeApi.NoticeItem>>(
'/system/notice/list',
{ params },
);
}
async function getNoticeDetail(id: string) {
return requestClient.get<SystemNoticeApi.NoticeItem>(`/system/notice/detail/${id}`);
}
async function createNotice(data: SystemNoticeApi.NoticeInput) {
return requestClient.post<string>('/system/notice/save', data);
}
async function updateNotice(id: string, data: SystemNoticeApi.NoticeInput) {
return requestClient.post('/system/notice/update', {
...data,
id,
});
}
async function deleteNotice(id: string) {
return requestClient.delete(`/system/notice/${id}`);
}
async function toggleNoticeStatus(id: string, status: SystemNoticeApi.NoticeItem['status']) {
return requestClient.post('/system/notice/toggle', undefined, {
params: {
id,
status,
},
});
}
async function toggleNoticeTop(id: string, isTop: boolean) {
return requestClient.post('/system/notice/top', undefined, {
params: {
id,
isTop: isTop ? 1 : 0,
},
});
}
export {
createNotice,
deleteNotice,
getNoticeDetail,
getNoticeList,
toggleNoticeStatus,
toggleNoticeTop,
updateNotice,
};

View File

@ -111,6 +111,31 @@
"total": "Total",
"unconfigured": "Loki Not Configured"
},
"notice": {
"content": "Content",
"createTime": "Create Time",
"deleteConfirm": "Confirm deleting \"{0}\"?",
"disableSuccess": "Notice disabled",
"enableSuccess": "Notice enabled",
"isTop": "Top",
"keyword": "Keyword",
"level": "Level",
"levelText": "Level {level}",
"name": "Notice",
"notifyUsers": "Notify Users",
"status": "Status",
"summary": "Summary",
"title": "Notices",
"top": "Pinned",
"topNo": "No",
"topYes": "Yes",
"topSuccess": "Pinned",
"cancelTopSuccess": "Unpinned",
"toggle": "Toggle",
"toggleStatusConfirm": "Confirm toggling \"{0}\" status?",
"topToggle": "Pin Toggle",
"toggleTopConfirm": "Confirm toggling \"{0}\" pin state?"
},
"role": {
"title": "Role Management",
"list": "Role List",

View File

@ -113,6 +113,31 @@
"total": "总量",
"unconfigured": "Loki 未配置"
},
"notice": {
"content": "内容",
"createTime": "创建时间",
"deleteConfirm": "确认删除「{0}」吗?",
"disableSuccess": "已停用站内信",
"enableSuccess": "已启用站内信",
"isTop": "置顶",
"keyword": "关键字",
"level": "等级",
"levelText": "等级 {level}",
"name": "站内信",
"notifyUsers": "通知用户",
"status": "状态",
"summary": "摘要",
"title": "站内信管理",
"top": "置顶",
"topNo": "否",
"topYes": "是",
"topSuccess": "置顶成功",
"cancelTopSuccess": "取消置顶成功",
"toggle": "启停",
"toggleStatusConfirm": "确认切换「{0}」启停状态吗?",
"topToggle": "置顶切换",
"toggleTopConfirm": "确认切换「{0}」置顶状态吗?"
},
"role": {
"title": "角色管理",
"list": "角色列表",

View File

@ -66,6 +66,15 @@ const routes: RouteRecordRaw[] = [
},
component: () => import('#/views/system/log/list.vue'),
},
{
path: '/system/notice',
name: 'SystemNotice',
meta: {
icon: 'mdi:bell-outline',
title: $t('system.notice.title'),
},
component: () => import('#/views/system/notice/list.vue'),
},
{
path: '/system/ktTableDemo',
name: 'SystemKtTableDemo',

View File

@ -0,0 +1,156 @@
import type { VbenFormSchema } from '#/adapter/form';
import { z } from '#/adapter/form';
import { $t } from '#/locales';
export const NOTICE_LEVEL_OPTIONS = [
{ label: '普通', value: 1 },
{ label: '重要', value: 2 },
{ label: '紧急', value: 3 },
];
export const NOTICE_STATUS_OPTIONS = [
{ color: 'success', label: $t('common.enabled'), value: 1 },
{ color: 'default', label: $t('common.disabled'), value: 0 },
];
export function getNoticeStatusOptions() {
return NOTICE_STATUS_OPTIONS;
}
export function useSearchSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
componentProps: {
allowClear: true,
},
fieldName: 'keyword',
label: $t('system.notice.keyword'),
},
{
component: 'Select',
componentProps: {
allowClear: true,
options: NOTICE_LEVEL_OPTIONS,
},
fieldName: 'level',
label: $t('system.notice.level'),
},
{
component: 'Select',
componentProps: {
allowClear: true,
options: NOTICE_STATUS_OPTIONS,
},
fieldName: 'status',
label: $t('system.notice.status'),
},
{
component: 'Select',
componentProps: {
allowClear: true,
options: [
{ label: $t('common.no'), value: 0 },
{ label: $t('common.yes'), value: 1 },
],
},
fieldName: 'isTop',
label: $t('system.notice.top'),
},
];
}
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '如 系统公告',
},
fieldName: 'title',
label: $t('system.notice.title'),
rules: z
.string()
.min(1, $t('ui.formRules.required', [$t('system.notice.title')]))
.max(120, $t('ui.formRules.maxLength', [$t('system.notice.title'), 120])),
},
{
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '如 站内信内容概要',
},
fieldName: 'summary',
label: $t('system.notice.summary'),
rules: z
.string()
.max(200, $t('ui.formRules.maxLength', [$t('system.notice.summary'), 200])),
},
{
component: 'TextArea',
componentProps: {
autoSize: {
maxRows: 10,
minRows: 4,
},
allowClear: true,
placeholder: '请输入完整内容',
},
fieldName: 'content',
label: $t('system.notice.content'),
rules: z
.string()
.min(1, $t('ui.formRules.required', [$t('system.notice.content')])),
},
{
component: 'Select',
componentProps: {
allowClear: true,
options: NOTICE_LEVEL_OPTIONS,
},
fieldName: 'level',
label: $t('system.notice.level'),
defaultValue: 1,
},
{
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '逗号分隔10001,10002留空表示全部用户可见',
},
fieldName: 'notifyUsers',
label: $t('system.notice.notifyUsers'),
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: [
{
label: $t('system.notice.topNo'),
value: false,
},
{
label: $t('system.notice.topYes'),
value: true,
},
],
},
defaultValue: false,
fieldName: 'isTop',
label: $t('system.notice.top'),
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: NOTICE_STATUS_OPTIONS,
},
defaultValue: 1,
fieldName: 'status',
label: $t('system.notice.status'),
},
];
}

View File

@ -0,0 +1,243 @@
<script lang="ts" setup>
import type { TableColumnType } from 'antdv-next';
import type { SystemNoticeApi } from '#/api/system/notice';
import type {
KtTableApi,
KtTableButton,
KtTableContext,
KtTableRowAction,
} from '#/components/ktTable';
import { h } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { message, Tag } from 'antdv-next';
import {
deleteNotice,
getNoticeList,
toggleNoticeStatus,
toggleNoticeTop,
} from '#/api/system/notice';
import { KtTable, useKtTable } from '#/components/ktTable';
import { $t } from '#/locales';
import { getNoticeStatusOptions, useSearchSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
const noticeStatusOptions = getNoticeStatusOptions();
const columns: Array<TableColumnType<SystemNoticeApi.NoticeItem>> = [
{
dataIndex: 'isTop',
fixed: 'left',
key: 'isTop',
title: $t('system.notice.top'),
width: 90,
},
{
dataIndex: 'title',
fixed: 'left',
key: 'title',
title: $t('system.notice.title'),
width: 260,
},
{
dataIndex: 'level',
key: 'level',
title: $t('system.notice.level'),
width: 100,
},
{
dataIndex: 'status',
key: 'status',
title: $t('system.notice.status'),
width: 100,
},
{
dataIndex: 'notifyUsers',
key: 'notifyUsers',
title: $t('system.notice.notifyUsers'),
width: 180,
},
{
dataIndex: 'createTime',
key: 'createTime',
title: $t('system.notice.createTime'),
width: 190,
},
{
align: 'left',
dataIndex: 'summary',
key: 'summary',
title: $t('system.notice.summary'),
width: 280,
},
];
const api: KtTableApi<SystemNoticeApi.NoticeItem> = {
list: async (params) => await getNoticeList(params),
};
const buttons: Array<KtTableButton<SystemNoticeApi.NoticeItem>> = [
{
icon: () => h(Plus, { class: 'kt-table__button-icon' }),
key: 'create',
label: $t('ui.actionTitle.create', [$t('system.notice.name')]),
onClick: onCreate,
permissionCodes: ['System:Notice:Create'],
type: 'primary',
},
];
const rowActions: Array<KtTableRowAction<SystemNoticeApi.NoticeItem>> = [
{
key: 'edit',
label: $t('common.edit'),
onClick: onEdit,
permissionCodes: ['System:Notice:Edit'],
},
{
confirm: (row) =>
$t('system.notice.toggleStatusConfirm', [row.title]),
key: 'toggle',
label: $t('system.notice.toggle'),
onClick: onToggleStatus,
permissionCodes: ['System:Notice:Edit'],
},
{
confirm: (row) =>
$t('system.notice.toggleTopConfirm', [row.title]),
key: 'top',
label: $t('system.notice.topToggle'),
onClick: onToggleTop,
permissionCodes: ['System:Notice:Edit'],
},
{
confirm: (row) =>
$t('system.notice.deleteConfirm', [row.title]),
danger: true,
key: 'delete',
label: $t('common.delete'),
onClick: onDelete,
permissionCodes: ['System:Notice:Delete'],
},
];
function getNoticeStatusOption(status: SystemNoticeApi.NoticeItem['status']) {
return noticeStatusOptions.find((item) => item.value === status);
}
function getNoticeLevelColor(level: SystemNoticeApi.NoticeItem['level']) {
if (level === 3) return 'red';
if (level === 2) return 'orange';
return 'blue';
}
const [registerTable, tableApi] = useKtTable<SystemNoticeApi.NoticeItem>({
api,
buttons,
columns,
formOptions: {
schema: useSearchSchema(),
},
rowActions,
rowKey: 'id',
showSelection: false,
tableTitle: $t('system.notice.title'),
});
function onCreate() {
formModalApi.setData(undefined).open();
}
function onEdit(row: SystemNoticeApi.NoticeItem) {
formModalApi.setData(row).open();
}
async function onToggleStatus(
row: SystemNoticeApi.NoticeItem,
context: KtTableContext<SystemNoticeApi.NoticeItem>,
) {
const nextStatus = row.status === 1 ? 0 : 1;
await toggleNoticeStatus(row.id, nextStatus);
message.success(
nextStatus === 1
? $t('system.notice.enableSuccess')
: $t('system.notice.disableSuccess'),
);
await context.reload();
}
async function onToggleTop(
row: SystemNoticeApi.NoticeItem,
context: KtTableContext<SystemNoticeApi.NoticeItem>,
) {
await toggleNoticeTop(row.id, !row.isTop);
message.success(
row.isTop
? $t('system.notice.cancelTopSuccess')
: $t('system.notice.topSuccess'),
);
await context.reload();
}
async function onDelete(
row: SystemNoticeApi.NoticeItem,
context?: KtTableContext<SystemNoticeApi.NoticeItem>,
) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.title]),
duration: 0,
key: 'action_process_msg',
});
try {
await deleteNotice(row.id);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.title]),
key: 'action_process_msg',
});
await (context || tableApi).reload();
} catch {
hideLoading();
}
}
function onRefresh() {
tableApi.reload();
}
</script>
<template>
<Page auto-content-height>
<FormModal @success="onRefresh" />
<KtTable @register="registerTable">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'isTop'">
<Tag :color="record.isTop ? 'warning' : 'default'">
{{ record.isTop ? $t('system.notice.topYes') : $t('system.notice.topNo') }}
</Tag>
</template>
<template v-else-if="column.key === 'level'">
<Tag :color="getNoticeLevelColor(record.level)">
{{ $t('system.notice.levelText', { level: record.level }) }}
</Tag>
</template>
<template v-else-if="column.key === 'status'">
<Tag :color="getNoticeStatusOption(record.status)?.color || 'default'">
{{ getNoticeStatusOption(record.status)?.label || record.status }}
</Tag>
</template>
</template>
</KtTable>
</Page>
</template>

View File

@ -0,0 +1,95 @@
<script lang="ts" setup>
import type { SystemNoticeApi } from '#/api/system/notice';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button } from 'antdv-next';
import { useVbenForm } from '#/adapter/form';
import {
createNotice,
updateNotice,
} from '#/api/system/notice';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits<{
success: [];
}>();
const formData = ref<Partial<SystemNoticeApi.NoticeItem>>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', [$t('system.notice.name')])
: $t('ui.actionTitle.create', [$t('system.notice.name')]);
});
const [Form, formApi] = useVbenForm({
layout: 'vertical',
schema: useFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) return;
modalApi.lock();
const data = (await formApi.getValues()) as SystemNoticeApi.NoticeInput;
try {
const currentId = formData.value?.id;
if (currentId) {
await updateNotice(currentId, data);
} else {
await createNotice(data);
}
modalApi.close();
emit('success');
} finally {
modalApi.lock(false);
}
},
onOpenChange(isOpen) {
if (!isOpen) return;
const data = modalApi.getData<Partial<SystemNoticeApi.NoticeItem>>();
formData.value = data || undefined;
resetForm();
},
});
function getFormValues() {
return {
content: formData.value?.content || '',
isTop: formData.value?.isTop ?? false,
level: formData.value?.level ?? 1,
notifyUsers: formData.value?.notifyUsers || '',
status: formData.value?.status ?? 1,
summary: formData.value?.summary || '',
title: formData.value?.title || '',
};
}
function resetForm() {
formApi.resetForm();
formApi.setValues(getFormValues());
}
</script>
<template>
<Modal :title="getTitle">
<Form class="mx-4" />
<template #prepend-footer>
<div class="flex-auto">
<Button type="primary" danger @click="resetForm">
{{ $t('common.reset') }}
</Button>
</div>
</template>
</Modal>
</template>