modify
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m13s

This commit is contained in:
sexygoat
2026-04-02 13:42:57 +08:00
parent 411206e039
commit 8df7024a45
83 changed files with 1830 additions and 1865 deletions

View File

@@ -58,7 +58,11 @@
</el-col>
<el-col :span="12">
<el-form-item label="告警级别" prop="alert_level">
<el-select v-model="form.alert_level" placeholder="请选择告警级别" style="width: 100%">
<el-select
v-model="form.alert_level"
placeholder="请选择告警级别"
style="width: 100%"
>
<el-option label="警告" value="warning" />
<el-option label="严重" value="critical" />
</el-select>
@@ -155,188 +159,33 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, nextTick, watch, h } from 'vue'
import { ElMessage, ElMessageBox, ElTag, type FormInstance, type FormRules } from 'element-plus'
import { PollingAlertService } from '@/api/modules'
import type { PollingAlertRule, CreatePollingAlertRuleRequest } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { ref, reactive, computed, nextTick, watch, h } from 'vue'
import { ElMessage, ElMessageBox, ElTag, type FormInstance, type FormRules } from 'element-plus'
import { PollingAlertService } from '@/api/modules'
import type { PollingAlertRule, CreatePollingAlertRuleRequest } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
const loading = ref(false)
const tableData = ref<PollingAlertRule[]>([])
const dialogVisible = ref(false)
const dialogType = ref<'create' | 'edit'>('create')
const formRef = ref<FormInstance>()
const contextMenuRef = ref()
const currentRow = ref<PollingAlertRule | null>(null)
const loading = ref(false)
const tableData = ref<PollingAlertRule[]>([])
const dialogVisible = ref(false)
const dialogType = ref<'create' | 'edit'>('create')
const formRef = ref<FormInstance>()
const contextMenuRef = ref()
const currentRow = ref<PollingAlertRule | null>(null)
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const form = reactive<CreatePollingAlertRuleRequest & { id?: number }>({
rule_name: '',
alert_level: '',
metric_type: '',
task_type: '',
threshold: 0,
operator: '>',
cooldown_minutes: 5,
notify_channels: ''
})
const rules: FormRules = {
rule_name: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
alert_level: [{ required: true, message: '请选择告警级别', trigger: 'change' }],
metric_type: [{ required: true, message: '请选择指标类型', trigger: 'change' }],
task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }],
threshold: [{ required: true, message: '请输入阈值', trigger: 'blur' }],
operator: [{ required: true, message: '请选择运算符', trigger: 'change' }],
cooldown_minutes: [{ required: true, message: '请输入冷却时间', trigger: 'blur' }]
}
const getAlertLevelLabel = (level: string) => {
return level === 'critical' ? '严重' : '警告'
}
const getMetricTypeLabel = (type: string) => {
const labels: Record<string, string> = {
queue_size: '队列大小',
success_rate: '成功率',
avg_duration: '平均执行时间',
concurrency: '并发数'
}
return labels[type] || type
}
const getTaskTypeLabel = (type: string) => {
const labels: Record<string, string> = {
'polling:realname': '实名认证轮询',
'polling:carddata': '卡数据轮询',
'polling:package': '套餐轮询'
}
return labels[type] || type
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'rule_name',
label: '规则名称',
minWidth: 150
},
{
prop: 'alert_level',
label: '告警级别',
width: 100,
slots: {
default: ({ row }: any) =>
h(
ElTag,
{ type: row.alert_level === 'critical' ? 'danger' : 'warning' },
() => getAlertLevelLabel(row.alert_level)
)
}
},
{
prop: 'metric_type_name',
label: '指标类型',
width: 120,
formatter: (row: any) => getMetricTypeLabel(row.metric_type)
},
{
prop: 'task_type_name',
label: '任务类型',
width: 150,
formatter: (row: any) => getTaskTypeLabel(row.task_type)
},
{
prop: 'operator',
label: '运算符',
width: 80
},
{
prop: 'threshold',
label: '阈值',
width: 100
},
{
prop: 'cooldown_minutes',
label: '冷却时间(分钟)',
width: 120
},
{
prop: 'status',
label: '状态',
width: 80,
slots: {
default: ({ row }: any) =>
h(ElTag, { type: row.status === 1 ? 'success' : 'info' }, () =>
row.status === 1 ? '启用' : '禁用'
)
}
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: any) => formatDateTime(row.created_at)
}
])
const columnOptions = computed(() =>
columns.value.map((col) => ({
label: col.label,
prop: col.prop || col.label,
visible: true
}))
)
// 右键菜单项
const contextMenuItems = computed<MenuItemType[]>(() => {
return [
{ key: 'edit', label: '编辑' },
{ key: 'toggle', label: currentRow.value?.status === 1 ? '禁用' : '启用' },
{ key: 'delete', label: '删除' }
]
})
watch(dialogVisible, (val) => {
if (!val) {
nextTick(() => {
formRef.value?.clearValidate()
})
}
})
const loadData = async () => {
loading.value = true
try {
const res = await PollingAlertService.getPollingAlertRules()
if (res.code === 0 && res.data) {
tableData.value = res.data.items
}
} catch (error) {
ElMessage.error('加载数据失败')
} finally {
loading.value = false
}
}
const handleRefresh = () => {
loadData()
}
const resetForm = () => {
Object.assign(form, {
id: undefined,
const form = reactive<CreatePollingAlertRuleRequest & { id?: number }>({
rule_name: '',
alert_level: '',
metric_type: '',
@@ -346,134 +195,287 @@ const resetForm = () => {
cooldown_minutes: 5,
notify_channels: ''
})
}
const showCreateDialog = () => {
resetForm()
dialogType.value = 'create'
dialogVisible.value = true
nextTick(() => {
formRef.value?.clearValidate()
const rules: FormRules = {
rule_name: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
alert_level: [{ required: true, message: '请选择告警级别', trigger: 'change' }],
metric_type: [{ required: true, message: '请选择指标类型', trigger: 'change' }],
task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }],
threshold: [{ required: true, message: '请输入阈值', trigger: 'blur' }],
operator: [{ required: true, message: '请选择运算符', trigger: 'change' }],
cooldown_minutes: [{ required: true, message: '请输入冷却时间', trigger: 'blur' }]
}
const getAlertLevelLabel = (level: string) => {
return level === 'critical' ? '严重' : '警告'
}
const getMetricTypeLabel = (type: string) => {
const labels: Record<string, string> = {
queue_size: '队列大小',
success_rate: '成功率',
avg_duration: '平均执行时间',
concurrency: '并发数'
}
return labels[type] || type
}
const getTaskTypeLabel = (type: string) => {
const labels: Record<string, string> = {
'polling:realname': '实名认证轮询',
'polling:carddata': '卡数据轮询',
'polling:package': '套餐轮询'
}
return labels[type] || type
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'rule_name',
label: '规则名称',
minWidth: 150
},
{
prop: 'alert_level',
label: '告警级别',
width: 100,
slots: {
default: ({ row }: any) =>
h(ElTag, { type: row.alert_level === 'critical' ? 'danger' : 'warning' }, () =>
getAlertLevelLabel(row.alert_level)
)
}
},
{
prop: 'metric_type_name',
label: '指标类型',
width: 120,
formatter: (row: any) => getMetricTypeLabel(row.metric_type)
},
{
prop: 'task_type_name',
label: '任务类型',
width: 150,
formatter: (row: any) => getTaskTypeLabel(row.task_type)
},
{
prop: 'operator',
label: '运算符',
width: 80
},
{
prop: 'threshold',
label: '阈值',
width: 100
},
{
prop: 'cooldown_minutes',
label: '冷却时间(分钟)',
width: 120
},
{
prop: 'status',
label: '状态',
width: 80,
slots: {
default: ({ row }: any) =>
h(ElTag, { type: row.status === 1 ? 'success' : 'info' }, () =>
row.status === 1 ? '启用' : '禁用'
)
}
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: any) => formatDateTime(row.created_at)
}
])
const columnOptions = computed(() =>
columns.value.map((col) => ({
label: col.label,
prop: col.prop || col.label,
visible: true
}))
)
// 右键菜单项
const contextMenuItems = computed<MenuItemType[]>(() => {
return [
{ key: 'edit', label: '编辑' },
{ key: 'toggle', label: currentRow.value?.status === 1 ? '禁用' : '启用' },
{ key: 'delete', label: '删除' }
]
})
}
const showEditDialog = async (row: PollingAlertRule) => {
try {
const res = await PollingAlertService.getPollingAlertRuleById(row.id)
if (res.code === 0 && res.data) {
Object.assign(form, {
id: res.data.id,
rule_name: res.data.rule_name,
alert_level: res.data.alert_level,
metric_type: res.data.metric_type,
task_type: res.data.task_type,
threshold: res.data.threshold,
operator: res.data.operator,
cooldown_minutes: res.data.cooldown_minutes,
notify_channels: res.data.notify_channels
})
dialogType.value = 'edit'
dialogVisible.value = true
watch(dialogVisible, (val) => {
if (!val) {
nextTick(() => {
formRef.value?.clearValidate()
})
}
} catch (error) {
ElMessage.error('获取规则详情失败')
}
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
try {
if (dialogType.value === 'create') {
const res = await PollingAlertService.createPollingAlertRule(form)
if (res.code === 0) {
ElMessage.success('创建成功')
dialogVisible.value = false
loadData()
}
} else {
const { id, metric_type, task_type, operator, ...updateData } = form
const res = await PollingAlertService.updatePollingAlertRule(id!, updateData)
if (res.code === 0) {
ElMessage.success('更新成功')
dialogVisible.value = false
loadData()
}
}
} catch (error) {
ElMessage.error(dialogType.value === 'create' ? '创建失败' : '更新失败')
}
}
})
}
const toggleStatus = async (row: PollingAlertRule) => {
try {
const newStatus = row.status === 1 ? 0 : 1
const res = await PollingAlertService.updatePollingAlertRule(row.id, { status: newStatus })
if (res.code === 0) {
ElMessage.success(newStatus === 1 ? '已启用' : '已禁用')
loadData()
}
} catch (error) {
ElMessage.error('操作失败')
}
}
const handleDelete = (row: PollingAlertRule) => {
ElMessageBox.confirm('确定要删除该告警规则吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const loadData = async () => {
loading.value = true
try {
const res = await PollingAlertService.deletePollingAlertRule(row.id)
const res = await PollingAlertService.getPollingAlertRules()
if (res.code === 0 && res.data) {
tableData.value = res.data.items
}
} catch (error) {
ElMessage.error('加载数据失败')
} finally {
loading.value = false
}
}
const handleRefresh = () => {
loadData()
}
const resetForm = () => {
Object.assign(form, {
id: undefined,
rule_name: '',
alert_level: '',
metric_type: '',
task_type: '',
threshold: 0,
operator: '>',
cooldown_minutes: 5,
notify_channels: ''
})
}
const showCreateDialog = () => {
resetForm()
dialogType.value = 'create'
dialogVisible.value = true
nextTick(() => {
formRef.value?.clearValidate()
})
}
const showEditDialog = async (row: PollingAlertRule) => {
try {
const res = await PollingAlertService.getPollingAlertRuleById(row.id)
if (res.code === 0 && res.data) {
Object.assign(form, {
id: res.data.id,
rule_name: res.data.rule_name,
alert_level: res.data.alert_level,
metric_type: res.data.metric_type,
task_type: res.data.task_type,
threshold: res.data.threshold,
operator: res.data.operator,
cooldown_minutes: res.data.cooldown_minutes,
notify_channels: res.data.notify_channels
})
dialogType.value = 'edit'
dialogVisible.value = true
nextTick(() => {
formRef.value?.clearValidate()
})
}
} catch (error) {
ElMessage.error('获取规则详情失败')
}
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
try {
if (dialogType.value === 'create') {
const res = await PollingAlertService.createPollingAlertRule(form)
if (res.code === 0) {
ElMessage.success('创建成功')
dialogVisible.value = false
loadData()
}
} else {
const { id, metric_type, task_type, operator, ...updateData } = form
const res = await PollingAlertService.updatePollingAlertRule(id!, updateData)
if (res.code === 0) {
ElMessage.success('更新成功')
dialogVisible.value = false
loadData()
}
}
} catch (error) {
ElMessage.error(dialogType.value === 'create' ? '创建失败' : '更新失败')
}
}
})
}
const toggleStatus = async (row: PollingAlertRule) => {
try {
const newStatus = row.status === 1 ? 0 : 1
const res = await PollingAlertService.updatePollingAlertRule(row.id, { status: newStatus })
if (res.code === 0) {
ElMessage.success('删除成功')
ElMessage.success(newStatus === 1 ? '已启用' : '已禁用')
loadData()
}
} catch (error) {
ElMessage.error('删除失败')
ElMessage.error('操作失败')
}
})
}
// 处理表格行右键菜单
const handleRowContextMenu = (row: PollingAlertRule, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'edit':
showEditDialog(currentRow.value)
break
case 'toggle':
toggleStatus(currentRow.value)
break
case 'delete':
handleDelete(currentRow.value)
break
}
}
loadData()
const handleDelete = (row: PollingAlertRule) => {
ElMessageBox.confirm('确定要删除该告警规则吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await PollingAlertService.deletePollingAlertRule(row.id)
if (res.code === 0) {
ElMessage.success('删除成功')
loadData()
}
} catch (error) {
ElMessage.error('删除失败')
}
})
}
// 处理表格行右键菜单
const handleRowContextMenu = (row: PollingAlertRule, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'edit':
showEditDialog(currentRow.value)
break
case 'toggle':
toggleStatus(currentRow.value)
break
case 'delete':
handleDelete(currentRow.value)
break
}
}
loadData()
</script>
<style scoped lang="scss">
.alert-rules-page {
height: 100%;
display: flex;
flex-direction: column;
}
.alert-rules-page {
display: flex;
flex-direction: column;
height: 100%;
}
</style>