This commit is contained in:
ygx
2026-03-28 14:24:23 +08:00
parent d9ddc4cd62
commit 8b16116690
8 changed files with 2108 additions and 721 deletions

View File

@@ -0,0 +1,206 @@
import { request } from '@/api/request'
// 告警模板类型定义
export interface AlertTemplate {
id?: number
name: string
category: string
description?: string
enabled?: boolean
tags?: string
rules: AlertRule[]
channels: ChannelRef[]
suppression_rule_ids?: number[]
created_at?: string
updated_at?: string
}
// 告警规则
export interface AlertRule {
name: string
data_source: string
metric_name: string
rule_type: 'static' | 'dynamic' | 'promql'
compare_op: '>' | '>=' | '<' | '<=' | '==' | '!='
threshold: string | number
duration: number
eval_interval: number
severity_code: string
labels?: Record<string, string>
annotations?: Record<string, string>
}
// 通知渠道引用
export interface ChannelRef {
channel_id: number
}
// 通知渠道
export interface NotificationChannel {
id: number
name: string
type: string
config?: Record<string, any>
enabled?: boolean
created_at?: string
updated_at?: string
}
// 抑制规则
export interface SuppressionRule {
id: number
name: string
description?: string
enabled?: boolean
matchers?: Record<string, any>
created_at?: string
updated_at?: string
}
// 告警级别
export interface AlertSeverity {
id: number
code: string
name: string
color?: string
level?: number
}
// 指标元数据
export interface MetricMeta {
metric_name: string
metric_unit: string
type: string
last_timestamp: string
}
// 指标聚合响应
export interface MetricsAggregateResponse {
data_source: string
server_identity: string
count: number
metrics: MetricMeta[]
}
// 模板分类选项
export const TEMPLATE_CATEGORIES = [
{ value: 'os', label: '操作系统监控模板' },
{ value: 'server_hardware', label: '服务器硬件监控模板' },
{ value: 'network_device', label: '网络设备监控模板' },
{ value: 'security_device', label: '安全设备监控模板' },
{ value: 'storage', label: '存储设备监控模板' },
{ value: 'database', label: '数据库监控模板' },
{ value: 'middleware', label: '中间件监控模板' },
{ value: 'virtualization', label: '虚拟化监控模板' },
{ value: 'power_env', label: '电力/UPS/空调/温湿度模板' },
{ value: 'safety_env', label: '消防/门禁/漏水/有害气体模板' },
]
// 数据源选项
export const DATA_SOURCES = [
{ value: 'dc-host', label: '主机/操作系统指标' },
{ value: 'dc-hardware', label: '服务器硬件指标' },
{ value: 'dc-network', label: '网络设备指标' },
{ value: 'dc-database', label: '数据库指标' },
{ value: 'dc-middleware', label: '中间件指标' },
{ value: 'dc-virtualization', label: '虚拟化指标' },
{ value: 'dc-env', label: '动力/环境/安防指标' },
]
// 比较运算符选项
export const COMPARE_OPERATORS = [
{ value: '>', label: '大于' },
{ value: '>=', label: '大于等于' },
{ value: '<', label: '小于' },
{ value: '<=', label: '小于等于' },
{ value: '==', label: '等于' },
{ value: '!=', label: '不等于' },
]
// 规则类型选项
export const RULE_TYPES = [
{ value: 'static', label: '静态阈值' },
{ value: 'dynamic', label: '动态阈值' },
{ value: 'promql', label: 'PromQL 表达式' },
]
// ==================== 告警模板接口 ====================
/** 获取告警模板列表 */
export const fetchTemplateList = (params?: {
page?: number
page_size?: number
name?: string
category?: string
enabled?: boolean
}) => {
return request.get('/Alert/v1/template/list', { params })
}
/** 获取告警模板详情 */
export const fetchTemplateDetail = (id: number) => {
return request.get(`/Alert/v1/template/get/${id}`)
}
/** 创建告警模板 */
export const createTemplate = (data: AlertTemplate) => {
return request.post('/Alert/v1/template/create', data)
}
/** 更新告警模板 */
export const updateTemplate = (data: AlertTemplate) => {
return request.post('/Alert/v1/template/update', data)
}
/** 删除告警模板 */
export const deleteTemplate = (id: number) => {
return request.delete(`/Alert/v1/template/delete/${id}`)
}
// ==================== 通知渠道接口 ====================
/** 获取通知渠道列表 */
export const fetchChannelList = (params?: { enabled?: boolean; keyword?: string }) => {
return request.get('/Alert/v1/channel/list', { params })
}
/** 获取通知渠道详情 */
export const fetchChannelDetail = (id: number) => {
return request.get(`/Alert/v1/channel/get/${id}`)
}
// ==================== 抑制规则接口 ====================
/** 获取抑制规则列表 */
export const fetchSuppressionList = (params?: { enabled?: boolean; keyword?: string }) => {
return request.get('/Alert/v1/suppression/list', { params })
}
/** 获取抑制规则详情 */
export const fetchSuppressionDetail = (id: number) => {
return request.get(`/Alert/v1/suppression/get/${id}`)
}
// ==================== 告警级别接口 ====================
/** 获取告警级别列表 */
export const fetchSeverityList = () => {
return request.get('/Alert/v1/severity/list')
}
/** 按 code 获取告警级别 */
export const fetchSeverityByCode = (code: string) => {
return request.get(`/Alert/v1/severity/get-by-code/${code}`)
}
// ==================== 指标元数据接口 ====================
/** 获取指标元数据 */
export const fetchMetricsMeta = (params: {
data_source: string
server_identity?: string
keyword?: string
limit?: number
}) => {
return request.get('/DC-Control/v1/services/metrics/meta', { params })
}

View File

@@ -446,6 +446,23 @@ export const localMenuFlatItems: MenuItem[] = [
sort_key: 30,
created_at: '2025-12-26T13:23:52.047548+08:00',
},
{
id: 4001,
identity: '019b591d-026f-785d-b473-ac804133e252',
title: '告警模版编辑',
title_en: 'Alert Template Edit',
code: 'ops:告警管理:告警模版编辑',
description: '告警管理 - 告警模版编辑',
app_id: 2,
parent_id: 39,
menu_path: '/alert/template/edit',
component: 'ops/pages/alert/template/edit/index',
menu_icon: 'appstore',
type: 1,
sort_key: 31,
hide_menu: true,
created_at: '2025-12-26T13:23:52.047548+08:00',
},
{
id: 41,
identity: '019b591d-027d-7eae-b9b9-23fd1b7ece75',

View File

@@ -479,6 +479,24 @@ export const localMenuItems: MenuItem[] = [
created_at: '2025-12-26T13:23:52.047548+08:00',
children: [],
},
{
id: 4001,
identity: '019b591d-026f-785d-b473-ac804133e252',
title: '告警模版编辑',
title_en: 'Alert Template Edit',
code: 'ops:告警管理:告警模版编辑',
description: '告警管理 - 告警模版编辑',
app_id: 2,
parent_id: 39,
menu_path: '/alert/template/edit',
component: 'ops/pages/alert/template/edit/index',
menu_icon: 'appstore',
type: 1,
sort_key: 7,
hide_menu: true,
created_at: '2025-12-26T13:23:52.047548+08:00',
children: [],
},
{
id: 41,
identity: '019b591d-027d-7eae-b9b9-23fd1b7ece75',

View File

@@ -0,0 +1,325 @@
<template>
<div class="rule-editor">
<a-form ref="ruleFormRef" :model="{ rules: localRules }" layout="vertical">
<div v-if="localRules.length === 0" class="empty-rules">
<a-empty description="暂无规则,请点击上方按钮添加" />
</div>
<div v-else class="rules-container">
<a-card
v-for="(rule, index) in localRules"
:key="index"
class="rule-card"
:title="`规则 ${index + 1}: ${rule.name || '未命名'}`"
>
<template #extra>
<a-button type="text" size="small" status="danger" @click="handleRemoveRule(index)">
<template #icon><icon-delete /></template>
删除
</a-button>
</template>
<!-- 第一行基本信息 -->
<div class="rule-section">
<div class="section-title">基本信息</div>
<a-row :gutter="16">
<a-col :xs="24" :sm="12" :md="6">
<a-form-item label="规则名称" :field="`rules[${index}].name`" :rules="getRuleFieldRules('name')">
<a-input v-model="rule.name" placeholder="请输入规则名称" @change="syncUpdate(index)" />
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12" :md="6">
<a-form-item label="数据源" :field="`rules[${index}].data_source`" :rules="getRuleFieldRules('data_source')">
<a-select v-model="rule.data_source" placeholder="请选择" @change="handleRuleDataSourceChange(index)">
<a-option v-for="item in DATA_SOURCES" :key="item.value" :value="item.value">
{{ item.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12" :md="6">
<a-form-item label="指标名称" :field="`rules[${index}].metric_name`" :rules="getRuleFieldRules('metric_name')">
<a-select
v-model="rule.metric_name"
placeholder="请选择指标"
allow-search
:loading="rule._metricsLoading"
@dropdown-visible-change="handleMetricsDropdownChange($event, index)"
@change="syncUpdate(index)"
>
<a-option v-for="metric in rule._metrics" :key="metric.metric_name" :value="metric.metric_name">
{{ metric.metric_name }} ({{ metric.metric_unit || '无单位' }})
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12" :md="6">
<a-form-item label="告警级别" :field="`rules[${index}].severity_code`" :rules="getRuleFieldRules('severity_code')">
<a-select v-model="rule.severity_code" placeholder="请选择" @change="syncUpdate(index)">
<a-option v-for="item in severityOptions" :key="item.code" :value="item.code">
{{ item.name }}
</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
</div>
<!-- 第二行条件配置 -->
<div class="rule-section">
<div class="section-title">触发条件</div>
<a-row :gutter="16">
<a-col :xs="24" :sm="12" :md="6">
<a-form-item label="规则类型" :field="`rules[${index}].rule_type`" :rules="getRuleFieldRules('rule_type')">
<a-select v-model="rule.rule_type" placeholder="请选择" @change="syncUpdate(index)">
<a-option v-for="item in RULE_TYPES" :key="item.value" :value="item.value">
{{ item.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12" :md="6">
<a-form-item label="比较运算符" :field="`rules[${index}].compare_op`" :rules="getRuleFieldRules('compare_op')">
<a-select v-model="rule.compare_op" placeholder="请选择" @change="syncUpdate(index)">
<a-option v-for="item in COMPARE_OPERATORS" :key="item.value" :value="item.value">
{{ item.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12" :md="6">
<a-form-item label="阈值" :field="`rules[${index}].threshold`" :rules="getRuleFieldRules('threshold')">
<a-input-number v-model="rule.threshold" placeholder="请输入阈值" style="width: 100%" @change="syncUpdate(index)" />
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12" :md="6">
<a-form-item label="持续时间" :field="`rules[${index}].duration`" :rules="getRuleFieldRules('duration')">
<a-input-number v-model="rule.duration" placeholder="请输入" :min="0" style="width: 100%" @change="syncUpdate(index)">
<template #suffix></template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
</div>
<!-- 第三行评估配置 -->
<div class="rule-section">
<div class="section-title">评估配置</div>
<a-row :gutter="16">
<a-col :xs="24" :sm="12" :md="6">
<a-form-item label="评估间隔" :field="`rules[${index}].eval_interval`" :rules="getRuleFieldRules('eval_interval')">
<a-input-number v-model="rule.eval_interval" placeholder="请输入" :min="0" style="width: 100%" @change="syncUpdate(index)">
<template #suffix></template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
</div>
</a-card>
</div>
</a-form>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import type { FormInstance } from '@arco-design/web-vue'
import {
fetchMetricsMeta,
DATA_SOURCES,
COMPARE_OPERATORS,
RULE_TYPES,
type AlertRule,
type AlertSeverity,
type MetricMeta,
} from '@/api/ops/alertTemplate'
interface RuleItem extends AlertRule {
_metrics?: MetricMeta[]
_metricsLoading?: boolean
}
const props = defineProps<{
rules: RuleItem[]
severityOptions: AlertSeverity[]
}>()
const emit = defineEmits<{
(e: 'update:rules', value: RuleItem[]): void
(e: 'remove', index: number): void
}>()
// 表单引用
const ruleFormRef = ref<FormInstance | null>(null)
// 本地规则数据(深拷贝)
const localRules = ref<RuleItem[]>([])
// 监听 props 变化,深拷贝到本地
watch(
() => props.rules,
(val) => {
localRules.value = JSON.parse(JSON.stringify(val))
},
{ immediate: true, deep: true }
)
// 动态生成验证规则
const getRuleFieldRules = (field: string) => {
const messages: Record<string, string> = {
name: '请输入规则名称',
data_source: '请选择数据源',
metric_name: '请选择指标名称',
rule_type: '请选择规则类型',
compare_op: '请选择比较运算符',
threshold: '请输入阈值',
duration: '请输入持续时间',
eval_interval: '请输入评估间隔',
severity_code: '请选择告警级别',
}
return [{ required: true, message: messages[field] || '该字段为必填项' }]
}
// 暴露验证方法给父组件
const validate = async () => {
return ruleFormRef.value?.validate()
}
defineExpose({
validate,
})
// 同步更新到父组件
const syncUpdate = (index: number) => {
const newRules = [...props.rules]
const { _metrics, _metricsLoading, ...rest } = localRules.value[index]
newRules[index] = { ...rest, _metrics, _metricsLoading } as RuleItem
emit('update:rules', newRules)
}
// 移除规则
const handleRemoveRule = (index: number) => {
emit('remove', index)
}
// 规则数据源变化
const handleRuleDataSourceChange = (index: number) => {
localRules.value[index].metric_name = ''
localRules.value[index]._metrics = []
syncUpdate(index)
// 数据源变化后自动加载指标
handleLoadMetrics(index)
}
// 下拉框展开时加载指标(备用触发)
const handleMetricsDropdownChange = (visible: boolean, index: number) => {
if (visible) {
handleLoadMetrics(index)
}
}
// 加载指标
const handleLoadMetrics = async (index: number) => {
// 每次都重新获取引用,避免引用失效
if (!localRules.value[index]?.data_source) {
return
}
if (localRules.value[index]._metrics && localRules.value[index]._metrics!.length > 0) {
return
}
localRules.value[index]._metricsLoading = true
try {
const res: any = await fetchMetricsMeta({
data_source: localRules.value[index].data_source,
limit: 500,
})
if (res.code === 0) {
localRules.value[index]._metrics = res.details?.metrics || []
// 同步更新到父组件
syncUpdate(index)
}
} catch (error) {
console.error('加载指标失败:', error)
} finally {
localRules.value[index]._metricsLoading = false
}
}
</script>
<script lang="ts">
export default {
name: 'RuleEditor',
}
</script>
<style scoped lang="less">
.rule-editor {
height: 100%;
}
.empty-rules {
padding: 40px 20px;
text-align: center;
}
.rules-container {
display: flex;
flex-direction: column;
gap: 16px;
}
.rule-card {
:deep(.arco-card-header) {
border-bottom: 1px solid var(--color-border);
background-color: var(--color-fill-1);
}
:deep(.arco-card-body) {
padding: 16px;
}
}
.rule-section {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 13px;
font-weight: 500;
color: var(--color-text-2);
margin-bottom: 12px;
padding-left: 8px;
border-left: 3px solid rgb(var(--primary-6));
}
}
// 表单项样式
:deep(.arco-form-item) {
margin-bottom: 0;
}
// 响应式布局
@media (max-width: 768px) {
.rule-card {
:deep(.arco-card-body) {
padding: 12px;
}
}
.rule-section {
.section-title {
margin-bottom: 8px;
}
}
:deep(.arco-form-item) {
margin-bottom: 12px;
}
}
</style>

View File

@@ -0,0 +1,169 @@
<template>
<a-modal
v-model:visible="modalVisible"
:title="title"
:width="900"
:footer="false"
@cancel="handleClose"
>
<div v-if="loading" class="loading-container">
<a-spin />
</div>
<div v-else-if="detail">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="模板 ID">
{{ detail.id }}
</a-descriptions-item>
<a-descriptions-item label="模板名称">
{{ detail.name }}
</a-descriptions-item>
<a-descriptions-item label="模板分类">
{{ getCategoryLabel(detail.category) }}
</a-descriptions-item>
<a-descriptions-item label="启用状态">
<a-tag :color="detail.enabled ? 'green' : 'red'">
{{ detail.enabled ? '已启用' : '已禁用' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="标签" :span="2">
<a-tag v-for="tag in (detail.tags || '').split(',').filter(Boolean)" :key="tag">
{{ tag }}
</a-tag>
<span v-if="!detail.tags">暂无标签</span>
</a-descriptions-item>
<a-descriptions-item label="描述" :span="2">
{{ detail.description || '暂无描述' }}
</a-descriptions-item>
</a-descriptions>
<!-- 规则列表 -->
<a-divider orientation="left">规则列表</a-divider>
<a-table
v-if="detail.rules && detail.rules.length > 0"
:data="detail.rules"
:columns="ruleColumns"
:pagination="false"
stripe
/>
<a-empty v-else description="暂无规则" />
<!-- 通知渠道 -->
<a-divider orientation="left">通知渠道</a-divider>
<div v-if="detail.channels && detail.channels.length > 0">
<a-tag v-for="channel in detail.channels" :key="channel.channel_id" color="arcoblue">
{{ getChannelName(channel.channel_id) }}
</a-tag>
</div>
<a-empty v-else description="暂无通知渠道" />
<!-- 抑制规则 -->
<a-divider orientation="left">抑制规则</a-divider>
<div v-if="detail.suppression_rule_ids && detail.suppression_rule_ids.length > 0">
<a-tag v-for="id in detail.suppression_rule_ids" :key="id" color="orangered">
{{ getSuppressionName(id) }}
</a-tag>
</div>
<a-empty v-else description="暂无抑制规则" />
</div>
<a-empty v-else description="暂无数据" />
</a-modal>
</template>
<script lang="ts" setup>
import { ref, watch, computed } from 'vue'
import { fetchTemplateDetail, TEMPLATE_CATEGORIES, type AlertTemplate } from '@/api/ops/alertTemplate'
import type { NotificationChannel, SuppressionRule } from '@/api/ops/alertTemplate'
const props = defineProps<{
visible: boolean
title: string
templateId?: number | null
channelOptions: NotificationChannel[]
suppressionOptions: SuppressionRule[]
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
}>()
// 计算属性处理 v-model
const modalVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val),
})
// 状态
const loading = ref(false)
const detail = ref<AlertTemplate | null>(null)
// 规则表格列
const ruleColumns = [
{ title: '规则名称', dataIndex: 'name', width: 150 },
{ title: '数据源', dataIndex: 'data_source', width: 100 },
{ title: '指标', dataIndex: 'metric_name', width: 150 },
{ title: '运算符', dataIndex: 'compare_op', width: 80 },
{ title: '阈值', dataIndex: 'threshold', width: 80 },
{ title: '持续时间', dataIndex: 'duration', width: 80 },
{ title: '告警级别', dataIndex: 'severity_code', width: 100 },
]
// 监听visible变化加载详情
watch(
() => props.visible,
async (val) => {
if (val && props.templateId) {
loading.value = true
try {
const res: any = await fetchTemplateDetail(props.templateId)
if (res.code === 0) {
detail.value = res.details
}
} catch (error) {
console.error('获取模板详情失败:', error)
} finally {
loading.value = false
}
} else {
detail.value = null
}
}
)
// 获取分类标签
const getCategoryLabel = (value: string) => {
const item = TEMPLATE_CATEGORIES.find(c => c.value === value)
return item?.label || value
}
// 获取渠道名称
const getChannelName = (id: number) => {
const item = props.channelOptions.find(c => c.id === id)
return item?.name || String(id)
}
// 获取抑制规则名称
const getSuppressionName = (id: number) => {
const item = props.suppressionOptions.find(s => s.id === id)
return item?.name || String(id)
}
// 关闭弹窗
const handleClose = () => {
emit('update:visible', false)
}
</script>
<script lang="ts">
export default {
name: 'TemplateDetailDialog',
}
</script>
<style scoped lang="less">
.loading-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
</style>

View File

@@ -0,0 +1,493 @@
<template>
<a-modal
v-model:visible="modalVisible"
:title="title"
:width="1000"
:ok-loading="submitting"
@before-ok="handleSubmit"
@cancel="handleClose"
>
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
layout="vertical"
>
<!-- 基础信息区 -->
<a-divider orientation="left">基础信息</a-divider>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="模板名称" field="name" required>
<a-input
v-model="formData.name"
placeholder="请输入模板名称"
allow-clear
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="模板分类" field="category" required>
<a-select v-model="formData.category" placeholder="请选择分类" @change="handleCategoryChange">
<a-option v-for="item in TEMPLATE_CATEGORIES" :key="item.value" :value="item.value">
{{ item.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="是否启用" field="enabled">
<a-switch v-model="formData.enabled" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item label="标签" field="tagsList" required>
<a-input-tag
v-model="formData.tagsList"
placeholder="输入后按回车添加"
allow-clear
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item label="描述" field="description">
<a-textarea
v-model="formData.description"
placeholder="请输入模板描述"
:max-length="500"
:auto-size="{ minRows: 2, maxRows: 4 }"
/>
</a-form-item>
</a-col>
</a-row>
<!-- 规则配置区 -->
<a-divider orientation="left">
规则配置
<a-button type="text" size="small" @click="handleAddRule">
<template #icon><icon-plus /></template>
添加规则
</a-button>
</a-divider>
<RuleEditor
v-model:rules="formData.rules"
:severity-options="severityOptions"
@remove="handleRemoveRule"
/>
<!-- 通知渠道区 -->
<a-divider orientation="left">通知渠道</a-divider>
<a-form-item label="选择通知渠道" field="selectedChannelIds" required>
<a-select
v-model="formData.selectedChannelIds"
placeholder="请选择通知渠道(可搜索)"
multiple
allow-search
:filter-option="false"
@search="handleChannelSearch"
@dropdown-visible-change="handleChannelDropdownChange"
>
<a-option v-for="item in channelOptions" :key="item.id" :value="item.id">
{{ item.name }}
</a-option>
</a-select>
</a-form-item>
<!-- 抑制规则区 -->
<a-divider orientation="left">抑制规则</a-divider>
<a-form-item label="选择抑制规则" field="suppression_rule_ids" required>
<a-select
v-model="formData.suppression_rule_ids"
placeholder="请选择抑制规则(可搜索)"
multiple
allow-search
:filter-option="false"
@search="handleSuppressionSearch"
@dropdown-visible-change="handleSuppressionDropdownChange"
>
<a-option v-for="item in suppressionOptions" :key="item.id" :value="item.id">
{{ item.name }}
</a-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, watch, computed } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { FormInstance } from '@arco-design/web-vue/es/form'
import RuleEditor from './RuleEditor.vue'
import {
createTemplate,
updateTemplate,
fetchChannelList,
fetchSuppressionList,
fetchMetricsMeta,
TEMPLATE_CATEGORIES,
type AlertTemplate,
type AlertRule,
type NotificationChannel,
type SuppressionRule,
type AlertSeverity,
type MetricMeta,
} from '@/api/ops/alertTemplate'
interface RuleItem extends AlertRule {
_metrics?: MetricMeta[]
_metricsLoading?: boolean
}
interface FormData {
name: string
category: string
description: string
enabled: boolean
tagsList: string[]
rules: RuleItem[]
selectedChannelIds: number[]
suppression_rule_ids: number[]
}
const props = defineProps<{
visible: boolean
title: string
isEdit: boolean
editData?: AlertTemplate | null
severityOptions: AlertSeverity[]
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
(e: 'success'): void
}>()
// 计算属性处理 v-model
const modalVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val),
})
// 状态
const submitting = ref(false)
const formRef = ref<FormInstance>()
// 下拉选项
const channelOptions = ref<NotificationChannel[]>([])
const suppressionOptions = ref<SuppressionRule[]>([])
// 表单数据
const formData = ref<FormData>({
name: '',
category: '',
description: '',
enabled: true,
tagsList: [],
rules: [],
selectedChannelIds: [],
suppression_rule_ids: [],
})
// 表单验证规则
const formRules = {
name: [
{ required: true, message: '请输入模板名称' },
{ minLength: 2, message: '模板名称至少2个字符' },
{ maxLength: 100, message: '模板名称最多100个字符' },
],
category: [{ required: true, message: '请选择模板分类' }],
tagsList: [{ required: true, message: '请添加标签' }],
selectedChannelIds: [{ required: true, message: '请选择通知渠道' }],
suppression_rule_ids: [{ required: true, message: '请选择抑制规则' }],
}
// 监听visible变化初始化表单
watch(
() => props.visible,
async (val) => {
if (val) {
loadChannelOptions()
loadSuppressionOptions()
if (props.isEdit && props.editData) {
// 编辑模式
const tagsList = (props.editData.tags || '').split(',').filter(Boolean)
const selectedChannelIds = (props.editData.channels || []).map(c => c.channel_id)
// 初始化规则数据
const rules = (props.editData.rules || []).map(r => ({
...r,
_metrics: [],
_metricsLoading: false,
}))
formData.value = {
name: props.editData.name,
category: props.editData.category,
description: props.editData.description || '',
enabled: props.editData.enabled ?? true,
tagsList,
rules,
selectedChannelIds,
suppression_rule_ids: props.editData.suppression_rule_ids || [],
}
// 为已有规则加载指标数据
for (let i = 0; i < rules.length; i++) {
if (rules[i].data_source) {
await loadMetricsForRule(i)
}
}
} else {
// 新建模式
formData.value = {
name: '',
category: '',
description: '',
enabled: true,
tagsList: [],
rules: [],
selectedChannelIds: [],
suppression_rule_ids: [],
}
}
}
}
)
// 为规则加载指标数据
const loadMetricsForRule = async (index: number) => {
const rule = formData.value.rules[index]
if (!rule.data_source || rule._metrics?.length) return
rule._metricsLoading = true
try {
const res: any = await fetchMetricsMeta({
data_source: rule.data_source,
limit: 500,
})
if (res.code === 0) {
rule._metrics = res.details?.metrics || []
}
} catch (error) {
console.error('加载指标失败:', error)
} finally {
rule._metricsLoading = false
}
}
// 加载通知渠道
const loadChannelOptions = async () => {
try {
const res: any = await fetchChannelList({ enabled: true })
if (res.code === 0) {
channelOptions.value = res.details?.data || res.details?.list || []
}
} catch (error) {
console.error('加载通知渠道失败:', error)
}
}
// 加载抑制规则
const loadSuppressionOptions = async () => {
try {
const res: any = await fetchSuppressionList({ enabled: true })
if (res.code === 0) {
suppressionOptions.value = res.details?.data || res.details?.list || []
}
} catch (error) {
console.error('加载抑制规则失败:', error)
}
}
// 通知渠道搜索
const handleChannelSearch = async (keyword: string) => {
try {
const res: any = await fetchChannelList({ enabled: true, keyword })
if (res.code === 0) {
channelOptions.value = res.details?.data || res.details?.list || []
}
} catch (error) {
console.error('搜索通知渠道失败:', error)
}
}
const handleChannelDropdownChange = (visible: boolean) => {
if (visible && channelOptions.value.length === 0) {
loadChannelOptions()
}
}
// 抑制规则搜索
const handleSuppressionSearch = async (keyword: string) => {
try {
const res: any = await fetchSuppressionList({ enabled: true, keyword })
if (res.code === 0) {
suppressionOptions.value = res.details?.data || res.details?.list || []
}
} catch (error) {
console.error('搜索抑制规则失败:', error)
}
}
const handleSuppressionDropdownChange = (visible: boolean) => {
if (visible && suppressionOptions.value.length === 0) {
loadSuppressionOptions()
}
}
// 分类变化时加载默认规则
const handleCategoryChange = (category: string) => {
if (!props.isEdit && formData.value.rules.length === 0) {
const defaultRules = getDefaultRules(category)
formData.value.rules = defaultRules.map(r => ({
...r,
_enabled: true,
_metrics: [],
_metricsLoading: false,
}))
}
}
// 获取默认规则
const getDefaultRules = (category: string): AlertRule[] => {
const defaultRulesMap: Record<string, AlertRule[]> = {
'os': [
{ name: 'CPU使用率过高', data_source: 'dc-host', metric_name: 'cpu_usage', rule_type: 'static', compare_op: '>=', threshold: 80, duration: 300, eval_interval: 60, severity_code: 'warning' },
{ name: '内存使用率过高', data_source: 'dc-host', metric_name: 'memory_usage', rule_type: 'static', compare_op: '>=', threshold: 80, duration: 300, eval_interval: 60, severity_code: 'warning' },
{ name: '磁盘使用率过高', data_source: 'dc-host', metric_name: 'disk_usage', rule_type: 'static', compare_op: '>=', threshold: 85, duration: 600, eval_interval: 60, severity_code: 'warning' },
],
'server_hardware': [
{ name: 'CPU温度过高', data_source: 'dc-hardware', metric_name: 'server_temperature_cpu', rule_type: 'static', compare_op: '>=', threshold: 75, duration: 300, eval_interval: 60, severity_code: 'warning' },
{ name: '风扇转速异常', data_source: 'dc-hardware', metric_name: 'fan_speed', rule_type: 'static', compare_op: '<=', threshold: 800, duration: 300, eval_interval: 60, severity_code: 'warning' },
],
'network_device': [
{ name: '接口Down告警', data_source: 'dc-network', metric_name: 'interface_status', rule_type: 'static', compare_op: '==', threshold: 'down', duration: 60, eval_interval: 30, severity_code: 'critical' },
{ name: '端口带宽利用率过高', data_source: 'dc-network', metric_name: 'interface_usage_percent', rule_type: 'static', compare_op: '>=', threshold: 80, duration: 300, eval_interval: 60, severity_code: 'warning' },
],
'database': [
{ name: '数据库连接数使用率过高', data_source: 'dc-database', metric_name: 'db_conn_usage_percent', rule_type: 'static', compare_op: '>=', threshold: 80, duration: 300, eval_interval: 60, severity_code: 'warning' },
{ name: '慢查询次数过多', data_source: 'dc-database', metric_name: 'slow_query_count', rule_type: 'static', compare_op: '>=', threshold: 50, duration: 300, eval_interval: 60, severity_code: 'warning' },
],
'middleware': [
{ name: '请求错误率过高', data_source: 'dc-middleware', metric_name: 'error_rate', rule_type: 'static', compare_op: '>=', threshold: 1, duration: 300, eval_interval: 60, severity_code: 'warning' },
{ name: '队列消息堆积', data_source: 'dc-middleware', metric_name: 'queue_depth', rule_type: 'static', compare_op: '>=', threshold: 10000, duration: 600, eval_interval: 60, severity_code: 'critical' },
],
}
return defaultRulesMap[category] || []
}
// 添加规则
const handleAddRule = () => {
formData.value.rules.push({
name: '',
data_source: formData.value.category ? getDefaultDataSource(formData.value.category) : 'dc-host',
metric_name: '',
rule_type: 'static',
compare_op: '>=',
threshold: 0,
duration: 300,
eval_interval: 60,
severity_code: 'warning',
_metrics: [],
_metricsLoading: false,
})
}
// 获取默认数据源
const getDefaultDataSource = (category: string): string => {
const map: Record<string, string> = {
'os': 'dc-host',
'server_hardware': 'dc-hardware',
'network_device': 'dc-network',
'database': 'dc-database',
'middleware': 'dc-middleware',
'virtualization': 'dc-virtualization',
'power_env': 'dc-env',
'safety_env': 'dc-env',
}
return map[category] || 'dc-host'
}
// 移除规则
const handleRemoveRule = (index: number) => {
formData.value.rules.splice(index, 1)
}
// 提交表单
const handleSubmit = async () => {
const valid = await formRef.value?.validate()
if (valid) {
return false // 校验失败,阻止弹窗关闭
}
submitting.value = true
try {
// 提取规则数据
const submitRules = formData.value.rules.map(r => {
const { _metrics, _metricsLoading, ...rest } = r as any
return rest
})
const submitData: AlertTemplate = {
name: formData.value.name,
category: formData.value.category,
description: formData.value.description,
enabled: formData.value.enabled,
tags: formData.value.tagsList.join(','),
rules: submitRules,
channels: formData.value.selectedChannelIds.map(id => ({ channel_id: id })),
suppression_rule_ids: formData.value.suppression_rule_ids,
}
if (props.isEdit && props.editData?.id) {
submitData.id = props.editData.id
const res: any = await updateTemplate(submitData)
if (res.code === 0) {
Message.success('模板更新成功')
emit('success')
return true // 成功,允许关闭弹窗
} else {
Message.error(res.msg || '更新失败')
return false
}
} else {
const res: any = await createTemplate(submitData)
if (res.code === 0) {
Message.success('模板创建成功')
emit('success')
return true // 成功,允许关闭弹窗
} else {
Message.error(res.msg || '创建失败')
return false
}
}
} catch (error: any) {
console.error('提交失败:', error)
Message.error(error.message || '操作失败')
return false
} finally {
submitting.value = false
}
}
// 关闭弹窗
const handleClose = () => {
emit('update:visible', false)
formRef.value?.resetFields()
}
</script>
<script lang="ts">
export default {
name: 'TemplateFormDialog',
}
</script>

View File

@@ -0,0 +1,708 @@
<template>
<div class="template-edit-page">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-left">
<a-button type="text" @click="handleBack">
<template #icon><icon-left /></template>
返回列表
</a-button>
<a-divider direction="vertical" />
<h2 class="page-title">{{ pageTitle }}</h2>
</div>
<div class="header-right">
<a-space>
<a-button @click="handleBack">取消</a-button>
<a-button type="primary" :loading="submitting" @click="handleSubmit">
{{ isEdit ? '保存' : '创建' }}
</a-button>
</a-space>
</div>
</div>
<!-- 表单内容 -->
<div class="page-content">
<a-spin :loading="loading" style="width: 100%">
<a-row :gutter="20">
<!-- 左侧基础信息 -->
<a-col :xs="24" :sm="24" :md="10" :lg="8">
<a-card class="info-card" title="基础信息">
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
layout="vertical"
>
<a-form-item label="模板名称" field="name" required>
<a-input
v-model="formData.name"
placeholder="请输入模板名称"
allow-clear
/>
</a-form-item>
<a-form-item label="模板分类" field="category" required>
<a-select v-model="formData.category" placeholder="请选择分类" @change="handleCategoryChange">
<a-option v-for="item in TEMPLATE_CATEGORIES" :key="item.value" :value="item.value">
{{ item.label }}
</a-option>
</a-select>
</a-form-item>
<a-form-item label="是否启用" field="enabled">
<a-switch v-model="formData.enabled" />
</a-form-item>
<a-form-item label="标签" field="tagsList" required>
<a-input-tag
v-model="formData.tagsList"
placeholder="输入后按回车添加"
allow-clear
/>
</a-form-item>
<a-form-item label="描述" field="description">
<a-textarea
v-model="formData.description"
placeholder="请输入模板描述"
:max-length="500"
:auto-size="{ minRows: 3, maxRows: 5 }"
show-word-limit
/>
</a-form-item>
</a-form>
</a-card>
<!-- 通知渠道 -->
<a-card class="info-card" title="通知渠道">
<a-form-item label="选择通知渠道" field="selectedChannelIds" required :rules="[{ required: true, message: '请选择通知渠道' }]">
<a-select
v-model="formData.selectedChannelIds"
placeholder="请选择通知渠道"
multiple
allow-search
:filter-option="false"
@search="handleChannelSearch"
@dropdown-visible-change="handleChannelDropdownChange"
>
<a-option v-for="item in channelOptions" :key="item.id" :value="item.id">
{{ item.name }}
</a-option>
</a-select>
</a-form-item>
</a-card>
<!-- 抑制规则 -->
<a-card class="info-card" title="抑制规则">
<a-form-item label="选择抑制规则" field="suppression_rule_ids" required :rules="[{ required: true, message: '请选择抑制规则' }]">
<a-select
v-model="formData.suppression_rule_ids"
placeholder="请选择抑制规则"
multiple
allow-search
:filter-option="false"
@search="handleSuppressionSearch"
@dropdown-visible-change="handleSuppressionDropdownChange"
>
<a-option v-for="item in suppressionOptions" :key="item.id" :value="item.id">
{{ item.name }}
</a-option>
</a-select>
</a-form-item>
</a-card>
</a-col>
<!-- 右侧规则配置 -->
<a-col :xs="24" :sm="24" :md="14" :lg="16">
<a-card class="rules-card">
<template #title>
<div class="rules-card-title">
<span>规则配置</span>
<a-badge :count="formData.rules.length" :dot-style="{ backgroundColor: '#165dff' }" />
</div>
</template>
<template #extra>
<a-button type="primary" size="small" @click="handleAddRule">
<template #icon><icon-plus /></template>
添加规则
</a-button>
</template>
<div class="rules-content">
<a-empty v-if="formData.rules.length === 0" description="暂无规则,请添加">
<a-button type="primary" @click="handleAddRule">
<template #icon><icon-plus /></template>
添加第一条规则
</a-button>
</a-empty>
<RuleEditor
v-else
ref="ruleEditorRef"
v-model:rules="formData.rules"
:severity-options="severityOptions"
@remove="handleRemoveRule"
/>
</div>
</a-card>
</a-col>
</a-row>
</a-spin>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Message } from '@arco-design/web-vue'
import type { FormInstance } from '@arco-design/web-vue/es/form'
import RuleEditor from '../components/RuleEditor.vue'
import {
createTemplate,
updateTemplate,
fetchTemplateDetail,
fetchChannelList,
fetchSuppressionList,
fetchSeverityList,
fetchMetricsMeta,
TEMPLATE_CATEGORIES,
type AlertTemplate,
type AlertRule,
type NotificationChannel,
type SuppressionRule,
type AlertSeverity,
type MetricMeta,
} from '@/api/ops/alertTemplate'
interface RuleItem extends AlertRule {
_metrics?: MetricMeta[]
_metricsLoading?: boolean
}
interface FormData {
name: string
category: string
description: string
enabled: boolean
tagsList: string[]
rules: RuleItem[]
selectedChannelIds: number[]
suppression_rule_ids: number[]
}
const route = useRoute()
const router = useRouter()
// 页面状态
const isEdit = ref(false)
const templateId = ref<number | null>(null)
const pageTitle = ref('新建告警模板')
const loading = ref(false)
const submitting = ref(false)
const formRef = ref<FormInstance>()
const ruleEditorRef = ref<{ validate: () => Promise<any> } | null>(null)
// 下拉选项
const channelOptions = ref<NotificationChannel[]>([])
const suppressionOptions = ref<SuppressionRule[]>([])
const severityOptions = ref<AlertSeverity[]>([])
// 表单数据
const formData = ref<FormData>({
name: '',
category: '',
description: '',
enabled: true,
tagsList: [],
rules: [],
selectedChannelIds: [],
suppression_rule_ids: [],
})
// 表单验证规则
const formRules = {
name: [
{ required: true, message: '请输入模板名称' },
{ minLength: 2, message: '模板名称至少2个字符' },
{ maxLength: 100, message: '模板名称最多100个字符' },
],
category: [{ required: true, message: '请选择模板分类' }],
tagsList: [{ required: true, message: '请添加标签' }],
selectedChannelIds: [{ required: true, message: '请选择通知渠道' }],
suppression_rule_ids: [{ required: true, message: '请选择抑制规则' }],
}
// 初始化页面
const initPage = async () => {
// 加载下拉选项
await loadSeverityOptions()
loadChannelOptions()
loadSuppressionOptions()
// 判断是否为编辑模式
const id = route.query.id
if (id) {
isEdit.value = true
templateId.value = Number(id)
pageTitle.value = '编辑告警模板'
await loadTemplateDetail()
}
}
// 加载模板详情
const loadTemplateDetail = async () => {
if (!templateId.value) return
loading.value = true
try {
const res: any = await fetchTemplateDetail(templateId.value)
if (res.code === 0) {
const details = res.details
// 解析 JSON 字符串字段
const rules = typeof details.rules === 'string' ? JSON.parse(details.rules) : (details.rules || [])
const channels = typeof details.channels === 'string' ? JSON.parse(details.channels) : (details.channels || [])
const suppressionIds = typeof details.suppression_rule_ids === 'string'
? JSON.parse(details.suppression_rule_ids)
: (details.suppression_rule_ids || [])
const tagsList = (details.tags || '').split(',').filter(Boolean)
const selectedChannelIds = (channels || []).map((c: any) => c.channel_id)
// 初始化规则数据
const rulesWithMetrics = rules.map((r: any) => ({
...r,
_metrics: [],
_metricsLoading: false,
}))
formData.value = {
name: details.name,
category: details.category,
description: details.description || '',
enabled: details.enabled ?? true,
tagsList,
rules: rulesWithMetrics,
selectedChannelIds,
suppression_rule_ids: suppressionIds,
}
// 为已有规则加载指标数据
for (let i = 0; i < rulesWithMetrics.length; i++) {
if (rulesWithMetrics[i].data_source) {
await loadMetricsForRule(i)
}
}
} else {
Message.error(res.msg || '获取模板详情失败')
router.push('/alert/template')
}
} catch (error: any) {
console.error('获取模板详情失败:', error)
Message.error(error.message || '获取模板详情失败')
router.push('/alert/template')
} finally {
loading.value = false
}
}
// 为规则加载指标数据
const loadMetricsForRule = async (index: number) => {
const rule = formData.value.rules[index]
if (!rule.data_source || rule._metrics?.length) return
rule._metricsLoading = true
try {
const res: any = await fetchMetricsMeta({
data_source: rule.data_source,
limit: 500,
})
if (res.code === 0) {
rule._metrics = res.details?.metrics || []
}
} catch (error) {
console.error('加载指标失败:', error)
} finally {
rule._metricsLoading = false
}
}
// 加载告警级别
const loadSeverityOptions = async () => {
try {
const res: any = await fetchSeverityList()
if (res.code === 0) {
severityOptions.value = res.details?.data || res.details?.list || []
}
} catch (error) {
console.error('加载告警级别失败:', error)
}
}
// 加载通知渠道
const loadChannelOptions = async () => {
try {
const res: any = await fetchChannelList({ enabled: true })
if (res.code === 0) {
channelOptions.value = res.details?.data || res.details?.list || []
}
} catch (error) {
console.error('加载通知渠道失败:', error)
}
}
// 加载抑制规则
const loadSuppressionOptions = async () => {
try {
const res: any = await fetchSuppressionList({ enabled: true })
if (res.code === 0) {
suppressionOptions.value = res.details?.data || res.details?.list || []
}
} catch (error) {
console.error('加载抑制规则失败:', error)
}
}
// 通知渠道搜索
const handleChannelSearch = async (keyword: string) => {
try {
const res: any = await fetchChannelList({ enabled: true, keyword })
if (res.code === 0) {
channelOptions.value = res.details?.data || res.details?.list || []
}
} catch (error) {
console.error('搜索通知渠道失败:', error)
}
}
const handleChannelDropdownChange = (visible: boolean) => {
if (visible && channelOptions.value.length === 0) {
loadChannelOptions()
}
}
// 抑制规则搜索
const handleSuppressionSearch = async (keyword: string) => {
try {
const res: any = await fetchSuppressionList({ enabled: true, keyword })
if (res.code === 0) {
suppressionOptions.value = res.details?.data || res.details?.list || []
}
} catch (error) {
console.error('搜索抑制规则失败:', error)
}
}
const handleSuppressionDropdownChange = (visible: boolean) => {
if (visible && suppressionOptions.value.length === 0) {
loadSuppressionOptions()
}
}
// 分类变化时加载默认规则
const handleCategoryChange = (category: string) => {
if (!isEdit.value && formData.value.rules.length === 0) {
const defaultRules = getDefaultRules(category)
formData.value.rules = defaultRules.map(r => ({
...r,
_enabled: true,
_metrics: [],
_metricsLoading: false,
}))
}
}
// 获取默认规则
const getDefaultRules = (category: string): AlertRule[] => {
const defaultRulesMap: Record<string, AlertRule[]> = {
'os': [
{ name: 'CPU使用率过高', data_source: 'dc-host', metric_name: 'cpu_usage', rule_type: 'static', compare_op: '>=', threshold: 80, duration: 300, eval_interval: 60, severity_code: 'warning' },
{ name: '内存使用率过高', data_source: 'dc-host', metric_name: 'memory_usage', rule_type: 'static', compare_op: '>=', threshold: 80, duration: 300, eval_interval: 60, severity_code: 'warning' },
{ name: '磁盘使用率过高', data_source: 'dc-host', metric_name: 'disk_usage', rule_type: 'static', compare_op: '>=', threshold: 85, duration: 600, eval_interval: 60, severity_code: 'warning' },
],
'server_hardware': [
{ name: 'CPU温度过高', data_source: 'dc-hardware', metric_name: 'server_temperature_cpu', rule_type: 'static', compare_op: '>=', threshold: 75, duration: 300, eval_interval: 60, severity_code: 'warning' },
{ name: '风扇转速异常', data_source: 'dc-hardware', metric_name: 'fan_speed', rule_type: 'static', compare_op: '<=', threshold: 800, duration: 300, eval_interval: 60, severity_code: 'warning' },
],
'network_device': [
{ name: '接口Down告警', data_source: 'dc-network', metric_name: 'interface_status', rule_type: 'static', compare_op: '==', threshold: 'down', duration: 60, eval_interval: 30, severity_code: 'critical' },
{ name: '端口带宽利用率过高', data_source: 'dc-network', metric_name: 'interface_usage_percent', rule_type: 'static', compare_op: '>=', threshold: 80, duration: 300, eval_interval: 60, severity_code: 'warning' },
],
'database': [
{ name: '数据库连接数使用率过高', data_source: 'dc-database', metric_name: 'db_conn_usage_percent', rule_type: 'static', compare_op: '>=', threshold: 80, duration: 300, eval_interval: 60, severity_code: 'warning' },
{ name: '慢查询次数过多', data_source: 'dc-database', metric_name: 'slow_query_count', rule_type: 'static', compare_op: '>=', threshold: 50, duration: 300, eval_interval: 60, severity_code: 'warning' },
],
'middleware': [
{ name: '请求错误率过高', data_source: 'dc-middleware', metric_name: 'error_rate', rule_type: 'static', compare_op: '>=', threshold: 1, duration: 300, eval_interval: 60, severity_code: 'warning' },
{ name: '队列消息堆积', data_source: 'dc-middleware', metric_name: 'queue_depth', rule_type: 'static', compare_op: '>=', threshold: 10000, duration: 600, eval_interval: 60, severity_code: 'critical' },
],
}
return defaultRulesMap[category] || []
}
// 添加规则
const handleAddRule = () => {
formData.value.rules.push({
name: '',
data_source: formData.value.category ? getDefaultDataSource(formData.value.category) : 'dc-host',
metric_name: '',
rule_type: 'static',
compare_op: '>=',
threshold: 0,
duration: 300,
eval_interval: 60,
severity_code: 'warning',
_metrics: [],
_metricsLoading: false,
})
}
// 获取默认数据源
const getDefaultDataSource = (category: string): string => {
const map: Record<string, string> = {
'os': 'dc-host',
'server_hardware': 'dc-hardware',
'network_device': 'dc-network',
'database': 'dc-database',
'middleware': 'dc-middleware',
'virtualization': 'dc-virtualization',
'power_env': 'dc-env',
'safety_env': 'dc-env',
}
return map[category] || 'dc-host'
}
// 移除规则
const handleRemoveRule = (index: number) => {
formData.value.rules.splice(index, 1)
}
// 校验规则字段
const validateRules = (): { valid: boolean; message: string } => {
if (formData.value.rules.length === 0) {
return { valid: false, message: '请至少添加一条规则' }
}
for (let i = 0; i < formData.value.rules.length; i++) {
const rule = formData.value.rules[i]
const ruleNum = i + 1
if (!rule.name || rule.name.trim() === '') {
return { valid: false, message: `规则${ruleNum}: 请输入规则名称` }
}
if (!rule.data_source) {
return { valid: false, message: `规则${ruleNum}: 请选择数据源` }
}
if (!rule.metric_name) {
return { valid: false, message: `规则${ruleNum}: 请选择指标名称` }
}
if (!rule.rule_type) {
return { valid: false, message: `规则${ruleNum}: 请选择规则类型` }
}
if (!rule.compare_op) {
return { valid: false, message: `规则${ruleNum}: 请选择比较运算符` }
}
if (rule.threshold === undefined || rule.threshold === null || rule.threshold === '') {
return { valid: false, message: `规则${ruleNum}: 请输入阈值` }
}
if (rule.duration === undefined || rule.duration === null) {
return { valid: false, message: `规则${ruleNum}: 请输入持续时间` }
}
if (rule.eval_interval === undefined || rule.eval_interval === null) {
return { valid: false, message: `规则${ruleNum}: 请输入评估间隔` }
}
if (!rule.severity_code) {
return { valid: false, message: `规则${ruleNum}: 请选择告警级别` }
}
}
return { valid: true, message: '' }
}
// 提交表单
const handleSubmit = async () => {
try {
// 校验基础表单
const valid = await formRef.value?.validate()
if (valid) {
Message.warning('请检查表单填写是否正确')
return
}
// 校验规则字段(使用 RuleEditor 的 validate 方法)
if (formData.value.rules.length === 0) {
Message.warning('请至少添加一条规则')
return
}
try {
await ruleEditorRef.value?.validate()
} catch (ruleErrors) {
Message.warning('请检查规则配置是否完整')
return
}
submitting.value = true
// 提取规则数据
const submitRules = formData.value.rules.map(r => {
const { _metrics, _metricsLoading, ...rest } = r as any
return rest
})
const submitData: AlertTemplate = {
name: formData.value.name,
category: formData.value.category,
description: formData.value.description,
enabled: formData.value.enabled,
tags: formData.value.tagsList.join(','),
rules: submitRules,
channels: formData.value.selectedChannelIds.map(id => ({ channel_id: id })),
suppression_rule_ids: formData.value.suppression_rule_ids,
}
if (isEdit.value && templateId.value) {
submitData.id = templateId.value
const res: any = await updateTemplate(submitData)
if (res.code === 0) {
Message.success('模板更新成功')
router.push('/alert/template')
} else {
Message.error(res.msg || '更新失败')
}
} else {
const res: any = await createTemplate(submitData)
if (res.code === 0) {
Message.success('模板创建成功')
router.push('/alert/template')
} else {
Message.error(res.msg || '创建失败')
}
}
} catch (error: any) {
console.error('提交失败:', error)
Message.error(error.message || '操作失败')
} finally {
submitting.value = false
}
}
// 返回列表
const handleBack = () => {
router.push('/alert/template')
}
// 初始化
onMounted(() => {
initPage()
})
</script>
<script lang="ts">
export default {
name: 'AlertTemplateEditPage',
}
</script>
<style scoped lang="less">
.template-edit-page {
height: 100%;
display: flex;
flex-direction: column;
background-color: var(--color-fill-2);
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background-color: var(--color-bg-2);
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
.header-left {
display: flex;
align-items: center;
.page-title {
margin: 0;
font-size: 18px;
font-weight: 500;
}
}
}
.page-content {
flex: 1;
padding: 20px;
overflow: auto;
}
.info-card {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.rules-card {
height: 100%;
min-height: 500px;
display: flex;
flex-direction: column;
:deep(.arco-card-body) {
flex: 1;
overflow: auto;
}
.rules-card-title {
display: flex;
align-items: center;
gap: 8px;
}
.rules-content {
height: 100%;
}
}
// 表单项样式优化
:deep(.arco-form-item) {
margin-bottom: 16px;
}
:deep(.arco-card) {
.arco-card-header {
border-bottom: 1px solid var(--color-border);
}
}
// 响应式布局
@media (max-width: 992px) {
.rules-card {
min-height: 400px;
margin-top: 16px;
}
}
@media (max-width: 768px) {
.page-header {
flex-direction: column;
gap: 12px;
align-items: flex-start;
.header-right {
width: 100%;
justify-content: flex-end;
}
}
.rules-card {
min-height: 300px;
}
}
</style>

File diff suppressed because it is too large Load Diff