feat:告警界面
This commit is contained in:
@@ -670,7 +670,7 @@ const local:any = {
|
||||
alert:'警告',
|
||||
gateway:'网关',
|
||||
switches:'交换机',
|
||||
clients:'装置',
|
||||
clients:'终端',
|
||||
search:'输入站点名称',
|
||||
total:'共',
|
||||
addsite:'添加站点',
|
||||
|
||||
@@ -514,6 +514,115 @@ export function getPaymentConfig() {
|
||||
});
|
||||
}
|
||||
|
||||
// 获取告警数据
|
||||
export function fetchAlertList(siteId: string, params: {
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
filters: {
|
||||
timeStart?: string;
|
||||
timeEnd?: string;
|
||||
module?: string;
|
||||
resolved?: boolean;
|
||||
}
|
||||
}) {
|
||||
return request<any>({
|
||||
url: `/system/log/alerts/${siteId}`,
|
||||
method: 'get',
|
||||
params: {
|
||||
pageNum: params.pageNum,
|
||||
pageSize: params.pageSize,
|
||||
'filters.timeStart': params.filters.timeStart,
|
||||
'filters.timeEnd': params.filters.timeEnd,
|
||||
'filters.module': params.filters.module,
|
||||
'filters.resolved': params.filters.resolved,
|
||||
}
|
||||
})
|
||||
}
|
||||
// 删除告警
|
||||
export function deleteAlerts(siteId: string, data: {
|
||||
selectType: 'unresolved' | 'resolved' | 'all';
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
logs: number[];
|
||||
}) {
|
||||
return request<any>({
|
||||
url: `/system/log/alerts/${siteId}`,
|
||||
method: 'delete',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 标记告警为已解决
|
||||
export function markAlertsResolved(siteId: string, data: {
|
||||
selectType: 'unresolved' | 'resolved' | 'all';
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
logs: string[];
|
||||
}) {
|
||||
return request<any>({
|
||||
url: `/system/log/alerts/${siteId}`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 获取白名单配置
|
||||
export function fetchAccessControl(siteId: string) {
|
||||
return request<any>({
|
||||
url: `/system/access-control/${siteId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
// 更新白名单配置
|
||||
export function updateAccessControl(siteId: string, data: {
|
||||
preAuthAccessEnable: boolean;
|
||||
freeAuthClientEnable: boolean;
|
||||
preAuthAccessPolicies: Array<{
|
||||
type: number;
|
||||
ip?: string;
|
||||
url?: string;
|
||||
}>;
|
||||
freeAuthClientPolicies: Array<{
|
||||
type: number;
|
||||
clientIp?: string;
|
||||
clientMac?: string;
|
||||
}>;
|
||||
}) {
|
||||
return request<any>({
|
||||
url: `/system/access-control/${siteId}`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 获取 Mesh 配置
|
||||
export function getMeshConfig(siteId: string) {
|
||||
return request<any>({
|
||||
url: `/system/site/${siteId}/mesh`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
// 更新 Mesh 配置
|
||||
export function updateMeshConfig(siteId: string, data: { meshEnable: boolean; autoFailoverEnable?: boolean }) {
|
||||
return request<any>({
|
||||
url: `/system/site/${siteId}/mesh`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
// 获取 Roaming 配置
|
||||
export function getRoamingConfig(siteId: string) {
|
||||
return request<any>({
|
||||
url: `/system/site/${siteId}/roaming`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
// 更新 Roaming 配置
|
||||
export function updateRoamingConfig(siteId: string, data: { fastRoamingEnable: boolean; noStickRoamingEnable?: boolean; aiRoamingEnable?: boolean }) {
|
||||
return request<any>({
|
||||
url: `/system/site/${siteId}/roaming`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
9
src/typings/auto-imports.d.ts
vendored
9
src/typings/auto-imports.d.ts
vendored
@@ -66,6 +66,7 @@ declare global {
|
||||
const defineStore: typeof import('pinia')['defineStore']
|
||||
const delData: typeof import('../service/api/dictData')['delData']
|
||||
const delJobLog: typeof import('../service/api/job')['delJobLog']
|
||||
const deleteAlerts: typeof import('../service/api/auth')['deleteAlerts']
|
||||
const deleteApDevices: typeof import('../service/api/auth')['deleteApDevices']
|
||||
const deletePackage: typeof import('../service/api/auth')['deletePackage']
|
||||
const deletePortal: typeof import('../service/api/auth')['deletePortal']
|
||||
@@ -133,6 +134,8 @@ declare global {
|
||||
const exportJobLog: typeof import('../service/api/jobLog')['exportJobLog']
|
||||
const extendRef: typeof import('@vueuse/core')['extendRef']
|
||||
const extractTabsByAllRoutes: typeof import('../store/modules/tab/shared')['extractTabsByAllRoutes']
|
||||
const fetchAccessControl: typeof import('../service/api/auth')['fetchAccessControl']
|
||||
const fetchAlertList: typeof import('../service/api/auth')['fetchAlertList']
|
||||
const fetchApDeviceList: typeof import('../service/api/auth')['fetchApDeviceList']
|
||||
const fetchBillList: typeof import('../service/api/auth')['fetchBillList']
|
||||
const fetchBillRuleList: typeof import('../service/api/auth')['fetchBillRuleList']
|
||||
@@ -183,8 +186,10 @@ declare global {
|
||||
const getFixedTabs: typeof import('../store/modules/tab/shared')['getFixedTabs']
|
||||
const getGlobalMenusByAuthRoutes: typeof import('../store/modules/route/shared')['getGlobalMenusByAuthRoutes']
|
||||
const getLocalizedTimeUnit: typeof import('../utils/units')['getLocalizedTimeUnit']
|
||||
const getMeshConfig: typeof import('../service/api/auth')['getMeshConfig']
|
||||
const getPaymentConfig: typeof import('../service/api/auth')['getPaymentConfig']
|
||||
const getPortalConfig: typeof import('../service/api/auth')['getPortalConfig']
|
||||
const getRoamingConfig: typeof import('../service/api/auth')['getRoamingConfig']
|
||||
const getRouteIcons: typeof import('../store/modules/tab/shared')['getRouteIcons']
|
||||
const getSelectedMenuKeyPathByKey: typeof import('../store/modules/route/shared')['getSelectedMenuKeyPathByKey']
|
||||
const getServiceBaseURL: typeof import('../utils/service')['getServiceBaseURL']
|
||||
@@ -216,6 +221,7 @@ declare global {
|
||||
const mapState: typeof import('pinia')['mapState']
|
||||
const mapStores: typeof import('pinia')['mapStores']
|
||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||
const markAlertsResolved: typeof import('../service/api/auth')['markAlertsResolved']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const omit: typeof import('lodash-es')['omit']
|
||||
@@ -307,15 +313,18 @@ declare global {
|
||||
const unref: typeof import('vue')['unref']
|
||||
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
||||
const until: typeof import('@vueuse/core')['until']
|
||||
const updateAccessControl: typeof import('../service/api/auth')['updateAccessControl']
|
||||
const updateApDeviceConfig: typeof import('../service/api/auth')['updateApDeviceConfig']
|
||||
const updateBillRule: typeof import('../service/api/auth')['updateBillRule']
|
||||
const updateData: typeof import('../service/api/dictData')['updateData']
|
||||
const updateJob: typeof import('../service/api/job')['updateJob']
|
||||
const updateLocaleOfGlobalMenus: typeof import('../store/modules/route/shared')['updateLocaleOfGlobalMenus']
|
||||
const updateMeshConfig: typeof import('../service/api/auth')['updateMeshConfig']
|
||||
const updatePackage: typeof import('../service/api/auth')['updatePackage']
|
||||
const updatePasswordByOld: typeof import('../service/api/auth')['updatePasswordByOld']
|
||||
const updatePaymentConfig: typeof import('../service/api/auth')['updatePaymentConfig']
|
||||
const updatePortalConfig: typeof import('../service/api/auth')['updatePortalConfig']
|
||||
const updateRoamingConfig: typeof import('../service/api/auth')['updateRoamingConfig']
|
||||
const updateSite: typeof import('../service/api/auth')['updateSite']
|
||||
const updateTabByI18nKey: typeof import('../store/modules/tab/shared')['updateTabByI18nKey']
|
||||
const updateTabsByI18nKey: typeof import('../store/modules/tab/shared')['updateTabsByI18nKey']
|
||||
|
||||
267
src/views/device/alerts/index.vue
Normal file
267
src/views/device/alerts/index.vue
Normal file
@@ -0,0 +1,267 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, computed } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { DeleteOutlined, ToolOutlined } from '@ant-design/icons-vue'
|
||||
// 假设API与portal页面一致
|
||||
import { fetchSiteList, fetchAlertList, deleteAlerts, markAlertsResolved } from '@/service/api/auth'
|
||||
|
||||
const siteList = ref<{ siteId: string, name: string }[]>([])
|
||||
const selectedSiteId = ref('')
|
||||
const siteLoading = ref(false)
|
||||
|
||||
const getSiteList = async () => {
|
||||
siteLoading.value = true
|
||||
const { data, error } = await fetchSiteList({ pageNum: 1, pageSize: 100 })
|
||||
if (!error) {
|
||||
siteList.value = data.rows || []
|
||||
selectedSiteId.value = siteList.value[0]?.siteId || ''
|
||||
}
|
||||
siteLoading.value = false
|
||||
}
|
||||
|
||||
// 日期选择器相关
|
||||
const now = dayjs()
|
||||
const startOfToday = now.startOf('day')
|
||||
const dateRange = ref<[any, any]>([startOfToday, now])
|
||||
|
||||
// 告警相关
|
||||
const activeMainTab = ref('Alerts')
|
||||
// const activeSubTab = ref('Resolved') // 已不再需要
|
||||
const statusTab = ref('Unresolved') // Unresolved/Resolved
|
||||
const typeTab = ref('All') // All/System/Device
|
||||
const alerts = ref<any[]>([])
|
||||
const selectedRowKeys = ref<string[]>([])
|
||||
const pageNum = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
|
||||
const getAlerts = async () => {
|
||||
if (!selectedSiteId.value) return
|
||||
const filters: any = {
|
||||
timeStart: dateRange.value[0] ? dateRange.value[0].valueOf() : undefined,
|
||||
timeEnd: dateRange.value[1] ? dateRange.value[1].valueOf() : undefined,
|
||||
module: typeTab.value === 'All' ? undefined : typeTab.value,
|
||||
resolved: statusTab.value === 'Resolved' ? true : false
|
||||
}
|
||||
const { data, error } = await fetchAlertList(selectedSiteId.value, {
|
||||
pageNum: pageNum.value,
|
||||
pageSize: pageSize.value,
|
||||
filters
|
||||
})
|
||||
if (!error && data) {
|
||||
alerts.value = (data.rows || []).map((item: any) => ({ ...item, id: String(item.id) }))
|
||||
total.value = data.total || 0
|
||||
}
|
||||
}
|
||||
|
||||
const handleSiteChange = async (value: string) => {
|
||||
selectedSiteId.value = value
|
||||
pageNum.value = 1
|
||||
await getAlerts()
|
||||
}
|
||||
|
||||
const onResolve = async (id: string | number) => {
|
||||
if (!selectedSiteId.value) {
|
||||
message.warning('请先选择站点')
|
||||
return
|
||||
}
|
||||
const { error } = await markAlertsResolved(selectedSiteId.value, {
|
||||
selectType: 'all',
|
||||
startTime: dateRange.value[0].valueOf(),
|
||||
endTime: dateRange.value[1].valueOf(),
|
||||
logs: [String(id)]
|
||||
})
|
||||
if (!error) {
|
||||
await getAlerts()
|
||||
message.success('已标记为已解决')
|
||||
} else {
|
||||
message.error('标记失败')
|
||||
}
|
||||
}
|
||||
|
||||
const onDelete = async (ids: number | number[]) => {
|
||||
if (!selectedSiteId.value) {
|
||||
message.warning('请先选择站点')
|
||||
return
|
||||
}
|
||||
|
||||
const deleteIds = Array.isArray(ids) ? ids : [ids]
|
||||
const deleteCount = deleteIds.length
|
||||
|
||||
const { error } = await deleteAlerts(selectedSiteId.value, {
|
||||
selectType: 'all', // 固定传 all
|
||||
startTime: dateRange.value[0].valueOf(),
|
||||
endTime: dateRange.value[1].valueOf(),
|
||||
logs: deleteIds
|
||||
})
|
||||
if (!error) {
|
||||
await getAlerts()
|
||||
message.success(`成功删除${deleteCount}条记录`)
|
||||
} else {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const onBatchDelete = () => {
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
message.warning('请先选择要删除的记录')
|
||||
return
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除选中的 ${selectedRowKeys.value.length} 条记录吗?`,
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => onDelete(selectedRowKeys.value)
|
||||
})
|
||||
}
|
||||
|
||||
watch([dateRange, statusTab, typeTab, selectedSiteId], () => {
|
||||
pageNum.value = 1
|
||||
getAlerts()
|
||||
})
|
||||
|
||||
const rowSelection = {
|
||||
selectedRowKeys: selectedRowKeys,
|
||||
onChange: (keys: string[]) => {
|
||||
selectedRowKeys.value = keys
|
||||
},
|
||||
type: 'checkbox'
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await getSiteList()
|
||||
await getAlerts()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 一级Tab + 时间控件 -->
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<a-tabs v-model:activeKey="activeMainTab" style="margin-bottom: 0;">
|
||||
<a-tab-pane key="Alerts" tab="Alerts" />
|
||||
</a-tabs>
|
||||
</div>
|
||||
<a-range-picker
|
||||
v-model:value="dateRange"
|
||||
show-time
|
||||
:allowClear="false"
|
||||
style="margin-left: 16px;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 二级分组 + 站点选择 + 操作按钮 -->
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<a-radio-group v-model:value="statusTab" style="margin-right: 16px;">
|
||||
<a-radio-button value="Unresolved">Unresolved</a-radio-button>
|
||||
<a-radio-button value="Resolved">Resolved</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-radio-group v-model:value="typeTab" style="margin-right: 16px;">
|
||||
<a-radio-button value="All">All</a-radio-button>
|
||||
<a-radio-button value="System">System</a-radio-button>
|
||||
<a-radio-button value="Device">Device</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-select
|
||||
v-model:value="selectedSiteId"
|
||||
:loading="siteLoading"
|
||||
placeholder="请选择站点"
|
||||
style="width: 200px; margin-right: 16px;"
|
||||
@change="handleSiteChange"
|
||||
>
|
||||
<a-select-option v-for="site in siteList" :key="site.siteId" :value="site.siteId">
|
||||
{{ site.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div>
|
||||
<a-button @click="onBatchDelete">Batch Delete</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<a-table
|
||||
:dataSource="alerts"
|
||||
:rowSelection="rowSelection"
|
||||
:pagination="false"
|
||||
:rowKey="record => record.id"
|
||||
bordered
|
||||
size="middle"
|
||||
>
|
||||
<a-table-column title="TYPE" dataIndex="key" key="key" />
|
||||
<a-table-column title="LEVEL" dataIndex="level" key="level">
|
||||
<template #default="{ record }">
|
||||
<span>
|
||||
<span :style="{
|
||||
color: record.level === 'Critical' ? 'red' :
|
||||
record.level === 'Error' ? 'orange' :
|
||||
record.level === 'Warning' ? '#faad14' :
|
||||
record.level === 'Info' ? '#52c41a' : '#d9d9d9',
|
||||
fontSize: '16px'
|
||||
}">●</span>
|
||||
<span style="margin-left: 4px;">{{ record.level }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="CONTENT" dataIndex="content" key="content" />
|
||||
<a-table-column title="TIME" dataIndex="time" key="time">
|
||||
<template #default="{ record }">
|
||||
<span>{{ record.time ? dayjs(record.time).format('YYYY-MM-DD HH:mm:ss') : '' }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="ACTION" key="action" width="120">
|
||||
<template #default="{ record }">
|
||||
<div style="display: flex; gap: 8px; justify-content: center;">
|
||||
<!-- 未解决状态显示解决按钮 -->
|
||||
<a-tooltip v-if="statusTab === 'Unresolved'" title="标记为已解决">
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
style="color: #52c41a"
|
||||
@click="onResolve(record.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<ToolOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<!-- 所有状态都显示删除按钮 -->
|
||||
<a-tooltip title="删除">
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
style="color: #ff4d4f"
|
||||
@click="() => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除该条告警记录吗?',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => onDelete(record.id)
|
||||
})
|
||||
}"
|
||||
>
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 16px;">
|
||||
<a-pagination :total="total" :pageSize="pageSize" :current="pageNum" showQuickJumper @change="(p, ps) => { pageNum = p; pageSize = ps; getAlerts(); }" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 可根据实际需求自定义样式 */
|
||||
</style>
|
||||
Reference in New Issue
Block a user