feat
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --config ./config/vite.config.dev.ts",
|
"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",
|
"lint-staged": "npx lint-staged",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"build:normal": "vite build --config ./config/vite.config.prod.ts",
|
"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 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-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
<!-- 自定义表单项插槽 -->
|
||||||
|
<slot name="form-items" />
|
||||||
</a-row>
|
</a-row>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|||||||
@@ -11,7 +11,11 @@
|
|||||||
@update:model-value="handleFormModelUpdate"
|
@update:model-value="handleFormModelUpdate"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@reset="handleReset"
|
@reset="handleReset"
|
||||||
/>
|
>
|
||||||
|
<template #form-items>
|
||||||
|
<slot name="form-items" />
|
||||||
|
</template>
|
||||||
|
</SearchForm>
|
||||||
|
|
||||||
<a-divider style="margin-top: 0" />
|
<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