日志管理
This commit is contained in:
@@ -10,6 +10,19 @@ export const fetchAlertLevelList = (data?: {
|
||||
return request.get("/Alert/v1/severity/list", { params: data || {} });
|
||||
};
|
||||
|
||||
/** 获取告警级别下拉选项(不分页),默认仅返回 enabled=true */
|
||||
export const fetchAlertLevelOptions = (data?: {
|
||||
keyword?: string;
|
||||
enabled?: 'true' | 'false';
|
||||
}) => {
|
||||
return request.get("/Alert/v1/severity/options", {
|
||||
params: {
|
||||
keyword: data?.keyword || undefined,
|
||||
enabled: data?.enabled ?? 'true',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取告警级别详情 */
|
||||
export const fetchAlertLevelDetail = (id: number) => {
|
||||
return request.get(`/Alert/v1/severity/get/${id}`);
|
||||
|
||||
@@ -112,6 +112,18 @@ export const fetchSeverityList = (data: {
|
||||
enabled?: string;
|
||||
}) => request.get("/Alert/v1/severity/list", { params: data });
|
||||
|
||||
/** 获取告警级别下拉选项(不分页),支持 keyword 模糊搜索与 enabled=true 过滤 */
|
||||
export const fetchSeverityOptions = (data?: {
|
||||
keyword?: string;
|
||||
enabled?: 'true' | 'false';
|
||||
}) =>
|
||||
request.get("/Alert/v1/severity/options", {
|
||||
params: {
|
||||
keyword: data?.keyword || undefined,
|
||||
enabled: data?.enabled ?? 'true',
|
||||
},
|
||||
});
|
||||
|
||||
/** 获取工单模板下拉选项 */
|
||||
export const fetchFeedbackTemplateOptions = (data?: {
|
||||
status?: 'active' | 'inactive'
|
||||
|
||||
@@ -64,6 +64,12 @@ export interface AlertSeverity {
|
||||
name: string
|
||||
color?: string
|
||||
level?: number
|
||||
description?: string
|
||||
icon?: string
|
||||
priority?: number
|
||||
enabled?: boolean
|
||||
is_default?: boolean
|
||||
config?: any
|
||||
}
|
||||
|
||||
// 指标元数据
|
||||
@@ -188,6 +194,19 @@ export const fetchSeverityList = () => {
|
||||
return request.get('/Alert/v1/severity/list')
|
||||
}
|
||||
|
||||
/** 获取告警级别下拉选项(不分页),支持 keyword 模糊搜索与 enabled=true 过滤 */
|
||||
export const fetchSeverityOptions = (params?: {
|
||||
keyword?: string
|
||||
enabled?: 'true' | 'false'
|
||||
}) => {
|
||||
return request.get('/Alert/v1/severity/options', {
|
||||
params: {
|
||||
keyword: params?.keyword || undefined,
|
||||
enabled: params?.enabled ?? 'true',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 按 code 获取告警级别 */
|
||||
export const fetchSeverityByCode = (code: string) => {
|
||||
return request.get(`/Alert/v1/severity/get-by-code/${code}`)
|
||||
|
||||
157
src/api/ops/logs.ts
Normal file
157
src/api/ops/logs.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { request } from '@/api/request'
|
||||
|
||||
const BASE = '/Logs/v1'
|
||||
|
||||
/** 解析 bsm-sdk 风格响应,取出业务 data */
|
||||
export function unwrapLogsPayload(res: any): any {
|
||||
if (res == null) return null
|
||||
if (typeof res.code === 'number' && res.code !== 0) return null
|
||||
return res.details ?? res.data ?? null
|
||||
}
|
||||
|
||||
export interface LogEvent {
|
||||
id: number
|
||||
created_at: string
|
||||
source_kind: string
|
||||
remote_addr: string
|
||||
raw_payload: string
|
||||
normalized_summary: string
|
||||
normalized_detail: string
|
||||
device_name: string
|
||||
severity_code: string
|
||||
trap_oid: string
|
||||
alert_sent: boolean
|
||||
}
|
||||
|
||||
export interface LogEntriesParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
source_kind?: string
|
||||
}
|
||||
|
||||
export interface LogEntriesResult {
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
items: LogEvent[]
|
||||
}
|
||||
|
||||
export interface SyslogRule {
|
||||
id?: number
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
name: string
|
||||
enabled: boolean
|
||||
priority: number
|
||||
device_name_contains: string
|
||||
keyword_regex: string
|
||||
alert_name: string
|
||||
severity_code: string
|
||||
policy_id: number
|
||||
}
|
||||
|
||||
export interface TrapRule {
|
||||
id?: number
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
name: string
|
||||
enabled: boolean
|
||||
priority: number
|
||||
oid_prefix: string
|
||||
varbind_match_regex: string
|
||||
alert_name: string
|
||||
severity_code: string
|
||||
policy_id: number
|
||||
}
|
||||
|
||||
export interface TrapDictionaryEntry {
|
||||
id?: number
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
oid_prefix: string
|
||||
title: string
|
||||
description: string
|
||||
severity_code: string
|
||||
recovery_message: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface TrapShield {
|
||||
id?: number
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
name: string
|
||||
enabled: boolean
|
||||
source_ip_cidr: string
|
||||
oid_prefix: string
|
||||
interface_hint: string
|
||||
time_windows_json: string
|
||||
}
|
||||
|
||||
export function fetchLogEntries(params?: LogEntriesParams) {
|
||||
return request.get(`${BASE}/entries`, { params })
|
||||
}
|
||||
|
||||
export function fetchSyslogRules() {
|
||||
return request.get(`${BASE}/syslog-rules`)
|
||||
}
|
||||
|
||||
export function createSyslogRule(data: Partial<SyslogRule>) {
|
||||
return request.post(`${BASE}/syslog-rules`, data)
|
||||
}
|
||||
|
||||
export function updateSyslogRule(id: number, data: Partial<SyslogRule>) {
|
||||
return request.put(`${BASE}/syslog-rules/${id}`, data)
|
||||
}
|
||||
|
||||
export function deleteSyslogRule(id: number) {
|
||||
return request.delete(`${BASE}/syslog-rules/${id}`)
|
||||
}
|
||||
|
||||
export function fetchTrapRules() {
|
||||
return request.get(`${BASE}/trap-rules`)
|
||||
}
|
||||
|
||||
export function createTrapRule(data: Partial<TrapRule>) {
|
||||
return request.post(`${BASE}/trap-rules`, data)
|
||||
}
|
||||
|
||||
export function updateTrapRule(id: number, data: Partial<TrapRule>) {
|
||||
return request.put(`${BASE}/trap-rules/${id}`, data)
|
||||
}
|
||||
|
||||
export function deleteTrapRule(id: number) {
|
||||
return request.delete(`${BASE}/trap-rules/${id}`)
|
||||
}
|
||||
|
||||
export function fetchTrapDictionary() {
|
||||
return request.get(`${BASE}/trap-dictionary`)
|
||||
}
|
||||
|
||||
export function createTrapDictionary(data: Partial<TrapDictionaryEntry>) {
|
||||
return request.post(`${BASE}/trap-dictionary`, data)
|
||||
}
|
||||
|
||||
export function updateTrapDictionary(id: number, data: Partial<TrapDictionaryEntry>) {
|
||||
return request.put(`${BASE}/trap-dictionary/${id}`, data)
|
||||
}
|
||||
|
||||
export function deleteTrapDictionary(id: number) {
|
||||
return request.delete(`${BASE}/trap-dictionary/${id}`)
|
||||
}
|
||||
|
||||
export function fetchTrapSuppressions() {
|
||||
return request.get(`${BASE}/trap-suppressions`)
|
||||
}
|
||||
|
||||
export function createTrapSuppression(data: Partial<TrapShield>) {
|
||||
return request.post(`${BASE}/trap-suppressions`, data)
|
||||
}
|
||||
|
||||
export function updateTrapSuppression(id: number, data: Partial<TrapShield>) {
|
||||
return request.put(`${BASE}/trap-suppressions/${id}`, data)
|
||||
}
|
||||
|
||||
export function deleteTrapSuppression(id: number) {
|
||||
return request.delete(`${BASE}/trap-suppressions/${id}`)
|
||||
}
|
||||
@@ -186,11 +186,12 @@ export const localMenuFlatItems: MenuItem[] = [
|
||||
title: '日志监控',
|
||||
title_en: 'Log Monitoring',
|
||||
code: 'ops:综合监控:日志监控',
|
||||
description: '综合监控 - 日志监控',
|
||||
description: '综合监控 - 日志监控(对接 Logs)',
|
||||
app_id: 2,
|
||||
parent_id: 23,
|
||||
menu_path: '/monitor/log',
|
||||
menu_icon: 'appstore',
|
||||
component: 'ops/pages/monitor/log',
|
||||
type: 1,
|
||||
sort_key: 13,
|
||||
created_at: '2025-12-26T13:23:51.907711+08:00',
|
||||
@@ -300,6 +301,91 @@ export const localMenuFlatItems: MenuItem[] = [
|
||||
sort_key: 20,
|
||||
created_at: '2025-12-26T13:23:51.828777+08:00',
|
||||
},
|
||||
{
|
||||
id: 12010,
|
||||
identity: '019c7000-0001-7000-8000-000000000001',
|
||||
title: '日志采集',
|
||||
title_en: 'Log Ingest',
|
||||
code: 'ops:日志采集',
|
||||
description: 'Syslog / SNMP Trap(Logs 服务)',
|
||||
app_id: 2,
|
||||
menu_path: '/log-mgmt/',
|
||||
menu_icon: 'File',
|
||||
type: 1,
|
||||
sort_key: 21,
|
||||
created_at: '2026-03-30T10:00:00+08:00',
|
||||
},
|
||||
{
|
||||
id: 12011,
|
||||
identity: '019c7000-0001-7000-8000-000000000011',
|
||||
title: '日志查询',
|
||||
title_en: 'Log Query',
|
||||
code: 'ops:日志采集:日志查询',
|
||||
app_id: 2,
|
||||
parent_id: 12010,
|
||||
menu_path: '/log-mgmt/entries',
|
||||
menu_icon: 'List',
|
||||
component: 'ops/pages/log-mgmt/entries',
|
||||
type: 1,
|
||||
sort_key: 22,
|
||||
created_at: '2026-03-30T10:00:00+08:00',
|
||||
},
|
||||
{
|
||||
id: 12012,
|
||||
identity: '019c7000-0001-7000-8000-000000000012',
|
||||
title: 'Syslog 规则',
|
||||
title_en: 'Syslog Rules',
|
||||
code: 'ops:日志采集:syslog规则',
|
||||
app_id: 2,
|
||||
parent_id: 12010,
|
||||
menu_path: '/log-mgmt/syslog-rules',
|
||||
component: 'ops/pages/log-mgmt/syslog-rules',
|
||||
type: 1,
|
||||
sort_key: 23,
|
||||
created_at: '2026-03-30T10:00:00+08:00',
|
||||
},
|
||||
{
|
||||
id: 12013,
|
||||
identity: '019c7000-0001-7000-8000-000000000013',
|
||||
title: 'Trap 规则',
|
||||
title_en: 'Trap Rules',
|
||||
code: 'ops:日志采集:trap规则',
|
||||
app_id: 2,
|
||||
parent_id: 12010,
|
||||
menu_path: '/log-mgmt/trap-rules',
|
||||
component: 'ops/pages/log-mgmt/trap-rules',
|
||||
type: 1,
|
||||
sort_key: 24,
|
||||
created_at: '2026-03-30T10:00:00+08:00',
|
||||
},
|
||||
{
|
||||
id: 12014,
|
||||
identity: '019c7000-0001-7000-8000-000000000014',
|
||||
title: 'Trap 字典',
|
||||
title_en: 'Trap Dictionary',
|
||||
code: 'ops:日志采集:trap字典',
|
||||
app_id: 2,
|
||||
parent_id: 12010,
|
||||
menu_path: '/log-mgmt/trap-dictionary',
|
||||
component: 'ops/pages/log-mgmt/trap-dictionary',
|
||||
type: 1,
|
||||
sort_key: 25,
|
||||
created_at: '2026-03-30T10:00:00+08:00',
|
||||
},
|
||||
{
|
||||
id: 12015,
|
||||
identity: '019c7000-0001-7000-8000-000000000015',
|
||||
title: 'Trap 屏蔽',
|
||||
title_en: 'Trap Suppressions',
|
||||
code: 'ops:日志采集:trap屏蔽',
|
||||
app_id: 2,
|
||||
parent_id: 12010,
|
||||
menu_path: '/log-mgmt/trap-suppressions',
|
||||
component: 'ops/pages/log-mgmt/trap-suppressions',
|
||||
type: 1,
|
||||
sort_key: 26,
|
||||
created_at: '2026-03-30T10:00:00+08:00',
|
||||
},
|
||||
{
|
||||
id: 35,
|
||||
identity: '019b591d-021a-74a3-8092-c15b990f3c7e',
|
||||
@@ -311,7 +397,7 @@ export const localMenuFlatItems: MenuItem[] = [
|
||||
menu_path: '/netarch/',
|
||||
menu_icon: 'Laptop',
|
||||
type: 1,
|
||||
sort_key: 21,
|
||||
sort_key: 30,
|
||||
created_at: '2025-12-26T13:23:51.969818+08:00',
|
||||
},
|
||||
{
|
||||
@@ -899,10 +985,11 @@ export const localMenuFlatItems: MenuItem[] = [
|
||||
title: '系统日志',
|
||||
title_en: 'System Logs',
|
||||
code: 'SystemSettingsSystemLogs',
|
||||
description: '系统日志',
|
||||
description: '系统日志(操作审计说明,演示见 index_bak)',
|
||||
app_id: 2,
|
||||
parent_id: 78,
|
||||
menu_path: '/system-settings/system-logs',
|
||||
component: 'ops/pages/system-settings/system-logs',
|
||||
type: 1,
|
||||
sort_key: 59,
|
||||
created_at: '2025-12-27T12:31:02.556301+08:00',
|
||||
|
||||
@@ -199,11 +199,12 @@ export const localMenuItems: MenuItem[] = [
|
||||
title: '日志监控',
|
||||
title_en: 'Log Monitoring',
|
||||
code: 'ops:综合监控:日志监控',
|
||||
description: '综合监控 - 日志监控',
|
||||
description: '综合监控 - 日志监控(对接 Logs,同日志查询)',
|
||||
app_id: 2,
|
||||
parent_id: 23,
|
||||
menu_path: '/monitor/log',
|
||||
menu_icon: 'appstore',
|
||||
component: 'ops/pages/monitor/log',
|
||||
type: 1,
|
||||
sort_key: 5,
|
||||
created_at: '2025-12-26T13:23:51.907711+08:00',
|
||||
@@ -323,6 +324,99 @@ export const localMenuItems: MenuItem[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 12010,
|
||||
identity: '019c7000-0001-7000-8000-000000000001',
|
||||
title: '日志采集',
|
||||
title_en: 'Log Ingest',
|
||||
code: 'ops:日志采集',
|
||||
description: 'Syslog / SNMP Trap 采集与规则(Logs 服务)',
|
||||
app_id: 2,
|
||||
menu_path: '/log-mgmt/',
|
||||
menu_icon: 'File',
|
||||
type: 1,
|
||||
sort_key: 5,
|
||||
created_at: '2026-03-30T10:00:00+08:00',
|
||||
children: [
|
||||
{
|
||||
id: 12011,
|
||||
identity: '019c7000-0001-7000-8000-000000000011',
|
||||
title: '日志查询',
|
||||
title_en: 'Log Query',
|
||||
code: 'ops:日志采集:日志查询',
|
||||
app_id: 2,
|
||||
parent_id: 12010,
|
||||
menu_path: '/log-mgmt/entries',
|
||||
menu_icon: 'List',
|
||||
component: 'ops/pages/log-mgmt/entries',
|
||||
type: 1,
|
||||
sort_key: 1,
|
||||
created_at: '2026-03-30T10:00:00+08:00',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 12012,
|
||||
identity: '019c7000-0001-7000-8000-000000000012',
|
||||
title: 'Syslog 规则',
|
||||
title_en: 'Syslog Rules',
|
||||
code: 'ops:日志采集:syslog规则',
|
||||
app_id: 2,
|
||||
parent_id: 12010,
|
||||
menu_path: '/log-mgmt/syslog-rules',
|
||||
menu_icon: 'Code',
|
||||
component: 'ops/pages/log-mgmt/syslog-rules',
|
||||
type: 1,
|
||||
sort_key: 2,
|
||||
created_at: '2026-03-30T10:00:00+08:00',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 12013,
|
||||
identity: '019c7000-0001-7000-8000-000000000013',
|
||||
title: 'Trap 规则',
|
||||
title_en: 'Trap Rules',
|
||||
code: 'ops:日志采集:trap规则',
|
||||
app_id: 2,
|
||||
parent_id: 12010,
|
||||
menu_path: '/log-mgmt/trap-rules',
|
||||
component: 'ops/pages/log-mgmt/trap-rules',
|
||||
type: 1,
|
||||
sort_key: 3,
|
||||
created_at: '2026-03-30T10:00:00+08:00',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 12014,
|
||||
identity: '019c7000-0001-7000-8000-000000000014',
|
||||
title: 'Trap 字典',
|
||||
title_en: 'Trap Dictionary',
|
||||
code: 'ops:日志采集:trap字典',
|
||||
app_id: 2,
|
||||
parent_id: 12010,
|
||||
menu_path: '/log-mgmt/trap-dictionary',
|
||||
component: 'ops/pages/log-mgmt/trap-dictionary',
|
||||
type: 1,
|
||||
sort_key: 4,
|
||||
created_at: '2026-03-30T10:00:00+08:00',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 12015,
|
||||
identity: '019c7000-0001-7000-8000-000000000015',
|
||||
title: 'Trap 屏蔽',
|
||||
title_en: 'Trap Suppressions',
|
||||
code: 'ops:日志采集:trap屏蔽',
|
||||
app_id: 2,
|
||||
parent_id: 12010,
|
||||
menu_path: '/log-mgmt/trap-suppressions',
|
||||
component: 'ops/pages/log-mgmt/trap-suppressions',
|
||||
type: 1,
|
||||
sort_key: 5,
|
||||
created_at: '2026-03-30T10:00:00+08:00',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 35,
|
||||
identity: '019b591d-021a-74a3-8092-c15b990f3c7e',
|
||||
@@ -968,10 +1062,11 @@ export const localMenuItems: MenuItem[] = [
|
||||
title: '系统日志',
|
||||
title_en: 'System Logs',
|
||||
code: 'SystemSettingsSystemLogs',
|
||||
description: '系统日志',
|
||||
description: '系统日志(操作审计说明页,演示数据见 index_bak)',
|
||||
app_id: 2,
|
||||
parent_id: 78,
|
||||
menu_path: '/system-settings/system-logs',
|
||||
component: 'ops/pages/system-settings/system-logs',
|
||||
type: 1,
|
||||
sort_key: 13,
|
||||
created_at: '2025-12-27T12:31:02.556301+08:00',
|
||||
|
||||
@@ -125,7 +125,7 @@ import { columns as columnsConfig } from './config/columns'
|
||||
import {
|
||||
fetchHistories,
|
||||
} from '@/api/ops/alertHistory'
|
||||
import { fetchAlertLevelList } from '@/api/ops/alertLevel'
|
||||
import { fetchAlertLevelOptions } from '@/api/ops/alertLevel'
|
||||
import HistoryDetailDialog from './components/HistoryDetailDialog.vue'
|
||||
import HistoryProcessListDialog from './components/HistoryProcessListDialog.vue'
|
||||
|
||||
@@ -231,10 +231,11 @@ const currentProcessAlertRecordId = computed(() => currentProcessAlertRecord.val
|
||||
// 加载告警级别列表
|
||||
const loadSeverityOptions = async () => {
|
||||
try {
|
||||
const res = await fetchAlertLevelList({ page: 1, page_size: 1000, enabled: 'true' })
|
||||
if (res.code === 0 && res.details?.data) {
|
||||
severityOptions.value = res.details.data
|
||||
}
|
||||
const res: any = await fetchAlertLevelOptions({ enabled: 'true' })
|
||||
const list = Array.isArray(res?.details)
|
||||
? res.details
|
||||
: res?.details?.data || res?.details?.list || res?.data || res
|
||||
severityOptions.value = Array.isArray(list) ? list : []
|
||||
} catch (error) {
|
||||
console.error('获取告警级别列表失败:', error)
|
||||
}
|
||||
|
||||
@@ -92,6 +92,10 @@
|
||||
placeholder="请选择告警级别(可多选)"
|
||||
multiple
|
||||
allow-clear
|
||||
allow-search
|
||||
:filter-option="false"
|
||||
:loading="severityLoading"
|
||||
@search="handleSeveritySearch"
|
||||
>
|
||||
<a-option
|
||||
v-for="level in severityLevels"
|
||||
@@ -197,7 +201,7 @@
|
||||
import { ref, watch, computed, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { createNoticeChannel, updateNoticeChannel } from '@/api/ops/noticeChannel'
|
||||
import { fetchAlertLevelList } from '@/api/ops/alertLevel'
|
||||
import { fetchAlertLevelOptions } from '@/api/ops/alertLevel'
|
||||
|
||||
interface NoticeChannel {
|
||||
id?: number
|
||||
@@ -256,22 +260,59 @@ const form = ref({
|
||||
// 是否为编辑模式
|
||||
const isEdit = computed(() => !!props.channel?.id)
|
||||
|
||||
// 获取告警级别列表
|
||||
const fetchSeverityLevels = async () => {
|
||||
const severityLoading = ref(false)
|
||||
const baseSeverityLevels = ref<any[]>([])
|
||||
|
||||
// 获取告警级别列表(支持 keyword 搜索)
|
||||
const fetchSeverityLevels = async (keyword?: string) => {
|
||||
severityLoading.value = true
|
||||
try {
|
||||
const res = await fetchAlertLevelList({
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
const res: any = await fetchAlertLevelOptions({
|
||||
keyword: keyword || undefined,
|
||||
enabled: 'true',
|
||||
})
|
||||
if (res.code === 0 && res.details?.data) {
|
||||
severityLevels.value = res.details.data
|
||||
|
||||
const list = Array.isArray(res?.details)
|
||||
? res.details
|
||||
: res?.details?.data || res?.details?.list || res?.data || res
|
||||
const fetchedOptions = Array.isArray(list) ? list : []
|
||||
|
||||
// keyword 为空时缓存“全量可用选项”
|
||||
const isBaseLoad = !keyword || keyword.trim().length === 0
|
||||
if (isBaseLoad) {
|
||||
baseSeverityLevels.value = fetchedOptions
|
||||
}
|
||||
|
||||
// 防止搜索后缩小 options 导致已选项标签无法展示
|
||||
const selectedCodes = Array.isArray(form.value.severity_filter) ? form.value.severity_filter : []
|
||||
const selectedOptions = baseSeverityLevels.value.filter((s) => selectedCodes.includes(s.code))
|
||||
|
||||
const mergedMap = new Map<string, any>()
|
||||
;[...fetchedOptions, ...selectedOptions].forEach((item) => {
|
||||
if (item?.code) {
|
||||
mergedMap.set(item.code, item)
|
||||
}
|
||||
})
|
||||
|
||||
severityLevels.value = Array.from(mergedMap.values())
|
||||
} catch (error) {
|
||||
console.error('获取告警级别列表失败:', error)
|
||||
} finally {
|
||||
severityLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
let severitySearchTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const handleSeveritySearch = (keyword: string) => {
|
||||
if (severitySearchTimer) {
|
||||
clearTimeout(severitySearchTimer)
|
||||
}
|
||||
|
||||
severitySearchTimer = setTimeout(() => {
|
||||
fetchSeverityLevels(keyword)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// 处理静默时间段变化
|
||||
const handleQuietHoursChange = () => {
|
||||
if (quietHoursEnabled.value) {
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { FormInstance } from '@arco-design/web-vue'
|
||||
import { createPolicy, fetchTemplateList, fetchSeverityList, fetchFeedbackTemplateOptions } from '@/api/ops/alertPolicy'
|
||||
import { createPolicy, fetchTemplateList, fetchSeverityOptions, fetchFeedbackTemplateOptions } from '@/api/ops/alertPolicy'
|
||||
import { fetchUserList } from '@/api/ops/rbac2'
|
||||
|
||||
interface Props {
|
||||
@@ -241,11 +241,12 @@ const loadTemplateOptions = async () => {
|
||||
|
||||
const loadSeverityList = async () => {
|
||||
try {
|
||||
const res = await fetchSeverityList({ page: 1, page_size: 100 })
|
||||
if (res.code === 0 && res.details?.data) {
|
||||
severityList.value = res.details.data
|
||||
initDispatchRuleData()
|
||||
}
|
||||
const res: any = await fetchSeverityOptions({ enabled: 'true' })
|
||||
const list = Array.isArray(res?.details)
|
||||
? res.details
|
||||
: res?.details?.data || res?.details?.list || res?.data || res
|
||||
severityList.value = Array.isArray(list) ? list : []
|
||||
initDispatchRuleData()
|
||||
} catch (error) {
|
||||
console.error('获取告警级别失败:', error)
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ import {
|
||||
fetchPolicyDetail,
|
||||
updatePolicy,
|
||||
fetchTemplateList,
|
||||
fetchSeverityList,
|
||||
fetchSeverityOptions,
|
||||
fetchFeedbackTemplateOptions,
|
||||
} from '@/api/ops/alertPolicy'
|
||||
import { fetchUserList } from '@/api/ops/rbac2'
|
||||
@@ -323,10 +323,11 @@ const loadTemplateOptions = async () => {
|
||||
// 加载告警级别
|
||||
const loadSeverityList = async () => {
|
||||
try {
|
||||
const res = await fetchSeverityList({ page: 1, page_size: 100 })
|
||||
if (res.code === 0 && res.details?.data) {
|
||||
severityList.value = res.details.data
|
||||
}
|
||||
const res: any = await fetchSeverityOptions({ enabled: 'true' })
|
||||
const list = Array.isArray(res?.details)
|
||||
? res.details
|
||||
: res?.details?.data || res?.details?.list || res?.data || res
|
||||
severityList.value = Array.isArray(list) ? list : []
|
||||
} catch (error) {
|
||||
console.error('获取告警级别失败:', error)
|
||||
}
|
||||
|
||||
@@ -59,6 +59,9 @@
|
||||
v-model="formData.severity_id"
|
||||
placeholder="请选择告警级别"
|
||||
:loading="severityLoading"
|
||||
allow-search
|
||||
:filter-option="false"
|
||||
@search="handleSeveritySearch"
|
||||
>
|
||||
<a-option
|
||||
v-for="severity in severityOptions"
|
||||
@@ -155,7 +158,7 @@ import {
|
||||
fetchRuleDetail,
|
||||
createRule,
|
||||
updateRule,
|
||||
fetchSeverityList,
|
||||
fetchSeverityOptions,
|
||||
} from '@/api/ops/alertPolicy'
|
||||
|
||||
// Props
|
||||
@@ -245,15 +248,40 @@ const formRules = {
|
||||
|
||||
// 告警级别选项
|
||||
const severityOptions = ref<any[]>([])
|
||||
const baseSeverityOptions = ref<any[]>([])
|
||||
|
||||
// 加载告警级别列表
|
||||
const loadSeverityOptions = async () => {
|
||||
const loadSeverityOptions = async (keyword?: string) => {
|
||||
severityLoading.value = true
|
||||
try {
|
||||
const res = await fetchSeverityList({ page: 1, page_size: 100 })
|
||||
if (res.code === 0 && res.details?.data) {
|
||||
severityOptions.value = res.details.data
|
||||
const res: any = await fetchSeverityOptions({
|
||||
keyword,
|
||||
enabled: 'true',
|
||||
})
|
||||
|
||||
const list = Array.isArray(res?.details)
|
||||
? res.details
|
||||
: res?.details?.data || res?.details?.list || res?.data || res
|
||||
const fetchedOptions = Array.isArray(list) ? list : []
|
||||
|
||||
// 首次/keyword 为空时缓存“全量可用选项”
|
||||
const isBaseLoad = !keyword || keyword.trim().length === 0
|
||||
if (isBaseLoad) {
|
||||
baseSeverityOptions.value = fetchedOptions
|
||||
}
|
||||
|
||||
// 防止搜索后缩小 options 导致当前已选项标签无法展示
|
||||
const selectedId = formData.severity_id
|
||||
const selectedOptions = baseSeverityOptions.value.filter((s) => s?.id === selectedId)
|
||||
|
||||
const mergedMap = new Map<number, any>()
|
||||
;[...fetchedOptions, ...selectedOptions].forEach((item) => {
|
||||
if (typeof item?.id === 'number') {
|
||||
mergedMap.set(item.id, item)
|
||||
}
|
||||
})
|
||||
|
||||
severityOptions.value = Array.from(mergedMap.values())
|
||||
} catch (error) {
|
||||
console.error('获取告警级别失败:', error)
|
||||
} finally {
|
||||
@@ -261,6 +289,17 @@ const loadSeverityOptions = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
let severitySearchTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const handleSeveritySearch = (keyword: string) => {
|
||||
if (severitySearchTimer) {
|
||||
clearTimeout(severitySearchTimer)
|
||||
}
|
||||
|
||||
severitySearchTimer = setTimeout(() => {
|
||||
loadSeverityOptions(keyword)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// 规则类型变化处理
|
||||
const handleRuleTypeChange = (value: string) => {
|
||||
// 根据规则类型清空相关字段
|
||||
|
||||
@@ -121,11 +121,7 @@
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { IconPlus } from '@arco-design/web-vue/es/icon'
|
||||
import {
|
||||
fetchRuleList,
|
||||
deleteRule,
|
||||
fetchSeverityList,
|
||||
} from '@/api/ops/alertPolicy'
|
||||
import { fetchRuleList, deleteRule } from '@/api/ops/alertPolicy'
|
||||
import RuleFormDialog from './RuleFormDialog.vue'
|
||||
|
||||
// Props
|
||||
|
||||
@@ -169,7 +169,7 @@ import SearchTable from '@/components/search-table/index.vue'
|
||||
import { searchFormConfig } from './config/search-form'
|
||||
import { columns } from './config/columns'
|
||||
import { fetchAlertRecords } from '@/api/ops/alertRecord'
|
||||
import { fetchAlertLevelList } from '@/api/ops/alertLevel'
|
||||
import { fetchAlertLevelOptions } from '@/api/ops/alertLevel'
|
||||
|
||||
import AckDialog from './components/AckDialog.vue'
|
||||
import ResolveDialog from './components/ResolveDialog.vue'
|
||||
@@ -223,9 +223,11 @@ onMounted(async () => {
|
||||
|
||||
const loadSeverityOptions = async () => {
|
||||
try {
|
||||
const res = await fetchAlertLevelList({ page: 1, page_size: 100 })
|
||||
const list = res.details?.data ?? (res as any).data ?? []
|
||||
severityOptions.value = list.map((item: any) => ({
|
||||
const res: any = await fetchAlertLevelOptions({ enabled: 'true' })
|
||||
const list = Array.isArray(res?.details)
|
||||
? res.details
|
||||
: res?.details?.data || res?.details?.list || res?.data || res || []
|
||||
severityOptions.value = (Array.isArray(list) ? list : []).map((item: any) => ({
|
||||
label: item.name || item.code,
|
||||
value: item.id,
|
||||
}))
|
||||
|
||||
@@ -55,8 +55,17 @@
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<a-form-item label="告警级别" :field="`rules[${index}].severity_code`" :rules="getRuleFieldRules('severity_code')">
|
||||
<a-select v-model="rule.severity_code" placeholder="请选择" @change="syncUpdate(index)">
|
||||
<a-option v-for="item in severityOptions" :key="item.code" :value="item.code">
|
||||
<a-select
|
||||
v-model="rule.severity_code"
|
||||
placeholder="请选择"
|
||||
:loading="severityLoading"
|
||||
allow-search
|
||||
:filter-option="false"
|
||||
@search="handleSeveritySearch"
|
||||
@dropdown-visible-change="handleSeverityDropdownChange"
|
||||
@change="syncUpdate(index)"
|
||||
>
|
||||
<a-option v-for="item in localSeverityOptions" :key="item.code" :value="item.code">
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
@@ -125,6 +134,7 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import type { FormInstance } from '@arco-design/web-vue'
|
||||
import {
|
||||
fetchSeverityOptions,
|
||||
fetchMetricsMeta,
|
||||
DATA_SOURCES,
|
||||
COMPARE_OPERATORS,
|
||||
@@ -155,6 +165,11 @@ const ruleFormRef = ref<FormInstance | null>(null)
|
||||
// 本地规则数据(深拷贝)
|
||||
const localRules = ref<RuleItem[]>([])
|
||||
|
||||
// 告警级别下拉选项(支持搜索后局部刷新)
|
||||
const baseSeverityOptions = ref<AlertSeverity[]>([])
|
||||
const localSeverityOptions = ref<AlertSeverity[]>([])
|
||||
const severityLoading = ref(false)
|
||||
|
||||
// 监听 props 变化,深拷贝到本地
|
||||
watch(
|
||||
() => props.rules,
|
||||
@@ -164,6 +179,70 @@ watch(
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
// 初始化/同步告警级别选项
|
||||
watch(
|
||||
() => props.severityOptions,
|
||||
(val) => {
|
||||
baseSeverityOptions.value = val || []
|
||||
localSeverityOptions.value = val || []
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
let severitySearchTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const loadSeverityOptions = async (keyword?: string) => {
|
||||
severityLoading.value = true
|
||||
try {
|
||||
const res: any = await fetchSeverityOptions({
|
||||
keyword: keyword || undefined,
|
||||
enabled: 'true',
|
||||
})
|
||||
|
||||
const list = Array.isArray(res?.details)
|
||||
? res.details
|
||||
: res?.details?.data || res?.details?.list || res?.data || res
|
||||
const fetchedOptions = Array.isArray(list) ? list : []
|
||||
|
||||
// 合并当前已选值,防止搜索后 options 缩小导致已选项标签无法展示
|
||||
const selectedCodes = new Set(
|
||||
localRules.value
|
||||
.map((r) => r?.severity_code)
|
||||
.filter((v): v is string => typeof v === 'string' && v.length > 0),
|
||||
)
|
||||
const selectedOptions = baseSeverityOptions.value.filter((s) => selectedCodes.has(s.code))
|
||||
|
||||
const mergedMap = new Map<string, AlertSeverity>()
|
||||
;[...fetchedOptions, ...selectedOptions].forEach((item) => {
|
||||
if (item?.code) {
|
||||
mergedMap.set(item.code, item)
|
||||
}
|
||||
})
|
||||
|
||||
localSeverityOptions.value = Array.from(mergedMap.values())
|
||||
} catch (error) {
|
||||
console.error('加载告警级别下拉选项失败:', error)
|
||||
} finally {
|
||||
severityLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSeverityDropdownChange = (visible: boolean) => {
|
||||
if (visible && localSeverityOptions.value.length === 0) {
|
||||
loadSeverityOptions()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSeveritySearch = (keyword: string) => {
|
||||
if (severitySearchTimer) {
|
||||
clearTimeout(severitySearchTimer)
|
||||
}
|
||||
|
||||
severitySearchTimer = setTimeout(() => {
|
||||
loadSeverityOptions(keyword)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// 动态生成验证规则
|
||||
const getRuleFieldRules = (field: string) => {
|
||||
const messages: Record<string, string> = {
|
||||
|
||||
@@ -163,7 +163,7 @@ import {
|
||||
fetchTemplateDetail,
|
||||
fetchChannelList,
|
||||
fetchSuppressionList,
|
||||
fetchSeverityList,
|
||||
fetchSeverityOptions,
|
||||
fetchMetricsMeta,
|
||||
TEMPLATE_CATEGORIES,
|
||||
type AlertTemplate,
|
||||
@@ -330,10 +330,12 @@ const loadMetricsForRule = async (index: number) => {
|
||||
// 加载告警级别
|
||||
const loadSeverityOptions = async () => {
|
||||
try {
|
||||
const res: any = await fetchSeverityList()
|
||||
if (res.code === 0) {
|
||||
severityOptions.value = res.details?.data || res.details?.list || []
|
||||
}
|
||||
const res: any = await fetchSeverityOptions({ enabled: 'true' })
|
||||
// 兼容:后端可能返回数组,也可能是 { code, details: { data } }
|
||||
const list = Array.isArray(res?.details)
|
||||
? res.details
|
||||
: res?.details?.data || res?.details?.list || res?.data || res
|
||||
severityOptions.value = Array.isArray(list) ? list : []
|
||||
} catch (error) {
|
||||
console.error('加载告警级别失败:', error)
|
||||
}
|
||||
|
||||
245
src/views/ops/pages/log-mgmt/entries/index.vue
Normal file
245
src/views/ops/pages/log-mgmt/entries/index.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="日志查询"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
:show-download="false"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #source_kind="{ record }">
|
||||
<a-tag>{{ sourceKindLabel(record.source_kind) }}</a-tag>
|
||||
</template>
|
||||
<template #raw_payload="{ record }">
|
||||
<span class="ellipsis-cell" :title="record.raw_payload">{{ truncate(record.raw_payload, 80) }}</span>
|
||||
</template>
|
||||
<template #alert_sent="{ record }">
|
||||
<a-tag :color="record.alert_sent ? 'green' : 'gray'">
|
||||
{{ record.alert_sent ? '已转发' : '否' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #operations="{ record }">
|
||||
<a-button type="text" size="small" @click="openDetail(record)">详情</a-button>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<a-drawer
|
||||
v-model:visible="detailVisible"
|
||||
width="640px"
|
||||
:title="detailRow ? `日志详情 #${detailRow.id}` : '日志详情'"
|
||||
:footer="false"
|
||||
unmount-on-close
|
||||
>
|
||||
<template v-if="detailRow">
|
||||
<a-descriptions :column="1" bordered size="large">
|
||||
<a-descriptions-item label="来源类型">
|
||||
{{ sourceKindLabel(detailRow.source_kind) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采集时间">{{ detailRow.created_at }}</a-descriptions-item>
|
||||
<a-descriptions-item label="来源地址">{{ detailRow.remote_addr || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="设备名">{{ detailRow.device_name || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="严重级别">{{ detailRow.severity_code || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="Trap OID">{{ detailRow.trap_oid || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="已转发告警">
|
||||
{{ detailRow.alert_sent ? '是' : '否' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="摘要">{{ detailRow.normalized_summary || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="详情">
|
||||
<pre class="pre-block">{{ detailRow.normalized_detail || '-' }}</pre>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="原始报文">
|
||||
<pre class="pre-block">{{ detailRow.raw_payload || '-' }}</pre>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</template>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import {
|
||||
fetchLogEntries,
|
||||
unwrapLogsPayload,
|
||||
type LogEvent,
|
||||
} from '@/api/ops/logs'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<LogEvent[]>([])
|
||||
const formModel = ref({
|
||||
source_kind: '' as string,
|
||||
})
|
||||
const detailVisible = ref(false)
|
||||
const detailRow = ref<LogEvent | null>(null)
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 50,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
const formItems = computed<FormItem[]>(() => [
|
||||
{
|
||||
field: 'source_kind',
|
||||
label: '来源类型',
|
||||
type: 'select',
|
||||
placeholder: '全部',
|
||||
span: 6,
|
||||
options: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: 'Syslog', value: 'syslog' },
|
||||
{ label: 'SNMP Trap', value: 'snmp_trap' },
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
const columns = computed<TableColumnData[]>(() => [
|
||||
{ title: 'ID', dataIndex: 'id', width: 80 },
|
||||
{
|
||||
title: '来源',
|
||||
dataIndex: 'source_kind',
|
||||
slotName: 'source_kind',
|
||||
width: 110,
|
||||
},
|
||||
{ title: '时间', dataIndex: 'created_at', width: 180, ellipsis: true, tooltip: true },
|
||||
{ title: '来源地址', dataIndex: 'remote_addr', width: 130, ellipsis: true, tooltip: true },
|
||||
{ title: '设备', dataIndex: 'device_name', width: 120, ellipsis: true, tooltip: true },
|
||||
{ title: '级别', dataIndex: 'severity_code', width: 90 },
|
||||
{ title: 'OID', dataIndex: 'trap_oid', width: 140, ellipsis: true, tooltip: true },
|
||||
{
|
||||
title: '原始报文',
|
||||
dataIndex: 'raw_payload',
|
||||
slotName: 'raw_payload',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '已告警',
|
||||
dataIndex: 'alert_sent',
|
||||
slotName: 'alert_sent',
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operations',
|
||||
slotName: 'operations',
|
||||
width: 88,
|
||||
fixed: 'right',
|
||||
},
|
||||
])
|
||||
|
||||
function sourceKindLabel(k: string) {
|
||||
if (k === 'syslog') return 'Syslog'
|
||||
if (k === 'snmp_trap') return 'SNMP Trap'
|
||||
return k || '-'
|
||||
}
|
||||
|
||||
function truncate(s: string, n: number) {
|
||||
if (!s) return '-'
|
||||
return s.length <= n ? s : `${s.slice(0, n)}…`
|
||||
}
|
||||
|
||||
function handleFormModelUpdate(v: Record<string, unknown>) {
|
||||
formModel.value = v as { source_kind: string }
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await fetchLogEntries({
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize,
|
||||
source_kind: formModel.value.source_kind || undefined,
|
||||
})
|
||||
const payload = unwrapLogsPayload(res) ?? {}
|
||||
tableData.value = payload.items ?? []
|
||||
pagination.total = payload.total ?? 0
|
||||
if (typeof res.code === 'number' && res.code !== 0) {
|
||||
Message.error(res.message || res.msg || '加载失败')
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
Message.error(e?.message || '加载失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.current = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
formModel.value = { source_kind: '' }
|
||||
pagination.current = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handlePageChange(c: number) {
|
||||
pagination.current = c
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handlePageSizeChange(size: number) {
|
||||
pagination.pageSize = size
|
||||
pagination.current = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function openDetail(row: LogEvent) {
|
||||
detailRow.value = row
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
fetchList()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'LogMgmtEntries' }
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.pre-block {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
max-height: 280px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.ellipsis-cell {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
455
src/views/ops/pages/log-mgmt/syslog-rules/index.vue
Normal file
455
src/views/ops/pages/log-mgmt/syslog-rules/index.vue
Normal file
@@ -0,0 +1,455 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="clientPagination"
|
||||
title="Syslog 匹配规则"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
:show-download="false"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@refresh="fetchList"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-button type="primary" @click="openCreate">
|
||||
<template #icon><icon-plus /></template>
|
||||
新建规则
|
||||
</a-button>
|
||||
</template>
|
||||
<template #enabled="{ record }">
|
||||
<a-tag :color="record.enabled ? 'green' : 'red'">{{ record.enabled ? '启用' : '禁用' }}</a-tag>
|
||||
</template>
|
||||
<template #operations="{ record }">
|
||||
<a-space>
|
||||
<a-button type="text" size="small" @click="openEdit(record)">编辑</a-button>
|
||||
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="modalVisible"
|
||||
:title="editingId ? `编辑规则 #${editingId}` : '新建 Syslog 规则'"
|
||||
width="640px"
|
||||
:confirm-loading="submitting"
|
||||
@ok="submitForm"
|
||||
@cancel="modalVisible = false"
|
||||
>
|
||||
<a-form ref="formRef" :model="formData" layout="vertical">
|
||||
<a-form-item field="name" label="规则名称" :rules="[{ required: true, message: '必填' }]">
|
||||
<a-input v-model="formData.name" placeholder="规则名称" />
|
||||
</a-form-item>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="enabled" label="启用">
|
||||
<a-switch v-model="formData.enabled" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="priority" label="优先级(越大越优先)">
|
||||
<a-input-number v-model="formData.priority" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item field="device_name_contains" label="设备名包含">
|
||||
<a-input v-model="formData.device_name_contains" placeholder="子串匹配" />
|
||||
</a-form-item>
|
||||
<a-form-item field="keyword_regex" label="关键字正则">
|
||||
<a-input v-model="formData.keyword_regex" placeholder="正则表达式" />
|
||||
</a-form-item>
|
||||
<a-form-item field="alert_name" label="告警名称">
|
||||
<a-input v-model="formData.alert_name" placeholder="匹配后告警标题" />
|
||||
</a-form-item>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="severity_code" label="严重级别代码">
|
||||
<a-select
|
||||
v-model="formData.severity_code"
|
||||
placeholder="请选择告警级别"
|
||||
:loading="severityLoading"
|
||||
allow-search
|
||||
:filter-option="false"
|
||||
@search="handleSeveritySearch"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in severityOptions"
|
||||
:key="item.code"
|
||||
:value="item.code"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="policy_id" label="告警策略">
|
||||
<a-select
|
||||
v-model="formData.policy_id"
|
||||
placeholder="请选择告警策略"
|
||||
:loading="policyLoading"
|
||||
allow-search
|
||||
:filter-option="false"
|
||||
@search="handlePolicySearch"
|
||||
@change="handlePolicyChange"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in policyOptions"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconPlus } from '@arco-design/web-vue/es/icon'
|
||||
import { fetchSeverityOptions, type AlertSeverity } from '@/api/ops/alertTemplate'
|
||||
import { fetchPolicyList } from '@/api/ops/alertPolicy'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import type { FormInstance } from '@arco-design/web-vue'
|
||||
import {
|
||||
createSyslogRule,
|
||||
deleteSyslogRule,
|
||||
fetchSyslogRules,
|
||||
unwrapLogsPayload,
|
||||
updateSyslogRule,
|
||||
type SyslogRule,
|
||||
} from '@/api/ops/logs'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<SyslogRule[]>([])
|
||||
const allRows = ref<SyslogRule[]>([])
|
||||
const formModel = ref({ keyword: '' })
|
||||
|
||||
function handleFormModelUpdate(v: Record<string, unknown>) {
|
||||
formModel.value = v as { keyword: string }
|
||||
}
|
||||
const modalVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const editingId = ref<number | null>(null)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const emptyForm = (): SyslogRule => ({
|
||||
name: '',
|
||||
enabled: true,
|
||||
priority: 0,
|
||||
device_name_contains: '',
|
||||
keyword_regex: '',
|
||||
alert_name: '',
|
||||
severity_code: '',
|
||||
policy_id: 0,
|
||||
})
|
||||
|
||||
const formData = reactive<SyslogRule>(emptyForm())
|
||||
|
||||
const severityOptions = ref<AlertSeverity[]>([])
|
||||
const baseSeverityOptions = ref<AlertSeverity[]>([])
|
||||
const severityLoading = ref(false)
|
||||
let severitySearchTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
type PolicyOption = { id: number; name: string }
|
||||
const policyLoading = ref(false)
|
||||
const policyOptions = ref<PolicyOption[]>([{ id: 0, name: '未绑定' }])
|
||||
const basePolicyOptions = ref<PolicyOption[]>([{ id: 0, name: '未绑定' }])
|
||||
let policySearchTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
async function loadPolicyOptions(keyword?: string) {
|
||||
policyLoading.value = true
|
||||
try {
|
||||
const res: any = await fetchPolicyList({
|
||||
page: 1,
|
||||
page_size: keyword ? 50 : 1000,
|
||||
enabled: true,
|
||||
keyword: keyword || undefined,
|
||||
})
|
||||
|
||||
const list = Array.isArray(res?.details)
|
||||
? res.details
|
||||
: res?.details?.data ?? res?.details?.list ?? res?.data?.data ?? res?.data ?? []
|
||||
|
||||
const fetchedOptions: PolicyOption[] = (Array.isArray(list) ? list : [])
|
||||
.map((p: any) => ({ id: Number(p.id), name: p.name || '' }))
|
||||
.filter((p: PolicyOption) => !Number.isNaN(p.id))
|
||||
|
||||
const isBaseLoad = !keyword || keyword.trim().length === 0
|
||||
if (isBaseLoad) {
|
||||
basePolicyOptions.value = [{ id: 0, name: '未绑定' }, ...fetchedOptions]
|
||||
}
|
||||
|
||||
const selectedId = formData.policy_id
|
||||
const selectedOptions = basePolicyOptions.value.filter((p) => p.id === selectedId)
|
||||
|
||||
const mergedMap = new Map<number, PolicyOption>()
|
||||
;[...fetchedOptions, ...selectedOptions].forEach((item) => {
|
||||
mergedMap.set(item.id, item)
|
||||
})
|
||||
mergedMap.set(0, { id: 0, name: '未绑定' })
|
||||
|
||||
policyOptions.value = Array.from(mergedMap.values())
|
||||
} catch (e: any) {
|
||||
console.error('加载告警策略失败:', e)
|
||||
policyOptions.value = [{ id: 0, name: '未绑定' }]
|
||||
basePolicyOptions.value = [{ id: 0, name: '未绑定' }]
|
||||
} finally {
|
||||
policyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handlePolicySearch(keyword: string) {
|
||||
if (policySearchTimer) {
|
||||
clearTimeout(policySearchTimer)
|
||||
}
|
||||
|
||||
policySearchTimer = setTimeout(() => {
|
||||
loadPolicyOptions(keyword)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function handlePolicyChange(v: any) {
|
||||
formData.policy_id = Number(v)
|
||||
}
|
||||
|
||||
async function loadSeverityOptions(keyword?: string) {
|
||||
severityLoading.value = true
|
||||
try {
|
||||
const res: any = await fetchSeverityOptions({
|
||||
keyword,
|
||||
enabled: 'true',
|
||||
})
|
||||
const list = Array.isArray(res?.details)
|
||||
? res.details
|
||||
: res?.details?.data || res?.details?.list || res?.data || res
|
||||
|
||||
const fetchedOptions = Array.isArray(list) ? list : []
|
||||
|
||||
const isBaseLoad = !keyword || keyword.trim().length === 0
|
||||
if (isBaseLoad) {
|
||||
baseSeverityOptions.value = fetchedOptions
|
||||
}
|
||||
|
||||
const selectedCode = formData.severity_code
|
||||
const selectedOptions = baseSeverityOptions.value.filter((s) => s.code === selectedCode)
|
||||
|
||||
const mergedMap = new Map<string, AlertSeverity>()
|
||||
;[...fetchedOptions, ...selectedOptions].forEach((item) => {
|
||||
if (item?.code) mergedMap.set(item.code, item)
|
||||
})
|
||||
severityOptions.value = Array.from(mergedMap.values())
|
||||
} catch (e: any) {
|
||||
console.error('加载告警级别失败:', e)
|
||||
severityOptions.value = []
|
||||
} finally {
|
||||
severityLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSeveritySearch(keyword: string) {
|
||||
if (severitySearchTimer) {
|
||||
clearTimeout(severitySearchTimer)
|
||||
}
|
||||
|
||||
severitySearchTimer = setTimeout(() => {
|
||||
loadSeverityOptions(keyword)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const clientPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
const formItems = computed<FormItem[]>(() => [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '规则名 / 告警名',
|
||||
span: 8,
|
||||
},
|
||||
])
|
||||
|
||||
const columns = computed<TableColumnData[]>(() => [
|
||||
{ title: 'ID', dataIndex: 'id', width: 72 },
|
||||
{ title: '名称', dataIndex: 'name', ellipsis: true, tooltip: true, width: 140 },
|
||||
{ title: '优先级', dataIndex: 'priority', width: 80 },
|
||||
{ title: '启用', dataIndex: 'enabled', slotName: 'enabled', width: 88 },
|
||||
{ title: '设备名包含', dataIndex: 'device_name_contains', ellipsis: true, tooltip: true },
|
||||
{ title: '关键字正则', dataIndex: 'keyword_regex', ellipsis: true, tooltip: true },
|
||||
{ title: '告警名', dataIndex: 'alert_name', ellipsis: true, tooltip: true },
|
||||
{ title: '级别', dataIndex: 'severity_code', width: 90 },
|
||||
{ title: '策略ID', dataIndex: 'policy_id', width: 88 },
|
||||
{ title: '操作', slotName: 'operations', width: 160, fixed: 'right' },
|
||||
])
|
||||
|
||||
function applyFilter(list: SyslogRule[]) {
|
||||
const kw = formModel.value.keyword?.trim()
|
||||
if (!kw) return list
|
||||
return list.filter(
|
||||
r =>
|
||||
r.name?.includes(kw) ||
|
||||
r.alert_name?.includes(kw) ||
|
||||
r.keyword_regex?.includes(kw)
|
||||
)
|
||||
}
|
||||
|
||||
function slicePage(list: SyslogRule[]) {
|
||||
const start = (clientPagination.current - 1) * clientPagination.pageSize
|
||||
tableData.value = list.slice(start, start + clientPagination.pageSize)
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await fetchSyslogRules()
|
||||
const payload = unwrapLogsPayload(res)
|
||||
const items = (payload?.items ?? []) as SyslogRule[]
|
||||
allRows.value = items
|
||||
const filtered = applyFilter(items)
|
||||
clientPagination.total = filtered.length
|
||||
slicePage(filtered)
|
||||
if (typeof res.code === 'number' && res.code !== 0) {
|
||||
Message.error(res.message || res.msg || '加载失败')
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
Message.error(e?.message || '加载失败')
|
||||
allRows.value = []
|
||||
tableData.value = []
|
||||
clientPagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
clientPagination.current = 1
|
||||
const filtered = applyFilter(allRows.value)
|
||||
clientPagination.total = filtered.length
|
||||
slicePage(filtered)
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
formModel.value = { keyword: '' }
|
||||
clientPagination.current = 1
|
||||
const filtered = applyFilter(allRows.value)
|
||||
clientPagination.total = filtered.length
|
||||
slicePage(filtered)
|
||||
}
|
||||
|
||||
function handlePageChange(c: number) {
|
||||
clientPagination.current = c
|
||||
slicePage(applyFilter(allRows.value))
|
||||
}
|
||||
|
||||
function handlePageSizeChange(size: number) {
|
||||
clientPagination.pageSize = size
|
||||
clientPagination.current = 1
|
||||
slicePage(applyFilter(allRows.value))
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = null
|
||||
Object.assign(formData, emptyForm())
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(row: SyslogRule) {
|
||||
if (!row.id) return
|
||||
editingId.value = row.id
|
||||
Object.assign(formData, {
|
||||
name: row.name,
|
||||
enabled: row.enabled,
|
||||
priority: row.priority,
|
||||
device_name_contains: row.device_name_contains ?? '',
|
||||
keyword_regex: row.keyword_regex ?? '',
|
||||
alert_name: row.alert_name ?? '',
|
||||
severity_code: row.severity_code ?? '',
|
||||
policy_id: row.policy_id ?? 0,
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
const err = await formRef.value?.validate()
|
||||
if (err) return
|
||||
submitting.value = true
|
||||
try {
|
||||
if (editingId.value != null) {
|
||||
const res: any = await updateSyslogRule(editingId.value, { ...formData })
|
||||
if (typeof res.code === 'number' && res.code !== 0) {
|
||||
Message.error(res.message || res.msg || '保存失败')
|
||||
return
|
||||
}
|
||||
Message.success('已保存')
|
||||
} else {
|
||||
const res: any = await createSyslogRule({ ...formData })
|
||||
if (typeof res.code === 'number' && res.code !== 0) {
|
||||
Message.error(res.message || res.msg || '创建失败')
|
||||
return
|
||||
}
|
||||
Message.success('已创建')
|
||||
}
|
||||
modalVisible.value = false
|
||||
await fetchList()
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || '请求失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete(row: SyslogRule) {
|
||||
if (!row.id) return
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `删除规则「${row.name}」?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res: any = await deleteSyslogRule(row.id!)
|
||||
if (typeof res.code === 'number' && res.code !== 0) {
|
||||
Message.error(res.message || res.msg || '删除失败')
|
||||
return
|
||||
}
|
||||
Message.success('已删除')
|
||||
await fetchList()
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || '删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fetchList()
|
||||
loadSeverityOptions()
|
||||
loadPolicyOptions()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'LogMgmtSyslogRules' }
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
353
src/views/ops/pages/log-mgmt/trap-dictionary/index.vue
Normal file
353
src/views/ops/pages/log-mgmt/trap-dictionary/index.vue
Normal file
@@ -0,0 +1,353 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="clientPagination"
|
||||
title="Trap 字典"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
:show-download="false"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@refresh="fetchList"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-button type="primary" @click="openCreate">
|
||||
<template #icon><icon-plus /></template>
|
||||
新建条目
|
||||
</a-button>
|
||||
</template>
|
||||
<template #enabled="{ record }">
|
||||
<a-tag :color="record.enabled ? 'green' : 'red'">{{ record.enabled ? '启用' : '禁用' }}</a-tag>
|
||||
</template>
|
||||
<template #operations="{ record }">
|
||||
<a-space>
|
||||
<a-button type="text" size="small" @click="openEdit(record)">编辑</a-button>
|
||||
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="modalVisible"
|
||||
:title="editingId ? `编辑字典 #${editingId}` : '新建 Trap 字典条目'"
|
||||
width="640px"
|
||||
:confirm-loading="submitting"
|
||||
@ok="submitForm"
|
||||
@cancel="modalVisible = false"
|
||||
>
|
||||
<a-form ref="formRef" :model="formData" layout="vertical">
|
||||
<a-form-item
|
||||
field="oid_prefix"
|
||||
label="OID 前缀"
|
||||
:rules="[{ required: true, message: '必填' }]"
|
||||
>
|
||||
<a-input v-model="formData.oid_prefix" placeholder="唯一前缀" />
|
||||
</a-form-item>
|
||||
<a-form-item field="title" label="标题" :rules="[{ required: true, message: '必填' }]">
|
||||
<a-input v-model="formData.title" />
|
||||
</a-form-item>
|
||||
<a-form-item field="description" label="描述">
|
||||
<a-textarea v-model="formData.description" :rows="3" />
|
||||
</a-form-item>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="severity_code" label="严重级别代码">
|
||||
<a-select
|
||||
v-model="formData.severity_code"
|
||||
placeholder="请选择告警级别"
|
||||
:loading="severityLoading"
|
||||
allow-search
|
||||
:filter-option="false"
|
||||
@search="handleSeveritySearch"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in severityOptions"
|
||||
:key="item.code"
|
||||
:value="item.code"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="enabled" label="启用">
|
||||
<a-switch v-model="formData.enabled" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item field="recovery_message" label="恢复说明">
|
||||
<a-textarea v-model="formData.recovery_message" :rows="2" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconPlus } from '@arco-design/web-vue/es/icon'
|
||||
import { fetchSeverityOptions, type AlertSeverity } from '@/api/ops/alertTemplate'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import type { FormInstance } from '@arco-design/web-vue'
|
||||
import {
|
||||
createTrapDictionary,
|
||||
deleteTrapDictionary,
|
||||
fetchTrapDictionary,
|
||||
unwrapLogsPayload,
|
||||
updateTrapDictionary,
|
||||
type TrapDictionaryEntry,
|
||||
} from '@/api/ops/logs'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<TrapDictionaryEntry[]>([])
|
||||
const allRows = ref<TrapDictionaryEntry[]>([])
|
||||
const formModel = ref({ keyword: '' })
|
||||
|
||||
function handleFormModelUpdate(v: Record<string, unknown>) {
|
||||
formModel.value = v as { keyword: string }
|
||||
}
|
||||
const modalVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const editingId = ref<number | null>(null)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const emptyForm = (): TrapDictionaryEntry => ({
|
||||
oid_prefix: '',
|
||||
title: '',
|
||||
description: '',
|
||||
severity_code: '',
|
||||
recovery_message: '',
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
const formData = reactive<TrapDictionaryEntry>(emptyForm())
|
||||
|
||||
const severityOptions = ref<AlertSeverity[]>([])
|
||||
const baseSeverityOptions = ref<AlertSeverity[]>([])
|
||||
const severityLoading = ref(false)
|
||||
let severitySearchTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
async function loadSeverityOptions(keyword?: string) {
|
||||
severityLoading.value = true
|
||||
try {
|
||||
const res: any = await fetchSeverityOptions({
|
||||
keyword,
|
||||
enabled: 'true',
|
||||
})
|
||||
const list = Array.isArray(res?.details)
|
||||
? res.details
|
||||
: res?.details?.data || res?.details?.list || res?.data || res
|
||||
|
||||
const fetchedOptions = Array.isArray(list) ? list : []
|
||||
|
||||
const isBaseLoad = !keyword || keyword.trim().length === 0
|
||||
if (isBaseLoad) {
|
||||
baseSeverityOptions.value = fetchedOptions
|
||||
}
|
||||
|
||||
const selectedCode = formData.severity_code
|
||||
const selectedOptions = baseSeverityOptions.value.filter((s) => s.code === selectedCode)
|
||||
|
||||
const mergedMap = new Map<string, AlertSeverity>()
|
||||
;[...fetchedOptions, ...selectedOptions].forEach((item) => {
|
||||
if (item?.code) mergedMap.set(item.code, item)
|
||||
})
|
||||
severityOptions.value = Array.from(mergedMap.values())
|
||||
} catch (e: any) {
|
||||
console.error('加载告警级别失败:', e)
|
||||
severityOptions.value = []
|
||||
} finally {
|
||||
severityLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSeveritySearch(keyword: string) {
|
||||
if (severitySearchTimer) {
|
||||
clearTimeout(severitySearchTimer)
|
||||
}
|
||||
|
||||
severitySearchTimer = setTimeout(() => {
|
||||
loadSeverityOptions(keyword)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const clientPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
const formItems = computed<FormItem[]>(() => [
|
||||
{ field: 'keyword', label: '关键词', type: 'input', placeholder: 'OID / 标题', span: 8 },
|
||||
])
|
||||
|
||||
const columns = computed<TableColumnData[]>(() => [
|
||||
{ title: 'ID', dataIndex: 'id', width: 72 },
|
||||
{ title: 'OID 前缀', dataIndex: 'oid_prefix', ellipsis: true, tooltip: true },
|
||||
{ title: '标题', dataIndex: 'title', ellipsis: true, tooltip: true },
|
||||
{ title: '级别', dataIndex: 'severity_code', width: 90 },
|
||||
{ title: '启用', dataIndex: 'enabled', slotName: 'enabled', width: 88 },
|
||||
{ title: '描述', dataIndex: 'description', ellipsis: true, tooltip: true },
|
||||
{ title: '操作', slotName: 'operations', width: 160, fixed: 'right' },
|
||||
])
|
||||
|
||||
function applyFilter(list: TrapDictionaryEntry[]) {
|
||||
const kw = formModel.value.keyword?.trim()
|
||||
if (!kw) return list
|
||||
return list.filter(
|
||||
r =>
|
||||
r.oid_prefix?.includes(kw) ||
|
||||
r.title?.includes(kw) ||
|
||||
r.description?.includes(kw)
|
||||
)
|
||||
}
|
||||
|
||||
function slicePage(list: TrapDictionaryEntry[]) {
|
||||
const start = (clientPagination.current - 1) * clientPagination.pageSize
|
||||
tableData.value = list.slice(start, start + clientPagination.pageSize)
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await fetchTrapDictionary()
|
||||
const payload = unwrapLogsPayload(res)
|
||||
const items = (payload?.items ?? []) as TrapDictionaryEntry[]
|
||||
allRows.value = items
|
||||
const filtered = applyFilter(items)
|
||||
clientPagination.total = filtered.length
|
||||
slicePage(filtered)
|
||||
if (typeof res.code === 'number' && res.code !== 0) {
|
||||
Message.error(res.message || res.msg || '加载失败')
|
||||
}
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || '加载失败')
|
||||
allRows.value = []
|
||||
tableData.value = []
|
||||
clientPagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
clientPagination.current = 1
|
||||
const filtered = applyFilter(allRows.value)
|
||||
clientPagination.total = filtered.length
|
||||
slicePage(filtered)
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
formModel.value = { keyword: '' }
|
||||
clientPagination.current = 1
|
||||
const filtered = applyFilter(allRows.value)
|
||||
clientPagination.total = filtered.length
|
||||
slicePage(filtered)
|
||||
}
|
||||
|
||||
function handlePageChange(c: number) {
|
||||
clientPagination.current = c
|
||||
slicePage(applyFilter(allRows.value))
|
||||
}
|
||||
|
||||
function handlePageSizeChange(size: number) {
|
||||
clientPagination.pageSize = size
|
||||
clientPagination.current = 1
|
||||
slicePage(applyFilter(allRows.value))
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = null
|
||||
Object.assign(formData, emptyForm())
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(row: TrapDictionaryEntry) {
|
||||
if (!row.id) return
|
||||
editingId.value = row.id
|
||||
Object.assign(formData, {
|
||||
oid_prefix: row.oid_prefix,
|
||||
title: row.title,
|
||||
description: row.description ?? '',
|
||||
severity_code: row.severity_code ?? '',
|
||||
recovery_message: row.recovery_message ?? '',
|
||||
enabled: row.enabled,
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
const err = await formRef.value?.validate()
|
||||
if (err) return
|
||||
submitting.value = true
|
||||
try {
|
||||
if (editingId.value != null) {
|
||||
const res: any = await updateTrapDictionary(editingId.value, { ...formData })
|
||||
if (typeof res.code === 'number' && res.code !== 0) {
|
||||
Message.error(res.message || res.msg || '保存失败')
|
||||
return
|
||||
}
|
||||
Message.success('已保存')
|
||||
} else {
|
||||
const res: any = await createTrapDictionary({ ...formData })
|
||||
if (typeof res.code === 'number' && res.code !== 0) {
|
||||
Message.error(res.message || res.msg || '创建失败')
|
||||
return
|
||||
}
|
||||
Message.success('已创建')
|
||||
}
|
||||
modalVisible.value = false
|
||||
await fetchList()
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || '请求失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete(row: TrapDictionaryEntry) {
|
||||
if (!row.id) return
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `删除 OID 前缀「${row.oid_prefix}」?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res: any = await deleteTrapDictionary(row.id!)
|
||||
if (typeof res.code === 'number' && res.code !== 0) {
|
||||
Message.error(res.message || res.msg || '删除失败')
|
||||
return
|
||||
}
|
||||
Message.success('已删除')
|
||||
await fetchList()
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || '删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fetchList()
|
||||
loadSeverityOptions()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'LogMgmtTrapDictionary' }
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
447
src/views/ops/pages/log-mgmt/trap-rules/index.vue
Normal file
447
src/views/ops/pages/log-mgmt/trap-rules/index.vue
Normal file
@@ -0,0 +1,447 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="clientPagination"
|
||||
title="SNMP Trap 匹配规则"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
:show-download="false"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@refresh="fetchList"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-button type="primary" @click="openCreate">
|
||||
<template #icon><icon-plus /></template>
|
||||
新建规则
|
||||
</a-button>
|
||||
</template>
|
||||
<template #enabled="{ record }">
|
||||
<a-tag :color="record.enabled ? 'green' : 'red'">{{ record.enabled ? '启用' : '禁用' }}</a-tag>
|
||||
</template>
|
||||
<template #operations="{ record }">
|
||||
<a-space>
|
||||
<a-button type="text" size="small" @click="openEdit(record)">编辑</a-button>
|
||||
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="modalVisible"
|
||||
:title="editingId ? `编辑规则 #${editingId}` : '新建 Trap 规则'"
|
||||
width="640px"
|
||||
:confirm-loading="submitting"
|
||||
@ok="submitForm"
|
||||
@cancel="modalVisible = false"
|
||||
>
|
||||
<a-form ref="formRef" :model="formData" layout="vertical">
|
||||
<a-form-item field="name" label="规则名称" :rules="[{ required: true, message: '必填' }]">
|
||||
<a-input v-model="formData.name" placeholder="规则名称" />
|
||||
</a-form-item>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="enabled" label="启用">
|
||||
<a-switch v-model="formData.enabled" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="priority" label="优先级">
|
||||
<a-input-number v-model="formData.priority" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item field="oid_prefix" label="OID 前缀">
|
||||
<a-input v-model="formData.oid_prefix" placeholder="如 1.3.6.1.4.1." />
|
||||
</a-form-item>
|
||||
<a-form-item field="varbind_match_regex" label="Varbind 匹配正则">
|
||||
<a-input v-model="formData.varbind_match_regex" placeholder="可选" />
|
||||
</a-form-item>
|
||||
<a-form-item field="alert_name" label="告警名称">
|
||||
<a-input v-model="formData.alert_name" />
|
||||
</a-form-item>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="severity_code" label="严重级别代码">
|
||||
<a-select
|
||||
v-model="formData.severity_code"
|
||||
placeholder="请选择告警级别"
|
||||
:loading="severityLoading"
|
||||
allow-search
|
||||
:filter-option="false"
|
||||
@search="handleSeveritySearch"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in severityOptions"
|
||||
:key="item.code"
|
||||
:value="item.code"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="policy_id" label="策略 ID">
|
||||
<a-select
|
||||
v-model="formData.policy_id"
|
||||
placeholder="请选择告警策略"
|
||||
:loading="policyLoading"
|
||||
allow-search
|
||||
:filter-option="false"
|
||||
@search="handlePolicySearch"
|
||||
@change="handlePolicyChange"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in policyOptions"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconPlus } from '@arco-design/web-vue/es/icon'
|
||||
import { fetchPolicyList } from '@/api/ops/alertPolicy'
|
||||
import { fetchSeverityOptions, type AlertSeverity } from '@/api/ops/alertTemplate'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import type { FormInstance } from '@arco-design/web-vue'
|
||||
import {
|
||||
createTrapRule,
|
||||
deleteTrapRule,
|
||||
fetchTrapRules,
|
||||
unwrapLogsPayload,
|
||||
updateTrapRule,
|
||||
type TrapRule,
|
||||
} from '@/api/ops/logs'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<TrapRule[]>([])
|
||||
const allRows = ref<TrapRule[]>([])
|
||||
const formModel = ref({ keyword: '' })
|
||||
|
||||
function handleFormModelUpdate(v: Record<string, unknown>) {
|
||||
formModel.value = v as { keyword: string }
|
||||
}
|
||||
const modalVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const editingId = ref<number | null>(null)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const emptyForm = (): TrapRule => ({
|
||||
name: '',
|
||||
enabled: true,
|
||||
priority: 0,
|
||||
oid_prefix: '',
|
||||
varbind_match_regex: '',
|
||||
alert_name: '',
|
||||
severity_code: '',
|
||||
policy_id: 0,
|
||||
})
|
||||
|
||||
const formData = reactive<TrapRule>(emptyForm())
|
||||
|
||||
const severityOptions = ref<AlertSeverity[]>([])
|
||||
const baseSeverityOptions = ref<AlertSeverity[]>([])
|
||||
const severityLoading = ref(false)
|
||||
let severitySearchTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
type PolicyOption = { id: number; name: string }
|
||||
const policyLoading = ref(false)
|
||||
const policyOptions = ref<PolicyOption[]>([{ id: 0, name: '未绑定' }])
|
||||
const basePolicyOptions = ref<PolicyOption[]>([{ id: 0, name: '未绑定' }])
|
||||
let policySearchTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
async function loadPolicyOptions(keyword?: string) {
|
||||
policyLoading.value = true
|
||||
try {
|
||||
const res: any = await fetchPolicyList({
|
||||
page: 1,
|
||||
page_size: keyword ? 50 : 1000,
|
||||
enabled: true,
|
||||
keyword: keyword || undefined,
|
||||
})
|
||||
|
||||
const list = Array.isArray(res?.details)
|
||||
? res.details
|
||||
: res?.details?.data ?? res?.details?.list ?? res?.data?.data ?? res?.data ?? []
|
||||
|
||||
const fetchedOptions: PolicyOption[] = (Array.isArray(list) ? list : [])
|
||||
.map((p: any) => ({ id: Number(p.id), name: p.name || '' }))
|
||||
.filter((p: PolicyOption) => !Number.isNaN(p.id))
|
||||
|
||||
const isBaseLoad = !keyword || keyword.trim().length === 0
|
||||
if (isBaseLoad) {
|
||||
basePolicyOptions.value = [{ id: 0, name: '未绑定' }, ...fetchedOptions]
|
||||
}
|
||||
|
||||
const selectedId = formData.policy_id
|
||||
const selectedOptions = basePolicyOptions.value.filter((p) => p.id === selectedId)
|
||||
|
||||
const mergedMap = new Map<number, PolicyOption>()
|
||||
;[...fetchedOptions, ...selectedOptions].forEach((item) => {
|
||||
mergedMap.set(item.id, item)
|
||||
})
|
||||
mergedMap.set(0, { id: 0, name: '未绑定' })
|
||||
policyOptions.value = Array.from(mergedMap.values())
|
||||
} catch (e: any) {
|
||||
console.error('加载告警策略失败:', e)
|
||||
policyOptions.value = [{ id: 0, name: '未绑定' }]
|
||||
basePolicyOptions.value = [{ id: 0, name: '未绑定' }]
|
||||
} finally {
|
||||
policyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handlePolicySearch(keyword: string) {
|
||||
if (policySearchTimer) {
|
||||
clearTimeout(policySearchTimer)
|
||||
}
|
||||
|
||||
policySearchTimer = setTimeout(() => {
|
||||
loadPolicyOptions(keyword)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function handlePolicyChange(v: any) {
|
||||
formData.policy_id = Number(v)
|
||||
}
|
||||
|
||||
async function loadSeverityOptions(keyword?: string) {
|
||||
severityLoading.value = true
|
||||
try {
|
||||
const res: any = await fetchSeverityOptions({
|
||||
keyword,
|
||||
enabled: 'true',
|
||||
})
|
||||
const list = Array.isArray(res?.details)
|
||||
? res.details
|
||||
: res?.details?.data || res?.details?.list || res?.data || res
|
||||
|
||||
const fetchedOptions = Array.isArray(list) ? list : []
|
||||
|
||||
const isBaseLoad = !keyword || keyword.trim().length === 0
|
||||
if (isBaseLoad) {
|
||||
baseSeverityOptions.value = fetchedOptions
|
||||
}
|
||||
|
||||
const selectedCode = formData.severity_code
|
||||
const selectedOptions = baseSeverityOptions.value.filter((s) => s.code === selectedCode)
|
||||
|
||||
const mergedMap = new Map<string, AlertSeverity>()
|
||||
;[...fetchedOptions, ...selectedOptions].forEach((item) => {
|
||||
if (item?.code) mergedMap.set(item.code, item)
|
||||
})
|
||||
severityOptions.value = Array.from(mergedMap.values())
|
||||
} catch (e: any) {
|
||||
console.error('加载告警级别失败:', e)
|
||||
severityOptions.value = []
|
||||
} finally {
|
||||
severityLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSeveritySearch(keyword: string) {
|
||||
if (severitySearchTimer) {
|
||||
clearTimeout(severitySearchTimer)
|
||||
}
|
||||
|
||||
severitySearchTimer = setTimeout(() => {
|
||||
loadSeverityOptions(keyword)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const clientPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
const formItems = computed<FormItem[]>(() => [
|
||||
{ field: 'keyword', label: '关键词', type: 'input', placeholder: '规则名 / OID', span: 8 },
|
||||
])
|
||||
|
||||
const columns = computed<TableColumnData[]>(() => [
|
||||
{ title: 'ID', dataIndex: 'id', width: 72 },
|
||||
{ title: '名称', dataIndex: 'name', ellipsis: true, tooltip: true },
|
||||
{ title: '优先级', dataIndex: 'priority', width: 80 },
|
||||
{ title: '启用', dataIndex: 'enabled', slotName: 'enabled', width: 88 },
|
||||
{ title: 'OID 前缀', dataIndex: 'oid_prefix', ellipsis: true, tooltip: true },
|
||||
{ title: 'Varbind 正则', dataIndex: 'varbind_match_regex', ellipsis: true, tooltip: true },
|
||||
{ title: '告警名', dataIndex: 'alert_name', ellipsis: true, tooltip: true },
|
||||
{ title: '级别', dataIndex: 'severity_code', width: 90 },
|
||||
{ title: '策略ID', dataIndex: 'policy_id', width: 88 },
|
||||
{ title: '操作', slotName: 'operations', width: 160, fixed: 'right' },
|
||||
])
|
||||
|
||||
function applyFilter(list: TrapRule[]) {
|
||||
const kw = formModel.value.keyword?.trim()
|
||||
if (!kw) return list
|
||||
return list.filter(
|
||||
r =>
|
||||
r.name?.includes(kw) ||
|
||||
r.oid_prefix?.includes(kw) ||
|
||||
r.alert_name?.includes(kw)
|
||||
)
|
||||
}
|
||||
|
||||
function slicePage(list: TrapRule[]) {
|
||||
const start = (clientPagination.current - 1) * clientPagination.pageSize
|
||||
tableData.value = list.slice(start, start + clientPagination.pageSize)
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await fetchTrapRules()
|
||||
const payload = unwrapLogsPayload(res)
|
||||
const items = (payload?.items ?? []) as TrapRule[]
|
||||
allRows.value = items
|
||||
const filtered = applyFilter(items)
|
||||
clientPagination.total = filtered.length
|
||||
slicePage(filtered)
|
||||
if (typeof res.code === 'number' && res.code !== 0) {
|
||||
Message.error(res.message || res.msg || '加载失败')
|
||||
}
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || '加载失败')
|
||||
allRows.value = []
|
||||
tableData.value = []
|
||||
clientPagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
clientPagination.current = 1
|
||||
const filtered = applyFilter(allRows.value)
|
||||
clientPagination.total = filtered.length
|
||||
slicePage(filtered)
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
formModel.value = { keyword: '' }
|
||||
clientPagination.current = 1
|
||||
const filtered = applyFilter(allRows.value)
|
||||
clientPagination.total = filtered.length
|
||||
slicePage(filtered)
|
||||
}
|
||||
|
||||
function handlePageChange(c: number) {
|
||||
clientPagination.current = c
|
||||
slicePage(applyFilter(allRows.value))
|
||||
}
|
||||
|
||||
function handlePageSizeChange(size: number) {
|
||||
clientPagination.pageSize = size
|
||||
clientPagination.current = 1
|
||||
slicePage(applyFilter(allRows.value))
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = null
|
||||
Object.assign(formData, emptyForm())
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(row: TrapRule) {
|
||||
if (!row.id) return
|
||||
editingId.value = row.id
|
||||
Object.assign(formData, {
|
||||
name: row.name,
|
||||
enabled: row.enabled,
|
||||
priority: row.priority,
|
||||
oid_prefix: row.oid_prefix ?? '',
|
||||
varbind_match_regex: row.varbind_match_regex ?? '',
|
||||
alert_name: row.alert_name ?? '',
|
||||
severity_code: row.severity_code ?? '',
|
||||
policy_id: row.policy_id ?? 0,
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
const err = await formRef.value?.validate()
|
||||
if (err) return
|
||||
submitting.value = true
|
||||
try {
|
||||
if (editingId.value != null) {
|
||||
const res: any = await updateTrapRule(editingId.value, { ...formData })
|
||||
if (typeof res.code === 'number' && res.code !== 0) {
|
||||
Message.error(res.message || res.msg || '保存失败')
|
||||
return
|
||||
}
|
||||
Message.success('已保存')
|
||||
} else {
|
||||
const res: any = await createTrapRule({ ...formData })
|
||||
if (typeof res.code === 'number' && res.code !== 0) {
|
||||
Message.error(res.message || res.msg || '创建失败')
|
||||
return
|
||||
}
|
||||
Message.success('已创建')
|
||||
}
|
||||
modalVisible.value = false
|
||||
await fetchList()
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || '请求失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete(row: TrapRule) {
|
||||
if (!row.id) return
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `删除规则「${row.name}」?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res: any = await deleteTrapRule(row.id!)
|
||||
if (typeof res.code === 'number' && res.code !== 0) {
|
||||
Message.error(res.message || res.msg || '删除失败')
|
||||
return
|
||||
}
|
||||
Message.success('已删除')
|
||||
await fetchList()
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || '删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fetchList()
|
||||
loadSeverityOptions()
|
||||
loadPolicyOptions()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'LogMgmtTrapRules' }
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
290
src/views/ops/pages/log-mgmt/trap-suppressions/index.vue
Normal file
290
src/views/ops/pages/log-mgmt/trap-suppressions/index.vue
Normal file
@@ -0,0 +1,290 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="clientPagination"
|
||||
title="Trap 屏蔽"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
:show-download="false"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@refresh="fetchList"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-button type="primary" @click="openCreate">
|
||||
<template #icon><icon-plus /></template>
|
||||
新建屏蔽规则
|
||||
</a-button>
|
||||
</template>
|
||||
<template #enabled="{ record }">
|
||||
<a-tag :color="record.enabled ? 'green' : 'red'">{{ record.enabled ? '启用' : '禁用' }}</a-tag>
|
||||
</template>
|
||||
<template #operations="{ record }">
|
||||
<a-space>
|
||||
<a-button type="text" size="small" @click="openEdit(record)">编辑</a-button>
|
||||
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="modalVisible"
|
||||
:title="editingId ? `编辑屏蔽 #${editingId}` : '新建 Trap 屏蔽'"
|
||||
width="640px"
|
||||
:confirm-loading="submitting"
|
||||
@ok="submitForm"
|
||||
@cancel="modalVisible = false"
|
||||
>
|
||||
<a-form ref="formRef" :model="formData" layout="vertical">
|
||||
<a-form-item field="name" label="名称" :rules="[{ required: true, message: '必填' }]">
|
||||
<a-input v-model="formData.name" />
|
||||
</a-form-item>
|
||||
<a-form-item field="enabled" label="启用">
|
||||
<a-switch v-model="formData.enabled" />
|
||||
</a-form-item>
|
||||
<a-form-item field="source_ip_cidr" label="源 IP / CIDR">
|
||||
<a-input v-model="formData.source_ip_cidr" placeholder="如 192.168.1.0/24" />
|
||||
</a-form-item>
|
||||
<a-form-item field="oid_prefix" label="OID 前缀">
|
||||
<a-input v-model="formData.oid_prefix" />
|
||||
</a-form-item>
|
||||
<a-form-item field="interface_hint" label="接口提示">
|
||||
<a-input v-model="formData.interface_hint" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="time_windows_json"
|
||||
label="时间窗 JSON"
|
||||
extra="后端约定的 JSON 字符串,可为空"
|
||||
>
|
||||
<a-textarea v-model="formData.time_windows_json" :rows="4" placeholder="{}" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconPlus } from '@arco-design/web-vue/es/icon'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import type { FormInstance } from '@arco-design/web-vue'
|
||||
import {
|
||||
createTrapSuppression,
|
||||
deleteTrapSuppression,
|
||||
fetchTrapSuppressions,
|
||||
unwrapLogsPayload,
|
||||
updateTrapSuppression,
|
||||
type TrapShield,
|
||||
} from '@/api/ops/logs'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<TrapShield[]>([])
|
||||
const allRows = ref<TrapShield[]>([])
|
||||
const formModel = ref({ keyword: '' })
|
||||
|
||||
function handleFormModelUpdate(v: Record<string, unknown>) {
|
||||
formModel.value = v as { keyword: string }
|
||||
}
|
||||
const modalVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const editingId = ref<number | null>(null)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const emptyForm = (): TrapShield => ({
|
||||
name: '',
|
||||
enabled: true,
|
||||
source_ip_cidr: '',
|
||||
oid_prefix: '',
|
||||
interface_hint: '',
|
||||
time_windows_json: '',
|
||||
})
|
||||
|
||||
const formData = reactive<TrapShield>(emptyForm())
|
||||
|
||||
const clientPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
const formItems = computed<FormItem[]>(() => [
|
||||
{ field: 'keyword', label: '关键词', type: 'input', placeholder: '名称 / OID / IP', span: 8 },
|
||||
])
|
||||
|
||||
const columns = computed<TableColumnData[]>(() => [
|
||||
{ title: 'ID', dataIndex: 'id', width: 72 },
|
||||
{ title: '名称', dataIndex: 'name', ellipsis: true, tooltip: true },
|
||||
{ title: '启用', dataIndex: 'enabled', slotName: 'enabled', width: 88 },
|
||||
{ title: '源 IP/CIDR', dataIndex: 'source_ip_cidr', ellipsis: true, tooltip: true },
|
||||
{ title: 'OID 前缀', dataIndex: 'oid_prefix', ellipsis: true, tooltip: true },
|
||||
{ title: '接口提示', dataIndex: 'interface_hint', ellipsis: true, tooltip: true },
|
||||
{ title: '操作', slotName: 'operations', width: 160, fixed: 'right' },
|
||||
])
|
||||
|
||||
function applyFilter(list: TrapShield[]) {
|
||||
const kw = formModel.value.keyword?.trim()
|
||||
if (!kw) return list
|
||||
return list.filter(
|
||||
r =>
|
||||
r.name?.includes(kw) ||
|
||||
r.oid_prefix?.includes(kw) ||
|
||||
r.source_ip_cidr?.includes(kw)
|
||||
)
|
||||
}
|
||||
|
||||
function slicePage(list: TrapShield[]) {
|
||||
const start = (clientPagination.current - 1) * clientPagination.pageSize
|
||||
tableData.value = list.slice(start, start + clientPagination.pageSize)
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await fetchTrapSuppressions()
|
||||
const payload = unwrapLogsPayload(res)
|
||||
const items = (payload?.items ?? []) as TrapShield[]
|
||||
allRows.value = items
|
||||
const filtered = applyFilter(items)
|
||||
clientPagination.total = filtered.length
|
||||
slicePage(filtered)
|
||||
if (typeof res.code === 'number' && res.code !== 0) {
|
||||
Message.error(res.message || res.msg || '加载失败')
|
||||
}
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || '加载失败')
|
||||
allRows.value = []
|
||||
tableData.value = []
|
||||
clientPagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
clientPagination.current = 1
|
||||
const filtered = applyFilter(allRows.value)
|
||||
clientPagination.total = filtered.length
|
||||
slicePage(filtered)
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
formModel.value = { keyword: '' }
|
||||
clientPagination.current = 1
|
||||
const filtered = applyFilter(allRows.value)
|
||||
clientPagination.total = filtered.length
|
||||
slicePage(filtered)
|
||||
}
|
||||
|
||||
function handlePageChange(c: number) {
|
||||
clientPagination.current = c
|
||||
slicePage(applyFilter(allRows.value))
|
||||
}
|
||||
|
||||
function handlePageSizeChange(size: number) {
|
||||
clientPagination.pageSize = size
|
||||
clientPagination.current = 1
|
||||
slicePage(applyFilter(allRows.value))
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = null
|
||||
Object.assign(formData, emptyForm())
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(row: TrapShield) {
|
||||
if (!row.id) return
|
||||
editingId.value = row.id
|
||||
Object.assign(formData, {
|
||||
name: row.name,
|
||||
enabled: row.enabled,
|
||||
source_ip_cidr: row.source_ip_cidr ?? '',
|
||||
oid_prefix: row.oid_prefix ?? '',
|
||||
interface_hint: row.interface_hint ?? '',
|
||||
time_windows_json: row.time_windows_json ?? '',
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
const err = await formRef.value?.validate()
|
||||
if (err) return
|
||||
const tw = formData.time_windows_json?.trim()
|
||||
if (tw) {
|
||||
try {
|
||||
JSON.parse(tw)
|
||||
} catch {
|
||||
Message.warning('时间窗 JSON 格式无效')
|
||||
return
|
||||
}
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
if (editingId.value != null) {
|
||||
const res: any = await updateTrapSuppression(editingId.value, { ...formData })
|
||||
if (typeof res.code === 'number' && res.code !== 0) {
|
||||
Message.error(res.message || res.msg || '保存失败')
|
||||
return
|
||||
}
|
||||
Message.success('已保存')
|
||||
} else {
|
||||
const res: any = await createTrapSuppression({ ...formData })
|
||||
if (typeof res.code === 'number' && res.code !== 0) {
|
||||
Message.error(res.message || res.msg || '创建失败')
|
||||
return
|
||||
}
|
||||
Message.success('已创建')
|
||||
}
|
||||
modalVisible.value = false
|
||||
await fetchList()
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || '请求失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete(row: TrapShield) {
|
||||
if (!row.id) return
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `删除「${row.name}」?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res: any = await deleteTrapSuppression(row.id!)
|
||||
if (typeof res.code === 'number' && res.code !== 0) {
|
||||
Message.error(res.message || res.msg || '删除失败')
|
||||
return
|
||||
}
|
||||
Message.success('已删除')
|
||||
await fetchList()
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || '删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fetchList()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'LogMgmtTrapSuppressions' }
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,542 +1,11 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-row">
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-primary">
|
||||
<icon-file />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">今日日志</div>
|
||||
<div class="stats-value">{{ stats.total }}</div>
|
||||
<div class="stats-desc">总采集量</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-danger">
|
||||
<icon-close-circle-fill />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">错误日志</div>
|
||||
<div class="stats-value">{{ stats.error }}</div>
|
||||
<div class="stats-desc text-danger">较昨日 +12%</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-warning">
|
||||
<icon-exclamation-circle-fill />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">警告日志</div>
|
||||
<div class="stats-value">{{ stats.warning }}</div>
|
||||
<div class="stats-desc text-success">较昨日 -5%</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-primary">
|
||||
<icon-info-circle-fill />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">信息日志</div>
|
||||
<div class="stats-value">{{ stats.info }}</div>
|
||||
<div class="stats-desc">正常范围</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<a-row :gutter="16" class="chart-row">
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-card title="日志采集趋势" :bordered="false">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<span class="legend-item">
|
||||
<span class="legend-dot legend-dot-1"></span>
|
||||
<span>总量</span>
|
||||
</span>
|
||||
<span class="legend-item">
|
||||
<span class="legend-dot legend-dot-2"></span>
|
||||
<span>异常</span>
|
||||
</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<Chart :options="logTrendChartOptions" height="280px" />
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-card title="日志来源分布" :bordered="false">
|
||||
<template #extra>
|
||||
<span class="text-muted">按系统统计</span>
|
||||
</template>
|
||||
<div class="source-list">
|
||||
<div v-for="item in logSources" :key="item.name" class="source-item">
|
||||
<div class="source-header">
|
||||
<span class="source-name">{{ item.name }}</span>
|
||||
<span class="source-value">{{ item.value }} ({{ item.percent }}%)</span>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="item.percent"
|
||||
:stroke-width="8"
|
||||
:show-text="false"
|
||||
:color="item.color"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<a-card title="实时日志" :bordered="false">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-input-search
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索日志..."
|
||||
style="width: 250px"
|
||||
allow-clear
|
||||
/>
|
||||
<a-select v-model="selectedLevel" placeholder="全部级别" style="width: 120px">
|
||||
<a-option value="">全部级别</a-option>
|
||||
<a-option value="error">ERROR</a-option>
|
||||
<a-option value="warn">WARN</a-option>
|
||||
<a-option value="info">INFO</a-option>
|
||||
<a-option value="debug">DEBUG</a-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-table
|
||||
:data="filteredLogs"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="timestamp"
|
||||
>
|
||||
<!-- 级别列 -->
|
||||
<template #level="{ record }">
|
||||
<a-tag :color="getLevelColor(record.levelValue)" bordered>
|
||||
{{ record.levelText }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 消息列 -->
|
||||
<template #message="{ record }">
|
||||
<span class="log-message">{{ record.message }}</span>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
<LogMgmtEntries />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import {
|
||||
IconFile,
|
||||
IconCloseCircleFill,
|
||||
IconExclamationCircleFill,
|
||||
IconInfoCircleFill,
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
import Breadcrumb from '@/components/breadcrumb/index.vue'
|
||||
import Chart from '@/components/chart/index.vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
|
||||
// 统计数据
|
||||
const stats = ref({
|
||||
total: '2.4M',
|
||||
error: '1,234',
|
||||
warning: '3,567',
|
||||
info: '2.39M',
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const selectedLevel = ref('')
|
||||
|
||||
// 表格列配置
|
||||
const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'timestamp',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '级别',
|
||||
dataIndex: 'level',
|
||||
slotName: 'level',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '来源',
|
||||
dataIndex: 'source',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '消息内容',
|
||||
dataIndex: 'message',
|
||||
slotName: 'message',
|
||||
},
|
||||
{
|
||||
title: '次数',
|
||||
dataIndex: 'count',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
},
|
||||
]
|
||||
|
||||
// 日志数据
|
||||
const logData = ref([
|
||||
{
|
||||
timestamp: '2024-01-15 14:32:45',
|
||||
levelValue: 'error',
|
||||
levelText: 'ERROR',
|
||||
source: 'Web-Server-01',
|
||||
message: 'Connection refused: Unable to connect to database server',
|
||||
count: '156',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:32:12',
|
||||
levelValue: 'warn',
|
||||
levelText: 'WARN',
|
||||
source: 'API-Gateway',
|
||||
message: 'Rate limit exceeded for IP 192.168.1.100',
|
||||
count: '89',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:31:58',
|
||||
levelValue: 'info',
|
||||
levelText: 'INFO',
|
||||
source: 'Auth-Service',
|
||||
message: 'User login successful: admin@example.com',
|
||||
count: '1',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:31:45',
|
||||
levelValue: 'error',
|
||||
levelText: 'ERROR',
|
||||
source: 'Payment-Service',
|
||||
message: 'Transaction failed: Insufficient funds',
|
||||
count: '23',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:31:30',
|
||||
levelValue: 'warn',
|
||||
levelText: 'WARN',
|
||||
source: 'Storage-Server',
|
||||
message: 'Disk usage exceeded 85% threshold',
|
||||
count: '5',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:31:15',
|
||||
levelValue: 'info',
|
||||
levelText: 'INFO',
|
||||
source: 'Scheduler',
|
||||
message: 'Backup job completed successfully',
|
||||
count: '1',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:31:00',
|
||||
levelValue: 'critical',
|
||||
levelText: 'CRITICAL',
|
||||
source: 'Core-Router',
|
||||
message: 'Interface GigabitEthernet0/1 down',
|
||||
count: '1',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:30:45',
|
||||
levelValue: 'debug',
|
||||
levelText: 'DEBUG',
|
||||
source: 'Cache-Server',
|
||||
message: 'Cache hit ratio: 94.5%',
|
||||
count: '1',
|
||||
},
|
||||
])
|
||||
|
||||
// 过滤后的日志列表
|
||||
const filteredLogs = computed(() => {
|
||||
let result = logData.value
|
||||
|
||||
if (selectedLevel.value) {
|
||||
result = result.filter((log) => log.levelValue === selectedLevel.value)
|
||||
}
|
||||
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
result = result.filter(
|
||||
(log) =>
|
||||
log.message.toLowerCase().includes(query) ||
|
||||
log.source.toLowerCase().includes(query)
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// 日志来源分布数据
|
||||
const logSources = ref([
|
||||
{ name: 'Web服务器', value: '856K', percent: 35, color: '#165DFF' },
|
||||
{ name: '数据库', value: '624K', percent: 26, color: '#14C9C9' },
|
||||
{ name: '应用服务', value: '480K', percent: 20, color: '#F7BA1E' },
|
||||
{ name: '网络设备', value: '288K', percent: 12, color: '#722ED1' },
|
||||
{ name: '安全设备', value: '168K', percent: 7, color: '#F53F3F' },
|
||||
])
|
||||
|
||||
// 日志趋势图表配置
|
||||
const logTrendChartOptions = ref({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '24:00'],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '数量',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '总量',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [1200, 800, 2800, 4500, 3800, 2200, 1500],
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#165DFF',
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#165DFF',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '异常',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [89, 45, 156, 234, 189, 112, 78],
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#F53F3F',
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#F53F3F',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// 获取级别颜色
|
||||
const getLevelColor = (level: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
error: 'red',
|
||||
critical: 'red',
|
||||
warn: 'orange',
|
||||
info: 'green',
|
||||
debug: 'gray',
|
||||
}
|
||||
return colorMap[level] || 'gray'
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
// TODO: 从API获取数据
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
import LogMgmtEntries from '@/views/ops/pages/log-mgmt/entries/index.vue'
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'LogMonitor',
|
||||
}
|
||||
export default { name: 'MonitorLog' }
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
height: 100%;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
|
||||
&-primary {
|
||||
background-color: rgba(22, 93, 255, 0.1);
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
|
||||
&-success {
|
||||
background-color: rgba(0, 180, 42, 0.1);
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
&-danger {
|
||||
background-color: rgba(245, 63, 63, 0.1);
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
|
||||
&-warning {
|
||||
background-color: rgba(255, 125, 0, 0.1);
|
||||
color: rgb(var(--warning-6));
|
||||
}
|
||||
|
||||
&-muted {
|
||||
background-color: rgba(134, 144, 156, 0.1);
|
||||
color: rgb(var(--gray-6));
|
||||
}
|
||||
}
|
||||
|
||||
.stats-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
|
||||
&-1 {
|
||||
background-color: #165DFF;
|
||||
}
|
||||
|
||||
&-2 {
|
||||
background-color: #F53F3F;
|
||||
}
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
.source-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.source-item {
|
||||
.source-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.source-name {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.source-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
|
||||
.log-message {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
542
src/views/ops/pages/monitor/log/index_bak.vue
Normal file
542
src/views/ops/pages/monitor/log/index_bak.vue
Normal file
@@ -0,0 +1,542 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-row">
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-primary">
|
||||
<icon-file />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">今日日志</div>
|
||||
<div class="stats-value">{{ stats.total }}</div>
|
||||
<div class="stats-desc">总采集量</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-danger">
|
||||
<icon-close-circle-fill />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">错误日志</div>
|
||||
<div class="stats-value">{{ stats.error }}</div>
|
||||
<div class="stats-desc text-danger">较昨日 +12%</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-warning">
|
||||
<icon-exclamation-circle-fill />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">警告日志</div>
|
||||
<div class="stats-value">{{ stats.warning }}</div>
|
||||
<div class="stats-desc text-success">较昨日 -5%</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-primary">
|
||||
<icon-info-circle-fill />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">信息日志</div>
|
||||
<div class="stats-value">{{ stats.info }}</div>
|
||||
<div class="stats-desc">正常范围</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<a-row :gutter="16" class="chart-row">
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-card title="日志采集趋势" :bordered="false">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<span class="legend-item">
|
||||
<span class="legend-dot legend-dot-1"></span>
|
||||
<span>总量</span>
|
||||
</span>
|
||||
<span class="legend-item">
|
||||
<span class="legend-dot legend-dot-2"></span>
|
||||
<span>异常</span>
|
||||
</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<Chart :options="logTrendChartOptions" height="280px" />
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-card title="日志来源分布" :bordered="false">
|
||||
<template #extra>
|
||||
<span class="text-muted">按系统统计</span>
|
||||
</template>
|
||||
<div class="source-list">
|
||||
<div v-for="item in logSources" :key="item.name" class="source-item">
|
||||
<div class="source-header">
|
||||
<span class="source-name">{{ item.name }}</span>
|
||||
<span class="source-value">{{ item.value }} ({{ item.percent }}%)</span>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="item.percent"
|
||||
:stroke-width="8"
|
||||
:show-text="false"
|
||||
:color="item.color"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<a-card title="实时日志" :bordered="false">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-input-search
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索日志..."
|
||||
style="width: 250px"
|
||||
allow-clear
|
||||
/>
|
||||
<a-select v-model="selectedLevel" placeholder="全部级别" style="width: 120px">
|
||||
<a-option value="">全部级别</a-option>
|
||||
<a-option value="error">ERROR</a-option>
|
||||
<a-option value="warn">WARN</a-option>
|
||||
<a-option value="info">INFO</a-option>
|
||||
<a-option value="debug">DEBUG</a-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-table
|
||||
:data="filteredLogs"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="timestamp"
|
||||
>
|
||||
<!-- 级别列 -->
|
||||
<template #level="{ record }">
|
||||
<a-tag :color="getLevelColor(record.levelValue)" bordered>
|
||||
{{ record.levelText }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 消息列 -->
|
||||
<template #message="{ record }">
|
||||
<span class="log-message">{{ record.message }}</span>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import {
|
||||
IconFile,
|
||||
IconCloseCircleFill,
|
||||
IconExclamationCircleFill,
|
||||
IconInfoCircleFill,
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
import Breadcrumb from '@/components/breadcrumb/index.vue'
|
||||
import Chart from '@/components/chart/index.vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
|
||||
// 统计数据
|
||||
const stats = ref({
|
||||
total: '2.4M',
|
||||
error: '1,234',
|
||||
warning: '3,567',
|
||||
info: '2.39M',
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const selectedLevel = ref('')
|
||||
|
||||
// 表格列配置
|
||||
const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'timestamp',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '级别',
|
||||
dataIndex: 'level',
|
||||
slotName: 'level',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '来源',
|
||||
dataIndex: 'source',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '消息内容',
|
||||
dataIndex: 'message',
|
||||
slotName: 'message',
|
||||
},
|
||||
{
|
||||
title: '次数',
|
||||
dataIndex: 'count',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
},
|
||||
]
|
||||
|
||||
// 日志数据
|
||||
const logData = ref([
|
||||
{
|
||||
timestamp: '2024-01-15 14:32:45',
|
||||
levelValue: 'error',
|
||||
levelText: 'ERROR',
|
||||
source: 'Web-Server-01',
|
||||
message: 'Connection refused: Unable to connect to database server',
|
||||
count: '156',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:32:12',
|
||||
levelValue: 'warn',
|
||||
levelText: 'WARN',
|
||||
source: 'API-Gateway',
|
||||
message: 'Rate limit exceeded for IP 192.168.1.100',
|
||||
count: '89',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:31:58',
|
||||
levelValue: 'info',
|
||||
levelText: 'INFO',
|
||||
source: 'Auth-Service',
|
||||
message: 'User login successful: admin@example.com',
|
||||
count: '1',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:31:45',
|
||||
levelValue: 'error',
|
||||
levelText: 'ERROR',
|
||||
source: 'Payment-Service',
|
||||
message: 'Transaction failed: Insufficient funds',
|
||||
count: '23',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:31:30',
|
||||
levelValue: 'warn',
|
||||
levelText: 'WARN',
|
||||
source: 'Storage-Server',
|
||||
message: 'Disk usage exceeded 85% threshold',
|
||||
count: '5',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:31:15',
|
||||
levelValue: 'info',
|
||||
levelText: 'INFO',
|
||||
source: 'Scheduler',
|
||||
message: 'Backup job completed successfully',
|
||||
count: '1',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:31:00',
|
||||
levelValue: 'critical',
|
||||
levelText: 'CRITICAL',
|
||||
source: 'Core-Router',
|
||||
message: 'Interface GigabitEthernet0/1 down',
|
||||
count: '1',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:30:45',
|
||||
levelValue: 'debug',
|
||||
levelText: 'DEBUG',
|
||||
source: 'Cache-Server',
|
||||
message: 'Cache hit ratio: 94.5%',
|
||||
count: '1',
|
||||
},
|
||||
])
|
||||
|
||||
// 过滤后的日志列表
|
||||
const filteredLogs = computed(() => {
|
||||
let result = logData.value
|
||||
|
||||
if (selectedLevel.value) {
|
||||
result = result.filter((log) => log.levelValue === selectedLevel.value)
|
||||
}
|
||||
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
result = result.filter(
|
||||
(log) =>
|
||||
log.message.toLowerCase().includes(query) ||
|
||||
log.source.toLowerCase().includes(query)
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// 日志来源分布数据
|
||||
const logSources = ref([
|
||||
{ name: 'Web服务器', value: '856K', percent: 35, color: '#165DFF' },
|
||||
{ name: '数据库', value: '624K', percent: 26, color: '#14C9C9' },
|
||||
{ name: '应用服务', value: '480K', percent: 20, color: '#F7BA1E' },
|
||||
{ name: '网络设备', value: '288K', percent: 12, color: '#722ED1' },
|
||||
{ name: '安全设备', value: '168K', percent: 7, color: '#F53F3F' },
|
||||
])
|
||||
|
||||
// 日志趋势图表配置
|
||||
const logTrendChartOptions = ref({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '24:00'],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '数量',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '总量',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [1200, 800, 2800, 4500, 3800, 2200, 1500],
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#165DFF',
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#165DFF',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '异常',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [89, 45, 156, 234, 189, 112, 78],
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#F53F3F',
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#F53F3F',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// 获取级别颜色
|
||||
const getLevelColor = (level: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
error: 'red',
|
||||
critical: 'red',
|
||||
warn: 'orange',
|
||||
info: 'green',
|
||||
debug: 'gray',
|
||||
}
|
||||
return colorMap[level] || 'gray'
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
// TODO: 从API获取数据
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'LogMonitor',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
height: 100%;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
|
||||
&-primary {
|
||||
background-color: rgba(22, 93, 255, 0.1);
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
|
||||
&-success {
|
||||
background-color: rgba(0, 180, 42, 0.1);
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
&-danger {
|
||||
background-color: rgba(245, 63, 63, 0.1);
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
|
||||
&-warning {
|
||||
background-color: rgba(255, 125, 0, 0.1);
|
||||
color: rgb(var(--warning-6));
|
||||
}
|
||||
|
||||
&-muted {
|
||||
background-color: rgba(134, 144, 156, 0.1);
|
||||
color: rgb(var(--gray-6));
|
||||
}
|
||||
}
|
||||
|
||||
.stats-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
|
||||
&-1 {
|
||||
background-color: #165DFF;
|
||||
}
|
||||
|
||||
&-2 {
|
||||
background-color: #F53F3F;
|
||||
}
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
.source-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.source-item {
|
||||
.source-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.source-name {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.source-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
|
||||
.log-message {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
@@ -2,331 +2,48 @@
|
||||
<div class="container">
|
||||
<Breadcrumb :items="['menu.ops.systemSettings', 'menu.ops.systemSettings.systemLogs']" />
|
||||
|
||||
<SearchTable
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="系统日志"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
download-button-text="导出"
|
||||
refresh-tooltip-text="刷新数据"
|
||||
density-tooltip-text="表格密度"
|
||||
column-setting-tooltip-text="列设置"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="handleRefresh"
|
||||
@download="handleDownload"
|
||||
>
|
||||
<template #level="{ record }">
|
||||
<a-tag :color="getLevelColor(record.level)">
|
||||
{{ record.level }}
|
||||
</a-tag>
|
||||
<a-card class="page-card" :bordered="false" :body-style="{ padding: 0 }">
|
||||
<template #title>
|
||||
<div class="page-title">日志管理</div>
|
||||
</template>
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||
</template>
|
||||
<template #operations="{ record }">
|
||||
<a-button type="text" size="small" @click="handleView(record)">
|
||||
查看
|
||||
</a-button>
|
||||
</template>
|
||||
</SearchTable>
|
||||
|
||||
<a-drawer
|
||||
v-model:visible="detailVisible"
|
||||
:width="480"
|
||||
placement="right"
|
||||
:title="detailRecord ? `日志详情 #${detailRecord.id}` : '日志详情'"
|
||||
:footer="false"
|
||||
unmount-on-close
|
||||
>
|
||||
<template v-if="detailRecord">
|
||||
<a-descriptions :column="1" size="large" bordered>
|
||||
<a-descriptions-item label="日志级别">
|
||||
<a-tag :color="getLevelColor(detailRecord.level)">
|
||||
{{ detailRecord.level }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="模块">
|
||||
{{ detailRecord.module }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="操作人">
|
||||
{{ detailRecord.operator }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="IP 地址">
|
||||
{{ detailRecord.ip }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="操作时间">
|
||||
{{ detailRecord.createdAt }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="请求 ID">
|
||||
{{ detailRecord.requestId }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="日志内容">
|
||||
<div class="detail-content">{{ detailRecord.content }}</div>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</template>
|
||||
</a-drawer>
|
||||
<a-tabs v-model:active-key="activeKey" size="small" destroy-on-hide>
|
||||
<a-tab-pane key="entries" title="日志查询">
|
||||
<LogMgmtEntries />
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="syslog-rules" title="Syslog 规则">
|
||||
<LogMgmtSyslogRules />
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="trap-rules" title="Trap 规则">
|
||||
<LogMgmtTrapRules />
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="trap-dictionary" title="Trap 字典">
|
||||
<LogMgmtTrapDictionary />
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="trap-suppressions" title="Trap 屏蔽/抑制">
|
||||
<LogMgmtTrapSuppressions />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, reactive } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface LogRecord {
|
||||
id: number
|
||||
level: string
|
||||
module: string
|
||||
content: string
|
||||
operator: string
|
||||
ip: string
|
||||
createdAt: string
|
||||
/** 用于时间范围筛选(毫秒时间戳) */
|
||||
timestamp: number
|
||||
requestId: string
|
||||
}
|
||||
import Breadcrumb from '@/components/breadcrumb/index.vue'
|
||||
|
||||
const generateMockData = (count: number): LogRecord[] => {
|
||||
const levels = ['INFO', 'WARN', 'ERROR', 'DEBUG']
|
||||
const modules = ['用户管理', '权限管理', '系统配置', '数据备份', '登录认证']
|
||||
const operators = ['管理员', '张三', '李四', '系统', '定时任务']
|
||||
import LogMgmtEntries from '@/views/ops/pages/log-mgmt/entries/index.vue'
|
||||
import LogMgmtSyslogRules from '@/views/ops/pages/log-mgmt/syslog-rules/index.vue'
|
||||
import LogMgmtTrapRules from '@/views/ops/pages/log-mgmt/trap-rules/index.vue'
|
||||
import LogMgmtTrapDictionary from '@/views/ops/pages/log-mgmt/trap-dictionary/index.vue'
|
||||
import LogMgmtTrapSuppressions from '@/views/ops/pages/log-mgmt/trap-suppressions/index.vue'
|
||||
|
||||
return Array.from({ length: count }, (_, i) => {
|
||||
const timestamp = Date.now() - i * 3600000
|
||||
return {
|
||||
id: i + 1,
|
||||
level: levels[i % levels.length],
|
||||
module: modules[i % modules.length],
|
||||
content: `日志内容描述 ${i + 1}:系统执行例行检查与状态同步。`,
|
||||
operator: operators[i % operators.length],
|
||||
ip: `192.168.${Math.floor(i / 255) % 256}.${i % 256}`,
|
||||
createdAt: new Date(timestamp).toLocaleString('zh-CN'),
|
||||
timestamp,
|
||||
requestId: `req-${10000 + i}`,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<LogRecord[]>([])
|
||||
const allFilteredData = ref<LogRecord[]>([])
|
||||
|
||||
const formModel = ref({
|
||||
level: '',
|
||||
module: '',
|
||||
operator: '',
|
||||
keyword: '',
|
||||
dateRange: [] as unknown[],
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const detailRecord = ref<LogRecord | null>(null)
|
||||
|
||||
const formItems = computed<FormItem[]>(() => [
|
||||
{
|
||||
field: 'level',
|
||||
label: '日志级别',
|
||||
type: 'select',
|
||||
placeholder: '请选择日志级别',
|
||||
options: [
|
||||
{ label: 'INFO', value: 'INFO' },
|
||||
{ label: 'WARN', value: 'WARN' },
|
||||
{ label: 'ERROR', value: 'ERROR' },
|
||||
{ label: 'DEBUG', value: 'DEBUG' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'module',
|
||||
label: '模块',
|
||||
type: 'select',
|
||||
placeholder: '请选择模块',
|
||||
options: [
|
||||
{ label: '用户管理', value: '用户管理' },
|
||||
{ label: '权限管理', value: '权限管理' },
|
||||
{ label: '系统配置', value: '系统配置' },
|
||||
{ label: '数据备份', value: '数据备份' },
|
||||
{ label: '登录认证', value: '登录认证' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'operator',
|
||||
label: '操作人',
|
||||
type: 'input',
|
||||
placeholder: '请输入操作人',
|
||||
},
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '搜索日志内容',
|
||||
},
|
||||
{
|
||||
field: 'dateRange',
|
||||
label: '时间范围',
|
||||
type: 'dateRange',
|
||||
placeholder: '选择时间范围',
|
||||
span: 16,
|
||||
},
|
||||
])
|
||||
|
||||
const columns = computed<TableColumnData[]>(() => [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '日志级别',
|
||||
dataIndex: 'level',
|
||||
slotName: 'level',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '模块',
|
||||
dataIndex: 'module',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '日志内容',
|
||||
dataIndex: 'content',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '操作人',
|
||||
dataIndex: 'operator',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: 'IP地址',
|
||||
dataIndex: 'ip',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '操作时间',
|
||||
dataIndex: 'createdAt',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operations',
|
||||
slotName: 'operations',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
},
|
||||
])
|
||||
|
||||
const getLevelColor = (level: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
INFO: 'arcoblue',
|
||||
WARN: 'orange',
|
||||
ERROR: 'red',
|
||||
DEBUG: 'gray',
|
||||
}
|
||||
return colorMap[level] || 'gray'
|
||||
}
|
||||
|
||||
function applyFilters(source: LogRecord[]): LogRecord[] {
|
||||
let data = source
|
||||
const f = formModel.value
|
||||
|
||||
if (f.level) {
|
||||
data = data.filter(item => item.level === f.level)
|
||||
}
|
||||
if (f.module) {
|
||||
data = data.filter(item => item.module === f.module)
|
||||
}
|
||||
if (f.operator) {
|
||||
data = data.filter(item => item.operator.includes(f.operator))
|
||||
}
|
||||
if (f.keyword?.trim()) {
|
||||
const kw = f.keyword.trim()
|
||||
data = data.filter(item => item.content.includes(kw))
|
||||
}
|
||||
if (f.dateRange && f.dateRange.length === 2) {
|
||||
const [start, end] = f.dateRange
|
||||
const startMs = new Date(start as string | Date).getTime()
|
||||
const endMs = new Date(end as string | Date).getTime()
|
||||
if (!Number.isNaN(startMs) && !Number.isNaN(endMs)) {
|
||||
data = data.filter(item => item.timestamp >= startMs && item.timestamp <= endMs)
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
function slicePage(data: LogRecord[]) {
|
||||
const start = (pagination.current - 1) * pagination.pageSize
|
||||
const end = start + pagination.pageSize
|
||||
tableData.value = data.slice(start, end)
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
await new Promise(resolve => setTimeout(resolve, 400))
|
||||
|
||||
const base = generateMockData(100)
|
||||
const filtered = applyFilters(base)
|
||||
allFilteredData.value = filtered
|
||||
pagination.total = filtered.length
|
||||
slicePage(filtered)
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
level: '',
|
||||
module: '',
|
||||
operator: '',
|
||||
keyword: '',
|
||||
dateRange: [],
|
||||
}
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
slicePage(allFilteredData.value)
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchData()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
const handleDownload = () => {
|
||||
Message.info('导出功能开发中...')
|
||||
}
|
||||
|
||||
const handleView = (record: LogRecord) => {
|
||||
detailRecord.value = record
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
fetchData()
|
||||
const activeKey = ref<'entries' | 'syslog-rules' | 'trap-rules' | 'trap-dictionary' | 'trap-suppressions'>('entries')
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -340,10 +57,17 @@ export default {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.6;
|
||||
.page-card {
|
||||
:deep(.arco-card-body) {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
349
src/views/ops/pages/system-settings/system-logs/index_bak.vue
Normal file
349
src/views/ops/pages/system-settings/system-logs/index_bak.vue
Normal file
@@ -0,0 +1,349 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<Breadcrumb :items="['menu.ops.systemSettings', 'menu.ops.systemSettings.systemLogs']" />
|
||||
|
||||
<SearchTable
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="系统日志"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
download-button-text="导出"
|
||||
refresh-tooltip-text="刷新数据"
|
||||
density-tooltip-text="表格密度"
|
||||
column-setting-tooltip-text="列设置"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="handleRefresh"
|
||||
@download="handleDownload"
|
||||
>
|
||||
<template #level="{ record }">
|
||||
<a-tag :color="getLevelColor(record.level)">
|
||||
{{ record.level }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||
</template>
|
||||
<template #operations="{ record }">
|
||||
<a-button type="text" size="small" @click="handleView(record)">
|
||||
查看
|
||||
</a-button>
|
||||
</template>
|
||||
</SearchTable>
|
||||
|
||||
<a-drawer
|
||||
v-model:visible="detailVisible"
|
||||
:width="480"
|
||||
placement="right"
|
||||
:title="detailRecord ? `日志详情 #${detailRecord.id}` : '日志详情'"
|
||||
:footer="false"
|
||||
unmount-on-close
|
||||
>
|
||||
<template v-if="detailRecord">
|
||||
<a-descriptions :column="1" size="large" bordered>
|
||||
<a-descriptions-item label="日志级别">
|
||||
<a-tag :color="getLevelColor(detailRecord.level)">
|
||||
{{ detailRecord.level }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="模块">
|
||||
{{ detailRecord.module }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="操作人">
|
||||
{{ detailRecord.operator }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="IP 地址">
|
||||
{{ detailRecord.ip }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="操作时间">
|
||||
{{ detailRecord.createdAt }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="请求 ID">
|
||||
{{ detailRecord.requestId }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="日志内容">
|
||||
<div class="detail-content">{{ detailRecord.content }}</div>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</template>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, reactive } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
|
||||
interface LogRecord {
|
||||
id: number
|
||||
level: string
|
||||
module: string
|
||||
content: string
|
||||
operator: string
|
||||
ip: string
|
||||
createdAt: string
|
||||
/** 用于时间范围筛选(毫秒时间戳) */
|
||||
timestamp: number
|
||||
requestId: string
|
||||
}
|
||||
|
||||
const generateMockData = (count: number): LogRecord[] => {
|
||||
const levels = ['INFO', 'WARN', 'ERROR', 'DEBUG']
|
||||
const modules = ['用户管理', '权限管理', '系统配置', '数据备份', '登录认证']
|
||||
const operators = ['管理员', '张三', '李四', '系统', '定时任务']
|
||||
|
||||
return Array.from({ length: count }, (_, i) => {
|
||||
const timestamp = Date.now() - i * 3600000
|
||||
return {
|
||||
id: i + 1,
|
||||
level: levels[i % levels.length],
|
||||
module: modules[i % modules.length],
|
||||
content: `日志内容描述 ${i + 1}:系统执行例行检查与状态同步。`,
|
||||
operator: operators[i % operators.length],
|
||||
ip: `192.168.${Math.floor(i / 255) % 256}.${i % 256}`,
|
||||
createdAt: new Date(timestamp).toLocaleString('zh-CN'),
|
||||
timestamp,
|
||||
requestId: `req-${10000 + i}`,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<LogRecord[]>([])
|
||||
const allFilteredData = ref<LogRecord[]>([])
|
||||
|
||||
const formModel = ref({
|
||||
level: '',
|
||||
module: '',
|
||||
operator: '',
|
||||
keyword: '',
|
||||
dateRange: [] as unknown[],
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const detailRecord = ref<LogRecord | null>(null)
|
||||
|
||||
const formItems = computed<FormItem[]>(() => [
|
||||
{
|
||||
field: 'level',
|
||||
label: '日志级别',
|
||||
type: 'select',
|
||||
placeholder: '请选择日志级别',
|
||||
options: [
|
||||
{ label: 'INFO', value: 'INFO' },
|
||||
{ label: 'WARN', value: 'WARN' },
|
||||
{ label: 'ERROR', value: 'ERROR' },
|
||||
{ label: 'DEBUG', value: 'DEBUG' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'module',
|
||||
label: '模块',
|
||||
type: 'select',
|
||||
placeholder: '请选择模块',
|
||||
options: [
|
||||
{ label: '用户管理', value: '用户管理' },
|
||||
{ label: '权限管理', value: '权限管理' },
|
||||
{ label: '系统配置', value: '系统配置' },
|
||||
{ label: '数据备份', value: '数据备份' },
|
||||
{ label: '登录认证', value: '登录认证' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'operator',
|
||||
label: '操作人',
|
||||
type: 'input',
|
||||
placeholder: '请输入操作人',
|
||||
},
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '搜索日志内容',
|
||||
},
|
||||
{
|
||||
field: 'dateRange',
|
||||
label: '时间范围',
|
||||
type: 'dateRange',
|
||||
placeholder: '选择时间范围',
|
||||
span: 16,
|
||||
},
|
||||
])
|
||||
|
||||
const columns = computed<TableColumnData[]>(() => [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '日志级别',
|
||||
dataIndex: 'level',
|
||||
slotName: 'level',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '模块',
|
||||
dataIndex: 'module',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '日志内容',
|
||||
dataIndex: 'content',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '操作人',
|
||||
dataIndex: 'operator',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: 'IP地址',
|
||||
dataIndex: 'ip',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '操作时间',
|
||||
dataIndex: 'createdAt',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operations',
|
||||
slotName: 'operations',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
},
|
||||
])
|
||||
|
||||
const getLevelColor = (level: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
INFO: 'arcoblue',
|
||||
WARN: 'orange',
|
||||
ERROR: 'red',
|
||||
DEBUG: 'gray',
|
||||
}
|
||||
return colorMap[level] || 'gray'
|
||||
}
|
||||
|
||||
function applyFilters(source: LogRecord[]): LogRecord[] {
|
||||
let data = source
|
||||
const f = formModel.value
|
||||
|
||||
if (f.level) {
|
||||
data = data.filter(item => item.level === f.level)
|
||||
}
|
||||
if (f.module) {
|
||||
data = data.filter(item => item.module === f.module)
|
||||
}
|
||||
if (f.operator) {
|
||||
data = data.filter(item => item.operator.includes(f.operator))
|
||||
}
|
||||
if (f.keyword?.trim()) {
|
||||
const kw = f.keyword.trim()
|
||||
data = data.filter(item => item.content.includes(kw))
|
||||
}
|
||||
if (f.dateRange && f.dateRange.length === 2) {
|
||||
const [start, end] = f.dateRange
|
||||
const startMs = new Date(start as string | Date).getTime()
|
||||
const endMs = new Date(end as string | Date).getTime()
|
||||
if (!Number.isNaN(startMs) && !Number.isNaN(endMs)) {
|
||||
data = data.filter(item => item.timestamp >= startMs && item.timestamp <= endMs)
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
function slicePage(data: LogRecord[]) {
|
||||
const start = (pagination.current - 1) * pagination.pageSize
|
||||
const end = start + pagination.pageSize
|
||||
tableData.value = data.slice(start, end)
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
await new Promise(resolve => setTimeout(resolve, 400))
|
||||
|
||||
const base = generateMockData(100)
|
||||
const filtered = applyFilters(base)
|
||||
allFilteredData.value = filtered
|
||||
pagination.total = filtered.length
|
||||
slicePage(filtered)
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
level: '',
|
||||
module: '',
|
||||
operator: '',
|
||||
keyword: '',
|
||||
dateRange: [],
|
||||
}
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
slicePage(allFilteredData.value)
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchData()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
const handleDownload = () => {
|
||||
Message.info('导出功能开发中...')
|
||||
}
|
||||
|
||||
const handleView = (record: LogRecord) => {
|
||||
detailRecord.value = record
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
fetchData()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SystemLogs',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user