feat: add disk monitor target editors
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
<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="10"
|
||||
@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-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="emit('confirm')">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
|
||||
import NotificationHttpEditor from './NotificationHttpEditor.vue'
|
||||
import NotificationPathEditor from './NotificationPathEditor.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'DiskMonitorTargetDialog'
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
modelValue: DiskMonitor.TargetItem
|
||||
title: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
'update:modelValue': [value: DiskMonitor.TargetItem]
|
||||
confirm: []
|
||||
}>()
|
||||
|
||||
const closeDialog = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
const handleWarningChange = (value: number | undefined) => {
|
||||
patchTarget({
|
||||
warningUsagePercent: Number(value || 0)
|
||||
})
|
||||
}
|
||||
|
||||
const handleAlarmChange = (value: number | undefined) => {
|
||||
patchTarget({
|
||||
alarmUsagePercent: Number(value || 0)
|
||||
})
|
||||
}
|
||||
|
||||
const patchTarget = (patch: Partial<DiskMonitor.TargetItem>) => {
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
...patch
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.target-form :deep(.el-form-item__content) {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.suffix-text {
|
||||
margin-left: 8px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div class="target-table-card">
|
||||
<div class="table-header">
|
||||
<div>
|
||||
<h3 class="table-title">监控目标</h3>
|
||||
<p class="table-description">维护需要监控的盘符与通知目标</p>
|
||||
</div>
|
||||
<el-button type="primary" @click="emit('add')">新增目标</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="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" @click="emit('edit', row, $index)">编辑</el-button>
|
||||
<el-button link type="danger" @click="emit('remove', $index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
|
||||
|
||||
defineOptions({
|
||||
name: 'DiskMonitorTargetTable'
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
rows: DiskMonitor.TargetItem[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
add: []
|
||||
edit: [row: DiskMonitor.TargetItem, index: number]
|
||||
remove: [index: number]
|
||||
}>()
|
||||
|
||||
const getStatusType = (status: DiskMonitor.MonitorStatus) => {
|
||||
if (status === 'NORMAL') return 'success'
|
||||
if (status === 'WARNING') return 'warning'
|
||||
if (status === 'ALARM') return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
const getStatusLabel = (status: DiskMonitor.MonitorStatus) => {
|
||||
if (status === 'NORMAL') return '正常'
|
||||
if (status === 'WARNING') return '预警'
|
||||
if (status === 'ALARM') return '告警'
|
||||
return '未知'
|
||||
}
|
||||
|
||||
const formatScanTime = (value?: string | null) => {
|
||||
if (!value) return '--'
|
||||
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
const formatUsedPercent = (value?: number | null) => {
|
||||
if (value === null || value === undefined) return '--'
|
||||
return `${value}%`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.target-table-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.table-title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 18px;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.table-description {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div class="notification-editor">
|
||||
<div class="editor-header">
|
||||
<span class="editor-title">HTTP 通知目标</span>
|
||||
<el-button type="primary" link @click="handleAdd">新增 HTTP 目标</el-button>
|
||||
</div>
|
||||
<div v-if="!props.modelValue.length" class="empty-text">暂无 HTTP 通知目标</div>
|
||||
<div v-else class="editor-list">
|
||||
<div v-for="(item, index) in props.modelValue" :key="index" class="editor-row">
|
||||
<el-input
|
||||
:model-value="item.name"
|
||||
placeholder="名称"
|
||||
@update:model-value="value => patchRow(index, 'name', String(value))"
|
||||
/>
|
||||
<el-input
|
||||
:model-value="item.url"
|
||||
placeholder="通知 URL"
|
||||
@update:model-value="value => patchRow(index, 'url', String(value))"
|
||||
/>
|
||||
<el-select
|
||||
:model-value="item.method"
|
||||
@update:model-value="value => patchRow(index, 'method', value as DiskMonitor.NotifyHttpTarget['method'])"
|
||||
>
|
||||
<el-option label="POST" value="POST" />
|
||||
</el-select>
|
||||
<el-input-number
|
||||
:model-value="item.timeoutMs"
|
||||
:min="100"
|
||||
:step="100"
|
||||
:controls="false"
|
||||
@update:model-value="handleTimeoutChange(index, $event)"
|
||||
/>
|
||||
<el-switch
|
||||
:model-value="item.enabled"
|
||||
@update:model-value="value => patchRow(index, 'enabled', Boolean(value))"
|
||||
/>
|
||||
<el-button type="danger" link @click="handleRemove(index)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
|
||||
import { createEmptyHttpTarget } from '../utils/form'
|
||||
|
||||
defineOptions({
|
||||
name: 'NotificationHttpEditor'
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: DiskMonitor.NotifyHttpTarget[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: DiskMonitor.NotifyHttpTarget[]]
|
||||
}>()
|
||||
|
||||
const updateList = (nextList: DiskMonitor.NotifyHttpTarget[]) => {
|
||||
emit('update:modelValue', nextList)
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
updateList([...props.modelValue, createEmptyHttpTarget()])
|
||||
}
|
||||
|
||||
const handleRemove = (index: number) => {
|
||||
updateList(props.modelValue.filter((_, rowIndex) => rowIndex !== index))
|
||||
}
|
||||
|
||||
const handleTimeoutChange = (index: number, value: number | undefined) => {
|
||||
patchRow(index, 'timeoutMs', Number(value || 0))
|
||||
}
|
||||
|
||||
const patchRow = <K extends keyof DiskMonitor.NotifyHttpTarget>(
|
||||
index: number,
|
||||
key: K,
|
||||
value: DiskMonitor.NotifyHttpTarget[K]
|
||||
) => {
|
||||
const nextList = props.modelValue.map((item, rowIndex) =>
|
||||
rowIndex === index
|
||||
? {
|
||||
...item,
|
||||
[key]: value
|
||||
}
|
||||
: item
|
||||
)
|
||||
updateList(nextList)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.notification-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.editor-title {
|
||||
font-size: 14px;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 13px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.editor-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.editor-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr) 100px 120px auto auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.editor-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="notification-editor">
|
||||
<div class="editor-header">
|
||||
<span class="editor-title">路径通知目标</span>
|
||||
<el-button type="primary" link @click="handleAdd">新增路径</el-button>
|
||||
</div>
|
||||
<div v-if="!props.modelValue.length" class="empty-text">暂无路径通知目标</div>
|
||||
<div v-else class="editor-list">
|
||||
<div v-for="(item, index) in props.modelValue" :key="index" class="editor-row">
|
||||
<el-input
|
||||
:model-value="item.name"
|
||||
placeholder="名称"
|
||||
@update:model-value="value => patchRow(index, 'name', String(value))"
|
||||
/>
|
||||
<el-input
|
||||
:model-value="item.path"
|
||||
placeholder="通知路径"
|
||||
@update:model-value="value => patchRow(index, 'path', String(value))"
|
||||
/>
|
||||
<el-switch
|
||||
:model-value="item.enabled"
|
||||
@update:model-value="value => patchRow(index, 'enabled', Boolean(value))"
|
||||
/>
|
||||
<el-button type="danger" link @click="handleRemove(index)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
|
||||
import { createEmptyPathTarget } from '../utils/form'
|
||||
|
||||
defineOptions({
|
||||
name: 'NotificationPathEditor'
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: DiskMonitor.NotifyPathTarget[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: DiskMonitor.NotifyPathTarget[]]
|
||||
}>()
|
||||
|
||||
const updateList = (nextList: DiskMonitor.NotifyPathTarget[]) => {
|
||||
emit('update:modelValue', nextList)
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
updateList([...props.modelValue, createEmptyPathTarget()])
|
||||
}
|
||||
|
||||
const handleRemove = (index: number) => {
|
||||
updateList(props.modelValue.filter((_, rowIndex) => rowIndex !== index))
|
||||
}
|
||||
|
||||
const patchRow = <K extends keyof DiskMonitor.NotifyPathTarget>(
|
||||
index: number,
|
||||
key: K,
|
||||
value: DiskMonitor.NotifyPathTarget[K]
|
||||
) => {
|
||||
const nextList = props.modelValue.map((item, rowIndex) =>
|
||||
rowIndex === index
|
||||
? {
|
||||
...item,
|
||||
[key]: value
|
||||
}
|
||||
: item
|
||||
)
|
||||
updateList(nextList)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.notification-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.editor-title {
|
||||
font-size: 14px;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 13px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.editor-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.editor-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) auto auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.editor-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -18,6 +18,15 @@
|
||||
@save="handleSave"
|
||||
@run="handleRun"
|
||||
/>
|
||||
|
||||
<DiskMonitorTargetTable :rows="targetList" @add="openAddTarget" @edit="openEditTarget" @remove="removeTarget" />
|
||||
|
||||
<DiskMonitorTargetDialog
|
||||
v-model:visible="targetDialogVisible"
|
||||
v-model="editingTarget"
|
||||
:title="editingTargetIndex >= 0 ? '编辑监控目标' : '新增监控目标'"
|
||||
@confirm="confirmTarget"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -34,7 +43,9 @@ import {
|
||||
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
|
||||
import DiskMonitorPolicyForm from './components/DiskMonitorPolicyForm.vue'
|
||||
import DiskMonitorSummary from './components/DiskMonitorSummary.vue'
|
||||
import { createDefaultPolicy, validatePolicy } from './utils/form'
|
||||
import DiskMonitorTargetDialog from './components/DiskMonitorTargetDialog.vue'
|
||||
import DiskMonitorTargetTable from './components/DiskMonitorTargetTable.vue'
|
||||
import { createDefaultPolicy, createEmptyTarget, validatePolicy, validateTarget } from './utils/form'
|
||||
|
||||
defineOptions({
|
||||
name: 'DiskMonitorPage'
|
||||
@@ -44,6 +55,9 @@ const router = useRouter()
|
||||
|
||||
const policyForm = ref<DiskMonitor.PolicyItem>(createDefaultPolicy())
|
||||
const targetList = ref<DiskMonitor.TargetItem[]>([])
|
||||
const targetDialogVisible = ref(false)
|
||||
const editingTargetIndex = ref(-1)
|
||||
const editingTarget = ref<DiskMonitor.TargetItem>(createEmptyTarget())
|
||||
const latestJob = ref<DiskMonitor.JobListItem | null>(null)
|
||||
const loading = reactive({
|
||||
init: false,
|
||||
@@ -57,6 +71,53 @@ const handleBack = async () => {
|
||||
await router.push('/systemMonitor')
|
||||
}
|
||||
|
||||
const cloneTarget = (target: DiskMonitor.TargetItem): DiskMonitor.TargetItem => ({
|
||||
...target,
|
||||
notifyPathList: target.notifyPathList.map(item => ({ ...item })),
|
||||
notifyHttpList: target.notifyHttpList.map(item => ({ ...item }))
|
||||
})
|
||||
|
||||
const openAddTarget = () => {
|
||||
editingTargetIndex.value = -1
|
||||
editingTarget.value = createEmptyTarget()
|
||||
targetDialogVisible.value = true
|
||||
}
|
||||
|
||||
const openEditTarget = (row: DiskMonitor.TargetItem, index: number) => {
|
||||
editingTargetIndex.value = index
|
||||
// 编辑时克隆当前行,避免未确认前直接污染列表数据
|
||||
editingTarget.value = cloneTarget(row)
|
||||
targetDialogVisible.value = true
|
||||
}
|
||||
|
||||
const confirmTarget = () => {
|
||||
// 提交前统一规范盘符并做去重、阈值关系校验
|
||||
const normalizedDriveLetter = editingTarget.value.driveLetter.trim().toUpperCase()
|
||||
const payload: DiskMonitor.TargetItem = {
|
||||
...editingTarget.value,
|
||||
driveLetter: normalizedDriveLetter
|
||||
}
|
||||
const exists = targetList.value
|
||||
.filter((_, index) => index !== editingTargetIndex.value)
|
||||
.map(item => item.driveLetter.trim().toUpperCase())
|
||||
const errorMessage = validateTarget(payload, exists)
|
||||
if (errorMessage) {
|
||||
ElMessage.warning(errorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
if (editingTargetIndex.value >= 0) {
|
||||
targetList.value.splice(editingTargetIndex.value, 1, payload)
|
||||
} else {
|
||||
targetList.value.push(payload)
|
||||
}
|
||||
targetDialogVisible.value = false
|
||||
}
|
||||
|
||||
const removeTarget = (index: number) => {
|
||||
targetList.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const loadPolicyDetail = async () => {
|
||||
const response = await getDiskMonitorPolicyDetail()
|
||||
const detail = response.data
|
||||
|
||||
Reference in New Issue
Block a user