2
0

feat:告警界面

This commit is contained in:
zhongzm
2025-06-10 19:09:39 +08:00
parent ed3e583f71
commit c518ebd921
4 changed files with 386 additions and 1 deletions

View File

@@ -670,7 +670,7 @@ const local:any = {
alert:'警告', alert:'警告',
gateway:'网关', gateway:'网关',
switches:'交换机', switches:'交换机',
clients:'装置', clients:'终端',
search:'输入站点名称', search:'输入站点名称',
total:'共', total:'共',
addsite:'添加站点', addsite:'添加站点',

View File

@@ -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
})
}

View File

@@ -66,6 +66,7 @@ declare global {
const defineStore: typeof import('pinia')['defineStore'] const defineStore: typeof import('pinia')['defineStore']
const delData: typeof import('../service/api/dictData')['delData'] const delData: typeof import('../service/api/dictData')['delData']
const delJobLog: typeof import('../service/api/job')['delJobLog'] 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 deleteApDevices: typeof import('../service/api/auth')['deleteApDevices']
const deletePackage: typeof import('../service/api/auth')['deletePackage'] const deletePackage: typeof import('../service/api/auth')['deletePackage']
const deletePortal: typeof import('../service/api/auth')['deletePortal'] const deletePortal: typeof import('../service/api/auth')['deletePortal']
@@ -133,6 +134,8 @@ declare global {
const exportJobLog: typeof import('../service/api/jobLog')['exportJobLog'] const exportJobLog: typeof import('../service/api/jobLog')['exportJobLog']
const extendRef: typeof import('@vueuse/core')['extendRef'] const extendRef: typeof import('@vueuse/core')['extendRef']
const extractTabsByAllRoutes: typeof import('../store/modules/tab/shared')['extractTabsByAllRoutes'] 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 fetchApDeviceList: typeof import('../service/api/auth')['fetchApDeviceList']
const fetchBillList: typeof import('../service/api/auth')['fetchBillList'] const fetchBillList: typeof import('../service/api/auth')['fetchBillList']
const fetchBillRuleList: typeof import('../service/api/auth')['fetchBillRuleList'] const fetchBillRuleList: typeof import('../service/api/auth')['fetchBillRuleList']
@@ -183,8 +186,10 @@ declare global {
const getFixedTabs: typeof import('../store/modules/tab/shared')['getFixedTabs'] const getFixedTabs: typeof import('../store/modules/tab/shared')['getFixedTabs']
const getGlobalMenusByAuthRoutes: typeof import('../store/modules/route/shared')['getGlobalMenusByAuthRoutes'] const getGlobalMenusByAuthRoutes: typeof import('../store/modules/route/shared')['getGlobalMenusByAuthRoutes']
const getLocalizedTimeUnit: typeof import('../utils/units')['getLocalizedTimeUnit'] const getLocalizedTimeUnit: typeof import('../utils/units')['getLocalizedTimeUnit']
const getMeshConfig: typeof import('../service/api/auth')['getMeshConfig']
const getPaymentConfig: typeof import('../service/api/auth')['getPaymentConfig'] const getPaymentConfig: typeof import('../service/api/auth')['getPaymentConfig']
const getPortalConfig: typeof import('../service/api/auth')['getPortalConfig'] 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 getRouteIcons: typeof import('../store/modules/tab/shared')['getRouteIcons']
const getSelectedMenuKeyPathByKey: typeof import('../store/modules/route/shared')['getSelectedMenuKeyPathByKey'] const getSelectedMenuKeyPathByKey: typeof import('../store/modules/route/shared')['getSelectedMenuKeyPathByKey']
const getServiceBaseURL: typeof import('../utils/service')['getServiceBaseURL'] const getServiceBaseURL: typeof import('../utils/service')['getServiceBaseURL']
@@ -216,6 +221,7 @@ declare global {
const mapState: typeof import('pinia')['mapState'] const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores'] const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState'] const mapWritableState: typeof import('pinia')['mapWritableState']
const markAlertsResolved: typeof import('../service/api/auth')['markAlertsResolved']
const markRaw: typeof import('vue')['markRaw'] const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick'] const nextTick: typeof import('vue')['nextTick']
const omit: typeof import('lodash-es')['omit'] const omit: typeof import('lodash-es')['omit']
@@ -307,15 +313,18 @@ declare global {
const unref: typeof import('vue')['unref'] const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement'] const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until'] const until: typeof import('@vueuse/core')['until']
const updateAccessControl: typeof import('../service/api/auth')['updateAccessControl']
const updateApDeviceConfig: typeof import('../service/api/auth')['updateApDeviceConfig'] const updateApDeviceConfig: typeof import('../service/api/auth')['updateApDeviceConfig']
const updateBillRule: typeof import('../service/api/auth')['updateBillRule'] const updateBillRule: typeof import('../service/api/auth')['updateBillRule']
const updateData: typeof import('../service/api/dictData')['updateData'] const updateData: typeof import('../service/api/dictData')['updateData']
const updateJob: typeof import('../service/api/job')['updateJob'] const updateJob: typeof import('../service/api/job')['updateJob']
const updateLocaleOfGlobalMenus: typeof import('../store/modules/route/shared')['updateLocaleOfGlobalMenus'] 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 updatePackage: typeof import('../service/api/auth')['updatePackage']
const updatePasswordByOld: typeof import('../service/api/auth')['updatePasswordByOld'] const updatePasswordByOld: typeof import('../service/api/auth')['updatePasswordByOld']
const updatePaymentConfig: typeof import('../service/api/auth')['updatePaymentConfig'] const updatePaymentConfig: typeof import('../service/api/auth')['updatePaymentConfig']
const updatePortalConfig: typeof import('../service/api/auth')['updatePortalConfig'] 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 updateSite: typeof import('../service/api/auth')['updateSite']
const updateTabByI18nKey: typeof import('../store/modules/tab/shared')['updateTabByI18nKey'] const updateTabByI18nKey: typeof import('../store/modules/tab/shared')['updateTabByI18nKey']
const updateTabsByI18nKey: typeof import('../store/modules/tab/shared')['updateTabsByI18nKey'] const updateTabsByI18nKey: typeof import('../store/modules/tab/shared')['updateTabsByI18nKey']

View 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>