This commit is contained in:
ygx
2026-03-28 23:24:34 +08:00
parent 5bc970fb6c
commit 8021c78cf2
13 changed files with 1343 additions and 2 deletions

136
src/api/ops/suppression.ts Normal file
View File

@@ -0,0 +1,136 @@
import { request } from '@/api/request'
// 抑制规则类型
export type SuppressionType = 'dedup' | 'aggregate' | 'dependency' | 'throttle' | 'schedule'
// 抑制规则接口定义
export interface SuppressionRule {
id?: number
name: string
description?: string
type: SuppressionType
enabled?: boolean
priority?: number
policy_id?: number
// dedup 类型字段
dedup_window?: number
dedup_keys?: string
// aggregate 类型字段
aggregate_window?: number
group_by?: string
aggregate_count?: number
// dependency 类型字段
source_matchers?: string
target_matchers?: string
// throttle 类型字段
throttle_count?: number
throttle_window?: number
// schedule 类型字段
schedule?: string
// 通用匹配条件
matchers?: string
// 运行态统计
hit_count?: number
suppressed_count?: number
last_hit_at?: string
created_at?: string
updated_at?: string
}
// 抑制规则列表查询参数
export interface SuppressionListParams {
page?: number
page_size?: number
keyword?: string
sort?: string
order?: string
type?: SuppressionType
policy_id?: number
enabled?: boolean
}
// 创建抑制规则参数
export interface SuppressionCreateParams {
name: string
type: SuppressionType
description?: string
enabled?: boolean
priority?: number
policy_id?: number
dedup_window?: number
dedup_keys?: string
aggregate_window?: number
group_by?: string
aggregate_count?: number
source_matchers?: string
target_matchers?: string
throttle_count?: number
throttle_window?: number
schedule?: string
matchers?: string
}
// 更新抑制规则参数
export interface SuppressionUpdateParams {
id: number
name?: string
type?: SuppressionType
description?: string
enabled?: boolean
priority?: number
policy_id?: number
dedup_window?: number
dedup_keys?: string
aggregate_window?: number
group_by?: string
aggregate_count?: number
source_matchers?: string
target_matchers?: string
throttle_count?: number
throttle_window?: number
schedule?: string
matchers?: string
}
// 抑制类型选项
export const SUPPRESSION_TYPE_OPTIONS = [
{ value: 'dedup', label: '去重' },
{ value: 'aggregate', label: '聚合' },
{ value: 'dependency', label: '依赖抑制' },
{ value: 'throttle', label: '限流' },
{ value: 'schedule', label: '定时屏蔽' },
]
// 抑制类型标签颜色
export const SUPPRESSION_TYPE_COLORS: Record<SuppressionType, string> = {
dedup: 'arcoblue',
aggregate: 'green',
dependency: 'orangered',
throttle: 'orange',
schedule: 'purple',
}
/** 获取抑制规则列表 */
export const fetchSuppressionList = (params?: SuppressionListParams) => {
return request.get('/Alert/v1/suppression/list', { params })
}
/** 获取抑制规则详情 */
export const fetchSuppressionDetail = (id: number) => {
return request.get(`/Alert/v1/suppression/get/${id}`)
}
/** 创建抑制规则 */
export const createSuppression = (data: SuppressionCreateParams) => {
return request.post('/Alert/v1/suppression/create', data)
}
/** 更新抑制规则 */
export const updateSuppression = (data: SuppressionUpdateParams) => {
return request.post('/Alert/v1/suppression/update', data)
}
/** 删除抑制规则 */
export const deleteSuppression = (id: number) => {
return request.delete(`/Alert/v1/suppression/delete/${id}`)
}

View File

@@ -0,0 +1,354 @@
<template>
<a-modal
:visible="visible"
:title="title"
:width="800"
:ok-loading="submitting"
:mask-closable="false"
@ok="handleOk"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
layout="vertical"
>
<a-row :gutter="16">
<!-- 基本信息 -->
<a-col :span="12">
<a-form-item label="规则名称" field="name" :rules="[{ required: true, message: '请输入规则名称' }]">
<a-input v-model="formData.name" placeholder="请输入规则名称" :max-length="100" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="抑制类型" field="type" :rules="[{ required: true, message: '请选择抑制类型' }]">
<a-select v-model="formData.type" placeholder="请选择抑制类型" :disabled="isEdit">
<a-option v-for="item in SUPPRESSION_TYPE_OPTIONS" :key="item.value" :value="item.value">
{{ item.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="优先级" field="priority">
<a-input-number
v-model="formData.priority"
placeholder="数值越小越先匹配"
:min="1"
:max="10000"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="关联策略" field="policy_id">
<a-select
v-model="formData.policy_id"
placeholder="选择策略或留空表示全局"
allow-clear
allow-search
>
<a-option :value="0">
<a-tag color="gray">全局</a-tag>
</a-option>
<a-option v-for="policy in policyOptions" :key="policy.id" :value="policy.id">
{{ policy.name }}
</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="启用状态" field="enabled">
<a-switch v-model="formData.enabled" />
</a-form-item>
</a-col>
</a-row>
<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-form-item label="匹配条件 (JSON)" field="matchers">
<a-textarea
v-model="formData.matchers"
placeholder="JSON 格式的匹配条件,如 {} 表示全部告警"
:auto-size="{ minRows: 3, maxRows: 6 }"
/>
</a-form-item>
<!-- dedup 类型配置 -->
<template v-if="formData.type === 'dedup'">
<a-divider>去重配置</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="去重窗口 (秒)" field="dedup_window">
<a-input-number
v-model="formData.dedup_window"
placeholder="请输入去重窗口时间"
:min="1"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="去重键" field="dedup_keys">
<a-input
v-model="formData.dedup_keys"
placeholder="逗号分隔,如 fingerprint,alert_name"
/>
</a-form-item>
</a-col>
</a-row>
</template>
<!-- aggregate 类型配置 -->
<template v-if="formData.type === 'aggregate'">
<a-divider>聚合配置</a-divider>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="聚合窗口 (秒)" field="aggregate_window">
<a-input-number
v-model="formData.aggregate_window"
placeholder="请输入聚合窗口时间"
:min="1"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="分组字段" field="group_by">
<a-input
v-model="formData.group_by"
placeholder="逗号分隔"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="聚合阈值" field="aggregate_count">
<a-input-number
v-model="formData.aggregate_count"
placeholder="触发阈值"
:min="1"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
</template>
<!-- dependency 类型配置 -->
<template v-if="formData.type === 'dependency'">
<a-divider>依赖抑制配置</a-divider>
<a-form-item label="源匹配器 (JSON)" field="source_matchers">
<a-textarea
v-model="formData.source_matchers"
placeholder="JSON 格式的源匹配条件"
:auto-size="{ minRows: 3, maxRows: 6 }"
/>
</a-form-item>
<a-form-item label="目标匹配器 (JSON)" field="target_matchers">
<a-textarea
v-model="formData.target_matchers"
placeholder="JSON 格式的目标匹配条件"
:auto-size="{ minRows: 3, maxRows: 6 }"
/>
</a-form-item>
</template>
<!-- throttle 类型配置 -->
<template v-if="formData.type === 'throttle'">
<a-divider>限流配置</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="限流次数" field="throttle_count">
<a-input-number
v-model="formData.throttle_count"
placeholder="允许发送次数"
:min="1"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="限流窗口 (秒)" field="throttle_window">
<a-input-number
v-model="formData.throttle_window"
placeholder="限流时间窗口"
:min="1"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
</template>
<!-- schedule 类型配置 -->
<template v-if="formData.type === 'schedule'">
<a-divider>定时屏蔽配置</a-divider>
<a-form-item label="时间计划 (JSON)" field="schedule">
<a-textarea
v-model="formData.schedule"
placeholder="JSON 格式的时间计划配置"
:auto-size="{ minRows: 5, maxRows: 10 }"
/>
</a-form-item>
</template>
<!-- 编辑时显示统计信息 -->
<template v-if="isEdit">
<a-divider>运行统计</a-divider>
<a-row :gutter="16">
<a-col :span="8">
<a-statistic title="命中次数" :value="formData.hit_count || 0" />
</a-col>
<a-col :span="8">
<a-statistic title="已抑制数量" :value="formData.suppressed_count || 0" />
</a-col>
<a-col :span="8">
<a-descriptions :column="1">
<a-descriptions-item label="最后命中时间">
{{ formData.last_hit_at ? formatDateTime(formData.last_hit_at) : '-' }}
</a-descriptions-item>
</a-descriptions>
</a-col>
</a-row>
</template>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue'
import { SUPPRESSION_TYPE_OPTIONS, type SuppressionRule } from '@/api/ops/suppression'
import type { PolicyOption, SuppressionFormData } from '../types'
import { DEFAULT_FORM_DATA, FORM_RULES } from '../constants'
import { formatDateTime, validateJsonFields, buildUpdateData, buildCreateData } from '../utils'
import { createSuppression, updateSuppression } from '@/api/ops/suppression'
import { Message } from '@arco-design/web-vue'
interface Props {
visible: boolean
isEdit: boolean
data: SuppressionFormData
policyOptions: PolicyOption[]
submitting: boolean
}
interface Emits {
(e: 'update:visible', value: boolean): void
(e: 'update:submitting', value: boolean): void
(e: 'success'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const formRef = ref()
const formData = ref<SuppressionFormData>({ ...DEFAULT_FORM_DATA })
const rules = FORM_RULES
// 弹窗标题
const title = computed(() =>
props.isEdit ? `编辑抑制规则 - ${formData.value.name}` : '新建抑制规则'
)
// 监听外部数据变化
watch(
() => props.data,
(newData) => {
if (newData) {
formData.value = { ...newData }
}
},
{ immediate: true, deep: true }
)
// 关闭弹窗
const handleCancel = () => {
emit('update:visible', false)
formRef.value?.resetFields()
}
// 提交表单
const handleOk = async () => {
// 先进行表单验证
try {
const valid = await formRef.value?.validate()
if (valid) return false
} catch (validateError) {
console.log('表单验证失败:', validateError)
return false
}
// 校验 JSON 字段
const jsonFields = ['matchers', 'source_matchers', 'target_matchers', 'schedule']
const jsonError = validateJsonFields(formData.value, jsonFields)
if (jsonError) {
Message.error(jsonError)
return false
}
try {
emit('update:submitting', true)
if (props.isEdit) {
// 更新
const updateData = buildUpdateData(formData.value)
const res = await updateSuppression(updateData)
if (res.code === 0) {
Message.success('更新成功')
handleCancel()
emit('success')
return true
} else {
Message.error(res.message || '更新失败')
return false
}
} else {
// 创建
const createData = buildCreateData(formData.value)
const res = await createSuppression(createData)
if (res.code === 0) {
Message.success('创建成功')
handleCancel()
emit('success')
return true
} else {
Message.error(res.message || '创建失败')
return false
}
}
} catch (error: any) {
console.error('提交失败:', error)
Message.error(error.message || '提交失败')
return false
} finally {
emit('update:submitting', false)
}
}
// 暴露方法供父组件调用
defineExpose({
resetFields: () => formRef.value?.resetFields(),
})
</script>
<script lang="ts">
export default {
name: 'SuppressionModal',
}
</script>

View File

@@ -0,0 +1,92 @@
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
/**
* 表格列配置
*/
export const getTableColumns = (): TableColumnData[] => [
{
title: 'ID',
dataIndex: 'id',
width: 80,
},
{
title: '规则名称',
dataIndex: 'name',
width: 180,
ellipsis: true,
tooltip: true,
},
{
title: '描述',
dataIndex: 'description',
width: 200,
ellipsis: true,
tooltip: true,
},
{
title: '抑制类型',
dataIndex: 'type',
width: 100,
slotName: 'type',
},
{
title: '启用状态',
dataIndex: 'enabled',
width: 100,
slotName: 'enabled',
},
{
title: '优先级',
dataIndex: 'priority',
width: 80,
},
{
title: '作用范围',
dataIndex: 'policy_id',
width: 120,
slotName: 'policy',
},
{
title: '关键配置',
dataIndex: 'config',
width: 200,
slotName: 'config',
ellipsis: true,
tooltip: true,
},
{
title: '匹配条件',
dataIndex: 'matchers',
width: 100,
slotName: 'matchers',
},
{
title: '命中次数',
dataIndex: 'hit_count',
width: 100,
},
{
title: '已抑制',
dataIndex: 'suppressed_count',
width: 100,
},
{
title: '最后命中',
dataIndex: 'last_hit_at',
width: 160,
slotName: 'last_hit_at',
},
{
title: '创建时间',
dataIndex: 'created_at',
width: 160,
slotName: 'created_at',
},
{
title: '操作',
dataIndex: 'operations',
width: 220,
slotName: 'operations',
fixed: 'right',
},
]

View File

@@ -0,0 +1,54 @@
import { computed } from 'vue'
import type { FormItem } from '@/components/search-form/types'
import { SUPPRESSION_TYPE_OPTIONS } from '@/api/ops/suppression'
import type { PolicyOption } from '../types'
/**
* 获取筛选表单项配置
*/
export const useFormItems = (policyOptions: PolicyOption[]) => {
return computed<FormItem[]>(() => [
{
field: 'keyword',
label: '关键字',
type: 'input',
span: 6,
placeholder: '搜索名称/描述',
},
{
field: 'type',
label: '抑制类型',
type: 'select',
span: 6,
placeholder: '请选择',
options: [
{ value: '', label: '全部' },
...SUPPRESSION_TYPE_OPTIONS,
],
},
{
field: 'policy_id',
label: '关联策略',
type: 'select',
span: 6,
placeholder: '请选择',
options: [
{ value: '', label: '全部' },
{ value: '0', label: '仅全局规则' },
...policyOptions.map((p) => ({ value: String(p.id), label: p.name })),
],
},
{
field: 'enabled',
label: '启用状态',
type: 'select',
span: 6,
placeholder: '请选择',
options: [
{ value: '', label: '全部' },
{ value: 'true', label: '已启用' },
{ value: 'false', label: '已禁用' },
],
},
])
}

View File

@@ -0,0 +1,59 @@
import type { SuppressionFormData } from './types'
/**
* 页面标题
*/
export const PAGE_TITLE = '告警抑制规则(降噪)管理'
/**
* 默认表单数据
*/
export const DEFAULT_FORM_DATA: SuppressionFormData = {
name: '',
type: 'dedup',
enabled: true,
priority: 100,
policy_id: 0,
description: '',
matchers: '{}',
dedup_window: 300,
dedup_keys: '',
aggregate_window: 300,
group_by: '',
aggregate_count: 5,
source_matchers: '{}',
target_matchers: '{}',
throttle_count: 1,
throttle_window: 300,
schedule: '{}',
}
/**
* 表单校验规则
*/
export const FORM_RULES = {
name: [{ required: true, message: '请输入规则名称' }],
type: [{ required: true, message: '请选择抑制类型' }],
}
/**
* 默认分页配置
*/
export const DEFAULT_PAGINATION = {
current: 1,
pageSize: 20,
total: 0,
showTotal: true,
showJumper: true,
showPageSize: true,
}
/**
* 默认筛选表单
*/
export const DEFAULT_SEARCH_FORM = {
keyword: '',
type: '',
policy_id: '',
enabled: '',
}

View File

@@ -0,0 +1,2 @@
export { usePolicy } from './usePolicy'
export { useTable } from './useTable'

View File

@@ -0,0 +1,42 @@
import { ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import { fetchPolicyList } from '@/api/ops/alertPolicy'
import type { PolicyOption } from '../types'
/**
* 策略相关逻辑
*/
export const usePolicy = () => {
const policyOptions = ref<PolicyOption[]>([])
/**
* 获取策略列表
*/
const fetchPolicyOptions = async () => {
try {
const res = await fetchPolicyList({ page_size: 1000 })
if (res.code === 0 && res.details) {
policyOptions.value = (res.details.data || []).map((p: any) => ({
id: p.id,
name: p.name,
}))
}
} catch (error) {
console.error('获取策略列表失败:', error)
}
}
/**
* 获取策略名称
*/
const getPolicyName = (policyId: number) => {
const policy = policyOptions.value.find((p) => p.id === policyId)
return policy?.name || ''
}
return {
policyOptions,
fetchPolicyOptions,
getPolicyName,
}
}

View File

@@ -0,0 +1,193 @@
import { ref, reactive } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import {
fetchSuppressionList,
updateSuppression,
deleteSuppression,
type SuppressionRule,
} from '@/api/ops/suppression'
import type { SearchFormModel, PaginationConfig, SwitchLoadingMap } from '../types'
import { DEFAULT_PAGINATION, DEFAULT_SEARCH_FORM } from '../constants'
/**
* 表格相关逻辑
*/
export const useTable = () => {
const loading = ref(false)
const tableData = ref<SuppressionRule[]>([])
const switchLoadingMap = ref<SwitchLoadingMap>({})
const formModel = ref<SearchFormModel>({ ...DEFAULT_SEARCH_FORM })
const pagination = reactive<PaginationConfig>({ ...DEFAULT_PAGINATION })
/**
* 获取抑制规则列表
*/
const fetchList = async () => {
loading.value = true
try {
const params: any = {
page: pagination.current,
page_size: pagination.pageSize,
}
if (formModel.value.keyword) {
params.keyword = formModel.value.keyword
}
if (formModel.value.type) {
params.type = formModel.value.type
}
if (formModel.value.policy_id) {
params.policy_id = Number(formModel.value.policy_id)
}
if (formModel.value.enabled) {
params.enabled = formModel.value.enabled === 'true'
}
const res = await fetchSuppressionList(params)
if (res.code === 0 && res.details) {
tableData.value = (res.details.data || []).map((item: any) => ({
...item,
switchLoading: false,
}))
pagination.total = res.details.total || 0
} else {
Message.error(res.message || '获取抑制规则列表失败')
}
} catch (error: any) {
console.error('获取抑制规则列表失败:', error)
Message.error(error.message || '获取抑制规则列表失败')
} finally {
loading.value = false
}
}
/**
* 处理表单模型更新
*/
const handleFormModelUpdate = (value: Record<string, any>) => {
formModel.value = value as SearchFormModel
}
/**
* 搜索
*/
const handleSearch = () => {
pagination.current = 1
fetchList()
}
/**
* 重置
*/
const handleReset = () => {
formModel.value = { ...DEFAULT_SEARCH_FORM }
pagination.current = 1
fetchList()
}
/**
* 刷新
*/
const handleRefresh = () => {
fetchList()
}
/**
* 分页变化
*/
const handlePageChange = (current: number) => {
pagination.current = current
fetchList()
}
/**
* 每页条数变化
*/
const handlePageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
pagination.current = 1
fetchList()
}
/**
* 切换启用状态
*/
const handleToggleEnabled = async (record: SuppressionRule) => {
if (!record.id) return
switchLoadingMap.value[record.id] = true
try {
const res = await updateSuppression({
id: record.id,
enabled: record.enabled,
})
if (res.code === 0) {
Message.success(record.enabled ? '已启用' : '已禁用')
} else {
// 恢复原状态
record.enabled = !record.enabled
Message.error(res.message || '操作失败')
}
} catch (error: any) {
record.enabled = !record.enabled
console.error('切换状态失败:', error)
Message.error(error.message || '操作失败')
} finally {
switchLoadingMap.value[record.id] = false
}
}
/**
* 删除抑制规则
*/
const handleDelete = (record: SuppressionRule) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除抑制规则「${record.name}」吗?删除后将影响告警降噪行为,请确认是否被模板引用。`,
okText: '删除',
cancelText: '取消',
okButtonProps: { status: 'danger' },
onOk: async () => {
try {
const res = await deleteSuppression(record.id!)
if (res.code === 0 || res.data === '删除成功') {
Message.success('删除成功')
fetchList()
} else {
Message.error(res.message || '删除失败')
}
} catch (error: any) {
console.error('删除失败:', error)
Message.error(error.message || '删除失败')
}
},
})
}
/**
* 跳转到策略详情
*/
const handleGoToPolicy = (policyId: number) => {
Message.info(`跳转到策略 #${policyId} 详情页`)
}
return {
loading,
tableData,
switchLoadingMap,
formModel,
pagination,
fetchList,
handleFormModelUpdate,
handleSearch,
handleReset,
handleRefresh,
handlePageChange,
handlePageSizeChange,
handleToggleEnabled,
handleDelete,
handleGoToPolicy,
}
}

View File

@@ -0,0 +1,208 @@
<template>
<div class="container">
<search-table
:form-model="formModel"
:form-items="formItems"
:data="tableData"
:columns="tableColumns"
:loading="loading"
:pagination="pagination"
:title="pageTitle"
@update:form-model="handleFormModelUpdate"
@search="handleSearch"
@reset="handleReset"
@refresh="handleRefresh"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<!-- 工具栏左侧新建按钮 -->
<template #toolbar-left>
<a-space>
<a-button type="primary" @click="handleOpenCreateModal">
<template #icon><icon-plus /></template>
新建抑制规则
</a-button>
</a-space>
</template>
<!-- 类型列 -->
<template #type="{ record }">
<a-tag :color="getTypeColor(record.type)">
{{ getTypeLabel(record.type) }}
</a-tag>
</template>
<!-- 启用状态列 -->
<template #enabled="{ record }">
<a-switch
v-model="record.enabled"
:loading="switchLoadingMap[record.id]"
@change="handleToggleEnabled(record)"
/>
</template>
<!-- 作用范围列 -->
<template #policy="{ record }">
<span v-if="record.policy_id === 0 || !record.policy_id">
<a-tag color="gray">全局</a-tag>
</span>
<span v-else>
<a-link @click="handleGoToPolicy(record.policy_id)">
{{ getPolicyName(record.policy_id) || `策略 #${record.policy_id}` }}
</a-link>
</span>
</template>
<!-- 关键配置列 -->
<template #config="{ record }">
<span class="config-text">{{ getConfigSummary(record) }}</span>
</template>
<!-- 匹配条件列 -->
<template #matchers="{ record }">
<a-tooltip v-if="record.matchers && record.matchers !== '{}'" :content="record.matchers">
<a-tag color="arcoblue">
<icon-code />
已配置
</a-tag>
</a-tooltip>
<span v-else class="text-gray">全部告警</span>
</template>
<!-- 最后命中时间列 -->
<template #last_hit_at="{ record }">
<span v-if="record.last_hit_at">{{ formatDateTime(record.last_hit_at) }}</span>
<span v-else class="text-gray">-</span>
</template>
<!-- 创建时间列 -->
<template #created_at="{ record }">
{{ formatDateTime(record.created_at) }}
</template>
<!-- 操作列 -->
<template #operations="{ record }">
<a-space>
<a-button type="text" size="small" @click="handleOpenEditModal(record)">
编辑
</a-button>
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">
删除
</a-button>
</a-space>
</template>
</search-table>
<!-- 新建/编辑抑制规则弹窗 -->
<SuppressionModal
v-model:visible="modalVisible"
v-model:submitting="submitting"
:is-edit="isEdit"
:data="formData"
:policy-options="policyOptions"
@success="fetchList"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import SearchTable from '@/components/search-table/index.vue'
import { fetchSuppressionDetail, type SuppressionRule } from '@/api/ops/suppression'
import SuppressionModal from './components/SuppressionModal.vue'
import { PAGE_TITLE, DEFAULT_FORM_DATA } from './constants'
import { getTableColumns } from './config/columns'
import { useFormItems } from './config/formItems'
import { getTypeColor, getTypeLabel, getConfigSummary, formatDateTime } from './utils'
import { usePolicy, useTable } from './hooks'
import type { SuppressionFormData } from './types'
// 页面标题
const pageTitle = PAGE_TITLE
// 策略相关
const { policyOptions, fetchPolicyOptions, getPolicyName } = usePolicy()
// 表格相关
const {
loading,
tableData,
switchLoadingMap,
formModel,
pagination,
fetchList,
handleFormModelUpdate,
handleSearch,
handleReset,
handleRefresh,
handlePageChange,
handlePageSizeChange,
handleToggleEnabled,
handleDelete,
handleGoToPolicy,
} = useTable()
// 筛选表单项配置
const formItems = useFormItems(policyOptions.value)
// 表格列配置
const tableColumns = computed(() => getTableColumns())
// 弹窗相关
const modalVisible = ref(false)
const isEdit = ref(false)
const submitting = ref(false)
const formData = ref<SuppressionFormData>({ ...DEFAULT_FORM_DATA })
// 打开新建弹窗
const handleOpenCreateModal = () => {
isEdit.value = false
formData.value = { ...DEFAULT_FORM_DATA }
modalVisible.value = true
}
// 打开编辑弹窗
const handleOpenEditModal = async (record: SuppressionRule) => {
isEdit.value = true
try {
const res = await fetchSuppressionDetail(record.id!)
if (res.code === 0 && res.details) {
formData.value = { ...res.details }
modalVisible.value = true
} else {
Message.error(res.message || '获取抑制规则详情失败')
}
} catch (error: any) {
console.error('获取抑制规则详情失败:', error)
Message.error(error.message || '获取抑制规则详情失败')
}
}
// 初始化
onMounted(() => {
fetchPolicyOptions()
fetchList()
})
</script>
<script lang="ts">
export default {
name: 'AlertSuppressPage',
}
</script>
<style scoped lang="less">
.container {
padding-top: 20px;
}
.config-text {
font-size: 13px;
color: var(--color-text-2);
}
.text-gray {
color: var(--color-text-3);
}
</style>

View File

@@ -0,0 +1,44 @@
import type { SuppressionRule, SuppressionType } from '@/api/ops/suppression'
/**
* 筛选表单模型
*/
export interface SearchFormModel {
keyword: string
type: string
policy_id: string
enabled: string
}
/**
* 策略选项
*/
export interface PolicyOption {
id: number
name: string
}
/**
* 分页配置
*/
export interface PaginationConfig {
current: number
pageSize: number
total: number
showTotal: boolean
showJumper: boolean
showPageSize: boolean
}
/**
* 表单数据(创建/编辑)
*/
export type SuppressionFormData = SuppressionRule
/**
* 开关加载状态映射
*/
export type SwitchLoadingMap = Record<number, boolean>
// 重新导出 API 类型
export type { SuppressionRule, SuppressionType }

View File

@@ -0,0 +1,136 @@
import {
SUPPRESSION_TYPE_OPTIONS,
SUPPRESSION_TYPE_COLORS,
type SuppressionType,
type SuppressionRule,
type SuppressionUpdateParams,
type SuppressionCreateParams,
} from '@/api/ops/suppression'
/**
* 获取抑制类型标签
*/
export const getTypeLabel = (type: SuppressionType) => {
const option = SUPPRESSION_TYPE_OPTIONS.find((o) => o.value === type)
return option?.label || type
}
/**
* 获取抑制类型颜色
*/
export const getTypeColor = (type: SuppressionType) => {
return SUPPRESSION_TYPE_COLORS[type] || 'gray'
}
/**
* 获取关键配置摘要
*/
export const getConfigSummary = (record: SuppressionRule) => {
switch (record.type) {
case 'dedup':
return `窗口 ${record.dedup_window || 0}s · 键 ${record.dedup_keys || '-'}`
case 'aggregate':
return `窗口 ${record.aggregate_window || 0}s · 分组 ${record.group_by || '-'} · 阈值 ${record.aggregate_count || 0}`
case 'throttle':
return `${record.throttle_count || 0} 次 / ${record.throttle_window || 0}s`
case 'schedule':
return '已配置时间窗'
case 'dependency':
return '源/目标匹配器已配置'
default:
return '-'
}
}
/**
* 格式化日期时间
*/
export const formatDateTime = (dateStr?: string) => {
if (!dateStr) return '-'
try {
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
} catch {
return dateStr
}
}
/**
* 校验 JSON 字段
* @returns 错误信息,如果校验通过则返回 null
*/
export const validateJsonFields = (formData: SuppressionRule, fields: string[]): string | null => {
for (const field of fields) {
const value = formData[field as keyof SuppressionRule]
if (value && typeof value === 'string' && value !== '{}' && value.trim() !== '') {
try {
JSON.parse(value)
} catch {
return `${field} 字段 JSON 格式不正确`
}
}
}
return null
}
/**
* 构建更新数据(只传非空字段)
*/
export const buildUpdateData = (formData: SuppressionRule): SuppressionUpdateParams => {
const updateData: SuppressionUpdateParams = { id: formData.id! }
if (formData.name) updateData.name = formData.name
if (formData.type) updateData.type = formData.type
if (formData.description) updateData.description = formData.description
updateData.enabled = formData.enabled
if (formData.priority && formData.priority > 0) updateData.priority = formData.priority
if (formData.policy_id && formData.policy_id > 0) updateData.policy_id = formData.policy_id
if (formData.dedup_window && formData.dedup_window > 0) updateData.dedup_window = formData.dedup_window
if (formData.dedup_keys) updateData.dedup_keys = formData.dedup_keys
if (formData.aggregate_window && formData.aggregate_window > 0) updateData.aggregate_window = formData.aggregate_window
if (formData.group_by) updateData.group_by = formData.group_by
if (formData.aggregate_count && formData.aggregate_count > 0) updateData.aggregate_count = formData.aggregate_count
if (formData.source_matchers) updateData.source_matchers = formData.source_matchers
if (formData.target_matchers) updateData.target_matchers = formData.target_matchers
if (formData.throttle_count && formData.throttle_count > 0) updateData.throttle_count = formData.throttle_count
if (formData.throttle_window && formData.throttle_window > 0) updateData.throttle_window = formData.throttle_window
if (formData.schedule) updateData.schedule = formData.schedule
if (formData.matchers) updateData.matchers = formData.matchers
return updateData
}
/**
* 构建创建数据
*/
export const buildCreateData = (formData: SuppressionRule): SuppressionCreateParams => {
const createData: SuppressionCreateParams = {
name: formData.name,
type: formData.type,
enabled: formData.enabled,
}
if (formData.description) createData.description = formData.description
if (formData.priority) createData.priority = formData.priority
if (formData.policy_id !== undefined) createData.policy_id = formData.policy_id
if (formData.dedup_window) createData.dedup_window = formData.dedup_window
if (formData.dedup_keys) createData.dedup_keys = formData.dedup_keys
if (formData.aggregate_window) createData.aggregate_window = formData.aggregate_window
if (formData.group_by) createData.group_by = formData.group_by
if (formData.aggregate_count) createData.aggregate_count = formData.aggregate_count
if (formData.source_matchers) createData.source_matchers = formData.source_matchers
if (formData.target_matchers) createData.target_matchers = formData.target_matchers
if (formData.throttle_count) createData.throttle_count = formData.throttle_count
if (formData.throttle_window) createData.throttle_window = formData.throttle_window
if (formData.schedule) createData.schedule = formData.schedule
if (formData.matchers) createData.matchers = formData.matchers
return createData
}

View File

@@ -23,7 +23,7 @@
<!-- 表单内容 -->
<div class="page-content">
<a-spin :loading="loading" style="width: 100%">
<a-form ref="formRef" :model="form" layout="vertical">
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
<!-- 基本信息 -->
<a-card class="info-card" title="基本信息">
<a-row :gutter="16">
@@ -450,6 +450,12 @@ const form = ref<AssetForm>({
remarks: '',
})
// 表单验证规则
const rules = {
asset_name: [{ required: true, message: '请输入资产名称' }],
asset_code: [{ required: true, message: '请输入资产编号' }],
}
// 提取列表数据的辅助函数
const extractList = (res: any): any[] => {
const candidate =

View File

@@ -23,7 +23,7 @@
<!-- 表单内容 -->
<div class="page-content">
<a-spin :loading="loading" style="width: 100%">
<a-form ref="formRef" :model="form" layout="vertical">
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
<!-- 基本信息 -->
<a-card class="info-card" title="基本信息">
<a-row :gutter="16">
@@ -351,6 +351,21 @@ const form = ref<Supplier>({
status: 'active',
})
// 表单验证规则
const rules = {
name: [{ required: true, message: '请输入供应商名称' }],
code: [{ required: true, message: '请输入供应商编码' }],
supplier_type: [{ required: true, message: '请选择供应商类型' }],
status: [{ required: true, message: '请选择状态' }],
contact_person: [{ required: true, message: '请输入联系人' }],
contact_phone: [{ required: true, message: '请输入联系电话' }],
contact_mobile: [{ required: true, message: '请输入备用联系电话' }],
contact_email: [
{ required: true, message: '请输入联系邮箱' },
{ type: 'email', message: '请输入正确的邮箱格式' },
],
}
// 初始化页面
const initPage = async () => {
const id = route.query.id