feat
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite --config ./config/vite.config.dev.ts",
|
||||
"build": "vue-tsc --noEmit --skipLibCheck && vite build --config ./config/vite.config.prod.ts",
|
||||
"build": "vite build --config ./config/vite.config.prod.ts",
|
||||
"lint-staged": "npx lint-staged",
|
||||
"prepare": "husky install",
|
||||
"build:normal": "vite build --config ./config/vite.config.prod.ts",
|
||||
|
||||
@@ -34,3 +34,23 @@ export const fetchAlertStatistics = (data: {
|
||||
|
||||
/** 获取 告警计数 */
|
||||
export const fetchAlertCount = () => request.get("/Alert/v1/record/count");
|
||||
|
||||
/** 获取 告警处理记录列表 */
|
||||
export const fetchAlertProcessList = (data: {
|
||||
alert_record_id?: number,
|
||||
action?: string,
|
||||
page?: number,
|
||||
page_size?: number,
|
||||
keyword?: string,
|
||||
sort?: string,
|
||||
order?: string
|
||||
}) => request.get("/Alert/v1/process/list", { params: data });
|
||||
|
||||
/** 获取 告警趋势 */
|
||||
export const fetchAlertTrend = (data: {
|
||||
granularity?: string, // day / month
|
||||
start_time?: string,
|
||||
end_time?: string,
|
||||
policy_id?: number,
|
||||
severity_id?: number
|
||||
}) => request.get("/Alert/v1/record/trend", { params: data });
|
||||
|
||||
109
src/api/ops/alertPolicy.ts
Normal file
109
src/api/ops/alertPolicy.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** 获取 告警策略列表 */
|
||||
export const fetchPolicyList = (data: {
|
||||
page?: number,
|
||||
page_size?: number,
|
||||
keyword?: string,
|
||||
enabled?: string,
|
||||
priority?: number[],
|
||||
created_at_start?: string,
|
||||
created_at_end?: string,
|
||||
order_by?: string,
|
||||
order?: string
|
||||
}) => request.get("/Alert/v1/policy/list", { params: data });
|
||||
|
||||
/** 获取 告警策略详情 */
|
||||
export const fetchPolicyDetail = (id: number) => request.get(`/Alert/v1/policy/get/${id}`);
|
||||
|
||||
/** 创建 告警策略 */
|
||||
export const createPolicy = (data: {
|
||||
name: string;
|
||||
description?: string;
|
||||
enabled?: boolean;
|
||||
priority?: number;
|
||||
labels?: string;
|
||||
template_id?: number;
|
||||
auto_create_ticket?: boolean;
|
||||
feedback_template_id?: number;
|
||||
dispatch_rule?: string;
|
||||
}) => request.post("/Alert/v1/policy/create", data);
|
||||
|
||||
/** 更新 告警策略 */
|
||||
export const updatePolicy = (data: {
|
||||
id: number;
|
||||
name?: string;
|
||||
description?: string;
|
||||
enabled?: boolean;
|
||||
priority?: number;
|
||||
labels?: string;
|
||||
template_id?: number;
|
||||
auto_create_ticket?: boolean;
|
||||
feedback_template_id?: number;
|
||||
dispatch_rule?: string;
|
||||
}) => request.post("/Alert/v1/policy/update", data);
|
||||
|
||||
/** 删除 告警策略 */
|
||||
export const deletePolicy = (id: number) => request.delete(`/Alert/v1/policy/delete/${id}`);
|
||||
|
||||
/** 获取 告警规则列表 */
|
||||
export const fetchRuleList = (data: {
|
||||
policy_id?: number;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
keyword?: string;
|
||||
sort?: string;
|
||||
order?: string
|
||||
}) => request.get("/Alert/v1/rule/list", { params: data });
|
||||
|
||||
/** 获取 告警规则详情 */
|
||||
export const fetchRuleDetail = (id: number) => request.get(`/Alert/v1/rule/get/${id}`);
|
||||
|
||||
/** 创建 告警规则 */
|
||||
export const createRule = (data: {
|
||||
policy_id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
rule_type: string;
|
||||
severity_id: number;
|
||||
enabled?: boolean;
|
||||
metric_name?: string;
|
||||
query_expr?: string;
|
||||
threshold?: number;
|
||||
compare_op?: string;
|
||||
duration?: number;
|
||||
labels?: string;
|
||||
}) => request.post("/Alert/v1/rule/create", data);
|
||||
|
||||
/** 更新 告警规则 */
|
||||
export const updateRule = (data: {
|
||||
id: number;
|
||||
name?: string;
|
||||
description?: string;
|
||||
rule_type?: string;
|
||||
severity_id?: number;
|
||||
enabled?: boolean;
|
||||
metric_name?: string;
|
||||
query_expr?: string;
|
||||
threshold?: number;
|
||||
compare_op?: string;
|
||||
duration?: number;
|
||||
labels?: string;
|
||||
}) => request.post("/Alert/v1/rule/update", data);
|
||||
|
||||
/** 删除 告警规则 */
|
||||
export const deleteRule = (id: number) => request.delete(`/Alert/v1/rule/delete/${id}`);
|
||||
|
||||
/** 获取 告警模板列表 */
|
||||
export const fetchTemplateList = (data: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
keyword?: string;
|
||||
}) => request.get("/Alert/v1/template/list", { params: data });
|
||||
|
||||
/** 获取 告警级别列表 */
|
||||
export const fetchSeverityList = (data: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
enabled?: string;
|
||||
}) => request.get("/Alert/v1/severity/list", { params: data });
|
||||
48
src/api/ops/alertRecord.ts
Normal file
48
src/api/ops/alertRecord.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** 获取 当前告警记录列表 */
|
||||
export const fetchAlertRecords = (data: {
|
||||
page?: number,
|
||||
page_size?: number,
|
||||
policy_id?: number,
|
||||
rule_id?: number,
|
||||
status?: string,
|
||||
severity_id?: number,
|
||||
start_time?: string,
|
||||
end_time?: string,
|
||||
keyword?: string
|
||||
}) => {
|
||||
return request.get("/Alert/v1/record/list", { params: data });
|
||||
};
|
||||
|
||||
/** 获取 告警记录详情 */
|
||||
export const fetchAlertRecordDetail = (id: number) => request.get(`/Alert/v1/record/get/${id}`);
|
||||
|
||||
/** 创建处理记录(执行确认/解决/屏蔽等操作) */
|
||||
export const createAlertProcess = (data: {
|
||||
alert_record_id: number;
|
||||
action: string; // ack / resolve / silence / comment / assign / escalate / close
|
||||
operator: string;
|
||||
operator_id?: string;
|
||||
comment?: string;
|
||||
assign_to?: string;
|
||||
assign_to_id?: string;
|
||||
silence_until?: string;
|
||||
silence_reason?: string;
|
||||
escalate_level?: string;
|
||||
escalate_to?: string;
|
||||
root_cause?: string;
|
||||
solution?: string;
|
||||
metadata?: Record<string, any>;
|
||||
}) => request.post("/Alert/v1/process/create", data);
|
||||
|
||||
/** 获取 告警处理记录列表 */
|
||||
export const fetchAlertProcessList = (data: {
|
||||
alert_record_id?: number,
|
||||
action?: string,
|
||||
page?: number,
|
||||
page_size?: number,
|
||||
keyword?: string,
|
||||
sort?: string,
|
||||
order?: string
|
||||
}) => request.get("/Alert/v1/process/list", { params: data });
|
||||
@@ -33,6 +33,8 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- 自定义表单项插槽 -->
|
||||
<slot name="form-items" />
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-col>
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
@update:model-value="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
/>
|
||||
>
|
||||
<template #form-items>
|
||||
<slot name="form-items" />
|
||||
</template>
|
||||
</SearchForm>
|
||||
|
||||
<a-divider style="margin-top: 0" />
|
||||
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="告警记录详情"
|
||||
width="900px"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
:footer="false"
|
||||
>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<a-descriptions :column="2" bordered v-if="recordDetail">
|
||||
<!-- 基础信息 -->
|
||||
<a-descriptions-item label="告警名称" :span="2">
|
||||
{{ recordDetail.alert_name || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="getStatusColor(recordDetail.status)">
|
||||
{{ getStatusText(recordDetail.status) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="告警级别">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<div
|
||||
v-if="recordDetail.severity"
|
||||
:style="{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
backgroundColor: recordDetail.severity.color,
|
||||
border: '1px solid var(--color-border-2)',
|
||||
borderRadius: '4px'
|
||||
}"
|
||||
></div>
|
||||
<span>{{ recordDetail.severity?.name || '-' }}</span>
|
||||
</div>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="摘要" :span="2">
|
||||
{{ recordDetail.summary || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="描述" :span="2">
|
||||
{{ recordDetail.description || '-' }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 时间信息 -->
|
||||
<a-descriptions-item label="开始时间">
|
||||
{{ formatDate(recordDetail.starts_at) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="结束时间">
|
||||
{{ formatDate(recordDetail.ends_at) || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="持续时长" :span="2">
|
||||
{{ formatDuration(recordDetail.duration) }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 处理信息 -->
|
||||
<a-descriptions-item label="处理状态" :span="2">
|
||||
<a-tag v-if="recordDetail.process_status" :color="getProcessStatusColor(recordDetail.process_status)">
|
||||
{{ getProcessStatusText(recordDetail.process_status) }}
|
||||
</a-tag>
|
||||
<span v-else>-</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="处理人">
|
||||
{{ recordDetail.processed_by || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="处理时间">
|
||||
{{ formatDate(recordDetail.processed_at) || '-' }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 数值信息 -->
|
||||
<a-descriptions-item label="当前值">
|
||||
{{ recordDetail.value || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="阈值">
|
||||
{{ recordDetail.threshold || '-' }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 通知信息 -->
|
||||
<a-descriptions-item label="通知次数">
|
||||
{{ recordDetail.notify_count || 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="通知状态">
|
||||
{{ recordDetail.notify_status || '-' }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 关联信息 -->
|
||||
<a-descriptions-item label="策略ID">
|
||||
{{ recordDetail.policy_id || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="规则ID">
|
||||
{{ recordDetail.rule_id || '-' }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 标签信息 -->
|
||||
<a-descriptions-item label="标签" :span="2">
|
||||
<a-space wrap v-if="recordDetail.labels && Object.keys(recordDetail.labels).length > 0">
|
||||
<a-tag v-for="(value, key) in recordDetail.labels" :key="key" color="blue">
|
||||
{{ key }}: {{ value }}
|
||||
</a-tag>
|
||||
</a-space>
|
||||
<span v-else>-</span>
|
||||
</a-descriptions-item>
|
||||
|
||||
<!-- 创建与更新时间 -->
|
||||
<a-descriptions-item label="创建时间">
|
||||
{{ formatDate(recordDetail.created_at) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">
|
||||
{{ formatDate(recordDetail.updated_at) }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { fetchHistoryDetail } from '@/api/ops/alertHistory'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
recordId?: number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const loading = ref(false)
|
||||
const recordDetail = ref<any>(null)
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化持续时长
|
||||
const formatDuration = (duration: number) => {
|
||||
if (!duration) return '-'
|
||||
|
||||
const hours = Math.floor(duration / 3600)
|
||||
const minutes = Math.floor((duration % 3600) / 60)
|
||||
const seconds = duration % 60
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}小时${minutes}分钟`
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}分钟${seconds}秒`
|
||||
} else {
|
||||
return `${seconds}秒`
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
pending: 'gray',
|
||||
firing: 'red',
|
||||
resolved: 'green',
|
||||
silenced: 'blue',
|
||||
suppressed: 'orange',
|
||||
acked: 'purple',
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
firing: '触发中',
|
||||
resolved: '已解决',
|
||||
silenced: '已屏蔽',
|
||||
suppressed: '已抑制',
|
||||
acked: '已确认',
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
// 获取处理状态颜色
|
||||
const getProcessStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
pending: 'gray',
|
||||
processing: 'blue',
|
||||
completed: 'green',
|
||||
failed: 'red',
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
}
|
||||
|
||||
// 获取处理状态文本
|
||||
const getProcessStatusText = (status: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
processing: '处理中',
|
||||
completed: '已完成',
|
||||
failed: '失败',
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
// 加载告警记录详情
|
||||
const loadRecordDetail = async () => {
|
||||
if (!props.recordId) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await fetchHistoryDetail(props.recordId)
|
||||
if (res.code === 0) {
|
||||
recordDetail.value = res.details
|
||||
} else {
|
||||
Message.error(res.message || '获取告警记录详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取告警记录详情失败:', error)
|
||||
Message.error('获取告警记录详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal && props.recordId) {
|
||||
loadRecordDetail()
|
||||
} else if (!newVal) {
|
||||
recordDetail.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'HistoryDetailDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
// 样式可以根据需要添加
|
||||
</style>
|
||||
96
src/views/ops/pages/alert/history/config/columns.ts
Normal file
96
src/views/ops/pages/alert/history/config/columns.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
|
||||
export const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '告警名称',
|
||||
dataIndex: 'alert_name',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '告警级别',
|
||||
dataIndex: 'severity',
|
||||
slotName: 'severity',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '摘要',
|
||||
dataIndex: 'summary',
|
||||
width: 250,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '开始时间',
|
||||
dataIndex: 'starts_at',
|
||||
slotName: 'starts_at',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '结束时间',
|
||||
dataIndex: 'ends_at',
|
||||
slotName: 'ends_at',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '持续时长',
|
||||
dataIndex: 'duration',
|
||||
slotName: 'duration',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '处理状态',
|
||||
dataIndex: 'process_status',
|
||||
slotName: 'process_status',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '处理人',
|
||||
dataIndex: 'processed_by',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '处理时间',
|
||||
dataIndex: 'processed_at',
|
||||
slotName: 'processed_at',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '当前值',
|
||||
dataIndex: 'value',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '阈值',
|
||||
dataIndex: 'threshold',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '通知次数',
|
||||
dataIndex: 'notify_count',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'actions',
|
||||
slotName: 'actions',
|
||||
fixed: 'right',
|
||||
width: 250,
|
||||
},
|
||||
]
|
||||
38
src/views/ops/pages/alert/history/config/search-form.ts
Normal file
38
src/views/ops/pages/alert/history/config/search-form.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
|
||||
export const searchFormConfig: FormItem[] = [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键字',
|
||||
type: 'input',
|
||||
placeholder: '请输入告警名称、摘要或描述',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '告警状态',
|
||||
type: 'select',
|
||||
placeholder: '请选择告警状态',
|
||||
options: [
|
||||
{ label: '待处理', value: 'pending' },
|
||||
{ label: '触发中', value: 'firing' },
|
||||
{ label: '已解决', value: 'resolved' },
|
||||
{ label: '已屏蔽', value: 'silenced' },
|
||||
{ label: '已抑制', value: 'suppressed' },
|
||||
{ label: '已确认', value: 'acked' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'rule_id',
|
||||
label: '规则',
|
||||
type: 'select',
|
||||
placeholder: '请选择规则',
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
field: 'severity_id',
|
||||
label: '告警级别',
|
||||
type: 'select',
|
||||
placeholder: '请选择告警级别',
|
||||
options: [],
|
||||
},
|
||||
]
|
||||
413
src/views/ops/pages/alert/history/index.vue
Normal file
413
src/views/ops/pages/alert/history/index.vue
Normal file
@@ -0,0 +1,413 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="历史告警列表"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<!-- 时间范围选择器插槽 -->
|
||||
<template #form-items>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="时间范围" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }">
|
||||
<a-space>
|
||||
<a-date-picker
|
||||
v-model="formModel.starts_at"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="开始时间"
|
||||
style="width: 180px;"
|
||||
/>
|
||||
<span>-</span>
|
||||
<a-date-picker
|
||||
v-model="formModel.ends_at"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="结束时间"
|
||||
style="width: 180px;"
|
||||
/>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</template>
|
||||
|
||||
<!-- 序号 -->
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||
</template>
|
||||
|
||||
<!-- 状态 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 告警级别 -->
|
||||
<template #severity="{ record }">
|
||||
<div v-if="record.severity" style="display: flex; align-items: center; gap: 8px;">
|
||||
<div
|
||||
:style="{
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
backgroundColor: record.severity.color,
|
||||
border: '1px solid var(--color-border-2)',
|
||||
borderRadius: '3px'
|
||||
}"
|
||||
></div>
|
||||
<span>{{ record.severity.name }}</span>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<!-- 开始时间 -->
|
||||
<template #starts_at="{ record }">
|
||||
{{ formatDate(record.starts_at) }}
|
||||
</template>
|
||||
|
||||
<!-- 结束时间 -->
|
||||
<template #ends_at="{ record }">
|
||||
{{ formatDate(record.ends_at) || '-' }}
|
||||
</template>
|
||||
|
||||
<!-- 持续时长 -->
|
||||
<template #duration="{ record }">
|
||||
{{ formatDuration(record.duration) }}
|
||||
</template>
|
||||
|
||||
<!-- 处理状态 -->
|
||||
<template #process_status="{ record }">
|
||||
<a-tag v-if="record.process_status" :color="getProcessStatusColor(record.process_status)">
|
||||
{{ getProcessStatusText(record.process_status) }}
|
||||
</a-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<!-- 处理时间 -->
|
||||
<template #processed_at="{ record }">
|
||||
{{ formatDate(record.processed_at) || '-' }}
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template #actions="{ record }">
|
||||
<a-button type="text" size="small" @click="handleDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleViewProcess(record)">
|
||||
处理记录
|
||||
</a-button>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<!-- 告警记录详情对话框 -->
|
||||
<history-detail-dialog
|
||||
v-model:visible="detailVisible"
|
||||
:record-id="currentRecordId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onMounted } 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 { searchFormConfig } from './config/search-form'
|
||||
import { columns as columnsConfig } from './config/columns'
|
||||
import {
|
||||
fetchHistories,
|
||||
} from '@/api/ops/alertHistory'
|
||||
import { fetchAlertLevelList } from '@/api/ops/alertLevel'
|
||||
import HistoryDetailDialog from './components/HistoryDetailDialog.vue'
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 初始化表单模型,设置默认时间范围(最近7天)
|
||||
const getDefaultDateRange = () => {
|
||||
const now = new Date()
|
||||
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
return {
|
||||
starts_at: formatDate(sevenDaysAgo),
|
||||
ends_at: formatDate(now),
|
||||
}
|
||||
}
|
||||
|
||||
const formModel = ref({
|
||||
keyword: '',
|
||||
status: 'resolved', // 默认显示已解决的告警
|
||||
rule_id: undefined,
|
||||
severity_id: undefined,
|
||||
...getDefaultDateRange(),
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 告警级别列表
|
||||
const severityOptions = ref<any[]>([])
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed<FormItem[]>(() => [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键字',
|
||||
type: 'input',
|
||||
placeholder: '请输入告警名称、摘要或描述',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '告警状态',
|
||||
type: 'select',
|
||||
placeholder: '请选择告警状态',
|
||||
options: [
|
||||
{ label: '待处理', value: 'pending' },
|
||||
{ label: '触发中', value: 'firing' },
|
||||
{ label: '已解决', value: 'resolved' },
|
||||
{ label: '已屏蔽', value: 'silenced' },
|
||||
{ label: '已抑制', value: 'suppressed' },
|
||||
{ label: '已确认', value: 'acked' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'rule_id',
|
||||
label: '规则',
|
||||
type: 'select',
|
||||
placeholder: '请选择规则',
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
field: 'severity_id',
|
||||
label: '告警级别',
|
||||
type: 'select',
|
||||
placeholder: '请选择告警级别',
|
||||
options: severityOptions.value.map((s: any) => ({
|
||||
label: s.name,
|
||||
value: s.id,
|
||||
})),
|
||||
},
|
||||
])
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed(() => columnsConfig)
|
||||
|
||||
// 当前选中的告警记录
|
||||
const currentRecordId = ref<number | undefined>(undefined)
|
||||
|
||||
// 对话框可见性
|
||||
const detailVisible = ref(false)
|
||||
|
||||
// 加载告警级别列表
|
||||
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
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取告警级别列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取告警记录列表
|
||||
const fetchHistoriesData = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize,
|
||||
keyword: formModel.value.keyword || undefined,
|
||||
status: formModel.value.status || undefined,
|
||||
rule_id: formModel.value.rule_id || undefined,
|
||||
severity_id: formModel.value.severity_id || undefined,
|
||||
starts_at: formModel.value.starts_at || undefined,
|
||||
ends_at: formModel.value.ends_at || undefined,
|
||||
}
|
||||
|
||||
const res = await fetchHistories(params)
|
||||
|
||||
tableData.value = res.details?.data || []
|
||||
pagination.total = res.details?.total || 0
|
||||
} catch (error) {
|
||||
console.error('获取告警记录列表失败:', error)
|
||||
Message.error('获取告警记录列表失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化持续时长
|
||||
const formatDuration = (duration: number) => {
|
||||
if (!duration) return '-'
|
||||
|
||||
const hours = Math.floor(duration / 3600)
|
||||
const minutes = Math.floor((duration % 3600) / 60)
|
||||
const seconds = duration % 60
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}小时${minutes}分钟`
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}分钟${seconds}秒`
|
||||
} else {
|
||||
return `${seconds}秒`
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
pending: 'gray',
|
||||
firing: 'red',
|
||||
resolved: 'green',
|
||||
silenced: 'blue',
|
||||
suppressed: 'orange',
|
||||
acked: 'purple',
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
firing: '触发中',
|
||||
resolved: '已解决',
|
||||
silenced: '已屏蔽',
|
||||
suppressed: '已抑制',
|
||||
acked: '已确认',
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
// 获取处理状态颜色
|
||||
const getProcessStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
pending: 'gray',
|
||||
processing: 'blue',
|
||||
completed: 'green',
|
||||
failed: 'red',
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
}
|
||||
|
||||
// 获取处理状态文本
|
||||
const getProcessStatusText = (status: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
processing: '处理中',
|
||||
completed: '已完成',
|
||||
failed: '失败',
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchHistoriesData()
|
||||
}
|
||||
|
||||
// 处理表单模型更新
|
||||
const handleFormModelUpdate = (value: any) => {
|
||||
formModel.value = {
|
||||
...formModel.value,
|
||||
...value,
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
keyword: '',
|
||||
status: 'resolved',
|
||||
rule_id: undefined,
|
||||
severity_id: undefined,
|
||||
...getDefaultDateRange(),
|
||||
}
|
||||
pagination.current = 1
|
||||
fetchHistoriesData()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
fetchHistoriesData()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchHistoriesData()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 详情
|
||||
const handleDetail = (record: any) => {
|
||||
currentRecordId.value = record.id
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 查看处理记录
|
||||
const handleViewProcess = (record: any) => {
|
||||
Message.info(`查看告警 ${record.id} 的处理记录功能待实现`)
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
onMounted(async () => {
|
||||
await loadSeverityOptions()
|
||||
fetchHistoriesData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AlertHistoryList',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,485 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="isEdit ? `编辑告警策略 - ${policyName}` : '新建告警策略'"
|
||||
width="800px"
|
||||
:mask-closable="false"
|
||||
@cancel="handleCancel"
|
||||
@before-ok="handleSubmit"
|
||||
@update:visible="handleVisibleChange"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
:style="{ marginBottom: '40px' }"
|
||||
>
|
||||
<!-- 策略基础信息 -->
|
||||
<a-divider orientation="left">基础信息</a-divider>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="name" label="策略名称" required>
|
||||
<a-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入策略名称"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="priority" label="优先级">
|
||||
<a-select
|
||||
v-model="formData.priority"
|
||||
placeholder="请选择优先级"
|
||||
allow-clear
|
||||
>
|
||||
<a-option :value="1">1 (最高)</a-option>
|
||||
<a-option :value="2">2</a-option>
|
||||
<a-option :value="3">3</a-option>
|
||||
<a-option :value="4">4</a-option>
|
||||
<a-option :value="5">5 (最低)</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item field="description" label="策略描述">
|
||||
<a-textarea
|
||||
v-model="formData.description"
|
||||
placeholder="请输入策略描述"
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="enabled" label="启用状态">
|
||||
<a-switch
|
||||
v-model="formData.enabled"
|
||||
:checked-value="true"
|
||||
:unchecked-value="false"
|
||||
>
|
||||
<template #checked>
|
||||
启用
|
||||
</template>
|
||||
<template #unchecked>
|
||||
禁用
|
||||
</template>
|
||||
</a-switch>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 模板与标签 -->
|
||||
<a-divider orientation="left">模板与标签</a-divider>
|
||||
|
||||
<a-form-item field="template_id" label="关联模板">
|
||||
<a-select
|
||||
v-model="formData.template_id"
|
||||
placeholder="请选择告警模板(可选)"
|
||||
allow-search
|
||||
allow-clear
|
||||
:loading="templateLoading"
|
||||
>
|
||||
<a-option
|
||||
v-for="template in templateOptions"
|
||||
:key="template.id"
|
||||
:value="template.id"
|
||||
>
|
||||
{{ template.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="labels" label="标签">
|
||||
<a-textarea
|
||||
v-model="formData.labels"
|
||||
placeholder='请输入标签,JSON格式,例如:{"env":"prod","team":"ops"}'
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
allow-clear
|
||||
/>
|
||||
<template #extra>
|
||||
请输入有效的 JSON 格式键值对
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 自动建单与派单配置 -->
|
||||
<a-divider orientation="left">自动建单与派单</a-divider>
|
||||
|
||||
<a-form-item field="auto_create_ticket" label="触发时自动建单">
|
||||
<a-switch
|
||||
v-model="formData.auto_create_ticket"
|
||||
:checked-value="true"
|
||||
:unchecked-value="false"
|
||||
>
|
||||
<template #checked>
|
||||
开启
|
||||
</template>
|
||||
<template #unchecked>
|
||||
关闭
|
||||
</template>
|
||||
</a-switch>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="feedback_template_id" label="工单模板 ID">
|
||||
<a-input-number
|
||||
v-model="formData.feedback_template_id"
|
||||
placeholder="请输入工单模板 ID"
|
||||
:min="0"
|
||||
allow-clear
|
||||
:style="{ width: '100%' }"
|
||||
/>
|
||||
<template #extra>
|
||||
0 表示不使用特定模板
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="派单规则">
|
||||
<a-table
|
||||
:data="dispatchRuleData"
|
||||
:pagination="false"
|
||||
:bordered="true"
|
||||
size="small"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column title="告警级别" data-index="severity" :width="150">
|
||||
<template #cell="{ record }">
|
||||
<a-tag :color="record.color">
|
||||
{{ record.name }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="处理人" data-index="assignee_id" :width="350">
|
||||
<template #cell="{ record }">
|
||||
<a-select
|
||||
v-model="record.assignee_id"
|
||||
placeholder="请选择处理人"
|
||||
allow-search
|
||||
allow-clear
|
||||
:loading="userLoading"
|
||||
:style="{ width: '100%' }"
|
||||
>
|
||||
<a-option
|
||||
v-for="user in userOptions"
|
||||
:key="user.id"
|
||||
:value="user.id"
|
||||
>
|
||||
{{ user.name || user.username }} ({{ user.username }})
|
||||
</a-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<div :style="{ marginTop: '16px' }">
|
||||
<a-space>
|
||||
<span :style="{ fontWeight: 'bold' }">默认处理人:</span>
|
||||
<a-select
|
||||
v-model="defaultAssigneeId"
|
||||
placeholder="请选择默认处理人"
|
||||
allow-search
|
||||
allow-clear
|
||||
:loading="userLoading"
|
||||
:style="{ width: '300px' }"
|
||||
>
|
||||
<a-option
|
||||
v-for="user in userOptions"
|
||||
:key="user.id"
|
||||
:value="user.id"
|
||||
>
|
||||
{{ user.name || user.username }} ({{ user.username }})
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<template #footer>
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
<a-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
保存
|
||||
</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { FormInstance } from '@arco-design/web-vue'
|
||||
import {
|
||||
fetchPolicyDetail,
|
||||
createPolicy,
|
||||
updatePolicy,
|
||||
fetchTemplateList,
|
||||
fetchSeverityList,
|
||||
} from '@/api/ops/alertPolicy'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
visible: boolean
|
||||
policyId?: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
// 表单引用
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 加载状态
|
||||
const submitLoading = ref(false)
|
||||
const templateLoading = ref(false)
|
||||
const userLoading = ref(false)
|
||||
|
||||
// 是否为编辑模式
|
||||
const isEdit = computed(() => !!props.policyId)
|
||||
const policyName = ref('')
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
enabled: true,
|
||||
priority: 3,
|
||||
labels: '',
|
||||
template_id: undefined,
|
||||
auto_create_ticket: false,
|
||||
feedback_template_id: 0,
|
||||
dispatch_rule: '',
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '请输入策略名称' }],
|
||||
labels: [
|
||||
{
|
||||
validator: (value: string, callback: any) => {
|
||||
if (!value) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
try {
|
||||
JSON.parse(value)
|
||||
callback()
|
||||
} catch (error) {
|
||||
callback('标签格式错误,请输入有效的 JSON 格式')
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// 模板选项
|
||||
const templateOptions = ref<any[]>([])
|
||||
|
||||
// 用户选项(模拟数据,实际应从用户服务获取)
|
||||
const userOptions = ref<any[]>([
|
||||
{ id: 1, name: '张三', username: 'zhangsan' },
|
||||
{ id: 2, name: '李四', username: 'lisi' },
|
||||
{ id: 3, name: '王五', username: 'wangwu' },
|
||||
])
|
||||
|
||||
// 告警级别列表
|
||||
const severityList = ref<any[]>([
|
||||
{ code: 'critical', name: '严重', color: 'red' },
|
||||
{ code: 'major', name: '重要', color: 'orange' },
|
||||
{ code: 'warning', name: '警告', color: 'yellow' },
|
||||
{ code: 'info', name: '信息', color: 'blue' },
|
||||
{ code: 'minor', name: '次要', color: 'gray' },
|
||||
])
|
||||
|
||||
// 派单规则数据
|
||||
const dispatchRuleData = ref<any[]>([])
|
||||
const defaultAssigneeId = ref<number | undefined>(undefined)
|
||||
|
||||
// 加载模板列表
|
||||
const loadTemplateOptions = async () => {
|
||||
templateLoading.value = true
|
||||
try {
|
||||
const res = await fetchTemplateList({ page: 1, page_size: 1000 })
|
||||
if (res.code === 0 && res.details?.data) {
|
||||
templateOptions.value = res.details.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取模板列表失败:', error)
|
||||
} finally {
|
||||
templateLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载告警级别
|
||||
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
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取告警级别失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化派单规则数据
|
||||
const initDispatchRuleData = () => {
|
||||
dispatchRuleData.value = severityList.value.map((severity) => ({
|
||||
severity_code: severity.code,
|
||||
name: severity.name,
|
||||
color: severity.color,
|
||||
assignee_id: undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
// 解析派单规则
|
||||
const parseDispatchRule = (ruleStr: string) => {
|
||||
try {
|
||||
if (!ruleStr) return
|
||||
const rule = JSON.parse(ruleStr)
|
||||
|
||||
if (rule.by_severity) {
|
||||
dispatchRuleData.value.forEach((item) => {
|
||||
if (rule.by_severity[item.severity_code] !== undefined) {
|
||||
item.assignee_id = rule.by_severity[item.severity_code]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (rule.default_assignee_id !== undefined) {
|
||||
defaultAssigneeId.value = rule.default_assignee_id
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析派单规则失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成派单规则 JSON
|
||||
const generateDispatchRule = (): string => {
|
||||
const by_severity: any = {}
|
||||
dispatchRuleData.value.forEach((item) => {
|
||||
if (item.assignee_id !== undefined) {
|
||||
by_severity[item.severity_code] = item.assignee_id
|
||||
}
|
||||
})
|
||||
|
||||
const rule: any = { by_severity }
|
||||
if (defaultAssigneeId.value !== undefined) {
|
||||
rule.default_assignee_id = defaultAssigneeId.value
|
||||
}
|
||||
|
||||
return JSON.stringify(rule)
|
||||
}
|
||||
|
||||
// 加载策略详情
|
||||
const loadPolicyDetail = async () => {
|
||||
if (!props.policyId) return
|
||||
|
||||
try {
|
||||
const res = await fetchPolicyDetail(props.policyId)
|
||||
if (res.code === 0 && res.details) {
|
||||
const policy = res.details
|
||||
|
||||
policyName.value = policy.name
|
||||
formData.name = policy.name
|
||||
formData.description = policy.description || ''
|
||||
formData.enabled = policy.enabled
|
||||
formData.priority = policy.priority || 3
|
||||
formData.labels = policy.labels || ''
|
||||
formData.template_id = policy.template_id
|
||||
formData.auto_create_ticket = policy.auto_create_ticket || false
|
||||
formData.feedback_template_id = policy.feedback_template_id || 0
|
||||
formData.dispatch_rule = policy.dispatch_rule || ''
|
||||
|
||||
// 解析派单规则
|
||||
parseDispatchRule(policy.dispatch_rule || '')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取策略详情失败:', error)
|
||||
Message.error('获取策略详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.name = ''
|
||||
formData.description = ''
|
||||
formData.enabled = true
|
||||
formData.priority = 3
|
||||
formData.labels = ''
|
||||
formData.template_id = undefined
|
||||
formData.auto_create_ticket = false
|
||||
formData.feedback_template_id = 0
|
||||
formData.dispatch_rule = ''
|
||||
policyName.value = ''
|
||||
defaultAssigneeId.value = undefined
|
||||
initDispatchRuleData()
|
||||
}
|
||||
|
||||
// 处理可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (!valid) return
|
||||
|
||||
submitLoading.value = true
|
||||
|
||||
try {
|
||||
// 生成派单规则
|
||||
formData.dispatch_rule = generateDispatchRule()
|
||||
|
||||
const data = { ...formData }
|
||||
|
||||
// 编辑模式
|
||||
if (isEdit.value && props.policyId) {
|
||||
await updatePolicy({ id: props.policyId, ...data })
|
||||
Message.success('策略更新成功')
|
||||
} else {
|
||||
// 新建模式
|
||||
await createPolicy(data)
|
||||
Message.success('策略创建成功')
|
||||
}
|
||||
|
||||
emit('success')
|
||||
handleCancel()
|
||||
} catch (error: any) {
|
||||
console.error('保存策略失败:', error)
|
||||
Message.error(error.message || '保存策略失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (visible) {
|
||||
if (isEdit.value) {
|
||||
loadPolicyDetail()
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化
|
||||
loadTemplateOptions()
|
||||
loadSeverityList()
|
||||
initDispatchRuleData()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'PolicyFormDialog',
|
||||
}
|
||||
</script>
|
||||
420
src/views/ops/pages/alert/setting/components/RuleFormDialog.vue
Normal file
420
src/views/ops/pages/alert/setting/components/RuleFormDialog.vue
Normal file
@@ -0,0 +1,420 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="isEdit ? '编辑告警规则' : '新建告警规则'"
|
||||
width="800px"
|
||||
:mask-closable="false"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
:style="{ marginBottom: '40px' }"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="name" label="规则名称" required>
|
||||
<a-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入规则名称"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="rule_type" label="规则类型" required>
|
||||
<a-select
|
||||
v-model="formData.rule_type"
|
||||
placeholder="请选择规则类型"
|
||||
@change="handleRuleTypeChange"
|
||||
>
|
||||
<a-option value="static">静态规则</a-option>
|
||||
<a-option value="dynamic">动态规则</a-option>
|
||||
<a-option value="promql">PromQL</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="enabled" label="启用状态">
|
||||
<a-switch
|
||||
v-model="formData.enabled"
|
||||
:checked-value="true"
|
||||
:unchecked-value="false"
|
||||
>
|
||||
<template #checked>
|
||||
启用
|
||||
</template>
|
||||
<template #unchecked>
|
||||
禁用
|
||||
</template>
|
||||
</a-switch>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="severity_id" label="告警级别" required>
|
||||
<a-select
|
||||
v-model="formData.severity_id"
|
||||
placeholder="请选择告警级别"
|
||||
:loading="severityLoading"
|
||||
>
|
||||
<a-option
|
||||
v-for="severity in severityOptions"
|
||||
:key="severity.id"
|
||||
:value="severity.id"
|
||||
>
|
||||
<a-tag :color="severity.color">{{ severity.name }}</a-tag>
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 指标/表达式 -->
|
||||
<a-form-item
|
||||
v-if="formData.rule_type === 'static'"
|
||||
field="metric_name"
|
||||
label="指标名称"
|
||||
required
|
||||
>
|
||||
<a-input
|
||||
v-model="formData.metric_name"
|
||||
placeholder="请输入指标名称,如:cpu_usage"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
v-if="formData.rule_type === 'dynamic'"
|
||||
field="metric_name"
|
||||
label="指标名称"
|
||||
required
|
||||
>
|
||||
<a-input
|
||||
v-model="formData.metric_name"
|
||||
placeholder="请输入动态指标名称,如:cpu_usage_{host}"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
v-if="formData.rule_type === 'promql'"
|
||||
field="query_expr"
|
||||
label="PromQL 表达式"
|
||||
required
|
||||
>
|
||||
<a-textarea
|
||||
v-model="formData.query_expr"
|
||||
placeholder="请输入 PromQL 表达式,如:rate(http_requests_total[5m])"
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 阈值条件 -->
|
||||
<a-row v-if="formData.rule_type !== 'promql'" :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-form-item field="compare_op" label="比较操作符" required>
|
||||
<a-select v-model="formData.compare_op" placeholder="请选择">
|
||||
<a-option value=">">></a-option>
|
||||
<a-option value="<"><</a-option>
|
||||
<a-option value=">=">≥</a-option>
|
||||
<a-option value="<=">≤</a-option>
|
||||
<a-option value="==">=</a-option>
|
||||
<a-option value="!=">≠</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item field="threshold" label="阈值" required>
|
||||
<a-input-number
|
||||
v-model="formData.threshold"
|
||||
placeholder="请输入阈值"
|
||||
:precision="2"
|
||||
allow-clear
|
||||
:style="{ width: '100%' }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="duration" label="持续时间(秒)">
|
||||
<a-input-number
|
||||
v-model="formData.duration"
|
||||
placeholder="请输入持续时间"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
allow-clear
|
||||
:style="{ width: '100%' }"
|
||||
/>
|
||||
<template #extra>
|
||||
持续时间达到设定值才会触发告警
|
||||
</template>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item field="description" label="规则描述">
|
||||
<a-textarea
|
||||
v-model="formData.description"
|
||||
placeholder="请输入规则描述"
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<template #footer>
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
<a-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
保存
|
||||
</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { FormInstance } from '@arco-design/web-vue'
|
||||
import {
|
||||
fetchRuleDetail,
|
||||
createRule,
|
||||
updateRule,
|
||||
fetchSeverityList,
|
||||
} from '@/api/ops/alertPolicy'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
visible: boolean
|
||||
ruleId?: number
|
||||
policyId?: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
// 表单引用
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 加载状态
|
||||
const submitLoading = ref(false)
|
||||
const severityLoading = ref(false)
|
||||
|
||||
// 是否为编辑模式
|
||||
const isEdit = computed(() => !!props.ruleId)
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
rule_type: 'static',
|
||||
enabled: true,
|
||||
severity_id: 0 as number,
|
||||
metric_name: '',
|
||||
query_expr: '',
|
||||
threshold: undefined as number | undefined,
|
||||
compare_op: '>',
|
||||
duration: 60,
|
||||
description: '',
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '请输入规则名称' }],
|
||||
rule_type: [{ required: true, message: '请选择规则类型' }],
|
||||
severity_id: [{ required: true, message: '请选择告警级别' }],
|
||||
metric_name: [
|
||||
{
|
||||
validator: (value: string, callback: any) => {
|
||||
if (formData.rule_type !== 'promql' && !value) {
|
||||
callback('请输入指标名称')
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
},
|
||||
],
|
||||
query_expr: [
|
||||
{
|
||||
validator: (value: string, callback: any) => {
|
||||
if (formData.rule_type === 'promql' && !value) {
|
||||
callback('请输入 PromQL 表达式')
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
},
|
||||
],
|
||||
compare_op: [
|
||||
{
|
||||
validator: (value: string, callback: any) => {
|
||||
if (formData.rule_type !== 'promql' && !value) {
|
||||
callback('请选择比较操作符')
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
},
|
||||
],
|
||||
threshold: [
|
||||
{
|
||||
validator: (value: number, callback: any) => {
|
||||
if (formData.rule_type !== 'promql' && value === undefined) {
|
||||
callback('请输入阈值')
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// 告警级别选项
|
||||
const severityOptions = ref<any[]>([])
|
||||
|
||||
// 加载告警级别列表
|
||||
const loadSeverityOptions = async () => {
|
||||
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
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取告警级别失败:', error)
|
||||
} finally {
|
||||
severityLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 规则类型变化处理
|
||||
const handleRuleTypeChange = (value: string) => {
|
||||
// 根据规则类型清空相关字段
|
||||
if (value === 'promql') {
|
||||
formData.metric_name = ''
|
||||
formData.compare_op = ''
|
||||
formData.threshold = undefined
|
||||
} else {
|
||||
formData.query_expr = ''
|
||||
if (!formData.compare_op) {
|
||||
formData.compare_op = '>'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载规则详情
|
||||
const loadRuleDetail = async () => {
|
||||
if (!props.ruleId) return
|
||||
|
||||
try {
|
||||
const res = await fetchRuleDetail(props.ruleId)
|
||||
if (res.code === 0 && res.details) {
|
||||
const rule = res.details
|
||||
|
||||
formData.name = rule.name
|
||||
formData.rule_type = rule.rule_type || 'static'
|
||||
formData.enabled = rule.enabled
|
||||
formData.severity_id = rule.severity_id
|
||||
formData.metric_name = rule.metric_name || ''
|
||||
formData.query_expr = rule.query_expr || ''
|
||||
formData.threshold = rule.threshold
|
||||
formData.compare_op = rule.compare_op || '>'
|
||||
formData.duration = rule.duration || 60
|
||||
formData.description = rule.description || ''
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取规则详情失败:', error)
|
||||
Message.error('获取规则详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.name = ''
|
||||
formData.rule_type = 'static'
|
||||
formData.enabled = true
|
||||
formData.severity_id = 0
|
||||
formData.metric_name = ''
|
||||
formData.query_expr = ''
|
||||
formData.threshold = undefined
|
||||
formData.compare_op = '>'
|
||||
formData.duration = 60
|
||||
formData.description = ''
|
||||
}
|
||||
|
||||
// 处理可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (!valid) return
|
||||
|
||||
if (!props.policyId) {
|
||||
Message.warning('缺少策略 ID')
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
|
||||
try {
|
||||
const data = {
|
||||
...formData,
|
||||
policy_id: props.policyId,
|
||||
severity_id: formData.severity_id, // 确保 severity_id 是 number 类型
|
||||
}
|
||||
|
||||
// 编辑模式
|
||||
if (isEdit.value && props.ruleId) {
|
||||
await updateRule({ id: props.ruleId, ...data })
|
||||
Message.success('规则更新成功')
|
||||
} else {
|
||||
// 新建模式
|
||||
await createRule(data)
|
||||
Message.success('规则创建成功')
|
||||
}
|
||||
|
||||
emit('success')
|
||||
handleCancel()
|
||||
} catch (error: any) {
|
||||
console.error('保存规则失败:', error)
|
||||
Message.error(error.message || '保存规则失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (visible) {
|
||||
if (isEdit.value) {
|
||||
loadRuleDetail()
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化
|
||||
loadSeverityOptions()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'RuleFormDialog',
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,311 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="`规则管理 - ${policyName}`"
|
||||
width="1200px"
|
||||
:mask-closable="false"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
>
|
||||
<div class="rule-manage-content">
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar">
|
||||
<a-input-search
|
||||
v-model="searchKeyword"
|
||||
placeholder="请输入规则名称搜索"
|
||||
style="width: 300px"
|
||||
allow-clear
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<a-button type="primary" @click="handleCreateRule">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
新建规则
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 规则列表 -->
|
||||
<a-table
|
||||
:data="ruleData"
|
||||
:loading="ruleLoading"
|
||||
:pagination="rulePagination"
|
||||
:bordered="true"
|
||||
size="small"
|
||||
@page-change="handlePageChange"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column title="规则名称" data-index="name" :width="200" />
|
||||
<a-table-column title="规则类型" data-index="rule_type" :width="100">
|
||||
<template #cell="{ record }">
|
||||
<a-tag>{{ getRuleTypeText(record.rule_type) }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="启用状态" data-index="enabled" :width="100">
|
||||
<template #cell="{ record }">
|
||||
<a-tag :color="record.enabled ? 'green' : 'gray'">
|
||||
{{ record.enabled ? '启用' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="指标/表达式" data-index="metric_expr" :width="200">
|
||||
<template #cell="{ record }">
|
||||
<span :title="record.metric_name || record.query_expr">
|
||||
{{ record.metric_name || record.query_expr || '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="阈值条件" data-index="threshold" :width="120">
|
||||
<template #cell="{ record }">
|
||||
<span v-if="record.threshold !== undefined">
|
||||
{{ getCompareOpText(record.compare_op) }} {{ record.threshold }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="持续时间" data-index="duration" :width="100">
|
||||
<template #cell="{ record }">
|
||||
<span v-if="record.duration">{{ formatDuration(record.duration) }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="告警级别" data-index="severity" :width="120">
|
||||
<template #cell="{ record }">
|
||||
<div v-if="record.severity" :style="{ display: 'flex', alignItems: 'center', gap: '8px' }">
|
||||
<div
|
||||
:style="{
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
backgroundColor: record.severity.color,
|
||||
border: '1px solid var(--color-border-2)',
|
||||
borderRadius: '3px'
|
||||
}"
|
||||
></div>
|
||||
<span>{{ record.severity.name }}</span>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="操作" data-index="actions" :width="150" fixed="right">
|
||||
<template #cell="{ record }">
|
||||
<a-space>
|
||||
<a-button type="text" size="small" @click="handleEditRule(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
content="确定要删除该规则吗?"
|
||||
@ok="handleDeleteRule(record)"
|
||||
>
|
||||
<a-button type="text" size="small" status="danger">
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑规则弹窗 -->
|
||||
<rule-form-dialog
|
||||
v-model:visible="ruleFormVisible"
|
||||
:rule-id="currentRuleId"
|
||||
:policy-id="policyId"
|
||||
@success="handleRuleFormSuccess"
|
||||
/>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
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 RuleFormDialog from './RuleFormDialog.vue'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
visible: boolean
|
||||
policyId?: number
|
||||
policyName?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
// 状态管理
|
||||
const ruleLoading = ref(false)
|
||||
const ruleData = ref<any[]>([])
|
||||
const searchKeyword = ref('')
|
||||
|
||||
// 规则分页
|
||||
const rulePagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 规则表单
|
||||
const ruleFormVisible = ref(false)
|
||||
const currentRuleId = ref<number | undefined>(undefined)
|
||||
|
||||
// 规则类型映射
|
||||
const ruleTypeMap: Record<string, string> = {
|
||||
static: '静态规则',
|
||||
dynamic: '动态规则',
|
||||
promql: 'PromQL',
|
||||
}
|
||||
|
||||
// 比较操作符映射
|
||||
const compareOpMap: Record<string, string> = {
|
||||
'>': '>',
|
||||
'<': '<',
|
||||
'>=': '≥',
|
||||
'<=': '≤',
|
||||
'==': '=',
|
||||
'!=': '≠',
|
||||
}
|
||||
|
||||
// 获取规则类型文本
|
||||
const getRuleTypeText = (type: string) => {
|
||||
return ruleTypeMap[type] || type
|
||||
}
|
||||
|
||||
// 获取比较操作符文本
|
||||
const getCompareOpText = (op: string) => {
|
||||
return compareOpMap[op] || op
|
||||
}
|
||||
|
||||
// 格式化持续时间
|
||||
const formatDuration = (seconds: number) => {
|
||||
if (seconds < 60) {
|
||||
return `${seconds}秒`
|
||||
} else if (seconds < 3600) {
|
||||
return `${Math.floor(seconds / 60)}分钟`
|
||||
} else {
|
||||
return `${Math.floor(seconds / 3600)}小时`
|
||||
}
|
||||
}
|
||||
|
||||
// 加载规则列表
|
||||
const loadRuleList = async () => {
|
||||
if (!props.policyId) return
|
||||
|
||||
ruleLoading.value = true
|
||||
|
||||
try {
|
||||
const res = await fetchRuleList({
|
||||
policy_id: props.policyId,
|
||||
page: rulePagination.current,
|
||||
page_size: rulePagination.pageSize,
|
||||
keyword: searchKeyword.value || undefined,
|
||||
})
|
||||
|
||||
ruleData.value = res.details?.data || []
|
||||
rulePagination.total = res.details?.total || 0
|
||||
} catch (error) {
|
||||
console.error('获取规则列表失败:', error)
|
||||
Message.error('获取规则列表失败')
|
||||
ruleData.value = []
|
||||
rulePagination.total = 0
|
||||
} finally {
|
||||
ruleLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
rulePagination.current = 1
|
||||
loadRuleList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
rulePagination.current = current
|
||||
loadRuleList()
|
||||
}
|
||||
|
||||
// 新建规则
|
||||
const handleCreateRule = () => {
|
||||
if (!props.policyId) {
|
||||
Message.warning('请先选择策略')
|
||||
return
|
||||
}
|
||||
currentRuleId.value = undefined
|
||||
ruleFormVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑规则
|
||||
const handleEditRule = (record: any) => {
|
||||
currentRuleId.value = record.id
|
||||
ruleFormVisible.value = true
|
||||
}
|
||||
|
||||
// 删除规则
|
||||
const handleDeleteRule = async (record: any) => {
|
||||
try {
|
||||
await deleteRule(record.id)
|
||||
Message.success('规则删除成功')
|
||||
|
||||
// 如果当前页只有一条数据,且不是第一页,则跳到上一页
|
||||
if (ruleData.value.length === 1 && rulePagination.current > 1) {
|
||||
rulePagination.current--
|
||||
}
|
||||
|
||||
loadRuleList()
|
||||
} catch (error) {
|
||||
console.error('删除规则失败:', error)
|
||||
Message.error('删除规则失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 规则表单成功回调
|
||||
const handleRuleFormSuccess = () => {
|
||||
loadRuleList()
|
||||
emit('success')
|
||||
}
|
||||
|
||||
// 处理可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (visible) {
|
||||
searchKeyword.value = ''
|
||||
rulePagination.current = 1
|
||||
loadRuleList()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'RuleManageDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.rule-manage-content {
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background-color: var(--color-fill-2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
79
src/views/ops/pages/alert/setting/config/columns.ts
Normal file
79
src/views/ops/pages/alert/setting/config/columns.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
|
||||
export const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '策略名称',
|
||||
dataIndex: 'name',
|
||||
slotName: 'name',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '策略描述',
|
||||
dataIndex: 'description',
|
||||
width: 250,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '启用状态',
|
||||
dataIndex: 'enabled',
|
||||
slotName: 'enabled',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '优先级',
|
||||
dataIndex: 'priority',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '规则数',
|
||||
dataIndex: 'rule_count',
|
||||
slotName: 'rule_count',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '通知渠道数',
|
||||
dataIndex: 'channel_count',
|
||||
slotName: 'channel_count',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '抑制规则数',
|
||||
dataIndex: 'suppression_rule_count',
|
||||
slotName: 'suppression_rule_count',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '自动建单',
|
||||
dataIndex: 'auto_create_ticket',
|
||||
slotName: 'auto_create_ticket',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
slotName: 'created_at',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '最后修改',
|
||||
dataIndex: 'updated_at',
|
||||
slotName: 'updated_at',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'actions',
|
||||
slotName: 'actions',
|
||||
fixed: 'right',
|
||||
width: 300,
|
||||
},
|
||||
]
|
||||
34
src/views/ops/pages/alert/setting/config/search-form.ts
Normal file
34
src/views/ops/pages/alert/setting/config/search-form.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
|
||||
export const searchFormConfig: FormItem[] = [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键字',
|
||||
type: 'input',
|
||||
placeholder: '请输入策略名称或描述',
|
||||
},
|
||||
{
|
||||
field: 'enabled',
|
||||
label: '启用状态',
|
||||
type: 'select',
|
||||
placeholder: '请选择启用状态',
|
||||
options: [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '已启用', value: 'true' },
|
||||
{ label: '已禁用', value: 'false' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'priority',
|
||||
label: '优先级',
|
||||
type: 'select',
|
||||
placeholder: '请选择优先级',
|
||||
options: [
|
||||
{ label: '1 (最高)', value: 1 },
|
||||
{ label: '2', value: 2 },
|
||||
{ label: '3', value: 3 },
|
||||
{ label: '4', value: 4 },
|
||||
{ label: '5 (最低)', value: 5 },
|
||||
],
|
||||
},
|
||||
]
|
||||
336
src/views/ops/pages/alert/setting/index.vue
Normal file
336
src/views/ops/pages/alert/setting/index.vue
Normal file
@@ -0,0 +1,336 @@
|
||||
<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="重置"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<!-- 序号 -->
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||
</template>
|
||||
|
||||
<!-- 策略名称 -->
|
||||
<template #name="{ record }">
|
||||
<a-space>
|
||||
<a-tag v-if="!record.enabled" color="gray">禁用</a-tag>
|
||||
<span>{{ record.name }}</span>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 启用状态 -->
|
||||
<template #enabled="{ record }">
|
||||
<a-switch
|
||||
:model-value="record.enabled"
|
||||
:checked-value="true"
|
||||
:unchecked-value="false"
|
||||
@change="handleToggleEnabled(record)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 规则数 -->
|
||||
<template #rule_count="{ record }">
|
||||
<span>{{ record.rules?.length || 0 }} 条</span>
|
||||
</template>
|
||||
|
||||
<!-- 通知渠道数 -->
|
||||
<template #channel_count="{ record }">
|
||||
<span>{{ record.channels?.length || 0 }} 个</span>
|
||||
</template>
|
||||
|
||||
<!-- 抑制规则数 -->
|
||||
<template #suppression_rule_count="{ record }">
|
||||
<span>{{ record.suppression_rules?.length || 0 }} 条</span>
|
||||
</template>
|
||||
|
||||
<!-- 自动建单 -->
|
||||
<template #auto_create_ticket="{ record }">
|
||||
<a-tag :color="record.auto_create_ticket ? 'green' : 'gray'">
|
||||
{{ record.auto_create_ticket ? '是' : '否' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 创建时间 -->
|
||||
<template #created_at="{ record }">
|
||||
{{ formatDate(record.created_at) }}
|
||||
</template>
|
||||
|
||||
<!-- 最后修改 -->
|
||||
<template #updated_at="{ record }">
|
||||
{{ formatDate(record.updated_at) }}
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template #actions="{ record }">
|
||||
<a-space>
|
||||
<a-button type="text" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleManageRules(record)">
|
||||
规则管理
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
content="确定要删除该策略吗?删除后将无法恢复,且会影响告警评估。"
|
||||
@ok="handleDelete(record)"
|
||||
>
|
||||
<a-button type="text" size="small" status="danger">
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 额外操作按钮 -->
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="handleCreate">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
新建告警策略
|
||||
</a-button>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<!-- 新建/编辑策略弹窗 -->
|
||||
<policy-form-dialog
|
||||
v-model:visible="policyFormVisible"
|
||||
:policy-id="currentPolicyId"
|
||||
@success="handlePolicyFormSuccess"
|
||||
/>
|
||||
|
||||
<!-- 规则管理弹窗 -->
|
||||
<rule-manage-dialog
|
||||
v-model:visible="ruleManageVisible"
|
||||
:policy-id="currentPolicyId"
|
||||
:policy-name="currentPolicyName"
|
||||
@success="handleRuleManageSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onMounted } 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 { searchFormConfig } from './config/search-form'
|
||||
import { columns as columnsConfig } from './config/columns'
|
||||
import {
|
||||
fetchPolicyList,
|
||||
deletePolicy,
|
||||
updatePolicy,
|
||||
} from '@/api/ops/alertPolicy'
|
||||
import PolicyFormDialog from './components/PolicyFormDialog.vue'
|
||||
import RuleManageDialog from './components/RuleManageDialog.vue'
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
const formModel = ref({
|
||||
keyword: '',
|
||||
enabled: 'all',
|
||||
priority: undefined,
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed<FormItem[]>(() => searchFormConfig)
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed(() => columnsConfig)
|
||||
|
||||
// 当前选中的策略
|
||||
const currentPolicyId = ref<number | undefined>(undefined)
|
||||
const currentPolicyName = ref('')
|
||||
|
||||
// 弹窗可见性
|
||||
const policyFormVisible = ref(false)
|
||||
const ruleManageVisible = ref(false)
|
||||
|
||||
// 获取策略列表
|
||||
const fetchPolicyData = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize,
|
||||
keyword: formModel.value.keyword || undefined,
|
||||
order_by: 'created_at',
|
||||
order: 'desc',
|
||||
}
|
||||
|
||||
// 处理启用状态筛选
|
||||
if (formModel.value.enabled && formModel.value.enabled !== 'all') {
|
||||
params.enabled = formModel.value.enabled
|
||||
}
|
||||
|
||||
// 处理优先级筛选
|
||||
if (formModel.value.priority) {
|
||||
params.priority = [formModel.value.priority]
|
||||
}
|
||||
|
||||
const res = await fetchPolicyList(params)
|
||||
|
||||
tableData.value = res.details?.data || []
|
||||
pagination.total = res.details?.total || 0
|
||||
} catch (error) {
|
||||
console.error('获取告警策略列表失败:', error)
|
||||
Message.error('获取告警策略列表失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchPolicyData()
|
||||
}
|
||||
|
||||
// 处理表单模型更新
|
||||
const handleFormModelUpdate = (value: any) => {
|
||||
formModel.value = {
|
||||
...formModel.value,
|
||||
...value,
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
keyword: '',
|
||||
enabled: 'all',
|
||||
priority: undefined,
|
||||
}
|
||||
pagination.current = 1
|
||||
fetchPolicyData()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
fetchPolicyData()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchPolicyData()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 新建策略
|
||||
const handleCreate = () => {
|
||||
currentPolicyId.value = undefined
|
||||
policyFormVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑策略
|
||||
const handleEdit = (record: any) => {
|
||||
currentPolicyId.value = record.id
|
||||
policyFormVisible.value = true
|
||||
}
|
||||
|
||||
// 切换启用状态
|
||||
const handleToggleEnabled = async (record: any) => {
|
||||
try {
|
||||
const newStatus = !record.enabled
|
||||
await updatePolicy({
|
||||
id: record.id,
|
||||
enabled: newStatus,
|
||||
})
|
||||
|
||||
Message.success(newStatus ? '策略已启用' : '策略已禁用')
|
||||
record.enabled = newStatus
|
||||
} catch (error) {
|
||||
console.error('更新策略状态失败:', error)
|
||||
Message.error('更新策略状态失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除策略
|
||||
const handleDelete = async (record: any) => {
|
||||
try {
|
||||
await deletePolicy(record.id)
|
||||
Message.success('策略删除成功')
|
||||
|
||||
// 如果当前页只有一条数据,且不是第一页,则跳到上一页
|
||||
if (tableData.value.length === 1 && pagination.current > 1) {
|
||||
pagination.current--
|
||||
}
|
||||
|
||||
fetchPolicyData()
|
||||
} catch (error) {
|
||||
console.error('删除策略失败:', error)
|
||||
Message.error('删除策略失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 规则管理
|
||||
const handleManageRules = (record: any) => {
|
||||
currentPolicyId.value = record.id
|
||||
currentPolicyName.value = record.name
|
||||
ruleManageVisible.value = true
|
||||
}
|
||||
|
||||
// 策略表单成功回调
|
||||
const handlePolicyFormSuccess = () => {
|
||||
fetchPolicyData()
|
||||
}
|
||||
|
||||
// 规则管理成功回调
|
||||
const handleRuleManageSuccess = () => {
|
||||
fetchPolicyData()
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
onMounted(() => {
|
||||
fetchPolicyData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AlertPolicySetting',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
85
src/views/ops/pages/alert/tackle/components/AckDialog.vue
Normal file
85
src/views/ops/pages/alert/tackle/components/AckDialog.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="确认告警"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleCancel"
|
||||
:confirm-loading="loading"
|
||||
width="600px"
|
||||
>
|
||||
<a-form :model="form" layout="vertical">
|
||||
<a-form-item label="告警ID">
|
||||
<a-input v-model="alertId" disabled />
|
||||
</a-form-item>
|
||||
<a-form-item label="备注">
|
||||
<a-textarea
|
||||
v-model="form.comment"
|
||||
placeholder="请输入备注信息(可选)"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { createAlertProcess } from '@/api/ops/alertRecord'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
alertRecordId: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
'success': []
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const form = ref({
|
||||
comment: '',
|
||||
})
|
||||
|
||||
const alertId = ref('')
|
||||
|
||||
watch(() => props.visible, (val) => {
|
||||
if (val) {
|
||||
alertId.value = String(props.alertRecordId)
|
||||
form.value.comment = ''
|
||||
}
|
||||
})
|
||||
|
||||
const handleOk = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await createAlertProcess({
|
||||
alert_record_id: props.alertRecordId,
|
||||
action: 'ack',
|
||||
operator: getCurrentUser(),
|
||||
comment: form.value.comment,
|
||||
})
|
||||
Message.success('确认成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} catch (error) {
|
||||
console.error('确认失败:', error)
|
||||
Message.error('确认失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
function getCurrentUser() {
|
||||
// TODO: 从全局状态获取当前用户
|
||||
return 'admin'
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="添加评论"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleCancel"
|
||||
:confirm-loading="loading"
|
||||
width="600px"
|
||||
>
|
||||
<a-form :model="form" layout="vertical">
|
||||
<a-form-item label="告警ID">
|
||||
<a-input v-model="alertId" disabled />
|
||||
</a-form-item>
|
||||
<a-form-item label="评论内容" required>
|
||||
<a-textarea
|
||||
v-model="form.comment"
|
||||
placeholder="请输入评论内容"
|
||||
:auto-size="{ minRows: 4, maxRows: 8 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { createAlertProcess } from '@/api/ops/alertRecord'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
alertRecordId: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
'success': []
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const form = ref({
|
||||
comment: '',
|
||||
})
|
||||
|
||||
const alertId = ref('')
|
||||
|
||||
watch(() => props.visible, (val) => {
|
||||
if (val) {
|
||||
alertId.value = String(props.alertRecordId)
|
||||
form.value.comment = ''
|
||||
}
|
||||
})
|
||||
|
||||
const handleOk = async () => {
|
||||
if (!form.value.comment) {
|
||||
Message.error('请输入评论内容')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
await createAlertProcess({
|
||||
alert_record_id: props.alertRecordId,
|
||||
action: 'comment',
|
||||
operator: getCurrentUser(),
|
||||
comment: form.value.comment,
|
||||
})
|
||||
Message.success('评论添加成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} catch (error) {
|
||||
console.error('添加评论失败:', error)
|
||||
Message.error('添加评论失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
function getCurrentUser() {
|
||||
// TODO: 从全局状态获取当前用户
|
||||
return 'admin'
|
||||
}
|
||||
</script>
|
||||
329
src/views/ops/pages/alert/tackle/components/DetailDialog.vue
Normal file
329
src/views/ops/pages/alert/tackle/components/DetailDialog.vue
Normal file
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="告警详情"
|
||||
:footer="false"
|
||||
width="1000px"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleCancel"
|
||||
>
|
||||
<a-spin :loading="loading">
|
||||
<div v-if="record" class="alert-detail">
|
||||
<!-- 基础信息 -->
|
||||
<a-descriptions title="基础信息" :column="2" bordered>
|
||||
<a-descriptions-item label="告警ID">{{ record.id }}</a-descriptions-item>
|
||||
<a-descriptions-item label="告警名称">{{ record.alert_name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="告警级别">
|
||||
<a-tag v-if="record.severity" :color="record.severity.color">
|
||||
{{ record.severity.name || record.severity.code }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="策略ID">{{ record.policy_id }}</a-descriptions-item>
|
||||
<a-descriptions-item label="规则ID">{{ record.rule_id }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 告警内容 -->
|
||||
<a-descriptions title="告警内容" :column="1" bordered class="mt-4">
|
||||
<a-descriptions-item label="摘要">
|
||||
{{ record.summary || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="描述">
|
||||
{{ record.description || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="当前值">
|
||||
{{ record.value || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="阈值">
|
||||
{{ record.threshold || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 时间信息 -->
|
||||
<a-descriptions title="时间信息" :column="2" bordered class="mt-4">
|
||||
<a-descriptions-item label="开始时间">
|
||||
{{ formatDateTime(record.starts_at) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="结束时间">
|
||||
{{ record.ends_at ? formatDateTime(record.ends_at) : '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="持续时长">
|
||||
{{ formatDuration(record.duration) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">
|
||||
{{ formatDateTime(record.created_at) }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 处理信息 -->
|
||||
<a-descriptions title="处理信息" :column="2" bordered class="mt-4">
|
||||
<a-descriptions-item label="处理状态">
|
||||
{{ record.process_status || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="处理人">
|
||||
{{ record.processed_by || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="处理时间">
|
||||
{{ record.processed_at ? formatDateTime(record.processed_at) : '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="关联工单">
|
||||
{{ record.feedback_ticket_id || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 通知信息 -->
|
||||
<a-descriptions title="通知信息" :column="2" bordered class="mt-4">
|
||||
<a-descriptions-item label="通知次数">
|
||||
{{ record.notify_count || 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="通知状态">
|
||||
<a-tag :color="getNotifyStatusColor(record.notify_status)">
|
||||
{{ getNotifyStatusText(record.notify_status) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 标签 -->
|
||||
<a-descriptions title="标签" :column="1" bordered class="mt-4">
|
||||
<a-descriptions-item label="Labels">
|
||||
<div v-if="labels">
|
||||
<a-tag v-for="(value, key) in labels" :key="key" class="mb-1">
|
||||
{{ key }}: {{ value }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 注解 -->
|
||||
<a-descriptions title="注解" :column="1" bordered class="mt-4">
|
||||
<a-descriptions-item label="Annotations">
|
||||
<div v-if="annotations">
|
||||
<a-tag v-for="(value, key) in annotations" :key="key" class="mb-1">
|
||||
{{ key }}: {{ value }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 处理记录 -->
|
||||
<div class="mt-6">
|
||||
<div class="mb-2 font-bold">处理记录</div>
|
||||
<a-table
|
||||
:data="processRecords"
|
||||
:columns="processColumns"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
>
|
||||
<template #action="{ record }">
|
||||
<a-tag :color="getActionColor(record.action)">
|
||||
{{ getActionText(record.action) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { fetchAlertRecordDetail, fetchAlertProcessList } from '@/api/ops/alertRecord'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
alertRecordId: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const record = ref<any>(null)
|
||||
const processRecords = ref<any[]>([])
|
||||
|
||||
const labels = computed(() => {
|
||||
if (!record.value?.labels) return null
|
||||
try {
|
||||
return JSON.parse(record.value.labels)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const annotations = computed(() => {
|
||||
if (!record.value?.annotations) return null
|
||||
try {
|
||||
return JSON.parse(record.value.annotations)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const processColumns = [
|
||||
{ title: '时间', dataIndex: 'created_at', slotName: 'created_at', width: 180 },
|
||||
{ title: '操作', dataIndex: 'action', slotName: 'action', width: 100 },
|
||||
{ title: '操作人', dataIndex: 'operator', width: 120 },
|
||||
{ title: '备注', dataIndex: 'comment', ellipsis: true, tooltip: true },
|
||||
]
|
||||
|
||||
watch(() => props.visible, (val) => {
|
||||
if (val) {
|
||||
loadDetail()
|
||||
loadProcessRecords()
|
||||
}
|
||||
})
|
||||
|
||||
const loadDetail = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await fetchAlertRecordDetail(props.alertRecordId)
|
||||
record.value = data.details
|
||||
} catch (error) {
|
||||
console.error('加载详情失败:', error)
|
||||
Message.error('加载详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadProcessRecords = async () => {
|
||||
try {
|
||||
const result = await fetchAlertProcessList({
|
||||
alert_record_id: props.alertRecordId,
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
})
|
||||
processRecords.value = result.details.data || []
|
||||
} catch (error) {
|
||||
console.error('加载处理记录失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 格式化函数
|
||||
const formatDateTime = (datetime: string) => {
|
||||
if (!datetime) return '-'
|
||||
return new Date(datetime).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
const formatDuration = (seconds: number) => {
|
||||
if (!seconds) return '-'
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}小时${minutes}分钟`
|
||||
}
|
||||
return `${minutes}分钟`
|
||||
}
|
||||
|
||||
// 状态相关
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
firing: 'red',
|
||||
pending: 'orange',
|
||||
acked: 'gold',
|
||||
resolved: 'green',
|
||||
silenced: 'gray',
|
||||
suppressed: 'lightgray',
|
||||
}
|
||||
return colorMap[status] || 'blue'
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
firing: '告警中',
|
||||
pending: '待处理',
|
||||
acked: '已确认',
|
||||
resolved: '已解决',
|
||||
silenced: '已屏蔽',
|
||||
suppressed: '已抑制',
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
const getNotifyStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
success: 'green',
|
||||
failed: 'red',
|
||||
pending: 'orange',
|
||||
}
|
||||
return colorMap[status] || 'blue'
|
||||
}
|
||||
|
||||
const getNotifyStatusText = (status: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
success: '成功',
|
||||
failed: '失败',
|
||||
pending: '待发送',
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
const getActionColor = (action: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
ack: 'gold',
|
||||
resolve: 'green',
|
||||
silence: 'gray',
|
||||
comment: 'blue',
|
||||
assign: 'purple',
|
||||
escalate: 'orange',
|
||||
close: 'red',
|
||||
}
|
||||
return colorMap[action] || 'blue'
|
||||
}
|
||||
|
||||
const getActionText = (action: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
ack: '确认',
|
||||
resolve: '解决',
|
||||
silence: '屏蔽',
|
||||
comment: '评论',
|
||||
assign: '分配',
|
||||
escalate: '升级',
|
||||
close: '关闭',
|
||||
}
|
||||
return textMap[action] || action
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.alert-detail {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.mb-1 {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
105
src/views/ops/pages/alert/tackle/components/ResolveDialog.vue
Normal file
105
src/views/ops/pages/alert/tackle/components/ResolveDialog.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="解决告警"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleCancel"
|
||||
:confirm-loading="loading"
|
||||
width="600px"
|
||||
>
|
||||
<a-form :model="form" layout="vertical">
|
||||
<a-form-item label="告警ID">
|
||||
<a-input v-model="alertId" disabled />
|
||||
</a-form-item>
|
||||
<a-form-item label="备注">
|
||||
<a-textarea
|
||||
v-model="form.comment"
|
||||
placeholder="请输入备注信息(可选)"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="根本原因">
|
||||
<a-textarea
|
||||
v-model="form.root_cause"
|
||||
placeholder="请输入根本原因(可选)"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="解决方案">
|
||||
<a-textarea
|
||||
v-model="form.solution"
|
||||
placeholder="请输入解决方案(可选)"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { createAlertProcess } from '@/api/ops/alertRecord'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
alertRecordId: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
'success': []
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const form = ref({
|
||||
comment: '',
|
||||
root_cause: '',
|
||||
solution: '',
|
||||
})
|
||||
|
||||
const alertId = ref('')
|
||||
|
||||
watch(() => props.visible, (val) => {
|
||||
if (val) {
|
||||
alertId.value = String(props.alertRecordId)
|
||||
form.value.comment = ''
|
||||
form.value.root_cause = ''
|
||||
form.value.solution = ''
|
||||
}
|
||||
})
|
||||
|
||||
const handleOk = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await createAlertProcess({
|
||||
alert_record_id: props.alertRecordId,
|
||||
action: 'resolve',
|
||||
operator: getCurrentUser(),
|
||||
comment: form.value.comment,
|
||||
root_cause: form.value.root_cause,
|
||||
solution: form.value.solution,
|
||||
})
|
||||
Message.success('解决成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} catch (error) {
|
||||
console.error('解决失败:', error)
|
||||
Message.error('解决失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
function getCurrentUser() {
|
||||
// TODO: 从全局状态获取当前用户
|
||||
return 'admin'
|
||||
}
|
||||
</script>
|
||||
116
src/views/ops/pages/alert/tackle/components/SilenceDialog.vue
Normal file
116
src/views/ops/pages/alert/tackle/components/SilenceDialog.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="屏蔽告警"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleCancel"
|
||||
:confirm-loading="loading"
|
||||
width="600px"
|
||||
>
|
||||
<a-form :model="form" layout="vertical">
|
||||
<a-form-item label="告警ID">
|
||||
<a-input v-model="alertId" disabled />
|
||||
</a-form-item>
|
||||
<a-form-item label="屏蔽时长" required>
|
||||
<a-select v-model="form.duration" placeholder="请选择屏蔽时长">
|
||||
<a-option :value="3600">1小时</a-option>
|
||||
<a-option :value="7200">2小时</a-option>
|
||||
<a-option :value="14400">4小时</a-option>
|
||||
<a-option :value="28800">8小时</a-option>
|
||||
<a-option :value="86400">1天</a-option>
|
||||
<a-option :value="172800">2天</a-option>
|
||||
<a-option :value="604800">1周</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="屏蔽原因">
|
||||
<a-textarea
|
||||
v-model="form.silence_reason"
|
||||
placeholder="请输入屏蔽原因"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注">
|
||||
<a-textarea
|
||||
v-model="form.comment"
|
||||
placeholder="请输入备注信息(可选)"
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { createAlertProcess } from '@/api/ops/alertRecord'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
alertRecordId: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
'success': []
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const form = ref({
|
||||
duration: 3600,
|
||||
silence_reason: '',
|
||||
comment: '',
|
||||
})
|
||||
|
||||
const alertId = ref('')
|
||||
|
||||
watch(() => props.visible, (val) => {
|
||||
if (val) {
|
||||
alertId.value = String(props.alertRecordId)
|
||||
form.value.duration = 3600
|
||||
form.value.silence_reason = ''
|
||||
form.value.comment = ''
|
||||
}
|
||||
})
|
||||
|
||||
const handleOk = async () => {
|
||||
if (!form.value.silence_reason) {
|
||||
Message.error('请输入屏蔽原因')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const silenceUntil = new Date(Date.now() + form.value.duration * 1000).toISOString()
|
||||
|
||||
await createAlertProcess({
|
||||
alert_record_id: props.alertRecordId,
|
||||
action: 'silence',
|
||||
operator: getCurrentUser(),
|
||||
silence_until: silenceUntil,
|
||||
silence_reason: form.value.silence_reason,
|
||||
comment: form.value.comment,
|
||||
})
|
||||
Message.success('屏蔽成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} catch (error) {
|
||||
console.error('屏蔽失败:', error)
|
||||
Message.error('屏蔽失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
function getCurrentUser() {
|
||||
// TODO: 从全局状态获取当前用户
|
||||
return 'admin'
|
||||
}
|
||||
</script>
|
||||
103
src/views/ops/pages/alert/tackle/config/columns.ts
Normal file
103
src/views/ops/pages/alert/tackle/config/columns.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
|
||||
export const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '告警名称',
|
||||
dataIndex: 'alert_name',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '告警级别',
|
||||
dataIndex: 'severity',
|
||||
slotName: 'severity',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '摘要',
|
||||
dataIndex: 'summary',
|
||||
width: 250,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '当前值',
|
||||
dataIndex: 'value',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '阈值',
|
||||
dataIndex: 'threshold',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '开始时间',
|
||||
dataIndex: 'starts_at',
|
||||
slotName: 'starts_at',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '持续时长',
|
||||
dataIndex: 'duration',
|
||||
slotName: 'duration',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '处理状态',
|
||||
dataIndex: 'process_status',
|
||||
slotName: 'process_status',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '处理人',
|
||||
dataIndex: 'processed_by',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '处理时间',
|
||||
dataIndex: 'processed_at',
|
||||
slotName: 'processed_at',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '通知次数',
|
||||
dataIndex: 'notify_count',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '通知状态',
|
||||
dataIndex: 'notify_status',
|
||||
slotName: 'notify_status',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '标签',
|
||||
dataIndex: 'labels',
|
||||
slotName: 'labels',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'actions',
|
||||
slotName: 'actions',
|
||||
fixed: 'right',
|
||||
width: 280,
|
||||
},
|
||||
]
|
||||
31
src/views/ops/pages/alert/tackle/config/search-form.ts
Normal file
31
src/views/ops/pages/alert/tackle/config/search-form.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
|
||||
export const searchFormConfig: FormItem[] = [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键字',
|
||||
type: 'input',
|
||||
placeholder: '请输入告警名称、摘要或描述',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '告警状态',
|
||||
type: 'select',
|
||||
placeholder: '请选择告警状态',
|
||||
options: [
|
||||
{ label: '待处理', value: 'pending' },
|
||||
{ label: '告警中', value: 'firing' },
|
||||
{ label: '已解决', value: 'resolved' },
|
||||
{ label: '已屏蔽', value: 'silenced' },
|
||||
{ label: '已抑制', value: 'suppressed' },
|
||||
{ label: '已确认', value: 'acked' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'severity_id',
|
||||
label: '告警级别',
|
||||
type: 'select',
|
||||
placeholder: '请选择告警级别',
|
||||
options: [],
|
||||
},
|
||||
]
|
||||
488
src/views/ops/pages/alert/tackle/index.vue
Normal file
488
src/views/ops/pages/alert/tackle/index.vue
Normal file
@@ -0,0 +1,488 @@
|
||||
<template>
|
||||
<div class="alert-tackle-container">
|
||||
<a-card :bordered="false" class="general-card">
|
||||
<search-form
|
||||
v-model="searchParams"
|
||||
:form-items="searchFormConfig"
|
||||
:show-buttons="false"
|
||||
>
|
||||
<template #extra>
|
||||
<a-range-picker
|
||||
v-model="timeRange"
|
||||
:time-picker-props="{ defaultValue: '00:00:00' }"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
show-time
|
||||
style="width: 380px; margin-right: 12px"
|
||||
/>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon>
|
||||
<icon-search />
|
||||
</template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
</template>
|
||||
重置
|
||||
</a-button>
|
||||
</template>
|
||||
</search-form>
|
||||
|
||||
<a-divider style="margin: 0" />
|
||||
|
||||
<a-row class="toolbar">
|
||||
<a-col :span="12">
|
||||
<a-space>
|
||||
<a-button
|
||||
v-if="selectedRowKeys.length > 0"
|
||||
type="primary"
|
||||
status="danger"
|
||||
@click="handleBatchAck"
|
||||
>
|
||||
批量确认 ({{ selectedRowKeys.length }})
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-table
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-selection="rowSelection"
|
||||
:scroll="{ x: 2000 }"
|
||||
@page-change="onPageChange"
|
||||
@page-size-change="onPageSizeChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-click="handleRowClick"
|
||||
>
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 }}
|
||||
</template>
|
||||
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template #severity="{ record }">
|
||||
<a-tag v-if="record.severity" :color="record.severity.color">
|
||||
{{ record.severity.name || record.severity.code }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template #starts_at="{ record }">
|
||||
{{ formatDateTime(record.starts_at) }}
|
||||
</template>
|
||||
|
||||
<template #duration="{ record }">
|
||||
{{ formatDuration(record.duration) }}
|
||||
</template>
|
||||
|
||||
<template #process_status="{ record }">
|
||||
<a-tag v-if="record.process_status" color="arcoblue">
|
||||
{{ record.process_status }}
|
||||
</a-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<template #processed_at="{ record }">
|
||||
{{ record.processed_at ? formatDateTime(record.processed_at) : '-' }}
|
||||
</template>
|
||||
|
||||
<template #notify_status="{ record }">
|
||||
<a-tag v-if="record.notify_status" :color="getNotifyStatusColor(record.notify_status)">
|
||||
{{ getNotifyStatusText(record.notify_status) }}
|
||||
</a-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<template #labels="{ record }">
|
||||
<a-space v-if="parsedLabels(record.labels)" wrap>
|
||||
<a-tag
|
||||
v-for="(value, key) in parsedLabels(record.labels)"
|
||||
:key="key"
|
||||
size="small"
|
||||
>
|
||||
{{ key }}: {{ value }}
|
||||
</a-tag>
|
||||
</a-space>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<template #actions="{ record }">
|
||||
<a-space>
|
||||
<a-button type="text" size="small" @click="handleAck(record)">
|
||||
<template #icon>
|
||||
<icon-check />
|
||||
</template>
|
||||
确认
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleResolve(record)">
|
||||
<template #icon>
|
||||
<icon-check-circle />
|
||||
</template>
|
||||
解决
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleSilence(record)">
|
||||
<template #icon>
|
||||
<icon-eye-invisible />
|
||||
</template>
|
||||
屏蔽
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleComment(record)">
|
||||
<template #icon>
|
||||
<icon-message />
|
||||
</template>
|
||||
评论
|
||||
</a-button>
|
||||
<a-dropdown @select="handleMoreAction($event, record)">
|
||||
<a-button type="text" size="small">
|
||||
更多
|
||||
<icon-down />
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-doption value="detail">详情</a-doption>
|
||||
<a-doption value="process">处理记录</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 确认对话框 -->
|
||||
<ack-dialog
|
||||
v-model:visible="ackDialogVisible"
|
||||
:alert-record-id="currentRecord.id"
|
||||
@success="handleSuccess"
|
||||
/>
|
||||
|
||||
<!-- 解决对话框 -->
|
||||
<resolve-dialog
|
||||
v-model:visible="resolveDialogVisible"
|
||||
:alert-record-id="currentRecord.id"
|
||||
@success="handleSuccess"
|
||||
/>
|
||||
|
||||
<!-- 屏蔽对话框 -->
|
||||
<silence-dialog
|
||||
v-model:visible="silenceDialogVisible"
|
||||
:alert-record-id="currentRecord.id"
|
||||
@success="handleSuccess"
|
||||
/>
|
||||
|
||||
<!-- 评论对话框 -->
|
||||
<comment-dialog
|
||||
v-model:visible="commentDialogVisible"
|
||||
:alert-record-id="currentRecord.id"
|
||||
@success="handleSuccess"
|
||||
/>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<detail-dialog
|
||||
v-model:visible="detailDialogVisible"
|
||||
:alert-record-id="currentRecord.id"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import {
|
||||
IconSearch,
|
||||
IconRefresh,
|
||||
IconCheck,
|
||||
IconCheckCircle,
|
||||
IconEyeInvisible,
|
||||
IconMessage,
|
||||
IconDown,
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import SearchForm from '@/components/search-form/index.vue'
|
||||
import { searchFormConfig } from './config/search-form'
|
||||
import { columns } from './config/columns'
|
||||
import { fetchAlertRecords, createAlertProcess } from '@/api/ops/alertRecord'
|
||||
import { fetchAlertLevelList } from '@/api/ops/alertLevel'
|
||||
|
||||
import AckDialog from './components/AckDialog.vue'
|
||||
import ResolveDialog from './components/ResolveDialog.vue'
|
||||
import SilenceDialog from './components/SilenceDialog.vue'
|
||||
import CommentDialog from './components/CommentDialog.vue'
|
||||
import DetailDialog from './components/DetailDialog.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const timeRange = ref<any[]>([])
|
||||
const selectedRowKeys = ref<number[]>([])
|
||||
const currentRecord = ref<any>({})
|
||||
|
||||
// 对话框状态
|
||||
const ackDialogVisible = ref(false)
|
||||
const resolveDialogVisible = ref(false)
|
||||
const silenceDialogVisible = ref(false)
|
||||
const commentDialogVisible = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
showTotal: true,
|
||||
showPageSize: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
})
|
||||
|
||||
// 行选择
|
||||
const rowSelection = computed(() => ({
|
||||
type: 'checkbox',
|
||||
showCheckedAll: true,
|
||||
}))
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = ref<any>({})
|
||||
|
||||
// 加载告警级别列表
|
||||
onMounted(async () => {
|
||||
await loadSeverityOptions()
|
||||
handleSearch()
|
||||
})
|
||||
|
||||
const loadSeverityOptions = async () => {
|
||||
try {
|
||||
const result = await fetchAlertLevelList({ page: 1, page_size: 100 })
|
||||
const severityConfig = searchFormConfig.find((item) => item.field === 'severity_id')
|
||||
if (severityConfig && result.details) {
|
||||
severityConfig.options = result.data.map((item: any) => ({
|
||||
label: item.name || item.code,
|
||||
value: item.id,
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载告警级别失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
timeRange.value = []
|
||||
searchParams.value = {}
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize,
|
||||
...searchParams.value,
|
||||
}
|
||||
|
||||
// 处理时间范围
|
||||
if (timeRange.value && timeRange.value.length === 2) {
|
||||
params.start_time = new Date(timeRange.value[0]).toISOString()
|
||||
params.end_time = new Date(timeRange.value[1]).toISOString()
|
||||
}
|
||||
|
||||
const result = await fetchAlertRecords(params)
|
||||
tableData.value = result.details.data || []
|
||||
pagination.total = result.details.total || 0
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
Message.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 分页
|
||||
const onPageChange = (page: number) => {
|
||||
pagination.current = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
const onPageSizeChange = (pageSize: number) => {
|
||||
pagination.pageSize = pageSize
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 行选择
|
||||
const handleSelectionChange = (rowKeys: number[]) => {
|
||||
selectedRowKeys.value = rowKeys
|
||||
}
|
||||
|
||||
// 行点击
|
||||
const handleRowClick = (record: any) => {
|
||||
currentRecord.value = record
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 处理操作
|
||||
const handleAck = (record: any) => {
|
||||
currentRecord.value = record
|
||||
ackDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleResolve = (record: any) => {
|
||||
currentRecord.value = record
|
||||
resolveDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSilence = (record: any) => {
|
||||
currentRecord.value = record
|
||||
silenceDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleComment = (record: any) => {
|
||||
currentRecord.value = record
|
||||
commentDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 批量确认
|
||||
const handleBatchAck = async () => {
|
||||
try {
|
||||
const promises = selectedRowKeys.value.map((id) =>
|
||||
createAlertProcess({
|
||||
alert_record_id: id,
|
||||
action: 'ack',
|
||||
operator: getCurrentUser(),
|
||||
comment: '批量确认',
|
||||
})
|
||||
)
|
||||
await Promise.all(promises)
|
||||
Message.success(`成功确认 ${selectedRowKeys.value.length} 条告警`)
|
||||
selectedRowKeys.value = []
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('批量确认失败:', error)
|
||||
Message.error('批量确认失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 更多操作
|
||||
const handleMoreAction = (action: string, record: any) => {
|
||||
currentRecord.value = record
|
||||
switch (action) {
|
||||
case 'detail':
|
||||
detailDialogVisible.value = true
|
||||
break
|
||||
case 'process':
|
||||
router.push({
|
||||
path: '/ops/alert/process',
|
||||
query: { alert_record_id: record.id },
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 操作成功回调
|
||||
const handleSuccess = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 格式化函数
|
||||
const formatDateTime = (datetime: string) => {
|
||||
if (!datetime) return '-'
|
||||
return new Date(datetime).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
const formatDuration = (seconds: number) => {
|
||||
if (!seconds) return '-'
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}小时${minutes}分钟`
|
||||
}
|
||||
return `${minutes}分钟`
|
||||
}
|
||||
|
||||
const parsedLabels = (labels: string) => {
|
||||
if (!labels) return null
|
||||
try {
|
||||
return JSON.parse(labels)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 状态相关
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
firing: 'red',
|
||||
pending: 'orange',
|
||||
acked: 'gold',
|
||||
resolved: 'green',
|
||||
silenced: 'gray',
|
||||
suppressed: 'lightgray',
|
||||
}
|
||||
return colorMap[status] || 'blue'
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
firing: '告警中',
|
||||
pending: '待处理',
|
||||
acked: '已确认',
|
||||
resolved: '已解决',
|
||||
silenced: '已屏蔽',
|
||||
suppressed: '已抑制',
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
const getNotifyStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
success: 'green',
|
||||
failed: 'red',
|
||||
pending: 'orange',
|
||||
}
|
||||
return colorMap[status] || 'blue'
|
||||
}
|
||||
|
||||
const getNotifyStatusText = (status: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
success: '成功',
|
||||
failed: '失败',
|
||||
pending: '待发送',
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
function getCurrentUser() {
|
||||
// TODO: 从全局状态获取当前用户
|
||||
return 'admin'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.alert-tackle-container {
|
||||
padding: 20px;
|
||||
|
||||
.general-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
padding: 16px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user