style(diskMonitor): 统一磁盘监控页面样式规范
- 添加页面边距、表格样式和按钮样式约定到 AGENTS.md 文档 - 为任务详情抽屉组件添加卡片样式和表格结构优化 - 重构任务表格组件,增加搜索功能和列设置选项 - 移除独立的策略表单组件,整合到汇总页面 - 优化监控摘要组件的网格布局和样式 - 重新设计目标对话框的表单分组和禁用状态处理 - 统一所有组件使用 card、table-main 等标准类名 - 添加文件编码规范要求确保 UTF-8 编码一致性
This commit is contained in:
@@ -17,6 +17,9 @@
|
||||
- 外科手术式修改:只改与任务直接相关的文件和代码行,不重构无关模块,不调整无关格式或注释。
|
||||
- 保持现有风格:遵循仓库已有包结构、分层方式、命名和写法,不按个人偏好重写。
|
||||
- 只清理自己造成的问题:可以删除因本次修改而产生的未使用 `import`、变量或方法;不要删除仓库中原本就存在的死代码,除非用户明确要求。
|
||||
- 页面边距约定:业务页面根节点默认跟随布局主内容区 `el-main` 的 `15px` 边距,不再额外叠加页面级外边距;如需特殊边距,必须先有明确的视觉参照页面或业务原因。
|
||||
- 表格样式约定:业务表格优先复用仓库现有 `table-main`、`card`、`table-header` 结构,参照 `dictdata` 页面;表格卡片内部默认不再额外堆叠页面标题、说明文案或自定义装饰区,表头左侧用于主操作、次操作和危险批量操作,右侧用于刷新、列设置、搜索等工具按钮;不要在单页里重复自定义表格卡片边框、表头按钮布局和表头配色,除非有明确视觉参照或业务原因。
|
||||
- 按钮样式约定:业务页面按钮参照 `dictdata` 页面;表头主操作使用 `type="primary"`,表头次操作使用 `type="primary" plain`,危险批量操作使用 `type="danger" plain`,表格行内操作统一使用 `link` 风格并优先保持 `primary` 语义与图标一致性;弹窗底部保持“取消”默认按钮、“主确认”使用 `primary`,同级辅助执行按钮使用 `primary plain`。
|
||||
- 先定义验证方式:执行方案里要写清楚“改哪里、怎么判断改对了”;默认通过代码路径、配置一致性、界面影响范围和启动链路检查进行验证。
|
||||
- 除非用户明确要求,否则不执行 `npm run build`、`npm run build-w`、`electron-builder`、加密打包或其他重型构建命令;通常只做静态检查、必要的代码阅读和轻量验证。
|
||||
|
||||
@@ -35,6 +38,8 @@
|
||||
## 代码风格与命名规范
|
||||
前端格式化规则定义在 `frontend/.prettierrc`:4 空格缩进、单引号、不写分号、单行 120 字符、LF 换行。Lint 规则基于 Vue 3 与 TypeScript。
|
||||
|
||||
文件编码规范:所有新增或修改的源码、脚本、配置、文档统一使用 UTF-8 编码(无 BOM)和 LF 换行,不要保存为 GBK、ANSI 或其他本地编码,避免再次出现乱码。
|
||||
|
||||
请遵循现有命名方式:
|
||||
- 页面或路由目录通常使用 `index.vue`,例如 `views/home/`
|
||||
- 通用组件使用 PascalCase,例如 `HomeToolCard.vue`
|
||||
|
||||
@@ -7,31 +7,31 @@
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="refresh">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
{{ $t('tabs.refresh') }}
|
||||
{{ t('tabs.refresh') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="maximize">
|
||||
<el-icon><FullScreen /></el-icon>
|
||||
{{ $t('tabs.maximize') }}
|
||||
{{ t('tabs.maximize') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided @click="closeCurrentTab">
|
||||
<el-icon><Remove /></el-icon>
|
||||
{{ $t('tabs.closeCurrent') }}
|
||||
{{ t('tabs.closeCurrent') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="tabStore.closeTabsOnSide(route.fullPath, 'left')">
|
||||
<el-dropdown-item @click="tabStore.closeTabsOnSide(currentTabPath, 'left')">
|
||||
<el-icon><DArrowLeft /></el-icon>
|
||||
{{ $t('tabs.closeLeft') }}
|
||||
{{ t('tabs.closeLeft') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="tabStore.closeTabsOnSide(route.fullPath, 'right')">
|
||||
<el-dropdown-item @click="tabStore.closeTabsOnSide(currentTabPath, 'right')">
|
||||
<el-icon><DArrowRight /></el-icon>
|
||||
{{ $t('tabs.closeRight') }}
|
||||
{{ t('tabs.closeRight') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided @click="tabStore.closeMultipleTab(route.fullPath)">
|
||||
<el-dropdown-item divided @click="tabStore.closeMultipleTab(currentTabPath)">
|
||||
<el-icon><CircleClose /></el-icon>
|
||||
{{ $t('tabs.closeOther') }}
|
||||
{{ t('tabs.closeOther') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="closeAllTab">
|
||||
<el-icon><FolderDelete /></el-icon>
|
||||
{{ $t('tabs.closeAll') }}
|
||||
{{ t('tabs.closeAll') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
@@ -39,7 +39,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, nextTick } from 'vue'
|
||||
import { computed, inject, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { HOME_URL } from '@/config'
|
||||
import { useTabsStore } from '@/stores/modules/tabs'
|
||||
import { useGlobalStore } from '@/stores/modules/global'
|
||||
@@ -48,10 +49,20 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const tabStore = useTabsStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const keepAliveStore = useKeepAliveStore()
|
||||
|
||||
const currentTabPath = computed(() => {
|
||||
const parentPath = route.meta.parentPath as string | undefined
|
||||
return route.meta.hideTab ? parentPath || route.fullPath : route.fullPath
|
||||
})
|
||||
|
||||
const currentTabRoute = computed(() => {
|
||||
return router.getRoutes().find(item => item.path === currentTabPath.value)
|
||||
})
|
||||
|
||||
// refresh current page
|
||||
const refreshCurrentPage: Function = inject('refresh') as Function
|
||||
const refresh = () => {
|
||||
@@ -72,8 +83,8 @@ const maximize = () => {
|
||||
|
||||
// Close Current
|
||||
const closeCurrentTab = () => {
|
||||
if (route.meta.isAffix) return
|
||||
tabStore.removeTabs(route.fullPath)
|
||||
if (currentTabRoute.value?.meta.isAffix) return
|
||||
tabStore.removeTabs(currentTabPath.value)
|
||||
}
|
||||
|
||||
// Close All
|
||||
|
||||
@@ -26,12 +26,16 @@
|
||||
import Sortable from 'sortablejs'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { TabPaneName, TabsPaneContext } from 'element-plus'
|
||||
import type { TabPaneName, TabsPaneContext } from 'element-plus'
|
||||
import { HOME_URL } from '@/config'
|
||||
import { useGlobalStore } from '@/stores/modules/global'
|
||||
import { useTabsStore } from '@/stores/modules/tabs'
|
||||
import MoreButton from './components/MoreButton.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'LayoutTabs'
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const tabStore = useTabsStore()
|
||||
@@ -41,17 +45,41 @@ const tabsMenuValue = ref(route.fullPath)
|
||||
const tabsMenuList = computed(() => tabStore.tabsMenuList)
|
||||
const tabsIcon = computed(() => globalStore.tabsIcon)
|
||||
|
||||
const resolveCurrentTabPath = () => {
|
||||
const parentPath = route.meta.parentPath as string | undefined
|
||||
return route.meta.hideTab ? parentPath || route.fullPath : route.fullPath
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
tabsDrop()
|
||||
initTabs()
|
||||
})
|
||||
|
||||
const ensureParentTab = () => {
|
||||
const parentPath = resolveCurrentTabPath()
|
||||
if (!parentPath || tabStore.tabsMenuList.some(item => item.path === parentPath)) return
|
||||
|
||||
const parentRoute = router.getRoutes().find(item => item.path === parentPath && item.name)
|
||||
if (!parentRoute) return
|
||||
|
||||
// 直接打开隐藏子页时补齐父级主标签
|
||||
tabStore.addTabs({
|
||||
icon: parentRoute.meta.icon as string,
|
||||
title: parentRoute.meta.title as string,
|
||||
path: parentRoute.path,
|
||||
name: parentRoute.name as string,
|
||||
close: !parentRoute.meta.isAffix,
|
||||
isKeepAlive: parentRoute.meta.isKeepAlive as boolean
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
if (route.meta.isFull) return
|
||||
if (route.meta.hideTab) {
|
||||
tabsMenuValue.value = route.meta.parentPath as string
|
||||
ensureParentTab()
|
||||
tabsMenuValue.value = resolveCurrentTabPath()
|
||||
} else {
|
||||
tabsMenuValue.value = route.fullPath
|
||||
const tabsParams = {
|
||||
@@ -112,7 +140,7 @@ const tabClick = (tabItem: TabsPaneContext) => {
|
||||
}
|
||||
|
||||
const tabRemove = (fullPath: TabPaneName) => {
|
||||
tabStore.removeTabs(fullPath as string, fullPath == route.fullPath)
|
||||
tabStore.removeTabs(fullPath as string, fullPath === tabsMenuValue.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@ import { ElNotification } from 'element-plus'
|
||||
import { useUserStore } from '@/stores/modules/user'
|
||||
import { useAuthStore } from '@/stores/modules/auth'
|
||||
|
||||
// 引入 views 文件夹下所有 vue 文件
|
||||
const modules = import.meta.glob('@/views/**/*.vue')
|
||||
const VIEWS_ALIAS_PREFIX = '@/views'
|
||||
const VIEWS_SRC_PREFIX = '/src/views'
|
||||
const STATIC_ROUTE_NAMES = new Set([
|
||||
'layout',
|
||||
'login',
|
||||
@@ -14,6 +15,7 @@ const STATIC_ROUTE_NAMES = new Set([
|
||||
'tools',
|
||||
'toolWaveform',
|
||||
'toolMmsMapping',
|
||||
'toolAddData',
|
||||
'systemMonitor',
|
||||
'diskMonitor',
|
||||
'403',
|
||||
@@ -24,7 +26,7 @@ const STATIC_ROUTE_NAMES = new Set([
|
||||
let isInitializing = false
|
||||
|
||||
/**
|
||||
* 清除已有的动态路由
|
||||
* 清除已有的动态路由,避免重复注入。
|
||||
*/
|
||||
const clearDynamicRoutes = () => {
|
||||
const routes = router.getRoutes()
|
||||
@@ -36,13 +38,19 @@ const clearDynamicRoutes = () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据菜单 component 路径查找对应的页面模块。
|
||||
* 兼容两种仓库写法:
|
||||
* 1. /foo/bar.vue
|
||||
* 2. /foo/bar/index.vue
|
||||
* 统一菜单 component 配置格式,只允许映射到 views 目录。
|
||||
*/
|
||||
const resolveComponentModule = (path: string) => {
|
||||
let normalizedPath = path.trim()
|
||||
const normalizeComponentPath = (path: string) => {
|
||||
let normalizedPath = path.trim().replace(/\\/g, '/')
|
||||
|
||||
if (normalizedPath.startsWith(VIEWS_ALIAS_PREFIX)) {
|
||||
normalizedPath = normalizedPath.slice(VIEWS_ALIAS_PREFIX.length)
|
||||
} else if (normalizedPath.startsWith(VIEWS_SRC_PREFIX)) {
|
||||
normalizedPath = normalizedPath.slice(VIEWS_SRC_PREFIX.length)
|
||||
} else if (normalizedPath.startsWith('src/views')) {
|
||||
normalizedPath = normalizedPath.slice('src/views'.length)
|
||||
}
|
||||
|
||||
if (!normalizedPath.startsWith('/')) {
|
||||
normalizedPath = '/' + normalizedPath
|
||||
}
|
||||
@@ -53,13 +61,31 @@ const resolveComponentModule = (path: string) => {
|
||||
normalizedPath = normalizedPath.slice(0, -1)
|
||||
}
|
||||
|
||||
const candidatePaths = [`/src/views${normalizedPath}.vue`, `/src/views${normalizedPath}/index.vue`]
|
||||
return normalizedPath
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据菜单 component 路径查找对应页面模块。
|
||||
* 兼容菜单资源里常见的多种写法,避免因为前缀或 index 差异导致误判。
|
||||
*/
|
||||
const resolveComponentModule = (path: string) => {
|
||||
const normalizedPath = normalizeComponentPath(path)
|
||||
const viewPath = normalizedPath.endsWith('/index') ? normalizedPath.slice(0, -'/index'.length) : normalizedPath
|
||||
const candidatePaths = Array.from(
|
||||
new Set([
|
||||
`${VIEWS_ALIAS_PREFIX}${normalizedPath}.vue`,
|
||||
`${VIEWS_ALIAS_PREFIX}${normalizedPath}/index.vue`,
|
||||
`${VIEWS_ALIAS_PREFIX}${viewPath}/index.vue`,
|
||||
`${VIEWS_SRC_PREFIX}${normalizedPath}.vue`,
|
||||
`${VIEWS_SRC_PREFIX}${normalizedPath}/index.vue`,
|
||||
`${VIEWS_SRC_PREFIX}${viewPath}/index.vue`
|
||||
])
|
||||
)
|
||||
|
||||
for (const candidatePath of candidatePaths) {
|
||||
const moduleLoader = modules[candidatePath]
|
||||
if (moduleLoader) {
|
||||
if (candidatePath in modules) {
|
||||
return {
|
||||
moduleLoader,
|
||||
moduleLoader: modules[candidatePath],
|
||||
resolvedPath: candidatePath
|
||||
}
|
||||
}
|
||||
@@ -72,7 +98,7 @@ const resolveComponentModule = (path: string) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 初始化动态路由
|
||||
* 初始化动态路由。
|
||||
*/
|
||||
export const initDynamicRouter = async () => {
|
||||
if (isInitializing) return Promise.reject(new Error('Dynamic router initialization in progress'))
|
||||
@@ -83,15 +109,13 @@ export const initDynamicRouter = async () => {
|
||||
const unresolvedRoutes: Array<{ name?: string; path?: string; component?: string; candidates: string[] }> = []
|
||||
|
||||
try {
|
||||
// 1. 获取菜单列表 && 按钮权限列表
|
||||
await authStore.getAuthMenuList()
|
||||
await authStore.getAuthButtonList()
|
||||
|
||||
// 2. 判断当前用户有没有菜单权限
|
||||
if (!authStore.authMenuListGet.length) {
|
||||
ElNotification({
|
||||
title: '无权限访问',
|
||||
message: '当前账号无任何菜单权限,请联系系统管理员!',
|
||||
message: '当前账号无任何菜单权限,请联系系统管理员',
|
||||
type: 'warning',
|
||||
duration: 3000
|
||||
})
|
||||
@@ -102,21 +126,17 @@ export const initDynamicRouter = async () => {
|
||||
return Promise.reject('No permission')
|
||||
}
|
||||
|
||||
// 3. 清理之前的动态路由
|
||||
clearDynamicRoutes()
|
||||
|
||||
// 4. 添加动态路由
|
||||
for (const item of authStore.flatMenuListGet) {
|
||||
// 删除 children 避免冗余嵌套
|
||||
if (item.children) delete item.children
|
||||
|
||||
// 处理组件映射
|
||||
// 动态菜单组件必须先映射成真实页面模块,否则 addRoute 后会直接落到 404。
|
||||
if (item.component && typeof item.component === 'string') {
|
||||
const { moduleLoader, resolvedPath } = resolveComponentModule(item.component)
|
||||
if (moduleLoader) {
|
||||
item.component = moduleLoader
|
||||
} else {
|
||||
// 动态路由组件一旦解析失败,对应菜单会落入 404,这里必须打印清楚候选路径。
|
||||
unresolvedRoutes.push({
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
@@ -127,7 +147,6 @@ export const initDynamicRouter = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 类型守卫:确保满足 RouteRecordRaw 接口要求
|
||||
if (
|
||||
typeof item.path === 'string' &&
|
||||
(typeof item.component === 'function' || typeof item.redirect === 'string')
|
||||
@@ -147,7 +166,6 @@ export const initDynamicRouter = async () => {
|
||||
console.error('[dynamic-router] unresolved route components', unresolvedRoutes)
|
||||
}
|
||||
} catch (error) {
|
||||
// 当按钮 || 菜单请求出错时,重定向到登陆页
|
||||
userStore.setAccessToken('')
|
||||
userStore.setRefreshToken('')
|
||||
userStore.setExp(0)
|
||||
|
||||
@@ -54,11 +54,19 @@ export const staticRouter: RouteRecordRaw[] = [
|
||||
path: '/tools/mmsMapping',
|
||||
name: 'toolMmsMapping',
|
||||
alias: ['/tools/mmsmapping', '/tools/mms-mapping'],
|
||||
component: () => import('@/views/tools/mmsmapping/index.vue'),
|
||||
component: () => import('@/views/tools/mmsMapping/index.vue'),
|
||||
meta: {
|
||||
title: 'MMS 映射'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/tools/addData',
|
||||
name: 'toolAddData',
|
||||
component: () => import('@/views/tools/addData/index.vue'),
|
||||
meta: {
|
||||
title: '模拟数据'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
name: '403',
|
||||
@@ -96,6 +104,10 @@ export const staticRouter: RouteRecordRaw[] = [
|
||||
name: 'diskMonitor',
|
||||
component: () => import('@/views/systemMonitor/diskMonitor/index.vue'),
|
||||
meta: {
|
||||
// 磁盘监控页复用系统监控主标签
|
||||
activeMenu: '/systemMonitor',
|
||||
hideTab: true,
|
||||
parentPath: '/systemMonitor',
|
||||
title: '磁盘监控'
|
||||
}
|
||||
},
|
||||
|
||||
2
frontend/src/types/global.d.ts
vendored
2
frontend/src/types/global.d.ts
vendored
@@ -12,6 +12,8 @@ declare namespace Menu {
|
||||
icon: string;
|
||||
title: string;
|
||||
activeMenu?: string;
|
||||
hideTab?: boolean;
|
||||
parentPath?: string;
|
||||
isLink?: string;
|
||||
isHide: boolean;
|
||||
isFull: boolean;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
class="job-detail-drawer"
|
||||
:model-value="props.visible"
|
||||
size="70%"
|
||||
title="任务详情"
|
||||
@@ -8,107 +9,128 @@
|
||||
>
|
||||
<div v-loading="props.loading" class="job-detail">
|
||||
<template v-if="props.detail">
|
||||
<div class="meta-grid">
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">任务编号</span>
|
||||
<span class="meta-value">{{ props.detail.job.jobNo }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">任务来源</span>
|
||||
<span class="meta-value">{{ getSourceLabel(props.detail.job.jobSource) }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">任务状态</span>
|
||||
<span class="meta-value">{{ getJobStatusLabel(props.detail.job.jobStatus) }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">预警数量</span>
|
||||
<span class="meta-value">{{ props.detail.job.warningCount }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">告警数量</span>
|
||||
<span class="meta-value">{{ props.detail.job.alarmCount }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">开始时间</span>
|
||||
<span class="meta-value">{{ formatTime(props.detail.job.startedAt) }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">结束时间</span>
|
||||
<span class="meta-value">{{ formatTime(props.detail.job.finishedAt) }}</span>
|
||||
<section class="card job-detail-meta-card">
|
||||
<div class="meta-grid">
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">任务编号</span>
|
||||
<span class="meta-value">{{ props.detail.job.jobNo }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">任务来源</span>
|
||||
<span class="meta-value">{{ getSourceLabel(props.detail.job.jobSource) }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">任务状态</span>
|
||||
<span class="meta-value">{{ getJobStatusLabel(props.detail.job.jobStatus) }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">预警数量</span>
|
||||
<span class="meta-value">{{ props.detail.job.warningCount }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">告警数量</span>
|
||||
<span class="meta-value">{{ props.detail.job.alarmCount }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">开始时间</span>
|
||||
<span class="meta-value">{{ formatTime(props.detail.job.startedAt) }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">结束时间</span>
|
||||
<span class="meta-value">{{ formatTime(props.detail.job.finishedAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="job-detail-table-sections">
|
||||
<section class="table-section">
|
||||
<h4 class="section-title">盘符结果</h4>
|
||||
<div class="table-main card job-detail-table-card">
|
||||
<el-table :data="props.detail.results" class="job-detail-el-table" border stripe height="100%">
|
||||
<el-table-column prop="driveLetter" label="盘符" min-width="100" align="center" />
|
||||
<el-table-column label="使用率" min-width="110" align="center">
|
||||
<template #default="{ row }">{{ row.usedPercent }}%</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="当前状态" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getMonitorStatusType(row.currentStatus)" effect="light">
|
||||
{{ getMonitorStatusLabel(row.currentStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上次状态" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getMonitorStatusType(row.previousStatus)" effect="light">
|
||||
{{ getMonitorStatusLabel(row.previousStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态变化" min-width="100" align="center">
|
||||
<template #default="{ row }">{{ formatBoolean(row.statusChanged) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否通知" min-width="100" align="center">
|
||||
<template #default="{ row }">{{ formatBoolean(row.shouldNotify) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="通知原因" min-width="130" align="center">
|
||||
<template #default="{ row }">{{ getNotifyReasonLabel(row.notifyReason) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="扫描时间" min-width="170" align="center">
|
||||
<template #default="{ row }">{{ formatTime(row.scanTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="message"
|
||||
label="说明"
|
||||
min-width="180"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
</el-table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="table-section">
|
||||
<h4 class="section-title">通知日志</h4>
|
||||
<div class="table-main card job-detail-table-card">
|
||||
<el-table :data="props.detail.notifyLogs" class="job-detail-el-table" border stripe height="100%">
|
||||
<el-table-column prop="driveLetter" label="盘符" min-width="100" align="center" />
|
||||
<el-table-column label="通知级别" min-width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getNotifyLevelType(row.notifyLevel)" effect="light">
|
||||
{{ getNotifyLevelLabel(row.notifyLevel) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="通知通道" min-width="120" align="center">
|
||||
<template #default="{ row }">{{ getChannelTypeLabel(row.channelType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="channelTarget"
|
||||
label="通知目标"
|
||||
min-width="220"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="发送状态" min-width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getSendStatusType(row.sendStatus)" effect="light">
|
||||
{{ getSendStatusLabel(row.sendStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="responseMessage"
|
||||
label="响应信息"
|
||||
min-width="220"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="发送时间" min-width="170" align="center">
|
||||
<template #default="{ row }">{{ formatTime(row.sentAt) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="table-section">
|
||||
<h4 class="section-title">盘符结果</h4>
|
||||
<el-table :data="props.detail.results" border stripe>
|
||||
<el-table-column prop="driveLetter" label="盘符" min-width="100" />
|
||||
<el-table-column label="使用率" min-width="110">
|
||||
<template #default="{ row }">{{ row.usedPercent }}%</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="当前状态" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getMonitorStatusType(row.currentStatus)" effect="light">
|
||||
{{ getMonitorStatusLabel(row.currentStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上次状态" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getMonitorStatusType(row.previousStatus)" effect="light">
|
||||
{{ getMonitorStatusLabel(row.previousStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态变化" min-width="100">
|
||||
<template #default="{ row }">{{ formatBoolean(row.statusChanged) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否通知" min-width="100">
|
||||
<template #default="{ row }">{{ formatBoolean(row.shouldNotify) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="通知原因" min-width="130">
|
||||
<template #default="{ row }">{{ getNotifyReasonLabel(row.notifyReason) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="扫描时间" min-width="170">
|
||||
<template #default="{ row }">{{ formatTime(row.scanTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="message" label="说明" min-width="180" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<section class="table-section">
|
||||
<h4 class="section-title">通知日志</h4>
|
||||
<el-table :data="props.detail.notifyLogs" border stripe>
|
||||
<el-table-column prop="driveLetter" label="盘符" min-width="100" />
|
||||
<el-table-column label="通知级别" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getNotifyLevelType(row.notifyLevel)" effect="light">
|
||||
{{ getNotifyLevelLabel(row.notifyLevel) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="通知通道" min-width="120">
|
||||
<template #default="{ row }">{{ getChannelTypeLabel(row.channelType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="channelTarget" label="通知目标" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column label="发送状态" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getSendStatusType(row.sendStatus)" effect="light">
|
||||
{{ getSendStatusLabel(row.sendStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="responseMessage"
|
||||
label="响应信息"
|
||||
min-width="220"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="发送时间" min-width="170">
|
||||
<template #default="{ row }">{{ formatTime(row.sentAt) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
</template>
|
||||
<el-empty v-else description="暂无详情数据" />
|
||||
</div>
|
||||
@@ -204,45 +226,121 @@ const formatTime = (value?: string | null) => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.job-detail-drawer :deep(.el-drawer__body) {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.job-detail {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.job-detail-meta-card {
|
||||
flex: none;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.job-detail-table-sections {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.job-detail-table-card {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.job-detail-el-table {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.job-detail-el-table :deep(.el-table__inner-wrapper) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.job-detail-el-table :deep(.el-table__body-wrapper) {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.job-detail-el-table :deep(.el-scrollbar),
|
||||
.job-detail-el-table :deep(.el-scrollbar__wrap),
|
||||
.job-detail-el-table :deep(.el-scrollbar__view) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.job-detail-el-table :deep(.el-table__empty-block) {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.job-detail-el-table :deep(.el-table__empty-text) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px 18px;
|
||||
padding: 14px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #f9fafb;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 14px 16px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: #6b7280;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
color: #111827;
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 500;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #111827;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.meta-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.meta-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,49 +1,102 @@
|
||||
<template>
|
||||
<div class="job-table-card">
|
||||
<div class="table-main card disk-monitor-table-card">
|
||||
<div class="table-header">
|
||||
<div>
|
||||
<h3 class="table-title">最近任务</h3>
|
||||
<p class="table-description">展示最近 10 条磁盘监控任务执行记录</p>
|
||||
<div class="header-button-ri table-tools">
|
||||
<el-button circle :icon="Refresh" :loading="loading" @click="emit('refresh')" />
|
||||
<el-popover trigger="click" placement="bottom-end" :width="220">
|
||||
<div class="column-setting-panel">
|
||||
<el-checkbox v-for="column in columnOptions" :key="column.key" v-model="column.visible">
|
||||
{{ column.label }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-button circle :icon="Operation" />
|
||||
</template>
|
||||
</el-popover>
|
||||
<el-button circle :icon="Search" @click="showSearch = !showSearch" />
|
||||
</div>
|
||||
<el-button :loading="loading" @click="emit('refresh')">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="rows" border stripe>
|
||||
<el-table-column prop="jobNo" label="任务编号" min-width="180" />
|
||||
<el-table-column label="来源" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ getSourceLabel(row.jobSource) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开始时间" min-width="170">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.startedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结束时间" min-width="170">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.finishedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.jobStatus)" effect="light">
|
||||
{{ getStatusLabel(row.jobStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="warningCount" label="预警数量" min-width="100" />
|
||||
<el-table-column prop="alarmCount" label="告警数量" min-width="100" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="emit('detail', row)">查看详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-show="showSearch" class="table-search">
|
||||
<el-form label-width="80px" class="disk-monitor-search-form">
|
||||
<div class="search-grid">
|
||||
<el-form-item label="任务编号">
|
||||
<el-input v-model="searchForm.jobNo" clearable placeholder="请输入任务编号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="任务来源">
|
||||
<el-select v-model="searchForm.jobSource" clearable placeholder="请选择来源">
|
||||
<el-option label="应用启动" value="APP_START" />
|
||||
<el-option label="定时任务" value="DAILY_SCHEDULE" />
|
||||
<el-option label="手动触发" value="MANUAL" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务状态">
|
||||
<el-select v-model="searchForm.jobStatus" clearable placeholder="请选择状态">
|
||||
<el-option label="成功" value="SUCCESS" />
|
||||
<el-option label="部分成功" value="PARTIAL_SUCCESS" />
|
||||
<el-option label="失败" value="FAILED" />
|
||||
<el-option label="运行中" value="RUNNING" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="operation">
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="disk-monitor-table-body">
|
||||
<el-table v-loading="loading" class="disk-monitor-el-table" :data="filteredRows" border stripe height="100%">
|
||||
<el-table-column v-if="isColumnVisible('jobNo')" prop="jobNo" label="任务编号" min-width="180" align="center" />
|
||||
<el-table-column v-if="isColumnVisible('jobSource')" label="来源" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ getSourceLabel(row.jobSource) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="isColumnVisible('startedAt')" label="开始时间" min-width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.startedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="isColumnVisible('finishedAt')" label="结束时间" min-width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.finishedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="isColumnVisible('jobStatus')" label="状态" min-width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.jobStatus)" effect="light">
|
||||
{{ getStatusLabel(row.jobStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('warningCount')"
|
||||
prop="warningCount"
|
||||
label="预警数量"
|
||||
min-width="100"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('alarmCount')"
|
||||
prop="alarmCount"
|
||||
label="告警数量"
|
||||
min-width="100"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :icon="View" @click="emit('detail', row)">查看详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { Operation, Refresh, Search, View } from '@element-plus/icons-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
|
||||
|
||||
@@ -51,7 +104,7 @@ defineOptions({
|
||||
name: 'DiskMonitorJobTable'
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
rows: DiskMonitor.JobListItem[]
|
||||
loading: boolean
|
||||
}>()
|
||||
@@ -61,6 +114,54 @@ const emit = defineEmits<{
|
||||
detail: [row: DiskMonitor.JobListItem]
|
||||
}>()
|
||||
|
||||
type JobColumnKey = 'jobNo' | 'jobSource' | 'startedAt' | 'finishedAt' | 'jobStatus' | 'warningCount' | 'alarmCount'
|
||||
|
||||
const showSearch = ref(false)
|
||||
const searchForm = reactive({
|
||||
jobNo: '',
|
||||
jobSource: '',
|
||||
jobStatus: ''
|
||||
})
|
||||
const columnOptions = reactive<{ key: JobColumnKey; label: string; visible: boolean }[]>([
|
||||
{ key: 'jobNo', label: '任务编号', visible: true },
|
||||
{ key: 'jobSource', label: '来源', visible: true },
|
||||
{ key: 'startedAt', label: '开始时间', visible: true },
|
||||
{ key: 'finishedAt', label: '结束时间', visible: true },
|
||||
{ key: 'jobStatus', label: '状态', visible: true },
|
||||
{ key: 'warningCount', label: '预警数量', visible: true },
|
||||
{ key: 'alarmCount', label: '告警数量', visible: true }
|
||||
])
|
||||
|
||||
const includesKeyword = (value?: string | number | null, keyword?: string) => {
|
||||
const normalizedKeyword = String(keyword || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
if (!normalizedKeyword) return true
|
||||
return String(value ?? '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.includes(normalizedKeyword)
|
||||
}
|
||||
|
||||
const isColumnVisible = (key: JobColumnKey) => {
|
||||
return columnOptions.find(column => column.key === key)?.visible ?? true
|
||||
}
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
return props.rows.filter(row => {
|
||||
const matchesJobNo = includesKeyword(row.jobNo, searchForm.jobNo)
|
||||
const matchesSource = !searchForm.jobSource || row.jobSource === searchForm.jobSource
|
||||
const matchesStatus = !searchForm.jobStatus || row.jobStatus === searchForm.jobStatus
|
||||
return matchesJobNo && matchesSource && matchesStatus
|
||||
})
|
||||
})
|
||||
|
||||
const resetSearch = () => {
|
||||
searchForm.jobNo = ''
|
||||
searchForm.jobSource = ''
|
||||
searchForm.jobStatus = ''
|
||||
}
|
||||
|
||||
const getSourceLabel = (source: DiskMonitor.JobSource) => {
|
||||
if (source === 'APP_START') return '应用启动'
|
||||
if (source === 'DAILY_SCHEDULE') return '定时任务'
|
||||
@@ -88,32 +189,66 @@ const formatTime = (value?: string | null) => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.job-table-card {
|
||||
.disk-monitor-table-card {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flow-root;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.table-tools {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 18px;
|
||||
color: #111827;
|
||||
.column-setting-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.table-description {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
.search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0 12px;
|
||||
}
|
||||
|
||||
.disk-monitor-table-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.disk-monitor-el-table {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.disk-monitor-table-body :deep(.el-table__inner-wrapper) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.table-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-button-ri {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.search-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
<template>
|
||||
<div class="policy-form-card">
|
||||
<div class="policy-header">
|
||||
<div>
|
||||
<h3 class="policy-title">全局策略</h3>
|
||||
<p class="policy-description">配置监控总开关、启动监控与每日统一时间。</p>
|
||||
</div>
|
||||
<div class="policy-actions">
|
||||
<el-button :loading="runLoading" :disabled="disabled" @click="emit('run')">立即执行监控</el-button>
|
||||
<el-button type="primary" :loading="saveLoading" :disabled="disabled" @click="emit('save')">保存配置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
class="policy-alert"
|
||||
title="通知规则"
|
||||
description="预警按状态变化通知,告警每次命中都通知"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
|
||||
<el-form label-width="130px" class="policy-form">
|
||||
<el-form-item label="启用监控">
|
||||
<el-switch
|
||||
:model-value="modelValue.monitorEnabled"
|
||||
:disabled="disabled"
|
||||
@update:model-value="handleMonitorEnabledChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="启动即监控">
|
||||
<el-switch
|
||||
:model-value="modelValue.runOnAppStart"
|
||||
:disabled="disabled"
|
||||
@update:model-value="handleRunOnAppStartChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="每日执行时间">
|
||||
<el-time-picker
|
||||
:model-value="modelValue.dailyRunTime"
|
||||
:disabled="disabled"
|
||||
format="HH:mm:ss"
|
||||
value-format="HH:mm:ss"
|
||||
placeholder="选择时间"
|
||||
@update:model-value="value => updatePolicy('dailyRunTime', typeof value === 'string' ? value : '')"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
|
||||
|
||||
defineOptions({
|
||||
name: 'DiskMonitorPolicyForm'
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: DiskMonitor.PolicyItem
|
||||
disabled?: boolean
|
||||
saveLoading?: boolean
|
||||
runLoading?: boolean
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
saveLoading: false,
|
||||
runLoading: false
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: DiskMonitor.PolicyItem]
|
||||
save: []
|
||||
run: []
|
||||
}>()
|
||||
|
||||
const handleMonitorEnabledChange = (value: string | number | boolean) => {
|
||||
updatePolicy('monitorEnabled', Boolean(value))
|
||||
}
|
||||
|
||||
const handleRunOnAppStartChange = (value: string | number | boolean) => {
|
||||
updatePolicy('runOnAppStart', Boolean(value))
|
||||
}
|
||||
|
||||
const updatePolicy = <K extends keyof DiskMonitor.PolicyItem>(key: K, value: DiskMonitor.PolicyItem[K]) => {
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
[key]: value
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.policy-form-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.policy-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.policy-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.policy-description {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.policy-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.policy-alert {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.policy-form :deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.policy-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.policy-actions {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,32 +1,34 @@
|
||||
<template>
|
||||
<div class="disk-monitor-summary">
|
||||
<div class="summary-card">
|
||||
<div class="summary-label">监控状态</div>
|
||||
<div class="summary-value">{{ policy.monitorEnabled ? '已启用' : '已停用' }}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-label">启动即监控</div>
|
||||
<div class="summary-value">{{ policy.runOnAppStart ? '是' : '否' }}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-label">每日执行时间</div>
|
||||
<div class="summary-value">{{ policy.dailyRunTime || '--' }}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-label">监控盘符数量</div>
|
||||
<div class="summary-value">{{ monitorTargetCount }}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-label">当前告警盘符</div>
|
||||
<div class="summary-value">{{ alarmCount }}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-label">最近执行时间</div>
|
||||
<div class="summary-value">{{ latestRunTime }}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-label">最近执行状态</div>
|
||||
<div class="summary-value">{{ latestJobStatus }}</div>
|
||||
<div class="card disk-monitor-summary-card">
|
||||
<div class="summary-grid">
|
||||
<div class="summary-item">
|
||||
<div class="summary-label">监控状态</div>
|
||||
<div class="summary-value">{{ policy.monitorEnabled ? '已启用' : '已停用' }}</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<div class="summary-label">启动即监控</div>
|
||||
<div class="summary-value">{{ policy.runOnAppStart ? '是' : '否' }}</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<div class="summary-label">每日执行时间</div>
|
||||
<div class="summary-value">{{ policy.dailyRunTime || '--' }}</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<div class="summary-label">监控目标数量</div>
|
||||
<div class="summary-value">{{ monitorTargetCount }}</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<div class="summary-label">当前告警目标</div>
|
||||
<div class="summary-value">{{ alarmCount }}</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<div class="summary-label">最近执行时间</div>
|
||||
<div class="summary-value">{{ latestRunTime }}</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<div class="summary-label">最近执行状态</div>
|
||||
<div class="summary-value">{{ latestJobStatus }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -67,40 +69,47 @@ const latestJobStatus = computed(() => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.disk-monitor-summary {
|
||||
.disk-monitor-summary-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
.summary-item {
|
||||
padding: 14px 16px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
background: var(--el-fill-color-lighter);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
color: var(--el-text-color-primary);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.disk-monitor-summary {
|
||||
.summary-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.disk-monitor-summary {
|
||||
.summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,78 +1,104 @@
|
||||
<template>
|
||||
<el-dialog :model-value="props.visible" :title="props.title" width="880px" @close="closeDialog">
|
||||
<el-form label-width="120px" class="target-form">
|
||||
<el-form-item label="盘符">
|
||||
<el-input
|
||||
:model-value="props.modelValue.driveLetter"
|
||||
placeholder="例如 C:"
|
||||
maxlength="2"
|
||||
@update:model-value="value => patchTarget({ driveLetter: String(value).trim().toUpperCase() })"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用监控">
|
||||
<el-switch
|
||||
:model-value="props.modelValue.monitorEnabled"
|
||||
@update:model-value="value => patchTarget({ monitorEnabled: Boolean(value) })"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="预警阈值">
|
||||
<el-input-number
|
||||
:model-value="props.modelValue.warningUsagePercent"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:controls="false"
|
||||
@update:model-value="handleWarningChange($event)"
|
||||
/>
|
||||
<span class="suffix-text">%</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="告警阈值">
|
||||
<el-input-number
|
||||
:model-value="props.modelValue.alarmUsagePercent"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:controls="false"
|
||||
@update:model-value="handleAlarmChange($event)"
|
||||
/>
|
||||
<span class="suffix-text">%</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="路径通知">
|
||||
<el-switch
|
||||
:model-value="props.modelValue.notifyPathEnabled"
|
||||
@update:model-value="value => patchTarget({ notifyPathEnabled: Boolean(value) })"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="props.modelValue.notifyPathEnabled" label="路径通知配置">
|
||||
<NotificationPathEditor
|
||||
:model-value="props.modelValue.notifyPathList"
|
||||
@update:model-value="value => patchTarget({ notifyPathList: value })"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="HTTP 通知">
|
||||
<el-switch
|
||||
:model-value="props.modelValue.notifyHttpEnabled"
|
||||
@update:model-value="value => patchTarget({ notifyHttpEnabled: Boolean(value) })"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="props.modelValue.notifyHttpEnabled" label="HTTP 通知配置">
|
||||
<NotificationHttpEditor
|
||||
:model-value="props.modelValue.notifyHttpList"
|
||||
@update:model-value="value => patchTarget({ notifyHttpList: value })"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:model-value="props.modelValue.remark"
|
||||
placeholder="可选"
|
||||
@update:model-value="value => patchTarget({ remark: String(value) })"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-dialog
|
||||
class="disk-monitor-dialog"
|
||||
:model-value="props.visible"
|
||||
:title="props.title"
|
||||
:show-close="!props.disabled"
|
||||
:close-on-click-modal="!props.disabled"
|
||||
:close-on-press-escape="!props.disabled"
|
||||
width="880px"
|
||||
@close="closeDialog"
|
||||
>
|
||||
<el-form :disabled="props.disabled" label-width="120px" class="target-form">
|
||||
<div class="dialog-section">
|
||||
<div class="section-title">监控基础信息</div>
|
||||
<el-form-item label="盘符">
|
||||
<el-input
|
||||
:model-value="props.modelValue.driveLetter"
|
||||
placeholder="例如 C:"
|
||||
maxlength="2"
|
||||
@update:model-value="value => patchTarget({ driveLetter: String(value).trim().toUpperCase() })"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用监控">
|
||||
<el-switch
|
||||
:model-value="props.modelValue.monitorEnabled"
|
||||
@update:model-value="value => patchTarget({ monitorEnabled: Boolean(value) })"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="预警阈值">
|
||||
<div class="threshold-field">
|
||||
<el-input-number
|
||||
:model-value="props.modelValue.warningUsagePercent"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:controls="false"
|
||||
@update:model-value="handleWarningChange($event)"
|
||||
/>
|
||||
<span class="suffix-text">%</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="告警阈值">
|
||||
<div class="threshold-field">
|
||||
<el-input-number
|
||||
:model-value="props.modelValue.alarmUsagePercent"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:controls="false"
|
||||
@update:model-value="handleAlarmChange($event)"
|
||||
/>
|
||||
<span class="suffix-text">%</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="dialog-section">
|
||||
<div class="section-title">通知配置</div>
|
||||
<el-form-item label="路径通知">
|
||||
<el-switch
|
||||
:model-value="props.modelValue.notifyPathEnabled"
|
||||
@update:model-value="value => patchTarget({ notifyPathEnabled: Boolean(value) })"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="props.modelValue.notifyPathEnabled" class="form-item--editor" label="路径通知配置">
|
||||
<NotificationPathEditor
|
||||
:model-value="props.modelValue.notifyPathList"
|
||||
@update:model-value="value => patchTarget({ notifyPathList: value })"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="HTTP 通知">
|
||||
<el-switch
|
||||
:model-value="props.modelValue.notifyHttpEnabled"
|
||||
@update:model-value="value => patchTarget({ notifyHttpEnabled: Boolean(value) })"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="props.modelValue.notifyHttpEnabled" class="form-item--editor" label="HTTP 通知配置">
|
||||
<NotificationHttpEditor
|
||||
:model-value="props.modelValue.notifyHttpList"
|
||||
@update:model-value="value => patchTarget({ notifyHttpList: value })"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="dialog-section">
|
||||
<div class="section-title">补充说明</div>
|
||||
<el-form-item class="form-item--remark" label="备注">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:model-value="props.modelValue.remark"
|
||||
placeholder="可选"
|
||||
@update:model-value="value => patchTarget({ remark: String(value) })"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="emit('confirm')">确定</el-button>
|
||||
<el-button :disabled="props.disabled" @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" :loading="props.confirmLoading" :disabled="props.disabled" @click="emit('confirm')">
|
||||
确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -87,11 +113,19 @@ defineOptions({
|
||||
name: 'DiskMonitorTargetDialog'
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
modelValue: DiskMonitor.TargetItem
|
||||
title: string
|
||||
}>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
visible: boolean
|
||||
modelValue: DiskMonitor.TargetItem
|
||||
title: string
|
||||
disabled?: boolean
|
||||
confirmLoading?: boolean
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
confirmLoading: false
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
@@ -124,13 +158,65 @@ const patchTarget = (patch: Partial<DiskMonitor.TargetItem>) => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.disk-monitor-dialog :deep(.el-dialog__body) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.target-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.dialog-section {
|
||||
padding: 16px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.target-form :deep(.el-form-item__content) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.target-form :deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.target-form :deep(.el-form-item:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.threshold-field {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.threshold-field :deep(.el-input-number) {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.suffix-text {
|
||||
margin-left: 8px;
|
||||
color: #6b7280;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.form-item--editor :deep(.el-form-item__content),
|
||||
.form-item--remark :deep(.el-form-item__content) {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.form-item--editor :deep(.el-form-item__content > *),
|
||||
.form-item--remark :deep(.el-form-item__content > *) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
|
||||
@@ -1,62 +1,119 @@
|
||||
<template>
|
||||
<div class="target-table-card">
|
||||
<div class="table-main card disk-monitor-table-card">
|
||||
<div class="table-header">
|
||||
<div>
|
||||
<h3 class="table-title">监控目标</h3>
|
||||
<p class="table-description">维护需要监控的盘符与通知目标</p>
|
||||
<div class="header-button-lf">
|
||||
<el-button type="primary" :icon="CirclePlus" :disabled="props.disabled" @click="emit('add')">
|
||||
新增目标
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="header-button-ri table-tools">
|
||||
<el-button circle :icon="Refresh" :disabled="props.disabled" @click="emit('refresh')" />
|
||||
<el-popover trigger="click" placement="bottom-end" :width="220">
|
||||
<div class="column-setting-panel">
|
||||
<el-checkbox v-for="column in columnOptions" :key="column.key" v-model="column.visible">
|
||||
{{ column.label }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-button circle :icon="Operation" />
|
||||
</template>
|
||||
</el-popover>
|
||||
<el-button circle :icon="Search" @click="showSearch = !showSearch" />
|
||||
</div>
|
||||
<el-button type="primary" :disabled="props.disabled" @click="emit('add')">新增目标</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="props.rows" border stripe>
|
||||
<el-table-column prop="driveLetter" label="盘符" min-width="90" />
|
||||
<el-table-column label="是否监控" min-width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.monitorEnabled ? '是' : '否' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预警使用率" min-width="110">
|
||||
<template #default="{ row }">
|
||||
{{ row.warningUsagePercent }}%
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="告警使用率" min-width="110">
|
||||
<template #default="{ row }">
|
||||
{{ row.alarmUsagePercent }}%
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="当前状态" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.lastStatus)" effect="light">
|
||||
{{ getStatusLabel(row.lastStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近扫描时间" min-width="170">
|
||||
<template #default="{ row }">
|
||||
{{ formatScanTime(row.lastScanTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近使用率" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatUsedPercent(row.lastUsedPercent) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="{ row, $index }">
|
||||
<el-button link type="primary" :disabled="props.disabled" @click="emit('edit', row, $index)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" :disabled="props.disabled" @click="emit('remove', $index)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-show="showSearch" class="table-search">
|
||||
<el-form label-width="80px" class="disk-monitor-search-form">
|
||||
<div class="search-grid">
|
||||
<el-form-item label="盘符">
|
||||
<el-input v-model="searchForm.driveLetter" clearable placeholder="请输入盘符" />
|
||||
</el-form-item>
|
||||
<el-form-item label="当前状态">
|
||||
<el-select v-model="searchForm.lastStatus" clearable placeholder="请选择状态">
|
||||
<el-option label="正常" value="NORMAL" />
|
||||
<el-option label="预警" value="WARNING" />
|
||||
<el-option label="告警" value="ALARM" />
|
||||
<el-option label="未知" value="UNKNOWN" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="operation">
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="disk-monitor-table-body">
|
||||
<el-table class="disk-monitor-el-table" :data="filteredRows" border stripe height="100%">
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('driveLetter')"
|
||||
prop="driveLetter"
|
||||
label="盘符"
|
||||
min-width="90"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column v-if="isColumnVisible('monitorEnabled')" label="是否监控" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.monitorEnabled ? '是' : '否' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="isColumnVisible('warningUsagePercent')" label="预警使用率" min-width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.warningUsagePercent }}%
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="isColumnVisible('alarmUsagePercent')" label="告警使用率" min-width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.alarmUsagePercent }}%
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="isColumnVisible('lastStatus')" label="当前状态" min-width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.lastStatus)" effect="light">
|
||||
{{ getStatusLabel(row.lastStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="isColumnVisible('lastScanTime')" label="最近扫描时间" min-width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatScanTime(row.lastScanTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="isColumnVisible('lastUsedPercent')" label="最近使用率" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatUsedPercent(row.lastUsedPercent) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row, $index }">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
:icon="EditPen"
|
||||
:disabled="props.disabled"
|
||||
@click="emit('edit', row, $index)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
:icon="Delete"
|
||||
:disabled="props.disabled"
|
||||
@click="emit('remove', $index)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { CirclePlus, Delete, EditPen, Operation, Refresh, Search } from '@element-plus/icons-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
|
||||
|
||||
@@ -78,8 +135,63 @@ const emit = defineEmits<{
|
||||
add: []
|
||||
edit: [row: DiskMonitor.TargetItem, index: number]
|
||||
remove: [index: number]
|
||||
refresh: []
|
||||
}>()
|
||||
|
||||
type TargetColumnKey =
|
||||
| 'driveLetter'
|
||||
| 'monitorEnabled'
|
||||
| 'warningUsagePercent'
|
||||
| 'alarmUsagePercent'
|
||||
| 'lastStatus'
|
||||
| 'lastScanTime'
|
||||
| 'lastUsedPercent'
|
||||
|
||||
const showSearch = ref(false)
|
||||
const searchForm = reactive({
|
||||
driveLetter: '',
|
||||
lastStatus: ''
|
||||
})
|
||||
const columnOptions = reactive<{ key: TargetColumnKey; label: string; visible: boolean }[]>([
|
||||
{ key: 'driveLetter', label: '盘符', visible: true },
|
||||
{ key: 'monitorEnabled', label: '是否监控', visible: true },
|
||||
{ key: 'warningUsagePercent', label: '预警使用率', visible: true },
|
||||
{ key: 'alarmUsagePercent', label: '告警使用率', visible: true },
|
||||
{ key: 'lastStatus', label: '当前状态', visible: true },
|
||||
{ key: 'lastScanTime', label: '最近扫描时间', visible: true },
|
||||
{ key: 'lastUsedPercent', label: '最近使用率', visible: true }
|
||||
])
|
||||
|
||||
const includesKeyword = (value?: string | number | null, keyword?: string) => {
|
||||
const normalizedKeyword = String(keyword || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
if (!normalizedKeyword) return true
|
||||
return String(value ?? '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.includes(normalizedKeyword)
|
||||
}
|
||||
|
||||
const isColumnVisible = (key: TargetColumnKey) => {
|
||||
return columnOptions.find(column => column.key === key)?.visible ?? true
|
||||
}
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
return props.rows.filter(row => {
|
||||
const matchesDriveLetter = includesKeyword(row.driveLetter, searchForm.driveLetter)
|
||||
const selectedStatus = searchForm.lastStatus
|
||||
const actualStatus = row.lastStatus || 'UNKNOWN'
|
||||
const matchesStatus = !selectedStatus || actualStatus === selectedStatus
|
||||
return matchesDriveLetter && matchesStatus
|
||||
})
|
||||
})
|
||||
|
||||
const resetSearch = () => {
|
||||
searchForm.driveLetter = ''
|
||||
searchForm.lastStatus = ''
|
||||
}
|
||||
|
||||
const getStatusType = (status: DiskMonitor.MonitorStatus) => {
|
||||
if (status === 'NORMAL') return 'success'
|
||||
if (status === 'WARNING') return 'warning'
|
||||
@@ -106,32 +218,67 @@ const formatUsedPercent = (value?: number | null) => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.target-table-card {
|
||||
.disk-monitor-table-card {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flow-root;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.table-tools {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 18px;
|
||||
color: #111827;
|
||||
.column-setting-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.table-description {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
.search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0 12px;
|
||||
}
|
||||
|
||||
.disk-monitor-table-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.disk-monitor-el-table {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.disk-monitor-table-body :deep(.el-table__inner-wrapper) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.table-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-button-lf,
|
||||
.header-button-ri {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.search-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="notification-editor">
|
||||
<div class="editor-header">
|
||||
<span class="editor-title">HTTP 通知目标</span>
|
||||
<el-button type="primary" link @click="handleAdd">新增 HTTP 目标</el-button>
|
||||
<el-button type="primary" plain size="small" :icon="CirclePlus" @click="handleAdd">新增 HTTP 目标</el-button>
|
||||
</div>
|
||||
<div v-if="!props.modelValue.length" class="empty-text">暂无 HTTP 通知目标</div>
|
||||
<div v-else class="editor-list">
|
||||
@@ -34,13 +34,14 @@
|
||||
:model-value="item.enabled"
|
||||
@update:model-value="value => patchRow(index, 'enabled', Boolean(value))"
|
||||
/>
|
||||
<el-button type="danger" link @click="handleRemove(index)">删除</el-button>
|
||||
<el-button type="primary" link :icon="Delete" @click="handleRemove(index)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CirclePlus, Delete } from '@element-plus/icons-vue'
|
||||
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
|
||||
import { createEmptyHttpTarget } from '../utils/form'
|
||||
|
||||
@@ -95,22 +96,34 @@ const patchRow = <K extends keyof DiskMonitor.NotifyHttpTarget>(
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.editor-title {
|
||||
font-size: 14px;
|
||||
color: #111827;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
padding: 14px 16px;
|
||||
font-size: 13px;
|
||||
color: #9ca3af;
|
||||
color: var(--el-text-color-secondary);
|
||||
background: var(--el-fill-color-lighter);
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.editor-list {
|
||||
@@ -124,6 +137,15 @@ const patchRow = <K extends keyof DiskMonitor.NotifyHttpTarget>(
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr) 100px 120px auto auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.editor-row :deep(.el-input),
|
||||
.editor-row :deep(.el-select),
|
||||
.editor-row :deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="notification-editor">
|
||||
<div class="editor-header">
|
||||
<span class="editor-title">路径通知目标</span>
|
||||
<el-button type="primary" link @click="handleAdd">新增路径</el-button>
|
||||
<el-button type="primary" plain size="small" :icon="CirclePlus" @click="handleAdd">新增路径</el-button>
|
||||
</div>
|
||||
<div v-if="!props.modelValue.length" class="empty-text">暂无路径通知目标</div>
|
||||
<div v-else class="editor-list">
|
||||
@@ -21,13 +21,14 @@
|
||||
:model-value="item.enabled"
|
||||
@update:model-value="value => patchRow(index, 'enabled', Boolean(value))"
|
||||
/>
|
||||
<el-button type="danger" link @click="handleRemove(index)">删除</el-button>
|
||||
<el-button type="primary" link :icon="Delete" @click="handleRemove(index)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CirclePlus, Delete } from '@element-plus/icons-vue'
|
||||
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
|
||||
import { createEmptyPathTarget } from '../utils/form'
|
||||
|
||||
@@ -77,22 +78,34 @@ const patchRow = <K extends keyof DiskMonitor.NotifyPathTarget>(
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.editor-title {
|
||||
font-size: 14px;
|
||||
color: #111827;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
padding: 14px 16px;
|
||||
font-size: 13px;
|
||||
color: #9ca3af;
|
||||
color: var(--el-text-color-secondary);
|
||||
background: var(--el-fill-color-lighter);
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.editor-list {
|
||||
@@ -106,6 +119,13 @@ const patchRow = <K extends keyof DiskMonitor.NotifyPathTarget>(
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) auto auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.editor-row :deep(.el-input) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
@@ -1,40 +1,61 @@
|
||||
<template>
|
||||
<div v-loading="loading.init" class="table-box disk-monitor-page">
|
||||
<div class="page-header">
|
||||
<div class="header-main">
|
||||
<el-button class="back-button" link type="primary" @click="handleBack">返回系统监控</el-button>
|
||||
<h2 class="page-title">磁盘监控</h2>
|
||||
<p class="page-description">查看当前策略状态并维护全局执行配置。</p>
|
||||
<div class="disk-monitor-summary-section">
|
||||
<DiskMonitorSummary :policy="policyForm" :targets="targetList" :latest-job="latestJob" />
|
||||
|
||||
<div class="disk-monitor-actions">
|
||||
<el-button type="primary" plain :icon="Setting" :disabled="formBusy" @click="openPolicyDialog">
|
||||
全局策略
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DiskMonitorSummary :policy="policyForm" :targets="targetList" :latest-job="latestJob" />
|
||||
|
||||
<DiskMonitorPolicyForm
|
||||
v-model="policyForm"
|
||||
<DiskMonitorPolicyDialog
|
||||
v-model:visible="policyDialogVisible"
|
||||
v-model="editingPolicy"
|
||||
:disabled="formBusy"
|
||||
:save-loading="loading.save"
|
||||
:run-loading="loading.run"
|
||||
@save="handleSave"
|
||||
@confirm="confirmPolicy"
|
||||
@run="handleRun"
|
||||
/>
|
||||
|
||||
<DiskMonitorTargetTable
|
||||
:rows="targetList"
|
||||
:disabled="formBusy"
|
||||
@add="openAddTarget"
|
||||
@edit="openEditTarget"
|
||||
@remove="removeTarget"
|
||||
/>
|
||||
|
||||
<DiskMonitorTargetDialog
|
||||
v-model:visible="targetDialogVisible"
|
||||
v-model="editingTarget"
|
||||
:title="editingTargetIndex >= 0 ? '编辑监控目标' : '新增监控目标'"
|
||||
:disabled="formBusy"
|
||||
:confirm-loading="loading.save"
|
||||
@confirm="confirmTarget"
|
||||
/>
|
||||
|
||||
<DiskMonitorJobTable :rows="jobList" :loading="loading.jobs" @refresh="loadJobList" @detail="openJobDetail" />
|
||||
<section class="disk-monitor-tabs-card">
|
||||
<el-tabs v-model="activeTab" class="disk-monitor-tabs">
|
||||
<el-tab-pane label="监控记录" name="jobs">
|
||||
<div class="disk-monitor-tab-panel">
|
||||
<DiskMonitorJobTable
|
||||
:rows="jobList"
|
||||
:loading="loading.jobs"
|
||||
@refresh="loadJobList"
|
||||
@detail="openJobDetail"
|
||||
/>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="监测目标" name="targets">
|
||||
<div class="disk-monitor-tab-panel">
|
||||
<DiskMonitorTargetTable
|
||||
:rows="targetList"
|
||||
:disabled="formBusy"
|
||||
@add="openAddTarget"
|
||||
@edit="openEditTarget"
|
||||
@remove="removeTarget"
|
||||
@refresh="loadPageData"
|
||||
/>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</section>
|
||||
|
||||
<DiskMonitorJobDetailDrawer
|
||||
:visible="jobDetailVisible"
|
||||
:detail="jobDetail"
|
||||
@@ -47,7 +68,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Setting } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDiskMonitorJobDetail,
|
||||
getDiskMonitorJobList,
|
||||
@@ -58,7 +79,7 @@ import {
|
||||
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
|
||||
import DiskMonitorJobDetailDrawer from './components/DiskMonitorJobDetailDrawer.vue'
|
||||
import DiskMonitorJobTable from './components/DiskMonitorJobTable.vue'
|
||||
import DiskMonitorPolicyForm from './components/DiskMonitorPolicyForm.vue'
|
||||
import DiskMonitorPolicyDialog from './components/DiskMonitorPolicyDialog.vue'
|
||||
import DiskMonitorSummary from './components/DiskMonitorSummary.vue'
|
||||
import DiskMonitorTargetDialog from './components/DiskMonitorTargetDialog.vue'
|
||||
import DiskMonitorTargetTable from './components/DiskMonitorTargetTable.vue'
|
||||
@@ -76,9 +97,11 @@ defineOptions({
|
||||
name: 'DiskMonitorPage'
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
type DiskMonitorTab = 'targets' | 'jobs'
|
||||
|
||||
const policyForm = ref<DiskMonitor.PolicyItem>(createDefaultPolicy())
|
||||
const policyDialogVisible = ref(false)
|
||||
const editingPolicy = ref<DiskMonitor.PolicyItem>(createDefaultPolicy())
|
||||
const targetList = ref<DiskMonitor.TargetItem[]>([])
|
||||
const targetDialogVisible = ref(false)
|
||||
const editingTargetIndex = ref(-1)
|
||||
@@ -89,6 +112,7 @@ const jobDetailVisible = ref(false)
|
||||
const jobDetail = ref<DiskMonitor.JobDetailData | null>(null)
|
||||
const detailLoading = ref(false)
|
||||
const jobDetailRequestSeq = ref(0)
|
||||
const activeTab = ref<DiskMonitorTab>('jobs')
|
||||
const loading = reactive({
|
||||
init: false,
|
||||
save: false,
|
||||
@@ -107,9 +131,10 @@ const getTimeValue = (value?: string | null) => {
|
||||
return Number.isNaN(timestamp) ? 0 : timestamp
|
||||
}
|
||||
|
||||
const handleBack = async () => {
|
||||
await router.push('/systemMonitor')
|
||||
}
|
||||
const clonePolicy = (policy: DiskMonitor.PolicyItem): DiskMonitor.PolicyItem => ({
|
||||
...createDefaultPolicy(),
|
||||
...policy
|
||||
})
|
||||
|
||||
const cloneTarget = (target: DiskMonitor.TargetItem): DiskMonitor.TargetItem => {
|
||||
const normalized = normalizeTargetItem(target)
|
||||
@@ -120,6 +145,12 @@ const cloneTarget = (target: DiskMonitor.TargetItem): DiskMonitor.TargetItem =>
|
||||
}
|
||||
}
|
||||
|
||||
const openPolicyDialog = () => {
|
||||
// 打开弹窗时复制当前策略,避免取消时污染页面已加载状态。
|
||||
editingPolicy.value = clonePolicy(policyForm.value)
|
||||
policyDialogVisible.value = true
|
||||
}
|
||||
|
||||
const openAddTarget = () => {
|
||||
editingTargetIndex.value = -1
|
||||
editingTarget.value = createEmptyTarget()
|
||||
@@ -128,13 +159,14 @@ const openAddTarget = () => {
|
||||
|
||||
const openEditTarget = (row: DiskMonitor.TargetItem, index: number) => {
|
||||
editingTargetIndex.value = index
|
||||
// 编辑时克隆当前行,避免未确认前直接污染列表数据
|
||||
// 编辑时克隆当前行,避免未确认前直接改动表格数据。
|
||||
editingTarget.value = cloneTarget(normalizeTargetItem(row))
|
||||
targetDialogVisible.value = true
|
||||
}
|
||||
|
||||
const confirmTarget = () => {
|
||||
// 提交前统一规范盘符并做去重、阈值与通知配置校验
|
||||
if (formBusy.value) return
|
||||
|
||||
const normalizedDriveLetter = editingTarget.value.driveLetter.trim().toUpperCase()
|
||||
const payload: DiskMonitor.TargetItem = {
|
||||
...normalizeTargetItem(editingTarget.value),
|
||||
@@ -156,16 +188,25 @@ const confirmTarget = () => {
|
||||
return
|
||||
}
|
||||
|
||||
const nextTargetList = targetList.value.map(item => cloneTarget(item))
|
||||
if (editingTargetIndex.value >= 0) {
|
||||
targetList.value.splice(editingTargetIndex.value, 1, payload)
|
||||
nextTargetList.splice(editingTargetIndex.value, 1, payload)
|
||||
} else {
|
||||
targetList.value.push(payload)
|
||||
nextTargetList.push(payload)
|
||||
}
|
||||
|
||||
// 目标配置先保留在页面本地状态里,统一通过“全局策略”弹窗中的保存入口提交。
|
||||
targetList.value = nextTargetList.map(item => cloneTarget(item))
|
||||
targetDialogVisible.value = false
|
||||
}
|
||||
|
||||
const removeTarget = (index: number) => {
|
||||
targetList.value.splice(index, 1)
|
||||
if (formBusy.value) return
|
||||
|
||||
// 删除目标时仅更新本地暂存列表,避免样式调整顺带改成“操作即落库”。
|
||||
targetList.value = targetList.value
|
||||
.filter((_, currentIndex) => currentIndex !== index)
|
||||
.map(item => cloneTarget(item))
|
||||
}
|
||||
|
||||
const loadPolicyDetail = async () => {
|
||||
@@ -177,14 +218,14 @@ const loadPolicyDetail = async () => {
|
||||
...createDefaultPolicy(),
|
||||
...(detail.policy || {})
|
||||
}
|
||||
// 后端列表字段允许为空,这里统一归一化为数组,避免编辑器和克隆流程出现空引用
|
||||
// 后端列表字段允许为空,这里统一归一化为数组,避免编辑器和克隆流程出现空引用。
|
||||
targetList.value = (detail.targets || []).map(item => normalizeTargetItem(item))
|
||||
}
|
||||
|
||||
const loadJobList = async () => {
|
||||
loading.jobs = true
|
||||
try {
|
||||
// 统一拉取最近任务列表,并按 startedAt 倒序确保摘要展示真实最新任务
|
||||
// 统一按 startedAt 倒序拉取任务,确保顶部摘要展示最新一条记录。
|
||||
const response = await getDiskMonitorJobList({
|
||||
pageNum: 1,
|
||||
pageSize: 100,
|
||||
@@ -198,7 +239,6 @@ const loadJobList = async () => {
|
||||
const sortedRecords = [...records].sort((a, b) => {
|
||||
return getTimeValue(b.startedAt) - getTimeValue(a.startedAt)
|
||||
})
|
||||
// 前端保留 startedAt 的兜底排序,同时通过显式排序参数要求后端返回真正的最新任务。
|
||||
jobList.value = sortedRecords.slice(0, 10)
|
||||
latestJob.value = jobList.value[0] || null
|
||||
} finally {
|
||||
@@ -209,7 +249,7 @@ const loadJobList = async () => {
|
||||
const handleJobDetailVisibleChange = (visible: boolean) => {
|
||||
jobDetailVisible.value = visible
|
||||
if (!visible) {
|
||||
// 抽屉关闭时让旧请求全部失效,避免回写已关闭的详情状态
|
||||
// 抽屉关闭时使旧请求失效,避免晚到数据回写已关闭的详情面板。
|
||||
jobDetailRequestSeq.value += 1
|
||||
detailLoading.value = false
|
||||
jobDetail.value = null
|
||||
@@ -230,7 +270,7 @@ const openJobDetail = async (row: DiskMonitor.JobListItem) => {
|
||||
detailLoading.value = true
|
||||
|
||||
try {
|
||||
// 仅允许最新一次详情请求回写,避免快速切换任务导致脏数据覆盖
|
||||
// 仅允许最后一次详情请求回写,避免快速切换任务时详情串台。
|
||||
const response = await getDiskMonitorJobDetail(jobId)
|
||||
if (currentSeq !== jobDetailRequestSeq.value || !jobDetailVisible.value) return
|
||||
jobDetail.value = response.data || null
|
||||
@@ -244,58 +284,85 @@ const openJobDetail = async (row: DiskMonitor.JobListItem) => {
|
||||
const loadPageData = async () => {
|
||||
loading.init = true
|
||||
try {
|
||||
// 页面刷新入口单一化:统一并行加载策略与最近任务
|
||||
// 页面刷新统一并行拉取策略与任务列表,减少顶部和 tab 区的状态抖动。
|
||||
await Promise.all([loadPolicyDetail(), loadJobList()])
|
||||
} finally {
|
||||
loading.init = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (formBusy.value) return
|
||||
|
||||
const errorMessage = validatePolicy(policyForm.value)
|
||||
const persistPolicyAndTargets = async (
|
||||
policy: DiskMonitor.PolicyItem,
|
||||
targets: DiskMonitor.TargetItem[] = targetList.value,
|
||||
successMessage: string | null = '配置保存成功'
|
||||
) => {
|
||||
const errorMessage = validatePolicy(policy)
|
||||
if (errorMessage) {
|
||||
ElMessage.warning(errorMessage)
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
const normalizedTargets = targetList.value.map(item => ({
|
||||
const normalizedPolicy = clonePolicy(policy)
|
||||
const normalizedTargets = targets.map(item => ({
|
||||
...normalizeTargetItem(item),
|
||||
driveLetter: item.driveLetter.trim().toUpperCase()
|
||||
}))
|
||||
const targetListErrorMessage = validateTargetList(normalizedTargets)
|
||||
if (targetListErrorMessage) {
|
||||
ElMessage.warning(targetListErrorMessage)
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
loading.save = true
|
||||
try {
|
||||
// 整页保存前再次规范化所有盘符配置,避免历史脏数据绕过弹窗校验链路。
|
||||
targetList.value = normalizedTargets
|
||||
await saveDiskMonitorPolicy({
|
||||
policy: policyForm.value,
|
||||
policy: normalizedPolicy,
|
||||
targets: normalizedTargets
|
||||
})
|
||||
ElMessage.success('配置保存成功')
|
||||
// 保存完成后重新拉取数据,避免本地状态与服务端策略偏差
|
||||
await loadPageData()
|
||||
// 仅在服务端保存成功后回写本地状态,避免页面误以为已持久化。
|
||||
policyForm.value = normalizedPolicy
|
||||
targetList.value = normalizedTargets.map(item => cloneTarget(item))
|
||||
if (successMessage) {
|
||||
ElMessage.success(successMessage)
|
||||
}
|
||||
try {
|
||||
await loadPageData()
|
||||
} catch {
|
||||
// 保存已成功时保留当前状态,后续刷新失败交由全局请求错误处理。
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
} finally {
|
||||
loading.save = false
|
||||
}
|
||||
}
|
||||
|
||||
const confirmPolicy = async () => {
|
||||
if (formBusy.value) return
|
||||
|
||||
const saved = await persistPolicyAndTargets(clonePolicy(editingPolicy.value))
|
||||
if (saved) {
|
||||
policyDialogVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRun = async () => {
|
||||
if (formBusy.value) return
|
||||
|
||||
if (policyDialogVisible.value) {
|
||||
// 弹窗内执行监控前先落当前策略草稿,避免运行的还是上一次已保存配置。
|
||||
const saved = await persistPolicyAndTargets(clonePolicy(editingPolicy.value), targetList.value, null)
|
||||
if (!saved) return
|
||||
policyDialogVisible.value = false
|
||||
}
|
||||
|
||||
loading.run = true
|
||||
try {
|
||||
await runDiskMonitorJob({
|
||||
jobSource: 'MANUAL'
|
||||
})
|
||||
ElMessage.success('监控任务已启动')
|
||||
// 手动触发任务后通过页面统一刷新流更新摘要和任务列表
|
||||
await loadPageData()
|
||||
} finally {
|
||||
loading.run = false
|
||||
@@ -312,35 +379,90 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
// 页面根节点跟随主内容区边距,不再额外叠加页面级外边距。
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
.disk-monitor-summary-section {
|
||||
position: relative;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.header-main {
|
||||
.disk-monitor-summary-section :deep(.disk-monitor-summary-card) {
|
||||
padding-bottom: 36px;
|
||||
}
|
||||
|
||||
.disk-monitor-actions {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: calc(50% + 44px);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.disk-monitor-tabs-card {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.disk-monitor-tabs {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
align-self: flex-start;
|
||||
padding-left: 0;
|
||||
.disk-monitor-tabs :deep(.el-tabs__header) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
color: #111827;
|
||||
.disk-monitor-tabs :deep(.el-tabs__nav-wrap::after) {
|
||||
background-color: var(--el-border-color-light);
|
||||
}
|
||||
|
||||
.page-description {
|
||||
margin: 0;
|
||||
.disk-monitor-tabs :deep(.el-tabs__content) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.disk-monitor-tabs :deep(.el-tab-pane) {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.disk-monitor-tabs :deep(.el-tabs__item) {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.disk-monitor-tab-panel {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.disk-monitor-tabs :deep(.el-tabs__nav) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.disk-monitor-tabs :deep(.el-tabs__item) {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
<div class="tool-name">MMS 映射</div>
|
||||
<div class="tool-text">进入 MMS 映射页面,后续可继续补充映射配置和预览能力。</div>
|
||||
</button>
|
||||
|
||||
<button class="tool-item" type="button" @click="handleNavigate('/tools/addData')">
|
||||
<div class="tool-name">addData</div>
|
||||
<div class="tool-text">进入 addData 页面壳子,后续在此扩展补录数据能力和业务交互。</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user