fix: 标签栏图标修正;概览页展示数据切换;消息通知异常显示修复
This commit is contained in:
@@ -9,9 +9,9 @@ VITE_APP_DESCRIPTION="default standard template"
|
||||
VITE_USE_MOCK=false
|
||||
|
||||
# API 基础URL
|
||||
# VITE_API_BASE_URL=https://ops-api.apinb.com
|
||||
VITE_API_BASE_URL=https://ops-api.apinb.com
|
||||
# 开发环境走 Vite 同源代理,避免依赖本机 80/443 nginx
|
||||
VITE_API_BASE_URL=
|
||||
# VITE_API_BASE_URL=
|
||||
|
||||
# Logs 本地调试地址(仅 logs 模块使用)
|
||||
VITE_LOGS_API_BASE_URL=http://127.0.0.1:12440
|
||||
|
||||
24
dev/components.d.ts
vendored
Normal file
24
dev/components.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// biome-ignore lint: disable
|
||||
// oxlint-disable
|
||||
// ------
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import { GlobalComponents } from 'vue'
|
||||
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
}
|
||||
|
||||
// For TSX support
|
||||
declare global {
|
||||
const RouterLink: typeof import('vue-router')['RouterLink']
|
||||
const RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="https://unpkg.byted-static.com/latest/byted/arco-config/assets/favicon.ico" />
|
||||
<link rel="icon" type="image/svg+xml" href="%BASE_URL%favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>智能运维管理系统</title>
|
||||
</head>
|
||||
|
||||
4
public/favicon.svg
Normal file
4
public/favicon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="33" height="33">
|
||||
<path d="M0 240.941176a240.941176 240.941176 0 0 1 240.941176-240.941176h542.117648a240.941176 240.941176 0 0 1 240.941176 240.941176v542.117648a240.941176 240.941176 0 0 1-240.941176 240.941176H240.941176a240.941176 240.941176 0 0 1-240.941176-240.941176V240.941176z" fill="#287BF7"></path>
|
||||
<path d="M340.931765 388.969412a120.621176 120.621176 0 1 0 115.260235 209.799529l70.776471 97.490824a241.091765 241.091765 0 1 1 82.522353-283.587765c15.751529 39.936 14.516706 83.546353 13.763764 110.290824-0.150588 4.879059-0.271059 9.216-0.271059 12.860235 0 22.648471 5.812706 48.519529 18.221177 66.831059 10.24 15.058824 26.654118 28.310588 61.108706 28.310588 31.804235 0 49.152-10.721882 59.331764-22.40753 11.264-12.950588 18.974118-33.370353 18.974118-59.452235 0-18.642824-9.396706-39.574588-27.166118-53.067294-16.022588-12.137412-43.550118-21.594353-86.768941-8.734118l-34.42447-115.440941c75.926588-22.618353 144.112941-9.637647 193.987764 28.190118 48.128 36.502588 74.842353 93.093647 74.842353 149.082353 0 48.489412-14.245647 99.087059-48.609882 138.541176-35.418353 40.719059-87.341176 63.728941-150.166588 63.728941-75.294118 0-128.662588-33.792-160.768-81.106823-29.936941-44.062118-39.032471-95.864471-39.032471-134.475294 0-9.396706 0.150588-17.588706 0.240941-24.847059 0.421647-25.057882 0.632471-38.972235-5.330823-54.091294a120.621176 120.621176 0 0 0-156.491294-67.915294z" fill="#FFFFFF"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -16,11 +16,6 @@
|
||||
@view-history="openHistory"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<template #extra>
|
||||
<a-button type="text" @click="emptyList">
|
||||
{{ $t('messageBox.tab.button') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-tabs>
|
||||
<a-drawer v-model:visible="historyVisible" width="760px" title="通知投递历史" :footer="false" unmount-on-close @open="fetchHistory">
|
||||
<a-table :columns="historyColumns" :data="historyList" :loading="historyLoading" :pagination="historyPagination" row-key="id" @page-change="onHistoryPageChange">
|
||||
@@ -50,7 +45,7 @@ import {
|
||||
import useLoading from '@/hooks/loading'
|
||||
import useNotificationSocket from '@/hooks/useNotificationSocket'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { computed, reactive, ref, toRefs } from 'vue'
|
||||
import { computed, reactive, ref, toRefs, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import List from './list.vue'
|
||||
|
||||
@@ -59,9 +54,13 @@ interface TabItem {
|
||||
title: string
|
||||
avatar?: string
|
||||
}
|
||||
const visibleMessageTypes = ['message']
|
||||
const { loading, setLoading } = useLoading(true)
|
||||
const messageType = ref('message')
|
||||
const { t } = useI18n()
|
||||
const emit = defineEmits<{
|
||||
(event: 'unread-count-change', count: number): void
|
||||
}>()
|
||||
const messageData = reactive<{
|
||||
renderList: MessageRecord[]
|
||||
messageList: MessageRecord[]
|
||||
@@ -92,14 +91,6 @@ const tabList: TabItem[] = [
|
||||
key: 'message',
|
||||
title: t('messageBox.tab.title.message'),
|
||||
},
|
||||
{
|
||||
key: 'notice',
|
||||
title: t('messageBox.tab.title.notice'),
|
||||
},
|
||||
{
|
||||
key: 'todo',
|
||||
title: t('messageBox.tab.title.todo'),
|
||||
},
|
||||
]
|
||||
async function fetchSourceData() {
|
||||
setLoading(true)
|
||||
@@ -176,6 +167,9 @@ const getDeliveryColor = (status: string) => {
|
||||
const renderList = computed(() => {
|
||||
return messageData.messageList.filter((item) => messageType.value === item.type)
|
||||
})
|
||||
const totalUnreadCount = computed(() => {
|
||||
return messageData.messageList.filter((item) => visibleMessageTypes.includes(item.type) && !item.status).length
|
||||
})
|
||||
const unreadCount = computed(() => {
|
||||
return renderList.value.filter((item) => !item.status).length
|
||||
})
|
||||
@@ -190,9 +184,7 @@ const formatUnreadLength = (type: string) => {
|
||||
const handleItemClick = (items: MessageListType) => {
|
||||
if (renderList.value.length) readMessage([...items])
|
||||
}
|
||||
const emptyList = () => {
|
||||
messageData.messageList = []
|
||||
}
|
||||
watch(totalUnreadCount, (count) => emit('unread-count-change', count), { immediate: true })
|
||||
fetchSourceData()
|
||||
useNotificationSocket<MessageRecord>({
|
||||
onNotification: upsertMessageToTop,
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
export default {
|
||||
'messageBox.tab.title.message': 'Message',
|
||||
'messageBox.tab.title.notice': 'Notice',
|
||||
'messageBox.tab.title.todo': 'Todo',
|
||||
'messageBox.tab.button': 'empty',
|
||||
'messageBox.allRead': 'All Read',
|
||||
'messageBox.viewMore': 'View More',
|
||||
'messageBox.noContent': 'No Content',
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
export default {
|
||||
'messageBox.tab.title.message': '消息',
|
||||
'messageBox.tab.title.notice': '通知',
|
||||
'messageBox.tab.title.todo': '待办',
|
||||
'messageBox.tab.button': '清空',
|
||||
'messageBox.allRead': '全部已读',
|
||||
'messageBox.viewMore': '查看更多',
|
||||
'messageBox.noContent': '暂无内容',
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<li>
|
||||
<a-tooltip :content="$t('settings.navbar.alerts')">
|
||||
<div class="message-box-trigger">
|
||||
<a-badge dot>
|
||||
<a-badge dot :count="notificationUnreadCount">
|
||||
<a-button class="nav-btn" type="outline" :shape="'circle'" @click="setPopoverVisible">
|
||||
<icon-notification />
|
||||
</a-button>
|
||||
@@ -67,7 +67,7 @@
|
||||
>
|
||||
<div ref="refBtn" class="ref-btn"></div>
|
||||
<template #content>
|
||||
<message-box />
|
||||
<message-box @unread-count-change="handleMessageBoxUnreadCountChange" />
|
||||
</template>
|
||||
</a-popover>
|
||||
</li>
|
||||
@@ -126,8 +126,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { MessageRecord, queryMessageList } from '@/api/message'
|
||||
import useLocale from '@/hooks/locale'
|
||||
import useUser from '@/hooks/user'
|
||||
import useNotificationSocket from '@/hooks/useNotificationSocket'
|
||||
import { LOCALE_OPTIONS } from '@/locale'
|
||||
import { useAppStore, useUserStore } from '@/store'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
@@ -173,8 +175,24 @@ const setVisible = () => {
|
||||
}
|
||||
const refBtn = ref()
|
||||
const triggerBtn = ref()
|
||||
const notificationUnreadCount = ref(0)
|
||||
const visibleMessageTypes = ['message']
|
||||
const passwordChangeVisible = ref(false)
|
||||
const profileChangeVisible = ref(false)
|
||||
const updateNotificationUnreadCount = (list: MessageRecord[] = []) => {
|
||||
notificationUnreadCount.value = list.filter((item) => visibleMessageTypes.includes(item.type) && !item.status).length
|
||||
}
|
||||
const fetchNotificationUnreadCount = async () => {
|
||||
try {
|
||||
const { data } = await queryMessageList()
|
||||
updateNotificationUnreadCount(data)
|
||||
} catch (err) {
|
||||
updateNotificationUnreadCount()
|
||||
}
|
||||
}
|
||||
const handleMessageBoxUnreadCountChange = (count: number) => {
|
||||
notificationUnreadCount.value = count
|
||||
}
|
||||
const setPopoverVisible = () => {
|
||||
const event = new MouseEvent('click', {
|
||||
view: window,
|
||||
@@ -201,6 +219,12 @@ const setDropDownVisible = () => {
|
||||
triggerBtn.value.dispatchEvent(event)
|
||||
}
|
||||
const toggleDrawerMenu = inject('toggleDrawerMenu') as () => void
|
||||
|
||||
fetchNotificationUnreadCount()
|
||||
useNotificationSocket<MessageRecord>({
|
||||
onNotification: fetchNotificationUnreadCount,
|
||||
onReconnectAfter: fetchNotificationUnreadCount,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
@@ -12,21 +12,20 @@ let isMenuLoading = false
|
||||
let isMenuLoaded = false
|
||||
|
||||
export default function setupPermissionGuard(router: Router) {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
router.beforeEach(async (to) => {
|
||||
const appStore = useAppStore()
|
||||
const userStore = useUserStore()
|
||||
const Permission = usePermission()
|
||||
const permissionsAllow = Permission.accessRouter(to)
|
||||
|
||||
try {
|
||||
if (appStore.menuFromServer) {
|
||||
// 针对来自服务端的菜单配置进行处理
|
||||
// Handle routing configuration from the server
|
||||
|
||||
// 检查是否在白名单中
|
||||
if (WHITE_LIST.find((el) => el.name === to.name)) {
|
||||
next()
|
||||
NProgress.done()
|
||||
return
|
||||
return true
|
||||
}
|
||||
console.log('[Permission Guard] Menu not loaded, loading...')
|
||||
|
||||
@@ -42,40 +41,31 @@ export default function setupPermissionGuard(router: Router) {
|
||||
isMenuLoaded = true
|
||||
console.log('[Permission Guard] Menu loaded, redirecting to:', to.path)
|
||||
// 路由注册后需要重新导航到当前路径
|
||||
next({ path: to.path, query: to.query, params: to.params, replace: true } as RouteLocationRaw)
|
||||
return { path: to.path, query: to.query, params: to.params, replace: true } as RouteLocationRaw
|
||||
} catch (error) {
|
||||
console.error('[Permission Guard] Failed to load menu:', error)
|
||||
isMenuLoading = false
|
||||
next(NOT_FOUND)
|
||||
return NOT_FOUND
|
||||
} finally {
|
||||
isMenuLoading = false
|
||||
}
|
||||
NProgress.done()
|
||||
return
|
||||
}
|
||||
|
||||
// 如果正在加载菜单,等待加载完成
|
||||
if (isMenuLoading) {
|
||||
next({ path: to.path, query: to.query, params: to.params, replace: true } as RouteLocationRaw)
|
||||
NProgress.done()
|
||||
return
|
||||
return { path: to.path, query: to.query, params: to.params, replace: true } as RouteLocationRaw
|
||||
}
|
||||
|
||||
// 动态路由已加载,直接放行
|
||||
// 因为路由已经通过 addRoute 注册,Vue Router 会自动匹配
|
||||
return permissionsAllow ? true : NOT_FOUND
|
||||
}
|
||||
|
||||
if (permissionsAllow) {
|
||||
next()
|
||||
} else {
|
||||
next(NOT_FOUND)
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line no-lonely-if
|
||||
if (permissionsAllow) next()
|
||||
else {
|
||||
const destination = Permission.findFirstPermissionRoute(appRoutes, userStore.role) || NOT_FOUND
|
||||
next(destination)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return Permission.findFirstPermissionRoute(appRoutes, userStore.role) || NOT_FOUND
|
||||
} finally {
|
||||
NProgress.done()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,40 +8,39 @@ import { isLogin } from '@/utils/auth'
|
||||
const whiteList = ['login', 'home']
|
||||
|
||||
export default function setupUserLoginInfoGuard(router: Router) {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
router.beforeEach(async (to) => {
|
||||
NProgress.start()
|
||||
const userStore = useUserStore()
|
||||
if (isLogin()) {
|
||||
if (userStore.role) {
|
||||
next()
|
||||
return true
|
||||
} else {
|
||||
try {
|
||||
await userStore.info()
|
||||
next()
|
||||
return true
|
||||
} catch (error) {
|
||||
await userStore.logout()
|
||||
next({
|
||||
return {
|
||||
name: 'login',
|
||||
query: {
|
||||
redirect: to.name,
|
||||
...to.query,
|
||||
} as LocationQueryRaw,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 在白名单中的路由不需要登录验证
|
||||
if (whiteList.includes(to.name as string)) {
|
||||
next()
|
||||
return
|
||||
return true
|
||||
}
|
||||
next({
|
||||
return {
|
||||
name: 'login',
|
||||
query: {
|
||||
redirect: to.name,
|
||||
...to.query,
|
||||
} as LocationQueryRaw,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<a-tag v-if="card.title === '警告' || card.title === '工单' || card.title === '审核'" size="small" class="stat-tag">
|
||||
待处理/总数
|
||||
</a-tag>
|
||||
<a-tag v-else size="small" class="stat-tag">在线/总数</a-tag>
|
||||
<a-tag v-else size="small" class="stat-tag">启用/总数</a-tag>
|
||||
</div>
|
||||
<div class="stat-icon" :style="{ background: getIconBackground(card) }">
|
||||
<component :is="getIconComponent(card.title)" :style="{ fontSize: '24px', color: getIconColor(card) }" />
|
||||
@@ -41,13 +41,21 @@
|
||||
</div>
|
||||
<span class="title-text">告警趋势</span>
|
||||
</div>
|
||||
<a-select style="width: 200px" v-model="month" size="small" class="month-select" @change="handleChange">
|
||||
<a-option value="2025年3月">2025年3月</a-option>
|
||||
<a-option value="2025年2月">2025年2月</a-option>
|
||||
<a-select style="width: 160px" v-model="month" size="small" class="month-select" @change="handleChange">
|
||||
<a-option v-for="item in monthOptions" :key="item.value" :value="item.value">
|
||||
{{ item.label }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<v-chart v-if="!chartLoading" :option="alertChartOption" class="chart" autoresize />
|
||||
<div v-if="!chartLoading && isTrendEmpty" class="chart-empty">
|
||||
<a-empty>
|
||||
<template #description>
|
||||
<span>当前月份暂无告警趋势数据</span>
|
||||
</template>
|
||||
</a-empty>
|
||||
</div>
|
||||
<v-chart v-else-if="!chartLoading" :option="alertChartOption" class="chart" autoresize />
|
||||
<div v-else class="chart-loading">
|
||||
<a-spin size="large" />
|
||||
<div class="loading-text">加载图表数据...</div>
|
||||
@@ -131,16 +139,53 @@ import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { BarChart } from 'echarts/charts'
|
||||
import { TitleComponent, TooltipComponent, GridComponent } from 'echarts/components'
|
||||
import dayjs from 'dayjs'
|
||||
import { fetchAlertCount, fetchHistories } from '@/api/ops/alertHistory'
|
||||
import { fetchAlertCount, fetchAlertTrend, fetchHistories } from '@/api/ops/alertHistory'
|
||||
import { fetchCollectorStatistics } from '@/api/ops/dcControl'
|
||||
import { fetchFeedbackTicketStatistics } from '@/api/ops/feedbackTicket'
|
||||
import { fetchNetworkDeviceList } from '@/api/ops/network-device'
|
||||
import { fetchReviewStats } from '@/api/kb/review'
|
||||
|
||||
// 注册必要的 ECharts 组件
|
||||
use([CanvasRenderer, BarChart, TitleComponent, TooltipComponent, GridComponent])
|
||||
|
||||
const router = useRouter()
|
||||
const month = ref('2025年3月')
|
||||
const getCurrentMonthValue = () => dayjs().format('YYYY-MM')
|
||||
|
||||
const formatMonthLabel = (value: string) => dayjs(`${value}-01`).format('YYYY年M月')
|
||||
|
||||
const monthOptions = computed(() => {
|
||||
return Array.from({ length: 6 }, (_, index) => {
|
||||
const value = dayjs().subtract(index, 'month').format('YYYY-MM')
|
||||
return {
|
||||
label: formatMonthLabel(value),
|
||||
value,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const getMonthRange = (value: string) => {
|
||||
const start = dayjs(`${value}-01`).startOf('month')
|
||||
const end = start.endOf('month')
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
startTime: start.toISOString(),
|
||||
endTime: end.toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
const buildMonthDays = (value: string) => {
|
||||
const { start, end } = getMonthRange(value)
|
||||
const days: string[] = []
|
||||
let cursor = start
|
||||
while (cursor.isBefore(end) || cursor.isSame(end, 'day')) {
|
||||
days.push(cursor.format('YYYY-MM-DD'))
|
||||
cursor = cursor.add(1, 'day')
|
||||
}
|
||||
return days
|
||||
}
|
||||
|
||||
const month = ref(getCurrentMonthValue())
|
||||
const loading = ref(true)
|
||||
const chartLoading = ref(false)
|
||||
const numberAnimated = ref([false, false, false, false, false, false])
|
||||
@@ -149,7 +194,7 @@ const animatedNumbers = ref([0, 0, 0, 0, 0, 0])
|
||||
// 统计数据
|
||||
const statistics = reactive({
|
||||
serverPc: { pending: 0, total: 0 },
|
||||
cluster: { pending: 0, total: 0 },
|
||||
database: { pending: 0, total: 0 },
|
||||
network: { pending: 0, total: 0 },
|
||||
alert: { pending: 0, total: 0 },
|
||||
ticket: { pending: 0, total: 0 },
|
||||
@@ -157,6 +202,21 @@ const statistics = reactive({
|
||||
})
|
||||
|
||||
const pendingAlerts = ref<any[]>([])
|
||||
const trendDates = ref<string[]>([])
|
||||
const trendCounts = ref<number[]>([])
|
||||
const isTrendEmpty = computed(() => trendCounts.value.length > 0 && trendCounts.value.every((count) => count === 0))
|
||||
|
||||
const unwrapDetails = (payload: any) => payload?.details ?? payload?.data ?? payload ?? {}
|
||||
|
||||
const normalizeCountPair = (source: any) => ({
|
||||
pending: Number(source?.collect_enabled ?? source?.enabled ?? 0) || 0,
|
||||
total: Number(source?.total ?? 0) || 0,
|
||||
})
|
||||
|
||||
const getPageTotal = (payload: any) => {
|
||||
const body = unwrapDetails(payload)
|
||||
return Number(body?.total ?? payload?.total ?? 0) || 0
|
||||
}
|
||||
|
||||
// 统计卡片数据
|
||||
const statCards = computed(() => [
|
||||
@@ -169,9 +229,9 @@ const statCards = computed(() => [
|
||||
iconColor: '#409EFF',
|
||||
},
|
||||
{
|
||||
title: '集群采集',
|
||||
pending: statistics.cluster.pending,
|
||||
total: statistics.cluster.total,
|
||||
title: '数据库服务',
|
||||
pending: statistics.database.pending,
|
||||
total: statistics.database.total,
|
||||
lightColor: 'rgba(255, 153, 0, 0.1)',
|
||||
darkColor: 'rgba(255, 153, 0, 0.15)',
|
||||
iconColor: '#FF9900',
|
||||
@@ -236,7 +296,8 @@ const alertChartOption = computed(() => ({
|
||||
const data = params[0]
|
||||
const date = data.axisValue
|
||||
const value = data.value
|
||||
const percent = ((value / 200) * 100).toFixed(1)
|
||||
const maxValue = Math.max(...trendCounts.value, 0)
|
||||
const percent = maxValue > 0 ? ((value / maxValue) * 100).toFixed(1) : '0.0'
|
||||
return `
|
||||
<div style="margin-bottom: 4px; font-size: 12px; color: rgba(255, 255, 255, 0.7);">${date}</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
@@ -257,7 +318,7 @@ const alertChartOption = computed(() => ({
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['03/01', '03/02', '03/03', '03/04', '03/05', '03/06', '03/07'],
|
||||
data: trendDates.value,
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
@@ -281,7 +342,7 @@ const alertChartOption = computed(() => ({
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 250,
|
||||
max: (value: { max: number }) => Math.max(5, Math.ceil(value.max * 1.2)),
|
||||
splitNumber: 5,
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
@@ -311,7 +372,7 @@ const alertChartOption = computed(() => ({
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'bar',
|
||||
data: [120, 200, 150, 80, 60, 110, 130],
|
||||
data: trendCounts.value,
|
||||
barWidth: '50%',
|
||||
itemStyle: {
|
||||
color: {
|
||||
@@ -371,31 +432,6 @@ const alertChartOption = computed(() => ({
|
||||
color: 'rgba(99, 102, 241, 0.05)',
|
||||
borderRadius: [12, 12, 0, 0],
|
||||
},
|
||||
markLine: {
|
||||
silent: true,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
color: 'rgba(245, 158, 11, 0.6)',
|
||||
width: 2,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
yAxis: 150,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'end',
|
||||
formatter: '预警值',
|
||||
fontSize: 12,
|
||||
color: 'rgba(245, 158, 11, 0.8)',
|
||||
fontWeight: 500,
|
||||
backgroundColor: 'rgba(245, 158, 11, 0.1)',
|
||||
padding: [4, 8],
|
||||
borderRadius: 4,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
@@ -419,7 +455,7 @@ const getIconColor = (card: any) => {
|
||||
const getIconComponent = (title: string) => {
|
||||
const iconMap: any = {
|
||||
服务器及PC: IconDesktop,
|
||||
集群采集: IconCloud,
|
||||
数据库服务: IconCloud,
|
||||
网络设备: IconWifi,
|
||||
警告: IconExclamationCircle,
|
||||
工单: IconFile,
|
||||
@@ -428,21 +464,47 @@ const getIconComponent = (title: string) => {
|
||||
return iconMap[title] || IconDesktop
|
||||
}
|
||||
|
||||
const normalizeTrendItems = (payload: any) => {
|
||||
const body = unwrapDetails(payload)
|
||||
return Array.isArray(body?.data) ? body.data : []
|
||||
}
|
||||
|
||||
const loadAlertTrend = async (monthValue: string) => {
|
||||
chartLoading.value = true
|
||||
const days = buildMonthDays(monthValue)
|
||||
try {
|
||||
const { startTime, endTime } = getMonthRange(monthValue)
|
||||
const response = await fetchAlertTrend({
|
||||
granularity: 'day',
|
||||
start_time: startTime,
|
||||
end_time: endTime,
|
||||
})
|
||||
const countMap = new Map<string, number>()
|
||||
normalizeTrendItems(response).forEach((item: any) => {
|
||||
countMap.set(String(item.date), Number(item.count) || 0)
|
||||
})
|
||||
trendDates.value = days.map((day) => dayjs(day).format('MM/DD'))
|
||||
trendCounts.value = days.map((day) => countMap.get(day) || 0)
|
||||
} catch (error) {
|
||||
console.warn('告警趋势数据加载失败:', error)
|
||||
trendDates.value = days.map((day) => dayjs(day).format('MM/DD'))
|
||||
trendCounts.value = days.map(() => 0)
|
||||
} finally {
|
||||
chartLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理月份选择变更
|
||||
const handleChange = (value: string) => {
|
||||
month.value = value
|
||||
chartLoading.value = true
|
||||
// 模拟加载延迟
|
||||
setTimeout(() => {
|
||||
chartLoading.value = false
|
||||
}, 500)
|
||||
loadAlertTrend(value)
|
||||
}
|
||||
|
||||
// 处理统计卡片点击
|
||||
const handleCardClick = (cardTitle: string) => {
|
||||
const routeMap: any = {
|
||||
服务器及PC: '/ops/monitor/server',
|
||||
集群采集: '/ops/monitor/collector',
|
||||
数据库服务: '/ops/dc/database',
|
||||
网络设备: '/ops/monitor/network',
|
||||
警告: '/alert/tackle',
|
||||
工单: '/ops/ticket',
|
||||
@@ -489,30 +551,45 @@ const loadStatistics = async () => {
|
||||
loading.value = true
|
||||
|
||||
// 并行请求所有统计接口和待处理告警
|
||||
const [alertData, collectorData, ticketData, alertListData, reviewStatsData] = await Promise.allSettled([
|
||||
const [
|
||||
alertData,
|
||||
collectorData,
|
||||
ticketData,
|
||||
alertListData,
|
||||
reviewStatsData,
|
||||
networkTotalData,
|
||||
networkEnabledData,
|
||||
] = await Promise.allSettled([
|
||||
fetchAlertCount().catch((e) => ({ error: e, success: false })),
|
||||
fetchCollectorStatistics().catch((e) => ({ error: e, success: false })),
|
||||
fetchFeedbackTicketStatistics().catch((e) => ({ error: e, success: false })),
|
||||
fetchHistories({ page: 1, page_size: 5, status: 'pending' }).catch((e) => ({ error: e, success: false })),
|
||||
fetchReviewStats({ resource_type: 'all' }).catch((e) => ({ error: e, success: false })),
|
||||
fetchNetworkDeviceList({ page: 1, size: 1 }).catch((e) => ({ error: e, success: false })),
|
||||
fetchNetworkDeviceList({ page: 1, size: 1, enabled: true }).catch((e) => ({ error: e, success: false })),
|
||||
])
|
||||
|
||||
// 处理服务器及PC统计数据
|
||||
if (collectorData.status === 'fulfilled' && collectorData.value?.success !== false) {
|
||||
statistics.serverPc = {
|
||||
pending: collectorData.value?.details?.servers?.online || 0,
|
||||
total: collectorData.value?.details?.servers?.total || 0,
|
||||
}
|
||||
statistics.cluster = {
|
||||
pending: collectorData.value?.details?.collectors?.online || 0,
|
||||
total: collectorData.value?.details?.collectors?.total || 0,
|
||||
const resourceStats = unwrapDetails(collectorData.value)
|
||||
statistics.serverPc = normalizeCountPair(resourceStats.servers)
|
||||
statistics.database = normalizeCountPair(resourceStats.database_services)
|
||||
} else {
|
||||
console.warn('资源统计数据加载失败:', collectorData.status === 'rejected' ? collectorData.reason : collectorData.value?.error)
|
||||
}
|
||||
|
||||
if (
|
||||
networkTotalData.status === 'fulfilled' &&
|
||||
networkTotalData.value?.success !== false &&
|
||||
networkEnabledData.status === 'fulfilled' &&
|
||||
networkEnabledData.value?.success !== false
|
||||
) {
|
||||
statistics.network = {
|
||||
pending: collectorData.value?.details?.network_devices?.online || 0,
|
||||
total: collectorData.value?.details?.network_devices?.total || 0,
|
||||
pending: getPageTotal(networkEnabledData.value),
|
||||
total: getPageTotal(networkTotalData.value),
|
||||
}
|
||||
} else {
|
||||
console.warn('采集器统计数据加载失败:', collectorData.status === 'rejected' ? collectorData.reason : collectorData.value?.error)
|
||||
console.warn('网络设备统计数据加载失败')
|
||||
}
|
||||
|
||||
// 处理告警统计数据
|
||||
@@ -559,7 +636,7 @@ const loadStatistics = async () => {
|
||||
// 启动数字动画
|
||||
const values = [
|
||||
statistics.serverPc.pending,
|
||||
statistics.cluster.pending,
|
||||
statistics.database.pending,
|
||||
statistics.network.pending,
|
||||
statistics.alert.pending,
|
||||
statistics.ticket.pending,
|
||||
@@ -646,6 +723,7 @@ const formatDate = (date: string) => {
|
||||
|
||||
onMounted(() => {
|
||||
loadStatistics()
|
||||
loadAlertTrend(month.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -874,6 +952,13 @@ onMounted(() => {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.chart-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: var(--color-text-3);
|
||||
font-size: 14px;
|
||||
|
||||
Reference in New Issue
Block a user