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

38 KiB
Raw Blame History

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: 增加 systemMonitordiskMonitor 的静态路由兜底,保证 /#/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:

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:

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:

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:

            {
                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:

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:

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:

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:

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:

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

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

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

cd D:\Work\SourceCode\CN_Tool_client\frontend
npm run type-check

Expected: 允许失败原因为“盘符编辑组件和任务列表组件尚未创建”,不允许出现 form.tsDiskMonitorSummary.vueDiskMonitorPolicyForm.vue 的类型错误。

  • Step 6: Commit the page state skeleton

Run:

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:

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

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

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

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

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:

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:

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:

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

<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: 抽屉中分两块表格展示 resultsnotifyLogs,字段名与规格文档一致。

  • 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:

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:

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:

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:

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:

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:

1. 访问 /#/systemMonitor/diskMonitor能看到摘要区、全局策略区、盘符列表区、最近任务区。
2. 修改“启用监控”“启动即监控”“每日执行时间”后点击“保存配置”,页面给出成功提示。
3. 新增两个盘符,例如 C: 与 D:,分别配置不同的预警/告警阈值。
4. 为其中一个盘符新增本地目录通知和 HTTP 通知目标,保存后刷新页面,配置仍正确回显。
5. 尝试录入重复盘符或告警阈值小于预警阈值,页面必须阻止提交并给出提示。

Expected: 五项都成立。

  • Step 3: Verify manual run and job detail behavior

Manual checklist:

1. 点击“立即执行监控”,页面提示任务已启动。
2. 最近任务列表出现一条新的 MANUAL 任务。
3. 打开任务详情抽屉,能看到盘符结果表和通知日志表。
4. 若后端暂未接通,页面应以接口错误提示结束,不得卡死或出现未捕获异常。

Expected: 四项都成立。

  • Step 4: Verify the SQL artifact matches the approved spec

Run:

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_policydisk_monitor_targetdisk_monitor_jobdisk_monitor_resultdisk_monitor_notify_log

  • Step 5: Record final verification status

Run:

cd D:\Work\SourceCode\CN_Tool_client\frontend
npm run lint
npm run type-check
git status --short

Expected: linttype-check 退出 0git status --short 只显示本功能相关改动和仓库原有未处理改动,不出现意外文件。

Self-Review

  • Spec coverage: 计划覆盖了静态路由兜底、前端 API 契约、数据库 SQL 文件、摘要区、全局策略区、盘符与通知编辑、手动执行、最近任务、详情抽屉和验证步骤,与已批准规格一致。
  • Placeholder scan: 没有 TODOTBD后续再说 类占位语;每个任务都给了明确文件路径、代码骨架、命令和预期结果。
  • Type consistency: 计划统一使用 DiskMonitor.PolicyItemDiskMonitor.TargetItemDiskMonitor.JobListItemDiskMonitor.JobDetailDatacreateDefaultPolicycreateEmptyTargetvalidatePolicyvalidateTarget 等命名,没有前后不一致的接口名。