feat
This commit is contained in:
206
src/api/ops/alertTemplate.ts
Normal file
206
src/api/ops/alertTemplate.ts
Normal 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 })
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
325
src/views/ops/pages/alert/template/components/RuleEditor.vue
Normal file
325
src/views/ops/pages/alert/template/components/RuleEditor.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
708
src/views/ops/pages/alert/template/edit/index.vue
Normal file
708
src/views/ops/pages/alert/template/edit/index.vue
Normal 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
Reference in New Issue
Block a user