feat(notice+job): 用户管理加通知渠道字段 + 通知模块(规则/渠道/日志) + 自动任务管理页面
This commit is contained in:
parent
7dc9bc6c2a
commit
8e16bdb2dc
@ -1,6 +1,8 @@
|
||||
# admin.xpcool.com 变更记录
|
||||
> 倒序:最新在上。格式:YYYY-MM-DD | 类型 | 摘要
|
||||
|
||||
2026-08-27 | FIX | 前后端联查修复「界面/菜单/工作台异常」:①本地库缺 010_server_log_enhance 增量列(admin_operation_log 无 ip_location/admin_username 等)致操作日志接口 500,已导;②本地缺工作台/招聘菜单+admin_role_menu 关联,已导 013_workbench_menu+recruitment/003_menu;③工作台快捷入口 /system/log/* 是死链(真实路由 /log/*)已改;④打通 Drawer 重构遗留编译错:6 页 @vben-core/popup-ui 误引改 @vben/common-ui、preferences 补 languageToggleButtonPosition、清未用变量、menu/log 页 lint 债(7dc9bc6,钩子全绿);⑤发现审计 admin_username/user_agent 长期为空=线上跑旧二进制,新构建验证 248 号记录字段完整,历史空账号已按 user_id 回填(121 条);⑥本地 xxcool/opuser 密码已重置为 opuser123 便于联调
|
||||
|
||||
2026-08-27 | CHG | 三大改造:①移除国际化多语言仅留中文——preferences.ts 固定 locale=zh-CN;删除 header/toolbar/user-dropdown 三处语言切换入口、偏好面板语言选择项(general.vue/appLocale/widget.vue languageToggle 位置配置);config.ts 禁 languageToggle 且 order 移除;删除主应用与 packages/locales 的 en-US 语言包;locales/index.ts loadDayjsLocale 仅留 zh-CN。②新增主题色管理——theme.css 品牌色改为引用 --primary 变量(不再写死 #4f5bff),背景/主按钮渐变用 color-mix 跟随主题色,配合 useTDesignDesignTokens 使偏好面板 BuiltinTheme 切换主题色真正生效;constants.ts default 主题色与 config.ts colorPrimary 统一为靛蓝 hsl(235 100% 65%)。③业务弹窗统一为 vben 抽屉——6 页(system/{role,menu,admin}、recruitment/push、house/{listing,community})的 t-dialog 全部替换为 useVbenDrawer(<Drawer>),drawerApi.open/close/setState(标题)/lock(提交中);community/listing 抽屉加宽(class w-160/w-150)
|
||||
2026-08-27 | CHG | 服务器安全日志页(挂在系统监控菜单下,id=96):views/monitor/security/index.vue——顶部 4 统计卡片(SSH爆破/fail2ban封禁/成功登录/总数)+ TOP 攻击源标签区 + 筛选(来源IP/事件类型/时间范围 t-date-range-picker)+ Vben VxeTable 列表(时间/来源IP/来源端口/目标端口/事件类型标签/详情 tooltip,事件标签按类型配色);api/serverSecurity.ts(list/stats 全 POST,无 URL 参数);typecheck 通过
|
||||
2026-08-27 | CHG | 全量表格重构:views 下全部 11 个 t-table 页面统一改为 Vben Vxe Table(useVbenVxeGrid,#/adapter/vxe-table)+ Vben TableAction 行操作——system/{log(范式),login-log,admin,role}、recruitment/{info,crawler,push}、house/{data,listing,community,presale}、dashboard/workbench(两处本地小表 proxyConfig.enabled=false + gridApi.setGridOptions 写数据)。要点:gridOptions 需注解 VxeTableGridOptions(否则 type:'seq' 推断 string 报错);单元格自定义用 columns[].slots.default + h(TDesign组件);远程分页走 proxyConfig.ajax.query 返回 {items,total};筛选区保留手写 TDesign 组件不动;admin/role/listing/community 行操作 ActionItem 带 auth 权限码、删除 danger 标红;listing 多选批量标记改 vxe checkboxConfig + gridApi.grid.getCheckboxRecords();confirm 弹窗交互(删除/重置密码)保留 DialogPlugin 不变;typecheck 通过
|
||||
|
||||
67
apps/web-tdesign/src/api/autoJob.ts
Normal file
67
apps/web-tdesign/src/api/autoJob.ts
Normal file
@ -0,0 +1,67 @@
|
||||
// 自动任务模块 API 客户端(任务管理/运行日志)。
|
||||
// 接口规范:全部 POST;URL 不含参数;入参一律 body。
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
// ---------- 任务 ----------
|
||||
export interface AutoJobItem {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
jobType: string;
|
||||
cronExpr: string;
|
||||
enabled: number;
|
||||
remark: string;
|
||||
lastRunAt: string;
|
||||
lastResult: number;
|
||||
lastError: string;
|
||||
}
|
||||
|
||||
export function getAutoJobList() {
|
||||
return requestClient.post<{ list: AutoJobItem[] }>(
|
||||
'/api/service/admin/auto-job/list',
|
||||
);
|
||||
}
|
||||
|
||||
export function saveAutoJob(data: {
|
||||
cronExpr?: string;
|
||||
enabled?: number;
|
||||
id: number;
|
||||
remark?: string;
|
||||
}) {
|
||||
return requestClient.post('/api/service/admin/auto-job/save', data);
|
||||
}
|
||||
|
||||
export function triggerAutoJob(data: { id: number }) {
|
||||
return requestClient.post<{ ok: boolean; summary: string }>(
|
||||
'/api/service/admin/auto-job/trigger',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- 运行日志 ----------
|
||||
export interface AutoJobLogItem {
|
||||
id: number;
|
||||
jobId: number;
|
||||
jobName: string;
|
||||
runAt: string;
|
||||
result: number;
|
||||
error: string;
|
||||
summary: string;
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
export interface AutoJobLogListResp {
|
||||
list: AutoJobLogItem[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export function getAutoJobLogList(data: {
|
||||
jobId?: number;
|
||||
page: number;
|
||||
size: number;
|
||||
}) {
|
||||
return requestClient.post<AutoJobLogListResp>(
|
||||
'/api/service/admin/auto-job/log/list',
|
||||
data,
|
||||
);
|
||||
}
|
||||
130
apps/web-tdesign/src/api/notice.ts
Normal file
130
apps/web-tdesign/src/api/notice.ts
Normal file
@ -0,0 +1,130 @@
|
||||
// 通知模块 API 客户端(渠道/规则/日志/测试)。
|
||||
// 接口规范:全部 POST;URL 不含参数;入参一律 body。
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
// ---------- 渠道 ----------
|
||||
export interface NoticeChannelItem {
|
||||
id: number;
|
||||
code: string;
|
||||
name: string;
|
||||
enabled: number;
|
||||
config: string;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export function getNoticeChannelList() {
|
||||
return requestClient.post<{ list: NoticeChannelItem[] }>(
|
||||
'/api/service/admin/notice/channel/list',
|
||||
);
|
||||
}
|
||||
|
||||
export function saveNoticeChannel(data: {
|
||||
code: string;
|
||||
config?: string;
|
||||
enabled?: number;
|
||||
id?: number;
|
||||
name: string;
|
||||
remark?: string;
|
||||
}) {
|
||||
return requestClient.post<{ id: number }>(
|
||||
'/api/service/admin/notice/channel/save',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- 规则 ----------
|
||||
export interface NoticeRuleItem {
|
||||
id: number;
|
||||
name: string;
|
||||
eventType: string;
|
||||
channelCodes: string[];
|
||||
userIds: number[];
|
||||
titleTemplate: string;
|
||||
bodyTemplate: string;
|
||||
enabled: number;
|
||||
}
|
||||
|
||||
export function getNoticeRuleList(data: { eventType?: string }) {
|
||||
return requestClient.post<{ list: NoticeRuleItem[] }>(
|
||||
'/api/service/admin/notice/rule/list',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function saveNoticeRule(data: {
|
||||
bodyTemplate?: string;
|
||||
channelCodes: string[];
|
||||
enabled?: number;
|
||||
eventType: string;
|
||||
id?: number;
|
||||
name: string;
|
||||
titleTemplate?: string;
|
||||
userIds: number[];
|
||||
}) {
|
||||
return requestClient.post<{ id: number }>(
|
||||
'/api/service/admin/notice/rule/save',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteNoticeRule(data: { id: number }) {
|
||||
return requestClient.post('/api/service/admin/notice/rule/delete', data);
|
||||
}
|
||||
|
||||
// ---------- 日志 ----------
|
||||
export interface NoticeLogItem {
|
||||
id: number;
|
||||
ruleId: number;
|
||||
eventType: string;
|
||||
channelCode: string;
|
||||
userId: number;
|
||||
target: string;
|
||||
title: string;
|
||||
body: string;
|
||||
result: number;
|
||||
error: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface NoticeLogListResp {
|
||||
list: NoticeLogItem[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export function getNoticeLogList(data: {
|
||||
channelCode?: string;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
eventType?: string;
|
||||
page: number;
|
||||
result?: number;
|
||||
size: number;
|
||||
}) {
|
||||
return requestClient.post<NoticeLogListResp>(
|
||||
'/api/service/admin/notice/log/list',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- 测试 ----------
|
||||
export function testNotice(data: {
|
||||
body?: string;
|
||||
channelCode?: string;
|
||||
ruleId?: number;
|
||||
target?: string;
|
||||
title?: string;
|
||||
userIds?: number[];
|
||||
}) {
|
||||
return requestClient.post<{ message: string; result: boolean }>(
|
||||
'/api/service/admin/notice/test',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
// 事件类型选项
|
||||
export const NOTICE_EVENT_OPTIONS = [
|
||||
{ label: '任务完成', value: 'job_done' },
|
||||
{ label: '任务失败', value: 'job_fail' },
|
||||
{ label: '安全告警', value: 'security_alert' },
|
||||
{ label: '招聘每日推送', value: 'recruit_daily' },
|
||||
];
|
||||
@ -1,11 +1,13 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
// ---------- 管理员(/api/service/admin/admin) ----------
|
||||
// ---------- 用户管理(/api/service/admin/admin) ----------
|
||||
export interface AdminItem {
|
||||
id: number;
|
||||
username: string;
|
||||
nickname: string;
|
||||
status: number;
|
||||
barkDeviceId: string;
|
||||
pushplusToken: string;
|
||||
roleIds: number[];
|
||||
roleNames: string[];
|
||||
createdAt: string;
|
||||
@ -28,8 +30,10 @@ export function getAdminList(data: {
|
||||
}
|
||||
|
||||
export function createAdmin(data: {
|
||||
barkDeviceId?: string;
|
||||
nickname: string;
|
||||
password: string;
|
||||
pushplusToken?: string;
|
||||
roleIds: number[];
|
||||
username: string;
|
||||
}) {
|
||||
@ -41,7 +45,13 @@ export function createAdmin(data: {
|
||||
|
||||
export function updateAdmin(
|
||||
id: number,
|
||||
data: { nickname: string; roleIds: number[]; status: number; },
|
||||
data: {
|
||||
barkDeviceId?: string;
|
||||
nickname: string;
|
||||
pushplusToken?: string;
|
||||
roleIds: number[];
|
||||
status: number;
|
||||
},
|
||||
) {
|
||||
return requestClient.post(`/api/service/admin/admin/update/${id}`, data);
|
||||
}
|
||||
|
||||
@ -19,15 +19,15 @@ import {
|
||||
updateAdmin,
|
||||
} from '#/api/system';
|
||||
|
||||
// 人员管理页面:Vben Vxe Table + Vben TableAction 行操作(权限码控制显隐)
|
||||
// 用户管理页面:Vben Vxe Table + Vben TableAction 行操作(权限码控制显隐)
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
const roleOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const editing = ref<AdminItem | null>(null);
|
||||
|
||||
// 新增/编辑人员抽屉(vben 封装抽屉组件)
|
||||
// 新增/编辑用户抽屉(vben 封装抽屉组件)
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
title: '新增人员',
|
||||
title: '新增用户',
|
||||
onConfirm: submit,
|
||||
});
|
||||
|
||||
@ -38,6 +38,8 @@ const form = reactive({
|
||||
password: '',
|
||||
nickname: '',
|
||||
status: 1,
|
||||
barkDeviceId: '',
|
||||
pushplusToken: '',
|
||||
roleIds: [] as number[],
|
||||
});
|
||||
|
||||
@ -56,9 +58,11 @@ function openCreate() {
|
||||
password: '',
|
||||
nickname: '',
|
||||
status: 1,
|
||||
barkDeviceId: '',
|
||||
pushplusToken: '',
|
||||
roleIds: [],
|
||||
});
|
||||
drawerApi.setState({ title: '新增人员' });
|
||||
drawerApi.setState({ title: '新增用户' });
|
||||
drawerApi.open();
|
||||
}
|
||||
|
||||
@ -69,9 +73,11 @@ function openEdit(row: AdminItem) {
|
||||
password: '',
|
||||
nickname: row.nickname,
|
||||
status: row.status,
|
||||
barkDeviceId: row.barkDeviceId ?? '',
|
||||
pushplusToken: row.pushplusToken ?? '',
|
||||
roleIds: row.roleIds ?? [],
|
||||
});
|
||||
drawerApi.setState({ title: '编辑人员' });
|
||||
drawerApi.setState({ title: '编辑用户' });
|
||||
drawerApi.open();
|
||||
}
|
||||
|
||||
@ -81,12 +87,16 @@ async function submit() {
|
||||
? await updateAdmin(editing.value.id, {
|
||||
nickname: form.nickname,
|
||||
status: form.status,
|
||||
barkDeviceId: form.barkDeviceId,
|
||||
pushplusToken: form.pushplusToken,
|
||||
roleIds: form.roleIds,
|
||||
})
|
||||
: await createAdmin({
|
||||
username: form.username,
|
||||
password: form.password,
|
||||
nickname: form.nickname,
|
||||
barkDeviceId: form.barkDeviceId,
|
||||
pushplusToken: form.pushplusToken,
|
||||
roleIds: form.roleIds,
|
||||
});
|
||||
MessagePlugin.success('保存成功');
|
||||
@ -94,10 +104,10 @@ async function submit() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 删除人员(保留原确认弹窗行为) */
|
||||
/** 删除用户(保留原确认弹窗行为) */
|
||||
function onDelete(row: AdminItem) {
|
||||
const d = DialogPlugin.confirm({
|
||||
header: '确认删除该人员?',
|
||||
header: '确认删除该用户?',
|
||||
onConfirm: async () => {
|
||||
await deleteAdmin(row.id);
|
||||
MessagePlugin.success('删除成功');
|
||||
@ -131,6 +141,18 @@ const gridOptions: VxeTableGridOptions = {
|
||||
{ field: 'id', title: 'ID', width: 70 },
|
||||
{ field: 'username', title: '用户名' },
|
||||
{ field: 'nickname', title: '昵称' },
|
||||
{
|
||||
field: 'barkDeviceId',
|
||||
title: 'Bark设备',
|
||||
width: 120,
|
||||
showOverflow: 'tooltip',
|
||||
},
|
||||
{
|
||||
field: 'pushplusToken',
|
||||
title: 'pushplus',
|
||||
width: 120,
|
||||
showOverflow: 'tooltip',
|
||||
},
|
||||
{
|
||||
field: 'statusText',
|
||||
title: '状态',
|
||||
@ -222,8 +244,8 @@ onMounted(loadRoles);
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="wb-page-head">
|
||||
<h2 class="wb-page-title">人员管理</h2>
|
||||
<p class="wb-page-sub">管理系统账号、昵称、状态与角色归属</p>
|
||||
<h2 class="wb-page-title">用户管理</h2>
|
||||
<p class="wb-page-sub">管理系统账号、昵称、状态、角色与通知渠道(Bark / pushplus)</p>
|
||||
</div>
|
||||
|
||||
<t-card :bordered="false" class="wb-filter-card">
|
||||
@ -243,17 +265,17 @@ onMounted(loadRoles);
|
||||
theme="primary"
|
||||
@click="openCreate"
|
||||
>
|
||||
新增人员
|
||||
新增用户
|
||||
</t-button>
|
||||
</div>
|
||||
</t-card>
|
||||
|
||||
<!-- 人员列表(Vben Vxe Table + TableAction) -->
|
||||
<!-- 用户列表(Vben Vxe Table + TableAction) -->
|
||||
<t-card :bordered="false" class="wb-table-card">
|
||||
<Grid />
|
||||
</t-card>
|
||||
|
||||
<!-- 新增/编辑人员抽屉(vben 封装抽屉组件) -->
|
||||
<!-- 新增/编辑用户抽屉(vben 封装抽屉组件) -->
|
||||
<Drawer>
|
||||
<t-form label-align="right" label-width="80px">
|
||||
<t-form-item label="用户名" required>
|
||||
@ -273,6 +295,18 @@ onMounted(loadRoles);
|
||||
<t-form-item label="昵称">
|
||||
<t-input v-model="form.nickname" placeholder="显示名称" />
|
||||
</t-form-item>
|
||||
<t-form-item label="Bark设备">
|
||||
<t-input
|
||||
v-model="form.barkDeviceId"
|
||||
placeholder="Bark 设备ID(形如 xxx/… 路径 key)"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="pushplus">
|
||||
<t-input
|
||||
v-model="form.pushplusToken"
|
||||
placeholder="pushplus token"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="状态">
|
||||
<t-switch v-model="form.status" :custom-value="[1, 0]" />
|
||||
</t-form-item>
|
||||
|
||||
182
apps/web-tdesign/src/views/system/auto-job/list/index.vue
Normal file
182
apps/web-tdesign/src/views/system/auto-job/list/index.vue
Normal file
@ -0,0 +1,182 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { h, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { Page, useVbenDrawer, VbenTableAction } from '@vben/common-ui';
|
||||
|
||||
import { DialogPlugin, MessagePlugin, Tag } from 'tdesign-vue-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
getAutoJobList,
|
||||
saveAutoJob,
|
||||
triggerAutoJob,
|
||||
type AutoJobItem,
|
||||
} from '#/api/autoJob';
|
||||
|
||||
// 自动任务管理:DB 驱动的定时任务(cron 表达式/启停/手动触发),执行结果自动写日志并通知
|
||||
const editing = ref<AutoJobItem | null>(null);
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
title: '编辑任务',
|
||||
onConfirm: submit,
|
||||
});
|
||||
|
||||
const form = reactive({
|
||||
cronExpr: '',
|
||||
remark: '',
|
||||
});
|
||||
|
||||
function openEdit(row: AutoJobItem) {
|
||||
editing.value = row;
|
||||
Object.assign(form, { cronExpr: row.cronExpr, remark: row.remark });
|
||||
drawerApi.setState({ title: `编辑「${row.name}」` });
|
||||
drawerApi.open();
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!editing.value) return;
|
||||
await saveAutoJob({
|
||||
id: editing.value.id,
|
||||
cronExpr: form.cronExpr,
|
||||
remark: form.remark,
|
||||
});
|
||||
MessagePlugin.success('保存成功,调度已更新');
|
||||
drawerApi.close();
|
||||
loadList();
|
||||
}
|
||||
|
||||
/** 启停切换(直接生效,动态更新调度) */
|
||||
async function onToggle(row: AutoJobItem, enabled: number) {
|
||||
await saveAutoJob({ id: row.id, enabled });
|
||||
MessagePlugin.success(enabled === 1 ? '任务已启用' : '任务已停用');
|
||||
loadList();
|
||||
}
|
||||
|
||||
/** 手动触发一次 */
|
||||
function onTrigger(row: AutoJobItem) {
|
||||
const d = DialogPlugin.confirm({
|
||||
header: `立即执行「${row.name}」?`,
|
||||
body: '将立即运行一次并记录日志',
|
||||
onConfirm: async () => {
|
||||
const r = await triggerAutoJob({ id: row.id });
|
||||
MessagePlugin[r.ok ? 'success' : 'error'](
|
||||
r.ok ? `执行完成:${r.summary || '成功'}` : `执行失败:${r.summary}`,
|
||||
);
|
||||
d.destroy();
|
||||
loadList();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '#' },
|
||||
{ field: 'name', title: '任务名称', width: 160 },
|
||||
{ field: 'code', title: '编码', width: 180, showOverflow: 'tooltip' },
|
||||
{ field: 'cronExpr', title: 'cron 表达式', width: 140 },
|
||||
{
|
||||
field: 'enabled',
|
||||
title: '启用',
|
||||
width: 90,
|
||||
slots: {
|
||||
default: ({ row }: { row: AutoJobItem }) =>
|
||||
h(
|
||||
Tag,
|
||||
{
|
||||
theme: row.enabled === 1 ? 'success' : 'default',
|
||||
variant: 'light',
|
||||
size: 'small',
|
||||
},
|
||||
row.enabled === 1 ? '启用' : '停用',
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'lastResultText',
|
||||
title: '上次结果',
|
||||
width: 100,
|
||||
slots: {
|
||||
default: ({ row }: { row: AutoJobItem }) => {
|
||||
if (row.lastResult === 0) return h('span', '-');
|
||||
return h(
|
||||
Tag,
|
||||
{
|
||||
theme: row.lastResult === 1 ? 'success' : 'danger',
|
||||
variant: 'light',
|
||||
size: 'small',
|
||||
},
|
||||
row.lastResult === 1 ? '成功' : '失败',
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{ field: 'lastRunAt', title: '上次运行', width: 165 },
|
||||
{ field: 'lastError', title: '上次错误', minWidth: 160, showOverflow: 'tooltip' },
|
||||
{
|
||||
field: 'op',
|
||||
title: '操作',
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
slots: {
|
||||
default: ({ row }: { row: AutoJobItem }) =>
|
||||
h(VbenTableAction, {
|
||||
actions: [
|
||||
{
|
||||
text: row.enabled === 1 ? '停用' : '启用',
|
||||
onClick: () => onToggle(row, row.enabled === 1 ? 0 : 1),
|
||||
},
|
||||
{ text: '执行', onClick: () => onTrigger(row) },
|
||||
{ text: '编辑', onClick: () => openEdit(row) },
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
proxyConfig: { enabled: false },
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
async function loadList() {
|
||||
const data = await getAutoJobList();
|
||||
gridApi.setGridOptions({ data: data.list ?? [] });
|
||||
}
|
||||
|
||||
onMounted(loadList);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="wb-page-head">
|
||||
<h2 class="wb-page-title">自动任务</h2>
|
||||
<p class="wb-page-sub">
|
||||
管理定时任务(招聘抓取/推送等):修改 cron、启停、手动执行;执行结果自动记录并可关联通知
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<t-card :bordered="false" class="wb-filter-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm text-gray-500">
|
||||
cron 表达式为 6 段式(秒 分 时 日 月 周),如 0 0 3 * * *
|
||||
</div>
|
||||
<t-button theme="primary" @click="loadList">刷新</t-button>
|
||||
</div>
|
||||
</t-card>
|
||||
|
||||
<t-card :bordered="false" class="wb-table-card">
|
||||
<Grid />
|
||||
</t-card>
|
||||
|
||||
<Drawer>
|
||||
<t-form label-align="right" label-width="90px">
|
||||
<t-form-item label="cron 表达式" required>
|
||||
<t-input v-model="form.cronExpr" placeholder="0 0 3 * * *" />
|
||||
</t-form-item>
|
||||
<t-form-item label="备注">
|
||||
<t-input v-model="form.remark" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</Drawer>
|
||||
</Page>
|
||||
</template>
|
||||
114
apps/web-tdesign/src/views/system/auto-job/log/index.vue
Normal file
114
apps/web-tdesign/src/views/system/auto-job/log/index.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { h, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Tag } from 'tdesign-vue-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
getAutoJobList,
|
||||
getAutoJobLogList,
|
||||
type AutoJobItem,
|
||||
type AutoJobLogItem,
|
||||
} from '#/api/autoJob';
|
||||
|
||||
// 任务运行日志:每次定时/手动执行的摘要、结果、耗时
|
||||
const jobOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const query = reactive({ jobId: 0 as number | undefined });
|
||||
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '#' },
|
||||
{ field: 'runAt', title: '运行时间', width: 165 },
|
||||
{
|
||||
field: 'jobName',
|
||||
title: '任务',
|
||||
width: 160,
|
||||
slots: {
|
||||
default: ({ row }: { row: AutoJobLogItem }) =>
|
||||
h('span', row.jobName || `#${row.jobId}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'result',
|
||||
title: '结果',
|
||||
width: 90,
|
||||
slots: {
|
||||
default: ({ row }: { row: AutoJobLogItem }) =>
|
||||
h(
|
||||
Tag,
|
||||
{
|
||||
theme: row.result === 1 ? 'success' : 'danger',
|
||||
variant: 'light',
|
||||
size: 'small',
|
||||
},
|
||||
row.result === 1 ? '成功' : '失败',
|
||||
),
|
||||
},
|
||||
},
|
||||
{ field: 'durationMs', title: '耗时(ms)', width: 100 },
|
||||
{ field: 'summary', title: '摘要', minWidth: 220, showOverflow: 'tooltip' },
|
||||
{ field: 'error', title: '错误', minWidth: 160, showOverflow: 'tooltip' },
|
||||
],
|
||||
pagerConfig: { pageSize: 20 },
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
}: {
|
||||
page: { currentPage: number; pageSize: number };
|
||||
}) => {
|
||||
const data = await getAutoJobLogList({
|
||||
page: page.currentPage,
|
||||
size: page.pageSize,
|
||||
jobId: query.jobId || undefined,
|
||||
});
|
||||
return { items: data.list ?? [], total: data.total ?? 0 };
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
function search() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
async function loadJobs() {
|
||||
const data = await getAutoJobList();
|
||||
jobOptions.value = (data.list ?? []).map((j: AutoJobItem) => ({
|
||||
label: j.name,
|
||||
value: j.id,
|
||||
}));
|
||||
}
|
||||
|
||||
onMounted(loadJobs);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="wb-page-head">
|
||||
<h2 class="wb-page-title">任务日志</h2>
|
||||
<p class="wb-page-sub">自动任务每次运行的摘要、结果与耗时</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 flex flex-wrap items-center gap-2">
|
||||
<t-select
|
||||
v-model="query.jobId"
|
||||
clearable
|
||||
:options="jobOptions"
|
||||
placeholder="按任务筛选"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<t-button theme="primary" @click="search">查询</t-button>
|
||||
</div>
|
||||
|
||||
<t-card :bordered="false" class="wb-table-card">
|
||||
<Grid />
|
||||
</t-card>
|
||||
</Page>
|
||||
</template>
|
||||
152
apps/web-tdesign/src/views/system/notice/channel/index.vue
Normal file
152
apps/web-tdesign/src/views/system/notice/channel/index.vue
Normal file
@ -0,0 +1,152 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { h, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { Page, useVbenDrawer, VbenTableAction } from '@vben/common-ui';
|
||||
|
||||
import { MessagePlugin, Tag } from 'tdesign-vue-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
getNoticeChannelList,
|
||||
saveNoticeChannel,
|
||||
type NoticeChannelItem,
|
||||
} from '#/api/notice';
|
||||
|
||||
// 通知渠道:Bark / pushplus 等推送通道的启用开关与配置
|
||||
const editing = ref<NoticeChannelItem | null>(null);
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
title: '编辑渠道',
|
||||
onConfirm: submit,
|
||||
});
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
code: '',
|
||||
enabled: 1,
|
||||
config: '',
|
||||
remark: '',
|
||||
});
|
||||
|
||||
function openEdit(row: NoticeChannelItem) {
|
||||
editing.value = row;
|
||||
Object.assign(form, {
|
||||
name: row.name,
|
||||
code: row.code,
|
||||
enabled: row.enabled,
|
||||
config: row.config ?? '',
|
||||
remark: row.remark ?? '',
|
||||
});
|
||||
drawerApi.setState({ title: `编辑渠道「${row.name}」` });
|
||||
drawerApi.open();
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!editing.value) return;
|
||||
await saveNoticeChannel({ id: editing.value.id, ...form });
|
||||
MessagePlugin.success('保存成功');
|
||||
drawerApi.close();
|
||||
loadList();
|
||||
}
|
||||
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '#' },
|
||||
{ field: 'code', title: '编码', width: 120 },
|
||||
{ field: 'name', title: '名称', width: 150 },
|
||||
{
|
||||
field: 'enabled',
|
||||
title: '启用',
|
||||
width: 90,
|
||||
slots: {
|
||||
default: ({ row }: { row: NoticeChannelItem }) =>
|
||||
h(
|
||||
Tag,
|
||||
{
|
||||
theme: row.enabled === 1 ? 'success' : 'default',
|
||||
variant: 'light',
|
||||
size: 'small',
|
||||
},
|
||||
row.enabled === 1 ? '启用' : '停用',
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'config',
|
||||
title: '配置(JSON)',
|
||||
minWidth: 220,
|
||||
showOverflow: 'tooltip',
|
||||
},
|
||||
{ field: 'remark', title: '备注', minWidth: 180, showOverflow: 'tooltip' },
|
||||
{
|
||||
field: 'op',
|
||||
title: '操作',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
slots: {
|
||||
default: ({ row }: { row: NoticeChannelItem }) =>
|
||||
h(VbenTableAction, {
|
||||
actions: [{ text: '编辑', onClick: () => openEdit(row) }],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
proxyConfig: { enabled: false },
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
async function loadList() {
|
||||
const data = await getNoticeChannelList();
|
||||
gridApi.setGridOptions({ data: data.list ?? [] });
|
||||
}
|
||||
|
||||
onMounted(loadList);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="wb-page-head">
|
||||
<h2 class="wb-page-title">通知渠道</h2>
|
||||
<p class="wb-page-sub">管理推送渠道的启用状态与配置(Bark 服务地址等)</p>
|
||||
</div>
|
||||
|
||||
<t-card :bordered="false" class="wb-filter-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm text-gray-500">
|
||||
停用渠道后,所有通知规则将不再通过该渠道发送
|
||||
</div>
|
||||
<t-button theme="primary" @click="loadList">刷新</t-button>
|
||||
</div>
|
||||
</t-card>
|
||||
|
||||
<t-card :bordered="false" class="wb-table-card">
|
||||
<Grid />
|
||||
</t-card>
|
||||
|
||||
<Drawer>
|
||||
<t-form label-align="right" label-width="90px">
|
||||
<t-form-item label="编码" required>
|
||||
<t-input v-model="form.code" disabled placeholder="bark / pushplus" />
|
||||
</t-form-item>
|
||||
<t-form-item label="名称" required>
|
||||
<t-input v-model="form.name" />
|
||||
</t-form-item>
|
||||
<t-form-item label="启用">
|
||||
<t-switch v-model="form.enabled" :custom-value="[1, 0]" />
|
||||
</t-form-item>
|
||||
<t-form-item label="配置(JSON)">
|
||||
<t-textarea
|
||||
v-model="form.config"
|
||||
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||
placeholder='如 Bark:{"baseUrl":"http://bark:8080"}'
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="备注">
|
||||
<t-input v-model="form.remark" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</Drawer>
|
||||
</Page>
|
||||
</template>
|
||||
150
apps/web-tdesign/src/views/system/notice/log/index.vue
Normal file
150
apps/web-tdesign/src/views/system/notice/log/index.vue
Normal file
@ -0,0 +1,150 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { h, reactive } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Tag } from 'tdesign-vue-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
getNoticeLogList,
|
||||
type NoticeLogItem,
|
||||
} from '#/api/notice';
|
||||
|
||||
// 通知日志:查看每条通知的发送目标/渠道/结果
|
||||
const query = reactive({
|
||||
eventType: '',
|
||||
channelCode: '',
|
||||
result: 0,
|
||||
dateRange: [] as string[],
|
||||
});
|
||||
|
||||
function rangeToParam(): { dateFrom?: string; dateTo?: string } {
|
||||
if (query.dateRange.length === 2 && query.dateRange[0] && query.dateRange[1]) {
|
||||
return {
|
||||
dateFrom: `${query.dateRange[0]} 00:00:00`,
|
||||
dateTo: `${query.dateRange[1]} 23:59:59`,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '#' },
|
||||
{ field: 'createdAt', title: '时间', width: 165 },
|
||||
{ field: 'eventType', title: '事件', width: 110 },
|
||||
{ field: 'channelCode', title: '渠道', width: 100 },
|
||||
{ field: 'target', title: '目标', width: 140, showOverflow: 'tooltip' },
|
||||
{ field: 'title', title: '标题', width: 180, showOverflow: 'tooltip' },
|
||||
{ field: 'body', title: '内容', minWidth: 200, showOverflow: 'tooltip' },
|
||||
{
|
||||
field: 'result',
|
||||
title: '结果',
|
||||
width: 90,
|
||||
slots: {
|
||||
default: ({ row }: { row: NoticeLogItem }) =>
|
||||
h(
|
||||
Tag,
|
||||
{
|
||||
theme: row.result === 1 ? 'success' : 'danger',
|
||||
variant: 'light',
|
||||
size: 'small',
|
||||
},
|
||||
row.result === 1 ? '成功' : '失败',
|
||||
),
|
||||
},
|
||||
},
|
||||
{ field: 'error', title: '错误', width: 160, showOverflow: 'tooltip' },
|
||||
],
|
||||
pagerConfig: { pageSize: 20 },
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
}: {
|
||||
page: { currentPage: number; pageSize: number };
|
||||
}) => {
|
||||
const data = await getNoticeLogList({
|
||||
page: page.currentPage,
|
||||
size: page.pageSize,
|
||||
eventType: query.eventType || undefined,
|
||||
channelCode: query.channelCode || undefined,
|
||||
result: query.result || undefined,
|
||||
...rangeToParam(),
|
||||
});
|
||||
return { items: data.list ?? [], total: data.total ?? 0 };
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
function search() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
function reset() {
|
||||
query.eventType = '';
|
||||
query.channelCode = '';
|
||||
query.result = 0;
|
||||
query.dateRange = [];
|
||||
gridApi.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="wb-page-head">
|
||||
<h2 class="wb-page-title">通知日志</h2>
|
||||
<p class="wb-page-sub">每条通知的发送记录:目标、渠道、结果与错误信息</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 flex flex-wrap items-center gap-2">
|
||||
<t-select
|
||||
v-model="query.eventType"
|
||||
clearable
|
||||
:options="[
|
||||
{ label: '任务完成', value: 'job_done' },
|
||||
{ label: '任务失败', value: 'job_fail' },
|
||||
{ label: '安全告警', value: 'security_alert' },
|
||||
{ label: '测试', value: 'test' },
|
||||
]"
|
||||
placeholder="事件类型"
|
||||
style="width: 140px"
|
||||
/>
|
||||
<t-input
|
||||
v-model="query.channelCode"
|
||||
clearable
|
||||
placeholder="渠道(bark/pushplus)"
|
||||
style="width: 150px"
|
||||
@enter="search"
|
||||
/>
|
||||
<t-select
|
||||
v-model="query.result"
|
||||
clearable
|
||||
:options="[
|
||||
{ label: '成功', value: 1 },
|
||||
{ label: '失败', value: 2 },
|
||||
]"
|
||||
placeholder="结果"
|
||||
style="width: 110px"
|
||||
/>
|
||||
<t-date-range-picker
|
||||
v-model="query.dateRange"
|
||||
clearable
|
||||
placeholder="时间范围"
|
||||
style="width: 260px"
|
||||
/>
|
||||
<t-button theme="primary" @click="search">查询</t-button>
|
||||
<t-button variant="outline" @click="reset">重置</t-button>
|
||||
</div>
|
||||
|
||||
<t-card :bordered="false" class="wb-table-card">
|
||||
<Grid />
|
||||
</t-card>
|
||||
</Page>
|
||||
</template>
|
||||
298
apps/web-tdesign/src/views/system/notice/rule/index.vue
Normal file
298
apps/web-tdesign/src/views/system/notice/rule/index.vue
Normal file
@ -0,0 +1,298 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { h, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { Page, useVbenDrawer, VbenTableAction } from '@vben/common-ui';
|
||||
|
||||
import { DialogPlugin, MessagePlugin, Tag } from 'tdesign-vue-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteNoticeRule,
|
||||
getNoticeChannelList,
|
||||
getNoticeRuleList,
|
||||
NOTICE_EVENT_OPTIONS,
|
||||
saveNoticeRule,
|
||||
testNotice,
|
||||
type NoticeChannelItem,
|
||||
type NoticeRuleItem,
|
||||
} from '#/api/notice';
|
||||
import { getAdminList } from '#/api/system';
|
||||
|
||||
// 通知规则:事件类型 → 渠道 → 接收用户 → 模板(统一推送中枢的"规则")
|
||||
const channelOptions = ref<{ label: string; value: string }[]>([]);
|
||||
const userOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const editing = ref<NoticeRuleItem | null>(null);
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
title: '新增规则',
|
||||
onConfirm: submit,
|
||||
});
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
eventType: '',
|
||||
channelCodes: [] as string[],
|
||||
userIds: [] as number[],
|
||||
titleTemplate: '',
|
||||
bodyTemplate: '',
|
||||
enabled: 1,
|
||||
});
|
||||
|
||||
async function loadBase() {
|
||||
const ch = await getNoticeChannelList();
|
||||
channelOptions.value = (ch.list ?? []).map((c: NoticeChannelItem) => ({
|
||||
label: `${c.name}${c.enabled === 1 ? '' : '(停用)'}`,
|
||||
value: c.code,
|
||||
}));
|
||||
const us = await getAdminList({ page: 1, size: 100 });
|
||||
userOptions.value = (us.list ?? []).map((u) => ({
|
||||
label: `${u.nickname || u.username}(${u.username})`,
|
||||
value: u.id,
|
||||
}));
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editing.value = null;
|
||||
Object.assign(form, {
|
||||
name: '',
|
||||
eventType: '',
|
||||
channelCodes: [],
|
||||
userIds: [],
|
||||
titleTemplate: '',
|
||||
bodyTemplate: '',
|
||||
enabled: 1,
|
||||
});
|
||||
drawerApi.setState({ title: '新增规则' });
|
||||
drawerApi.open();
|
||||
}
|
||||
|
||||
function openEdit(row: NoticeRuleItem) {
|
||||
editing.value = row;
|
||||
Object.assign(form, {
|
||||
name: row.name,
|
||||
eventType: row.eventType,
|
||||
channelCodes: row.channelCodes ?? [],
|
||||
userIds: row.userIds ?? [],
|
||||
titleTemplate: row.titleTemplate ?? '',
|
||||
bodyTemplate: row.bodyTemplate ?? '',
|
||||
enabled: row.enabled,
|
||||
});
|
||||
drawerApi.setState({ title: '编辑规则' });
|
||||
drawerApi.open();
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
editing.value
|
||||
? await saveNoticeRule({ ...form, id: editing.value.id })
|
||||
: await saveNoticeRule(form);
|
||||
MessagePlugin.success('保存成功');
|
||||
drawerApi.close();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
function onDelete(row: NoticeRuleItem) {
|
||||
const d = DialogPlugin.confirm({
|
||||
header: `确认删除规则「${row.name}」?`,
|
||||
onConfirm: async () => {
|
||||
await deleteNoticeRule({ id: row.id });
|
||||
MessagePlugin.success('删除成功');
|
||||
gridApi.query();
|
||||
d.destroy();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 测试该规则:输入标题/内容发送一条 */
|
||||
function onTest(row: NoticeRuleItem) {
|
||||
let title = '';
|
||||
let body = '';
|
||||
const d = DialogPlugin.confirm({
|
||||
header: `测试规则「${row.name}」`,
|
||||
body: () =>
|
||||
h('div', { class: 'flex flex-col gap-2' }, [
|
||||
h('input', {
|
||||
class: 't-input__inner',
|
||||
placeholder: '标题(留空用模板默认)',
|
||||
style: 'padding:6px;border:1px solid #dcdcdc;border-radius:4px',
|
||||
onInput: (e: Event) => (title = (e.target as HTMLInputElement).value),
|
||||
}),
|
||||
h('input', {
|
||||
class: 't-input__inner',
|
||||
placeholder: '内容(留空用模板默认)',
|
||||
style: 'padding:6px;border:1px solid #dcdcdc;border-radius:4px;margin-top:6px',
|
||||
onInput: (e: Event) => (body = (e.target as HTMLInputElement).value),
|
||||
}),
|
||||
]),
|
||||
onConfirm: async () => {
|
||||
const r = await testNotice({
|
||||
ruleId: row.id,
|
||||
title: title || undefined,
|
||||
body: body || undefined,
|
||||
});
|
||||
MessagePlugin[r.result ? 'success' : 'error'](
|
||||
r.result ? `测试发送成功:${r.message}` : `发送失败:${r.message}`,
|
||||
);
|
||||
d.destroy();
|
||||
gridApi.query();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '#' },
|
||||
{ field: 'name', title: '规则名称', width: 140 },
|
||||
{ field: 'eventType', title: '事件类型', width: 120 },
|
||||
{
|
||||
field: 'channelCodesText',
|
||||
title: '渠道',
|
||||
width: 160,
|
||||
slots: {
|
||||
default: ({ row }: { row: NoticeRuleItem }) =>
|
||||
h(
|
||||
'span',
|
||||
(row.channelCodes ?? []).map((c) => c.toUpperCase()).join(' / ') || '-',
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'userIdsText',
|
||||
title: '接收用户',
|
||||
width: 120,
|
||||
slots: {
|
||||
default: ({ row }: { row: NoticeRuleItem }) =>
|
||||
h('span', `ID: ${(row.userIds ?? []).join(',')}` || '-'),
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'enabled',
|
||||
title: '启用',
|
||||
width: 80,
|
||||
slots: {
|
||||
default: ({ row }: { row: NoticeRuleItem }) =>
|
||||
h(
|
||||
Tag,
|
||||
{
|
||||
theme: row.enabled === 1 ? 'success' : 'default',
|
||||
variant: 'light',
|
||||
size: 'small',
|
||||
},
|
||||
row.enabled === 1 ? '启用' : '停用',
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'op',
|
||||
title: '操作',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
slots: {
|
||||
default: ({ row }: { row: NoticeRuleItem }) =>
|
||||
h(VbenTableAction, {
|
||||
actions: [
|
||||
{ text: '编辑', onClick: () => openEdit(row) },
|
||||
{ text: '测试', onClick: () => onTest(row) },
|
||||
{
|
||||
text: '删除',
|
||||
danger: true,
|
||||
onClick: () => onDelete(row),
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
proxyConfig: { enabled: false }, // 本地数据:onMounted 加载
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
async function loadList() {
|
||||
const data = await getNoticeRuleList({});
|
||||
gridApi.setGridOptions({
|
||||
data: data.list ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
function search() {
|
||||
loadList();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadBase();
|
||||
await loadList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="wb-page-head">
|
||||
<h2 class="wb-page-title">通知规则</h2>
|
||||
<p class="wb-page-sub">
|
||||
定义事件类型 → 通知渠道 → 接收用户 → 标题/内容模板;任务完成/失败、安全告警等均走这里统一下发
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<t-card :bordered="false" class="wb-filter-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<t-button theme="primary" @click="search">刷新</t-button>
|
||||
</div>
|
||||
<t-button theme="primary" @click="openCreate">新增规则</t-button>
|
||||
</div>
|
||||
</t-card>
|
||||
|
||||
<t-card :bordered="false" class="wb-table-card">
|
||||
<Grid />
|
||||
</t-card>
|
||||
|
||||
<Drawer>
|
||||
<t-form label-align="right" label-width="90px">
|
||||
<t-form-item label="规则名称" required>
|
||||
<t-input v-model="form.name" placeholder="如:任务失败告警" />
|
||||
</t-form-item>
|
||||
<t-form-item label="事件类型" required>
|
||||
<t-select
|
||||
v-model="form.eventType"
|
||||
:options="NOTICE_EVENT_OPTIONS"
|
||||
placeholder="选择事件类型"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="通知渠道" required>
|
||||
<t-select
|
||||
v-model="form.channelCodes"
|
||||
multiple
|
||||
:options="channelOptions"
|
||||
placeholder="选择渠道(Bark/pushplus)"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="接收用户" required>
|
||||
<t-select
|
||||
v-model="form.userIds"
|
||||
multiple
|
||||
:options="userOptions"
|
||||
placeholder="选择接收用户"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="标题模板">
|
||||
<t-input
|
||||
v-model="form.titleTemplate"
|
||||
placeholder="支持 {jobName} {summary} {error} 等占位符"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="内容模板">
|
||||
<t-textarea
|
||||
v-model="form.bodyTemplate"
|
||||
:autosize="{ minRows: 3, maxRows: 6 }"
|
||||
placeholder="支持 {jobName} {summary} {error}"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="启用">
|
||||
<t-switch v-model="form.enabled" :custom-value="[1, 0]" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</Drawer>
|
||||
</Page>
|
||||
</template>
|
||||
Loading…
Reference in New Issue
Block a user