This commit is contained in:
zxr
2026-03-21 17:39:39 +08:00
parent 6ac9550133
commit 2c3195b903
7 changed files with 265 additions and 244 deletions

View File

@@ -60,7 +60,12 @@
:data="data"
:bordered="bordered"
:size="size"
:row-selection="rowSelection"
:scroll="scroll"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@selection-change="onSelectionChange"
@row-click="onRowClick"
>
<!-- 动态插槽根据 columns slotName 动态渲染 -->
<template v-for="col in slotColumns" :key="col.dataIndex" #[String(col.slotName)]="slotProps">
@@ -72,7 +77,7 @@
<script lang="ts" setup>
import { computed, ref, watch, nextTick, onUnmounted, PropType } from 'vue'
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
import type { TableColumnData, TableRowSelection } from '@arco-design/web-vue/es/table/interface'
import cloneDeep from 'lodash/cloneDeep'
import Sortable from 'sortablejs'
@@ -107,6 +112,14 @@ const props = defineProps({
type: Boolean,
default: false,
},
rowSelection: {
type: Object as PropType<TableRowSelection | undefined>,
default: undefined,
},
scroll: {
type: Object as PropType<{ x?: number | string; y?: number | string } | undefined>,
default: undefined,
},
showToolbar: {
type: Boolean,
default: true,
@@ -147,6 +160,9 @@ const props = defineProps({
const emit = defineEmits<{
(e: 'page-change', current: number): void
(e: 'page-size-change', pageSize: number): void
(e: 'selection-change', rowKeys: (string | number)[]): void
(e: 'row-click', record: any, ev: Event): void
(e: 'refresh'): void
(e: 'download'): void
(e: 'density-change', size: SizeProps): void
@@ -178,6 +194,18 @@ const onPageChange = (current: number) => {
emit('page-change', current)
}
const onPageSizeChange = (pageSize: number) => {
emit('page-size-change', pageSize)
}
const onSelectionChange = (rowKeys: (string | number)[]) => {
emit('selection-change', rowKeys)
}
const onRowClick = (record: any, ev: Event) => {
emit('row-click', record, ev)
}
const handleRefresh = () => {
emit('refresh')
}

View File

@@ -26,6 +26,8 @@
:loading="loading"
:pagination="pagination"
:bordered="bordered"
:row-selection="rowSelection"
:scroll="scroll"
:show-toolbar="showToolbar"
:show-download="showDownload"
:show-refresh="showRefresh"
@@ -36,6 +38,9 @@
:density-tooltip-text="densityTooltipText"
:column-setting-tooltip-text="columnSettingTooltipText"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
@selection-change="handleSelectionChange"
@row-click="handleRowClick"
@refresh="handleRefresh"
@download="handleDownload"
@density-change="handleDensityChange"
@@ -61,7 +66,7 @@ import { computed, PropType } from 'vue'
import SearchForm from '../search-form/index.vue'
import type { FormItem } from '../search-form/types'
import DataTable from '../data-table/index.vue'
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
import type { TableColumnData, TableRowSelection } from '@arco-design/web-vue/es/table/interface'
type SizeProps = 'mini' | 'small' | 'medium' | 'large'
@@ -115,6 +120,14 @@ const props = defineProps({
type: Boolean,
default: false,
},
rowSelection: {
type: Object as PropType<TableRowSelection | undefined>,
default: undefined,
},
scroll: {
type: Object as PropType<{ x?: number | string; y?: number | string } | undefined>,
default: undefined,
},
// 工具栏相关
showToolbar: {
type: Boolean,
@@ -164,6 +177,9 @@ const emit = defineEmits<{
(e: 'search'): void
(e: 'reset'): void
(e: 'page-change', current: number): void
(e: 'page-size-change', pageSize: number): void
(e: 'selection-change', rowKeys: (string | number)[]): void
(e: 'row-click', record: any, ev: Event): void
(e: 'refresh'): void
(e: 'download'): void
(e: 'density-change', size: SizeProps): void
@@ -192,6 +208,18 @@ const handlePageChange = (current: number) => {
emit('page-change', current)
}
const handlePageSizeChange = (pageSize: number) => {
emit('page-size-change', pageSize)
}
const handleSelectionChange = (rowKeys: (string | number)[]) => {
emit('selection-change', rowKeys)
}
const handleRowClick = (record: any, ev: Event) => {
emit('row-click', record, ev)
}
const handleRefresh = () => {
emit('refresh')
}

View File

@@ -1,7 +1,7 @@
<template>
<div class="notice-channel-container">
<SearchTable
v-model:form-model="formModel"
<div class="container">
<search-table
:form-model="formModel"
:form-items="formItems"
:data="tableData"
:columns="columns"
@@ -10,6 +10,10 @@
title="通知渠道管理"
search-button-text="查询"
reset-button-text="重置"
refresh-tooltip-text="刷新数据"
density-tooltip-text="表格密度"
column-setting-tooltip-text="列设置"
@update:form-model="handleFormModelUpdate"
@search="handleSearch"
@reset="handleReset"
@page-change="handlePageChange"
@@ -56,7 +60,7 @@
</a-popconfirm>
</a-space>
</template>
</SearchTable>
</search-table>
<!-- 表单对话框 -->
<channel-form-dialog
@@ -95,12 +99,15 @@ const pagination = reactive({
total: 0,
})
// 搜索表单数据
const formModel = ref<Record<string, any>>({
name: '',
type: '',
})
const handleFormModelUpdate = (value: Record<string, any>) => {
formModel.value = value
}
// 表单项配置
const formItems = computed<FormItem[]>(() => [
{
@@ -240,9 +247,9 @@ const handlePageChange = (current: number) => {
fetchList()
}
// 刷新
const handleRefresh = () => {
fetchList()
Message.success('数据已刷新')
}
// 新建
@@ -292,7 +299,7 @@ export default {
</script>
<style scoped lang="less">
.notice-channel-container {
padding: 0 20px 20px 20px;
.container {
margin-top: 20px;
}
</style>

View File

@@ -138,7 +138,7 @@
:data="dispatchRuleData"
:pagination="false"
:bordered="true"
size="small"
size="medium"
>
<template #columns>
<a-table-column title="告警级别" data-index="severity" :width="150">

View File

@@ -31,7 +31,7 @@
:loading="ruleLoading"
:pagination="rulePagination"
:bordered="true"
size="small"
size="medium"
@page-change="handlePageChange"
>
<template #columns>

View File

@@ -118,7 +118,7 @@
:data="processRecords"
:columns="processColumns"
:pagination="false"
size="small"
size="medium"
>
<template #action="{ record }">
<a-tag :color="getActionColor(record.action)">

View File

@@ -1,65 +1,46 @@
<template>
<div class="alert-tackle-container">
<a-card :bordered="false" class="general-card">
<search-form
v-model="searchParams"
:form-items="searchFormConfig"
:show-buttons="false"
<div class="container">
<search-table
:form-model="formModel"
:form-items="formItems"
:data="tableData"
:columns="columns"
:loading="loading"
:pagination="pagination"
title="告警受理处理"
search-button-text="查询"
reset-button-text="重置"
refresh-tooltip-text="刷新数据"
density-tooltip-text="表格密度"
column-setting-tooltip-text="列设置"
@update:form-model="handleFormModelUpdate"
@search="handleSearch"
@reset="handleReset"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
@row-click="handleRowClick"
@refresh="handleRefresh"
>
<template #form-items>
<a-col :span="16">
<a-form-item
label="时间范围"
:label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 18 }"
>
<template #extra>
<a-range-picker
v-model="timeRange"
:time-picker-props="{ defaultValue: '00:00:00' }"
format="YYYY-MM-DD HH:mm:ss"
show-time
style="width: 380px; margin-right: 12px"
style="width: 100%; max-width: 420px"
/>
<a-button type="primary" @click="handleSearch">
<template #icon>
<icon-search />
</template>
查询
</a-button>
<a-button @click="handleReset">
<template #icon>
<icon-refresh />
</template>
重置
</a-button>
</template>
</search-form>
<a-divider style="margin: 0" />
<a-row class="toolbar">
<a-col :span="12">
<a-space>
<a-button
v-if="selectedRowKeys.length > 0"
type="primary"
status="danger"
@click="handleBatchAck"
>
批量确认 ({{ selectedRowKeys.length }})
</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</template>
<a-table
:data="tableData"
:columns="columns"
:loading="loading"
:pagination="pagination"
:row-selection="rowSelection"
:scroll="{ x: 2000 }"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@selection-change="handleSelectionChange"
@row-click="handleRowClick"
>
<template #index="{ rowIndex }">
{{ rowIndex + 1 }}
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
</template>
<template #status="{ record }">
@@ -101,48 +82,29 @@
</template>
<template #labels="{ record }">
<a-space v-if="parsedLabels(record.labels)" wrap>
<a-tag
v-for="(value, key) in parsedLabels(record.labels)"
:key="key"
size="small"
>
{{ key }}: {{ value }}
</a-tag>
</a-space>
<a-tooltip v-if="formatLabelsLine(record.labels)" :content="formatLabelsLine(record.labels)">
<span class="cell-ellipsis">{{ formatLabelsLine(record.labels) }}</span>
</a-tooltip>
<span v-else>-</span>
</template>
<template #actions="{ record }">
<a-space>
<a-button type="text" size="small" @click="handleAck(record)">
<template #icon>
<icon-check />
</template>
<a-space size="small" :wrap="false">
<a-button type="text" size="small" @click.stop="handleAck(record)">
确认
</a-button>
<a-button type="text" size="small" @click="handleResolve(record)">
<template #icon>
<icon-check-circle />
</template>
<a-button type="text" size="small" @click.stop="handleResolve(record)">
解决
</a-button>
<a-button type="text" size="small" @click="handleSilence(record)">
<template #icon>
<icon-eye-invisible />
</template>
<a-button type="text" size="small" @click.stop="handleSilence(record)">
屏蔽
</a-button>
<a-button type="text" size="small" @click="handleComment(record)">
<template #icon>
<icon-message />
</template>
<a-button type="text" size="small" @click.stop="handleComment(record)">
评论
</a-button>
<a-dropdown @select="handleMoreAction($event, record)">
<a-button type="text" size="small">
<a-dropdown @select="(v) => handleMoreSelect(v, record)">
<a-button type="text" size="small" @click.stop>
更多
<icon-down />
</a-button>
<template #content>
<a-doption value="detail">详情</a-doption>
@@ -151,38 +113,32 @@
</a-dropdown>
</a-space>
</template>
</a-table>
</a-card>
</search-table>
<!-- 确认对话框 -->
<ack-dialog
v-model:visible="ackDialogVisible"
:alert-record-id="currentRecord.id"
@success="handleSuccess"
/>
<!-- 解决对话框 -->
<resolve-dialog
v-model:visible="resolveDialogVisible"
:alert-record-id="currentRecord.id"
@success="handleSuccess"
/>
<!-- 屏蔽对话框 -->
<silence-dialog
v-model:visible="silenceDialogVisible"
:alert-record-id="currentRecord.id"
@success="handleSuccess"
/>
<!-- 评论对话框 -->
<comment-dialog
v-model:visible="commentDialogVisible"
:alert-record-id="currentRecord.id"
@success="handleSuccess"
/>
<!-- 详情对话框 -->
<detail-dialog
v-model:visible="detailDialogVisible"
:alert-record-id="currentRecord.id"
@@ -193,21 +149,13 @@
<script lang="ts" setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import {
IconSearch,
IconRefresh,
IconCheck,
IconCheckCircle,
IconEyeInvisible,
IconMessage,
IconDown,
} from '@arco-design/web-vue/es/icon'
import { useRouter } from 'vue-router'
import SearchForm from '@/components/search-form/index.vue'
import type { FormItem } from '@/components/search-form/types'
import type { SelectOptionData } from '@arco-design/web-vue/es/select/interface'
import SearchTable from '@/components/search-table/index.vue'
import { searchFormConfig } from './config/search-form'
import { columns } from './config/columns'
import { fetchAlertRecords, createAlertProcess } from '@/api/ops/alertRecord'
import { fetchAlertRecords } from '@/api/ops/alertRecord'
import { fetchAlertLevelList } from '@/api/ops/alertLevel'
import AckDialog from './components/AckDialog.vue'
@@ -218,21 +166,17 @@ import DetailDialog from './components/DetailDialog.vue'
const router = useRouter()
// 状态管理
const loading = ref(false)
const tableData = ref<any[]>([])
const timeRange = ref<any[]>([])
const selectedRowKeys = ref<number[]>([])
const currentRecord = ref<any>({})
// 对话框状态
const ackDialogVisible = ref(false)
const resolveDialogVisible = ref(false)
const silenceDialogVisible = ref(false)
const commentDialogVisible = ref(false)
const detailDialogVisible = ref(false)
// 分页
const pagination = reactive({
current: 1,
pageSize: 20,
@@ -242,16 +186,23 @@ const pagination = reactive({
pageSizeOptions: ['10', '20', '50', '100'],
})
// 行选择
const rowSelection = computed(() => ({
type: 'checkbox',
showCheckedAll: true,
}))
const severityOptions = ref<SelectOptionData[]>([])
// 搜索参数
const searchParams = ref<any>({})
const formModel = ref<Record<string, any>>({
keyword: '',
status: '',
severity_id: '',
})
const formItems = computed<FormItem[]>(() =>
searchFormConfig.map((item) => {
if (item.field === 'severity_id') {
return { ...item, options: severityOptions.value }
}
return item
}),
)
// 加载告警级别列表
onMounted(async () => {
await loadSeverityOptions()
handleSearch()
@@ -259,51 +210,58 @@ onMounted(async () => {
const loadSeverityOptions = async () => {
try {
const result = await fetchAlertLevelList({ page: 1, page_size: 100 })
const severityConfig = searchFormConfig.find((item) => item.field === 'severity_id')
if (severityConfig && result.details) {
severityConfig.options = result.data.map((item: any) => ({
const res = await fetchAlertLevelList({ page: 1, page_size: 100 })
const list = res.details?.data ?? (res as any).data ?? []
severityOptions.value = list.map((item: any) => ({
label: item.name || item.code,
value: item.id,
}))
}
} catch (error) {
console.error('加载告警级别失败:', error)
}
}
// 搜索
const handleFormModelUpdate = (value: Record<string, any>) => {
formModel.value = value
}
const handleSearch = () => {
pagination.current = 1
loadData()
}
const handleReset = () => {
formModel.value = {
keyword: '',
status: '',
severity_id: '',
}
timeRange.value = []
searchParams.value = {}
pagination.current = 1
loadData()
}
// 加载数据
const loadData = async () => {
loading.value = true
try {
const params: any = {
page: pagination.current,
page_size: pagination.pageSize,
...searchParams.value,
keyword: formModel.value.keyword || undefined,
status: formModel.value.status || undefined,
}
if (formModel.value.severity_id !== '' && formModel.value.severity_id != null) {
params.severity_id = formModel.value.severity_id
}
// 处理时间范围
if (timeRange.value && timeRange.value.length === 2) {
params.start_time = new Date(timeRange.value[0]).toISOString()
params.end_time = new Date(timeRange.value[1]).toISOString()
}
const result = await fetchAlertRecords(params)
tableData.value = result.details.data || []
pagination.total = result.details.total || 0
tableData.value = result.details?.data || []
pagination.total = result.details?.total || 0
} catch (error) {
console.error('加载数据失败:', error)
Message.error('加载数据失败')
@@ -312,30 +270,22 @@ const loadData = async () => {
}
}
// 分页
const onPageChange = (page: number) => {
const handlePageChange = (page: number) => {
pagination.current = page
loadData()
}
const onPageSizeChange = (pageSize: number) => {
const handlePageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
pagination.current = 1
loadData()
}
// 行选择
const handleSelectionChange = (rowKeys: number[]) => {
selectedRowKeys.value = rowKeys
}
// 行点击
const handleRowClick = (record: any) => {
const handleRowClick = (record: any, _ev: Event) => {
currentRecord.value = record
detailDialogVisible.value = true
}
// 处理操作
const handleAck = (record: any) => {
currentRecord.value = record
ackDialogVisible.value = true
@@ -356,28 +306,6 @@ const handleComment = (record: any) => {
commentDialogVisible.value = true
}
// 批量确认
const handleBatchAck = async () => {
try {
const promises = selectedRowKeys.value.map((id) =>
createAlertProcess({
alert_record_id: id,
action: 'ack',
operator: getCurrentUser(),
comment: '批量确认',
})
)
await Promise.all(promises)
Message.success(`成功确认 ${selectedRowKeys.value.length} 条告警`)
selectedRowKeys.value = []
loadData()
} catch (error) {
console.error('批量确认失败:', error)
Message.error('批量确认失败')
}
}
// 更多操作
const handleMoreAction = (action: string, record: any) => {
currentRecord.value = record
switch (action) {
@@ -393,12 +321,22 @@ const handleMoreAction = (action: string, record: any) => {
}
}
// 操作成功回调
const handleMoreSelect = (
value: string | number | Record<string, unknown> | undefined,
record: any,
) => {
handleMoreAction(String(value), record)
}
const handleSuccess = () => {
loadData()
}
// 格式化函数
const handleRefresh = () => {
loadData()
Message.success('数据已刷新')
}
const formatDateTime = (datetime: string) => {
if (!datetime) return '-'
return new Date(datetime).toLocaleString('zh-CN')
@@ -424,7 +362,15 @@ const parsedLabels = (labels: string) => {
}
}
// 状态相关
/** 单行展示,避免 a-space wrap + 多 tag 撑高整行 td与其它告警列表页行高一致 */
const formatLabelsLine = (labels: string) => {
const obj = parsedLabels(labels)
if (!obj || typeof obj !== 'object') return ''
return Object.entries(obj as Record<string, unknown>)
.map(([k, v]) => `${k}: ${v == null ? '' : String(v)}`)
.join(', ')
}
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
firing: 'red',
@@ -466,23 +412,35 @@ const getNotifyStatusText = (status: string) => {
}
return textMap[status] || status
}
</script>
function getCurrentUser() {
// TODO: 从全局状态获取当前用户
return 'admin'
<script lang="ts">
export default {
name: 'AlertTackle',
}
</script>
<style scoped lang="less">
.alert-tackle-container {
padding: 20px;
.container {
margin-top: 20px;
.general-card {
padding: 20px;
/* 与未设置 table scroll 的告警列表页一致行高;宽表在表格外层横向滚动,避免 Arco scroll 模式改变单元格高度 */
:deep(.search-table-container .data-table) {
overflow-x: auto;
}
.toolbar {
padding: 16px 0;
/* 标签列单行省略,与其它页纯文本单元格行高一致 */
.cell-ellipsis {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 200px;
}
/* 与其它告警 SearchTable 页一致:正文单元格垂直居中 */
:deep(.arco-table-size-medium .arco-table-td) {
vertical-align: middle;
}
}
</style>