Files
CN_Tool_client/docs/superpowers/plans/2026-04-22-disk-monitor-implementation-plan.md
2026-04-23 11:09:06 +08:00

989 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Disk Monitor Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在当前仓库内完成磁盘监控页面、前端 API 契约、数据库 SQL 交付文件和手工验证入口,为后端接入完整磁盘监控能力提供可直接联调的前端实现基础。
**Architecture:** 以前端单页容器 `frontend/src/views/systemMonitor/diskMonitor/index.vue` 负责编排状态、加载配置、保存配置、触发手动执行和展示历史结果;页面拆分为摘要卡片、全局策略表单、盘符编辑器、通知编辑器、任务历史与详情抽屉。后端按已确认的规格提供 `/disk-monitor/**` 接口,本仓库额外产出一份 `doc/系统磁盘监控数据库设计.sql` 作为数据库交付物。该计划不在当前仓库内实现真实磁盘扫描、定时器和通知发送逻辑,只实现页面、契约和 SQL 文件。
**Tech Stack:** Vue 3 `<script setup>`, TypeScript, Element Plus, Axios 封装 `frontend/src/api/index.ts`, Vue Router, ESLint, `vue-tsc`, MySQL SQL 文件
---
## File Map
- Modify: `D:\Work\SourceCode\CN_Tool_client\frontend\src\routers\modules\staticRouter.ts`
Purpose: 增加 `systemMonitor``diskMonitor` 的静态路由兜底,保证 `/#/systemMonitor/diskMonitor` 可直接访问。此文件当前已有用户改动,执行前必须先读 diff 并只做外科式追加。
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\api\system\diskMonitor\interface\index.ts`
Purpose: 定义磁盘监控页面所需的策略、盘符、通知目标、任务列表、任务详情等前端类型。
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\api\system\diskMonitor\index.ts`
Purpose: 封装 `/disk-monitor/policy/detail``/disk-monitor/policy/save``/disk-monitor/job/run``/disk-monitor/job/list``/disk-monitor/job/{id}/detail``/disk-monitor/notify/test` 接口。
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\utils\form.ts`
Purpose: 维护默认表单、空盘符模板、空通知目标模板和同步校验函数。
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorSummary.vue`
Purpose: 展示监控总开关、执行时间、最近任务、盘符数量、告警数量等摘要信息。
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorPolicyForm.vue`
Purpose: 编辑全局策略,展示固定的通知规则说明,并暴露保存/立即执行操作。
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\NotificationPathEditor.vue`
Purpose: 编辑单个盘符下的本地目录/网络路径通知目标数组。
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\NotificationHttpEditor.vue`
Purpose: 编辑单个盘符下的 HTTP 回调目标数组。
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorTargetDialog.vue`
Purpose: 盘符新增/编辑弹窗,内含阈值字段和两种通知编辑器。
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorTargetTable.vue`
Purpose: 展示盘符列表并提供新增、编辑、删除入口。
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorJobTable.vue`
Purpose: 展示最近任务列表,并暴露刷新和查看详情事件。
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorJobDetailDrawer.vue`
Purpose: 展示某次任务下的盘符结果和通知日志。
- Modify: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue`
Purpose: 用页面级状态编排替换占位内容,连接所有组件和 API。
- Create: `D:\Work\SourceCode\CN_Tool_client\doc\系统磁盘监控数据库设计.sql`
Purpose: 交付五张表的建表 SQL内容与已批准规格保持一致。
## Task 1: Add Route Fallback, API Contracts, And SQL Artifact
**Files:**
- Modify: `D:\Work\SourceCode\CN_Tool_client\frontend\src\routers\modules\staticRouter.ts`
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\api\system\diskMonitor\interface\index.ts`
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\api\system\diskMonitor\index.ts`
- Create: `D:\Work\SourceCode\CN_Tool_client\doc\系统磁盘监控数据库设计.sql`
- [ ] **Step 1: Review the existing router diff before touching the file**
Run:
```powershell
git diff -- D:\Work\SourceCode\CN_Tool_client\frontend\src\routers\modules\staticRouter.ts
```
Expected: 只看当前已有改动,确认后续只追加磁盘监控路由,不覆盖用户其他改动。
- [ ] **Step 2: Create the disk monitor API interface namespace**
Create `D:\Work\SourceCode\CN_Tool_client\frontend\src\api\system\diskMonitor\interface\index.ts` with:
```ts
import type { ReqPage, ResPage } from '@/api/interface'
export namespace DiskMonitor {
export type MonitorStatus = 'UNKNOWN' | 'NORMAL' | 'WARNING' | 'ALARM'
export type NotifyMode = 'STATUS_CHANGE' | 'EVERY_TIME'
export type JobSource = 'APP_START' | 'DAILY_SCHEDULE' | 'MANUAL'
export type JobStatus = 'RUNNING' | 'SUCCESS' | 'PARTIAL_SUCCESS' | 'FAILED'
export type NotifyLevel = 'WARNING' | 'ALARM' | 'RECOVER'
export type NotifyChannelType = 'PATH' | 'HTTP'
export type NotifySendStatus = 'SUCCESS' | 'FAILED'
export interface NotifyPathTarget {
path: string
name: string
enabled: boolean
}
export interface NotifyHttpTarget {
url: string
name: string
method: 'POST'
timeoutMs: number
enabled: boolean
}
export interface PolicyItem {
id?: number
policyName: string
monitorEnabled: boolean
runOnAppStart: boolean
dailyRunTime: string
warningNotifyMode: NotifyMode
alarmNotifyMode: NotifyMode
lastJobId?: number | null
remark: string
}
export interface TargetItem {
id?: number
policyId?: number
driveLetter: string
monitorEnabled: boolean
warningUsagePercent: number
alarmUsagePercent: number
notifyPathEnabled: boolean
notifyPathList: NotifyPathTarget[]
notifyHttpEnabled: boolean
notifyHttpList: NotifyHttpTarget[]
lastStatus: MonitorStatus
lastScanTime?: string | null
lastUsedPercent?: number | null
remark: string
}
export interface PolicyDetailData {
policy: PolicyItem
targets: TargetItem[]
}
export interface SavePolicyParams {
policy: PolicyItem
targets: TargetItem[]
}
export interface RunJobParams {
jobSource: 'MANUAL'
}
export interface RunJobResult {
jobId: number
jobNo: string
}
export interface JobListParams extends ReqPage {}
export interface JobListItem {
id: number
jobNo: string
jobSource: JobSource
startedAt: string
finishedAt?: string | null
jobStatus: JobStatus
targetCount: number
warningCount: number
alarmCount: number
message?: string
}
export interface ResultItem {
resultId: number
targetId: number
driveLetter: string
totalBytes: number
usedBytes: number
freeBytes: number
usedPercent: number
currentStatus: MonitorStatus
previousStatus: MonitorStatus
statusChanged: boolean
shouldNotify: boolean
notifyReason: 'ALARM_EVERY_TIME' | 'STATUS_CHANGED' | 'NO_NOTIFY'
scanTime: string
message?: string
}
export interface NotifyLogItem {
id: number
resultId: number
driveLetter: string
notifyLevel: NotifyLevel
channelType: NotifyChannelType
channelTarget: string
sendStatus: NotifySendStatus
responseMessage?: string
sentAt: string
}
export interface JobDetailData {
job: JobListItem
results: ResultItem[]
notifyLogs: NotifyLogItem[]
}
export interface NotifyTestParams {
driveLetter: string
}
export interface JobPageData extends ResPage<JobListItem> {}
}
```
- [ ] **Step 3: Create the API wrapper module**
Create `D:\Work\SourceCode\CN_Tool_client\frontend\src\api\system\diskMonitor\index.ts` with:
```ts
import http from '@/api'
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
export const getDiskMonitorPolicyDetail = () => {
return http.get<DiskMonitor.PolicyDetailData>('/disk-monitor/policy/detail')
}
export const saveDiskMonitorPolicy = (params: DiskMonitor.SavePolicyParams) => {
return http.post('/disk-monitor/policy/save', params)
}
export const runDiskMonitorJob = (params: DiskMonitor.RunJobParams) => {
return http.post<DiskMonitor.RunJobResult>('/disk-monitor/job/run', params)
}
export const getDiskMonitorJobList = (params: DiskMonitor.JobListParams) => {
return http.post<DiskMonitor.JobPageData>('/disk-monitor/job/list', params)
}
export const getDiskMonitorJobDetail = (jobId: number) => {
return http.get<DiskMonitor.JobDetailData>(`/disk-monitor/job/${jobId}/detail`)
}
export const testDiskMonitorNotify = (params: DiskMonitor.NotifyTestParams) => {
return http.post('/disk-monitor/notify/test', params)
}
```
- [ ] **Step 4: Add static route fallbacks for local verification**
Append the following two children inside the existing `layout` route in `D:\Work\SourceCode\CN_Tool_client\frontend\src\routers\modules\staticRouter.ts` without rewriting unrelated lines:
```ts
{
path: '/systemMonitor',
name: 'systemMonitor',
component: () => import('@/views/systemMonitor/index.vue'),
meta: {
title: '系统监控'
}
},
{
path: '/systemMonitor/diskMonitor',
name: 'diskMonitor',
component: () => import('@/views/systemMonitor/diskMonitor/index.vue'),
meta: {
title: '磁盘监控'
}
},
```
- [ ] **Step 5: Create the SQL delivery file from the approved spec**
Create `D:\Work\SourceCode\CN_Tool_client\doc\系统磁盘监控数据库设计.sql` and copy the exact five `CREATE TABLE` statements from `D:\Work\SourceCode\CN_Tool_client\docs\superpowers\specs\2026-04-22-disk-monitor-design.md` section `5.6 MySQL 建表 SQL`, preserving:
```sql
CREATE TABLE IF NOT EXISTS `disk_monitor_policy` ( ... );
CREATE TABLE IF NOT EXISTS `disk_monitor_target` ( ... );
CREATE TABLE IF NOT EXISTS `disk_monitor_job` ( ... );
CREATE TABLE IF NOT EXISTS `disk_monitor_result` ( ... );
CREATE TABLE IF NOT EXISTS `disk_monitor_notify_log` ( ... );
```
Expected: `doc/系统磁盘监控数据库设计.sql` 成为 DBA 或后端可直接引用的交付文件,语句内容与规格文档完全一致。
- [ ] **Step 6: Run static verification for the new route/API files**
Run:
```powershell
cd D:\Work\SourceCode\CN_Tool_client\frontend
npm run lint -- src/api/system/diskMonitor/index.ts src/api/system/diskMonitor/interface/index.ts src/routers/modules/staticRouter.ts
npm run type-check
```
Expected: 两个命令都退出 `0`;如果 `type-check` 失败,应只允许失败原因为后续页面组件尚未创建,不允许出现 API 或路由类型错误。
- [ ] **Step 7: Commit the contract and SQL baseline**
Run:
```powershell
git add D:\Work\SourceCode\CN_Tool_client\frontend\src\routers\modules\staticRouter.ts D:\Work\SourceCode\CN_Tool_client\frontend\src\api\system\diskMonitor\index.ts D:\Work\SourceCode\CN_Tool_client\frontend\src\api\system\diskMonitor\interface\index.ts D:\Work\SourceCode\CN_Tool_client\doc\系统磁盘监控数据库设计.sql
git commit -m "feat: add disk monitor contracts and sql"
```
## Task 2: Replace The Placeholder Page With Summary And Policy State
**Files:**
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\utils\form.ts`
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorSummary.vue`
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorPolicyForm.vue`
- Modify: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue`
- [ ] **Step 1: Add page-level defaults and validation helpers**
Create `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\utils\form.ts` with:
```ts
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
export const createDefaultPolicy = (): DiskMonitor.PolicyItem => ({
policyName: '默认磁盘监控策略',
monitorEnabled: true,
runOnAppStart: true,
dailyRunTime: '08:30:00',
warningNotifyMode: 'STATUS_CHANGE',
alarmNotifyMode: 'EVERY_TIME',
remark: ''
})
export const createEmptyPathTarget = (): DiskMonitor.NotifyPathTarget => ({
path: '',
name: '',
enabled: true
})
export const createEmptyHttpTarget = (): DiskMonitor.NotifyHttpTarget => ({
url: '',
name: '',
method: 'POST',
timeoutMs: 5000,
enabled: true
})
export const createEmptyTarget = (): DiskMonitor.TargetItem => ({
driveLetter: '',
monitorEnabled: true,
warningUsagePercent: 80,
alarmUsagePercent: 90,
notifyPathEnabled: false,
notifyPathList: [],
notifyHttpEnabled: false,
notifyHttpList: [],
lastStatus: 'UNKNOWN',
lastScanTime: null,
lastUsedPercent: null,
remark: ''
})
export const validatePolicy = (policy: DiskMonitor.PolicyItem) => {
if (!policy.dailyRunTime) return '每日统一执行时间不能为空'
return ''
}
export const validateTarget = (target: DiskMonitor.TargetItem, exists: string[]) => {
if (!target.driveLetter) return '盘符不能为空'
if (exists.includes(target.driveLetter)) return '盘符不能重复'
if (target.warningUsagePercent < 1 || target.warningUsagePercent > 100) return '预警使用率必须在 1-100 之间'
if (target.alarmUsagePercent < 1 || target.alarmUsagePercent > 100) return '告警使用率必须在 1-100 之间'
if (target.alarmUsagePercent < target.warningUsagePercent) return '告警使用率不能小于预警使用率'
return ''
}
```
- [ ] **Step 2: Create the summary card component**
Create `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorSummary.vue` with:
```vue
<template>
<div class="summary-grid">
<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">{{ targets.length }}</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">{{ latestJob?.jobStatus || '--' }}</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
defineOptions({ name: 'DiskMonitorSummary' })
const props = defineProps<{
policy: DiskMonitor.PolicyItem
targets: DiskMonitor.TargetItem[]
latestJob: DiskMonitor.JobListItem | null
}>()
const alarmCount = computed(() => props.targets.filter(item => item.lastStatus === 'ALARM').length)
</script>
```
- [ ] **Step 3: Create the global policy form component**
Create `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorPolicyForm.vue` with:
```vue
<template>
<section class="policy-card">
<div class="card-header">
<div>
<h2 class="card-title">全局策略</h2>
<p class="card-description">配置监控总开关启动监控与每日统一时间</p>
</div>
<div class="card-actions">
<el-button :loading="runLoading" @click="$emit('run')">立即执行监控</el-button>
<el-button type="primary" :loading="saveLoading" @click="$emit('save')">保存配置</el-button>
</div>
</div>
<el-form label-width="120px">
<el-form-item label="启用监控">
<el-switch :model-value="modelValue.monitorEnabled" @update:model-value="updateField('monitorEnabled', $event)" />
</el-form-item>
<el-form-item label="启动即监控">
<el-switch :model-value="modelValue.runOnAppStart" @update:model-value="updateField('runOnAppStart', $event)" />
</el-form-item>
<el-form-item label="每日执行时间">
<el-time-picker
:model-value="modelValue.dailyRunTime"
value-format="HH:mm:ss"
placeholder="选择时间"
@update:model-value="updateField('dailyRunTime', $event)"
/>
</el-form-item>
<el-form-item label="通知规则">
<el-alert title="预警按状态变化通知,告警每次命中都通知" type="info" :closable="false" />
</el-form-item>
</el-form>
</section>
</template>
<script setup lang="ts">
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
defineOptions({ name: 'DiskMonitorPolicyForm' })
const props = defineProps<{
modelValue: DiskMonitor.PolicyItem
saveLoading: boolean
runLoading: boolean
}>()
const emit = defineEmits<{
(event: 'update:modelValue', value: DiskMonitor.PolicyItem): void
(event: 'save'): void
(event: 'run'): void
}>()
const updateField = <K extends keyof DiskMonitor.PolicyItem>(key: K, value: DiskMonitor.PolicyItem[K]) => {
emit('update:modelValue', {
...props.modelValue,
[key]: value
})
}
</script>
```
- [ ] **Step 4: Replace the placeholder page with container state and data loading**
Replace `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue` with a container that imports the new API and components, keeping the back navigation and adding concise Chinese comments on the main business flow:
```vue
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
import {
getDiskMonitorPolicyDetail,
getDiskMonitorJobList,
runDiskMonitorJob,
saveDiskMonitorPolicy
} from '@/api/system/diskMonitor'
import { createDefaultPolicy, createEmptyTarget, validatePolicy } from './utils/form'
import DiskMonitorSummary from './components/DiskMonitorSummary.vue'
import DiskMonitorPolicyForm from './components/DiskMonitorPolicyForm.vue'
defineOptions({ name: 'DiskMonitorView' })
const policyForm = ref<DiskMonitor.PolicyItem>(createDefaultPolicy())
const targetList = ref<DiskMonitor.TargetItem[]>([])
const latestJob = ref<DiskMonitor.JobListItem | null>(null)
const loading = reactive({
init: false,
save: false,
run: false,
jobs: false
})
// 页面初始化时同时拉取全局策略和最近任务摘要。
const loadPageData = async () => {
loading.init = true
try {
const [policyResp, jobsResp] = await Promise.all([
getDiskMonitorPolicyDetail(),
getDiskMonitorJobList({ pageNum: 1, pageSize: 10 })
])
policyForm.value = policyResp.data.policy
targetList.value = policyResp.data.targets || []
latestJob.value = jobsResp.data.records?.[0] || null
} finally {
loading.init = false
}
}
// 保存前先做全局策略校验,避免向后端提交无效时间配置。
const handleSave = async () => {
const error = validatePolicy(policyForm.value)
if (error) {
ElMessage.warning(error)
return
}
loading.save = true
try {
await saveDiskMonitorPolicy({
policy: policyForm.value,
targets: targetList.value
})
ElMessage.success('配置保存成功')
await loadPageData()
} finally {
loading.save = false
}
}
// 手动执行入口用于联调后端执行链路和验证页面摘要刷新。
const handleRunNow = async () => {
loading.run = true
try {
await runDiskMonitorJob({ jobSource: 'MANUAL' })
ElMessage.success('监控任务已启动')
await loadPageData()
} finally {
loading.run = false
}
}
onMounted(loadPageData)
</script>
```
Expected: 页面不再显示占位摘要和占位面板,而是渲染摘要卡片和全局策略卡片,并能在挂载时请求配置与最近任务。
- [ ] **Step 5: Run the first full type-check after replacing the placeholder**
Run:
```powershell
cd D:\Work\SourceCode\CN_Tool_client\frontend
npm run type-check
```
Expected: 允许失败原因为“盘符编辑组件和任务列表组件尚未创建”,不允许出现 `form.ts``DiskMonitorSummary.vue``DiskMonitorPolicyForm.vue` 的类型错误。
- [ ] **Step 6: Commit the page state skeleton**
Run:
```powershell
git add D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\utils\form.ts D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorSummary.vue D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorPolicyForm.vue D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue
git commit -m "feat: scaffold disk monitor page state"
```
## Task 3: Build The Target Editor And Notification Editors
**Files:**
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\NotificationPathEditor.vue`
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\NotificationHttpEditor.vue`
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorTargetDialog.vue`
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorTargetTable.vue`
- Modify: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue`
- [ ] **Step 1: Create the path notification array editor**
Create `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\NotificationPathEditor.vue` with:
```vue
<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<{ (event: 'update:modelValue', value: DiskMonitor.NotifyPathTarget[]): void }>()
const patchRows = (rows: DiskMonitor.NotifyPathTarget[]) => emit('update:modelValue', rows)
const addRow = () => patchRows([...props.modelValue, createEmptyPathTarget()])
const removeRow = (index: number) => patchRows(props.modelValue.filter((_, rowIndex) => rowIndex !== index))
const updateRow = (index: number, key: keyof DiskMonitor.NotifyPathTarget, value: string | boolean) => {
patchRows(
props.modelValue.map((row, rowIndex) =>
rowIndex === index ? { ...row, [key]: value } : row
)
)
}
</script>
```
Expected: 组件支持新增、删除、编辑路径通知目标,不自行维护状态。
- [ ] **Step 2: Create the HTTP notification array editor**
Create `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\NotificationHttpEditor.vue` with:
```vue
<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<{ (event: 'update:modelValue', value: DiskMonitor.NotifyHttpTarget[]): void }>()
const patchRows = (rows: DiskMonitor.NotifyHttpTarget[]) => emit('update:modelValue', rows)
const addRow = () => patchRows([...props.modelValue, createEmptyHttpTarget()])
const removeRow = (index: number) => patchRows(props.modelValue.filter((_, rowIndex) => rowIndex !== index))
const updateRow = (
index: number,
key: keyof DiskMonitor.NotifyHttpTarget,
value: string | number | boolean
) => {
patchRows(
props.modelValue.map((row, rowIndex) =>
rowIndex === index ? { ...row, [key]: value } : row
)
)
}
</script>
```
- [ ] **Step 3: Create the target edit dialog**
Create `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorTargetDialog.vue` with a dialog shell that hosts the two editor components:
```vue
<script setup lang="ts">
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
import NotificationPathEditor from './NotificationPathEditor.vue'
import NotificationHttpEditor from './NotificationHttpEditor.vue'
defineOptions({ name: 'DiskMonitorTargetDialog' })
const props = defineProps<{
visible: boolean
modelValue: DiskMonitor.TargetItem
title: string
}>()
const emit = defineEmits<{
(event: 'update:visible', value: boolean): void
(event: 'update:modelValue', value: DiskMonitor.TargetItem): void
(event: 'confirm'): void
}>()
const patchTarget = <K extends keyof DiskMonitor.TargetItem>(key: K, value: DiskMonitor.TargetItem[K]) => {
emit('update:modelValue', {
...props.modelValue,
[key]: value
})
}
</script>
```
Expected: 弹窗内至少包含盘符、启用开关、预警阈值、告警阈值、路径通知开关与编辑器、HTTP 通知开关与编辑器、备注。
- [ ] **Step 4: Create the target table with add/edit/delete events**
Create `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorTargetTable.vue` with:
```vue
<script setup lang="ts">
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
defineOptions({ name: 'DiskMonitorTargetTable' })
defineProps<{ rows: DiskMonitor.TargetItem[] }>()
defineEmits<{
(event: 'add'): void
(event: 'edit', row: DiskMonitor.TargetItem, index: number): void
(event: 'remove', index: number): void
}>()
</script>
```
Expected: 表格列至少显示盘符、是否监控、预警使用率、告警使用率、当前状态、最近扫描时间、最近使用率、操作按钮。
- [ ] **Step 5: Wire target CRUD into the page container**
Update `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue` to add:
```ts
import { createEmptyTarget, validateTarget } from './utils/form'
import DiskMonitorTargetTable from './components/DiskMonitorTargetTable.vue'
import DiskMonitorTargetDialog from './components/DiskMonitorTargetDialog.vue'
const targetDialogVisible = ref(false)
const editingTargetIndex = ref(-1)
const editingTarget = ref<DiskMonitor.TargetItem>(createEmptyTarget())
const openAddTarget = () => {
editingTargetIndex.value = -1
editingTarget.value = createEmptyTarget()
targetDialogVisible.value = true
}
const openEditTarget = (row: DiskMonitor.TargetItem, index: number) => {
editingTargetIndex.value = index
editingTarget.value = JSON.parse(JSON.stringify(row))
targetDialogVisible.value = true
}
const confirmTarget = () => {
const duplicatePool = targetList.value
.filter((_, index) => index !== editingTargetIndex.value)
.map(item => item.driveLetter)
const error = validateTarget(editingTarget.value, duplicatePool)
if (error) {
ElMessage.warning(error)
return
}
if (editingTargetIndex.value === -1) {
targetList.value = [...targetList.value, JSON.parse(JSON.stringify(editingTarget.value))]
} else {
targetList.value = targetList.value.map((item, index) =>
index === editingTargetIndex.value ? JSON.parse(JSON.stringify(editingTarget.value)) : item
)
}
targetDialogVisible.value = false
}
const removeTarget = (index: number) => {
targetList.value = targetList.value.filter((_, rowIndex) => rowIndex !== index)
}
```
Expected: 页面可以新增、编辑、删除多个盘符,并能在保存前阻止重复盘符和非法阈值。
- [ ] **Step 6: Run lint and type-check after target editor wiring**
Run:
```powershell
cd D:\Work\SourceCode\CN_Tool_client\frontend
npm run lint -- src/views/systemMonitor/diskMonitor/index.vue src/views/systemMonitor/diskMonitor/components/NotificationPathEditor.vue src/views/systemMonitor/diskMonitor/components/NotificationHttpEditor.vue src/views/systemMonitor/diskMonitor/components/DiskMonitorTargetDialog.vue src/views/systemMonitor/diskMonitor/components/DiskMonitorTargetTable.vue
npm run type-check
```
Expected: 两个命令退出 `0`;不允许出现盘符编辑器和通知编辑器的 props/emits 类型错误。
- [ ] **Step 7: Commit the target editor slice**
Run:
```powershell
git add D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\NotificationPathEditor.vue D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\NotificationHttpEditor.vue D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorTargetDialog.vue D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorTargetTable.vue D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue
git commit -m "feat: add disk monitor target editors"
```
## Task 4: Add Manual Run, Job History, And Job Detail Views
**Files:**
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorJobTable.vue`
- Create: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorJobDetailDrawer.vue`
- Modify: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue`
- [ ] **Step 1: Create the recent job table component**
Create `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorJobTable.vue` with:
```vue
<script setup lang="ts">
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
defineOptions({ name: 'DiskMonitorJobTable' })
defineProps<{
rows: DiskMonitor.JobListItem[]
loading: boolean
}>()
defineEmits<{
(event: 'refresh'): void
(event: 'detail', row: DiskMonitor.JobListItem): void
}>()
</script>
```
Expected: 表格列至少包含任务编号、来源、开始时间、结束时间、状态、预警数量、告警数量和“查看详情”按钮。
- [ ] **Step 2: Create the job detail drawer**
Create `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorJobDetailDrawer.vue` with:
```vue
<script setup lang="ts">
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
defineOptions({ name: 'DiskMonitorJobDetailDrawer' })
defineProps<{
visible: boolean
detail: DiskMonitor.JobDetailData | null
loading: boolean
}>()
defineEmits<{ (event: 'update:visible', value: boolean): void }>()
</script>
```
Expected: 抽屉中分两块表格展示 `results``notifyLogs`,字段名与规格文档一致。
- [ ] **Step 3: Wire manual run and history loading into the page**
Update `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue` with:
```ts
import { getDiskMonitorJobDetail } from '@/api/system/diskMonitor'
import DiskMonitorJobTable from './components/DiskMonitorJobTable.vue'
import DiskMonitorJobDetailDrawer from './components/DiskMonitorJobDetailDrawer.vue'
const jobList = ref<DiskMonitor.JobListItem[]>([])
const jobDetailVisible = ref(false)
const jobDetail = ref<DiskMonitor.JobDetailData | null>(null)
const detailLoading = ref(false)
const loadJobList = async () => {
loading.jobs = true
try {
const resp = await getDiskMonitorJobList({ pageNum: 1, pageSize: 10 })
jobList.value = resp.data.records || []
latestJob.value = jobList.value[0] || null
} finally {
loading.jobs = false
}
}
const openJobDetail = async (row: DiskMonitor.JobListItem) => {
detailLoading.value = true
jobDetailVisible.value = true
try {
const resp = await getDiskMonitorJobDetail(row.id)
jobDetail.value = resp.data
} finally {
detailLoading.value = false
}
}
```
Expected: 手动执行完任务后可以刷新最近任务列表,并且可点开详情查看每个盘符结果与通知日志。
- [ ] **Step 4: Keep the page refresh flow single-sourced**
Update `loadPageData` in `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue` so it only loads config + recent任务列表 once:
```ts
const loadPageData = async () => {
loading.init = true
try {
const policyResp = await getDiskMonitorPolicyDetail()
policyForm.value = policyResp.data.policy
targetList.value = policyResp.data.targets || []
await loadJobList()
} finally {
loading.init = false
}
}
```
Expected: 保存配置、手动执行、页面初始化都复用同一套刷新入口,不出现多处重复请求逻辑。
- [ ] **Step 5: Run the full frontend verification commands**
Run:
```powershell
cd D:\Work\SourceCode\CN_Tool_client\frontend
npm run lint
npm run type-check
```
Expected: 两个命令都退出 `0`
- [ ] **Step 6: Commit the job history UI**
Run:
```powershell
git add D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorJobTable.vue D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\DiskMonitorJobDetailDrawer.vue D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue
git commit -m "feat: add disk monitor job views"
```
## Task 5: Perform Manual Verification On The Hash Route
**Files:**
- Verify only: `D:\Work\SourceCode\CN_Tool_client\frontend\src\routers\modules\staticRouter.ts`
- Verify only: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\index.vue`
- Verify only: `D:\Work\SourceCode\CN_Tool_client\frontend\src\views\systemMonitor\diskMonitor\components\*.vue`
- Verify only: `D:\Work\SourceCode\CN_Tool_client\doc\系统磁盘监控数据库设计.sql`
- [ ] **Step 1: Start the frontend dev server**
Run:
```powershell
cd D:\Work\SourceCode\CN_Tool_client\frontend
npm run dev
```
Expected: Vite 启动成功;当前开发环境使用 `hash` 路由,因此目标页面地址为 `/#/systemMonitor/diskMonitor`
- [ ] **Step 2: Verify configuration load and save behavior**
Manual checklist:
```text
1. 访问 /#/systemMonitor/diskMonitor能看到摘要区、全局策略区、盘符列表区、最近任务区。
2. 修改“启用监控”“启动即监控”“每日执行时间”后点击“保存配置”,页面给出成功提示。
3. 新增两个盘符,例如 C: 与 D:,分别配置不同的预警/告警阈值。
4. 为其中一个盘符新增本地目录通知和 HTTP 通知目标,保存后刷新页面,配置仍正确回显。
5. 尝试录入重复盘符或告警阈值小于预警阈值,页面必须阻止提交并给出提示。
```
Expected: 五项都成立。
- [ ] **Step 3: Verify manual run and job detail behavior**
Manual checklist:
```text
1. 点击“立即执行监控”,页面提示任务已启动。
2. 最近任务列表出现一条新的 MANUAL 任务。
3. 打开任务详情抽屉,能看到盘符结果表和通知日志表。
4. 若后端暂未接通,页面应以接口错误提示结束,不得卡死或出现未捕获异常。
```
Expected: 四项都成立。
- [ ] **Step 4: Verify the SQL artifact matches the approved spec**
Run:
```powershell
Get-Content -Raw D:\Work\SourceCode\CN_Tool_client\doc\系统磁盘监控数据库设计.sql
Get-Content -Raw D:\Work\SourceCode\CN_Tool_client\docs\superpowers\specs\2026-04-22-disk-monitor-design.md
```
Expected: SQL 文件包含同样的五张表和字段命名:`disk_monitor_policy``disk_monitor_target``disk_monitor_job``disk_monitor_result``disk_monitor_notify_log`
- [ ] **Step 5: Record final verification status**
Run:
```powershell
cd D:\Work\SourceCode\CN_Tool_client\frontend
npm run lint
npm run type-check
git status --short
```
Expected: `lint``type-check` 退出 `0``git status --short` 只显示本功能相关改动和仓库原有未处理改动,不出现意外文件。
## Self-Review
- Spec coverage: 计划覆盖了静态路由兜底、前端 API 契约、数据库 SQL 文件、摘要区、全局策略区、盘符与通知编辑、手动执行、最近任务、详情抽屉和验证步骤,与已批准规格一致。
- Placeholder scan: 没有 `TODO``TBD``后续再说` 类占位语;每个任务都给了明确文件路径、代码骨架、命令和预期结果。
- Type consistency: 计划统一使用 `DiskMonitor.PolicyItem``DiskMonitor.TargetItem``DiskMonitor.JobListItem``DiskMonitor.JobDetailData``createDefaultPolicy``createEmptyTarget``validatePolicy``validateTarget` 等命名,没有前后不一致的接口名。