系统之家-日志清理-布局与组件结构描述.md 40 KB

系统之家-日志清理 - 布局与组件结构描述

页面概览

日志清理是系统之家一级菜单下的二级页面,提供系统日志的自动清理和手动清理功能,帮助管理员维护系统日志,防止日志文件过度占用磁盘空间。


1. 布局与组件结构

1.1 整体布局结构

Page (日志清理)
├── Tabs Component (选项卡容器)
│   ├── Tab Panel 1: 自动清理
│   └── Tab Panel 2: 手动清理
└── Footer: 操作按钮区域
    ├── Button: 取消
    └── Button: 保存/开始清理

布局类型: Tabs Layout (选项卡布局)
排列方向: Horizontal Tabs (水平标签)
区域划分: 2个独立的Tab Panel,通过标签切换
固定底部: Footer区域固定在页面底部,包含操作按钮


1.2 Tab 1: 手动清理页面

1.2.1 层级结构

Tab Panel (手动清理)
├── Section: 状态提示区域
│   └── Alert/Text: 上一次清理时间提示 (红色高亮显示)
├── Section: 快捷选择区域
│   ├── Section Header: "快捷选择"
│   └── Radio.Group: 时间范围选项
│       ├── Radio: 全部日志
│       ├── Radio: 保留近3天
│       └── Radio: 保留近10天
├── Section: 选择时间范围区域
│   ├── Section Header: "选择时间范围"
│   └── Container: 时间选择器组
│       ├── Form.Item: 起始时间
│       │   ├── Label: "起始时间"
│       │   └── DatePicker: 开始日期选择器
│       └── Form.Item: 截止时间
│           ├── Label: "截止时间"
│           └── DatePicker: 结束日期选择器
└── Footer: 操作按钮区域
    ├── Button: 取消
    └── Button: 开始清理 (Primary)

布局特点:

  • Vertical Stack Layout: 各区域垂直堆叠排列
  • Section分组: 功能区域通过标题分组,视觉层次清晰
  • Horizontal Form Layout: 时间选择器水平排列
  • Fixed Footer: 操作按钮固定在底部
  • Responsive: 根据内容自适应高度

1.2.2 组件详情

组件类型 实例 参数配置 说明
Alert/Text 上一次清理时间提示 type="info" 或使用红色文字 显示最近一次清理时间,红色高亮提醒
Radio.Group 快捷选择 defaultValue="all" 互斥选择,提供快捷时间范围
Radio 全部日志 value="all" 选中时清理所有日志
Radio 保留近3天 value="recent3" 选中时保留最近3天日志
Radio 保留近10天 value="recent10" 选中时保留最近10天日志
DatePicker 起始时间 placeholder="请选择开始日期", format="YYYY-MM-DD" 自定义起始日期
DatePicker 截止时间 placeholder="请选择结束日期", format="YYYY-MM-DD" 自定义结束日期
Button 取消 - 取消操作,关闭或返回
Button 开始清理 type="primary" 执行手动清理操作

1.3 Tab 2: 自动清理页面

1.3.1 层级结构

Tab Panel (自动清理)
├── Section: 按照时间周期清理
│   ├── Section Header: "按照时间周期清理"
│   ├── Form.Item: 开关控制
│   │   ├── Label: "按照时间周期清理"
│   │   └── Switch: 启用/禁用时间周期清理
│   ├── Form.Item: 清理周期
│   │   ├── Label: "清理周期(天)" + Required Mark
│   │   └── InputNumber: 周期天数输入
│   │       └── Suffix: 字符计数 (1/3)
│   └── Form.Item: 保存周期
│       ├── Label: "保存周期(天)" + Required Mark
│       └── InputNumber: 保存天数输入
│           └── Suffix: 字符计数 (2/3)
├── Divider: 分隔线
├── Section: 按照磁盘空间清理
│   ├── Section Header: "按照磁盘空间清理"
│   ├── Form.Item: 开关控制
│   │   ├── Label: "按照磁盘空间清理"
│   │   └── Switch: 启用/禁用空间清理
│   ├── Form.Item: 磁盘阈值
│   │   ├── Label: "磁盘小于该值时进行清理(G)" + Required Mark
│   │   └── InputNumber: 磁盘空间阈值输入
│   │       └── Suffix: 字符计数 (2/3)
│   └── Form.Item: 保存周期
│       ├── Label: "保存周期(天)" + Required Mark
│       └── InputNumber: 保存天数输入
│           └── Suffix: 字符计数 (1/3)
└── Footer: 操作按钮区域
    ├── Button: 取消
    └── Button: 保存 (Primary)

布局特点:

  • Vertical Stack Layout: 两个配置区域垂直堆叠
  • Form Layout: 每个配置项使用Form.Item统一管理
  • Horizontal Label: 标签与控件水平排列
  • Divider分隔: 使用分隔线区分不同清理策略
  • Conditional Display: 开关关闭时,相关输入框可能禁用
  • Input Validation: 输入框带字符计数,限制输入范围

1.3.2 组件详情

组件类型 实例 参数配置 说明
Switch 按照时间周期清理开关 defaultChecked=true 启用/禁用时间周期清理功能
InputNumber 清理周期(天) min=1, max=999, precision=0, required 设置自动清理的时间间隔
InputNumber 保存周期(天) min=1, max=999, precision=0, required 设置日志保留天数
Divider 分隔线 - 分隔不同的清理策略配置
Switch 按照磁盘空间清理开关 defaultChecked=true 启用/禁用空间清理功能
InputNumber 磁盘阈值(G) min=1, max=999, precision=0, required 设置触发清理的磁盘空间阈值
InputNumber 保存周期(天) min=1, max=999, precision=0, required 设置日志保留天数
Button 取消 - 取消修改,恢复到上次保存的值
Button 保存 type="primary" 保存自动清理配置

2. Ant Design 组件选择理由

2.1 Tabs 组件

选择理由:

  • 功能分离: 自动清理和手动清理是两种不同的使用场景,Tabs可以清晰分离,避免界面混乱
  • 空间利用: 在有限的页面空间内组织多个功能模块,避免页面过长
  • 用户习惯: 符合用户对配置页面的常见认知,自动化配置和手动操作分开管理
  • 状态隔离: 两个Tab的表单状态相互独立,互不影响

2.2 Radio.Group 组件

选择理由:

  • 互斥选择: 快捷选择功能需要用户从预设选项中选择一个,Radio是最合适的组件
  • 视觉清晰: 单选框让用户清楚当前选择的是哪个时间范围
  • 快速操作: 预设的常用选项(全部/近3天/近10天)提供快捷操作,提升效率
  • 与自定义互斥: 选择快捷选项后,自定义时间范围可以禁用或清空

2.3 DatePicker 组件

选择理由:

  • 精确控制: 自定义时间范围需要精确到日期,DatePicker提供日历选择界面
  • 格式统一: 自动格式化日期显示,保证日期格式的一致性
  • 范围验证: 可以设置起始时间不能晚于截止时间的验证规则
  • 用户友好: 日历界面比直接输入日期字符串更直观易用

2.4 Switch 组件

选择理由:

  • 开关控制: 自动清理功能的启用/禁用是典型的开关场景
  • 视觉反馈: Switch的开/关状态视觉清晰,用户一眼就能看出功能是否启用
  • 即时效果: 切换时可以立即禁用或启用相关的输入框,提供即时反馈
  • 节省空间: 相比Checkbox,Switch更简洁,适合功能开关场景

2.5 InputNumber 组件

选择理由:

  • 数值输入: 清理周期、保存周期、磁盘阈值都是数值型参数
  • 范围限制: 可以设置min/max,防止用户输入无效值(如负数、过大的数)
  • 精度控制: precision=0确保只能输入整数,符合天数和GB的语义
  • 增减按钮: 内置的增减按钮方便用户微调数值
  • 输入验证: 自动拒绝非数字输入,减少前端验证代码

2.6 Button 组件

选择理由:

  • 操作触发: 保存配置、开始清理等操作需要明确的触发按钮
  • 视觉层级: type="primary"区分主要操作(保存/开始清理)和次要操作(取消)
  • 状态反馈: 支持loading状态,清理过程中可以显示加载动画
  • 禁用控制: 可以根据表单验证结果禁用按钮,防止提交无效数据

2.7 Form 组件

选择理由:

  • 表单管理: 自动清理Tab是典型的表单场景,Form组件提供统一的表单管理
  • 验证机制: 内置表单验证,必填项、数值范围等规则易于配置
  • 布局统一: Form.Item提供统一的标签和控件布局,视觉一致
  • 数据收集: 方便收集和提交表单数据

2.8 Divider 组件

选择理由:

  • 视觉分隔: 将"时间周期清理"和"磁盘空间清理"两个策略清晰分隔
  • 层次清晰: 帮助用户理解这是两个独立的配置模块
  • 简洁美观: 比空白间距更有视觉引导性

2.9 Alert/Text 组件

选择理由:

  • 信息提示: 显示上一次清理时间,给用户提供上下文信息
  • 视觉强调: 红色文字或Alert组件可以突出重要信息
  • 非阻塞式: 不会打断用户操作,信息自然融入页面

3. 页面功能描述

3.1 手动清理功能

3.1.1 快捷选择功能

  • 全部日志: 选中后将清理所有系统日志,不保留任何历史记录
  • 保留近3天: 清理3天以前的日志,保留最近3天的日志记录
  • 保留近10天: 清理10天以前的日志,保留最近10天的日志记录
  • 互斥关系: 快捷选择与自定义时间范围互斥,选择快捷选项后,时间选择器应禁用或清空

3.1.2 自定义时间范围

  • 起始时间: 用户可以选择清理的开始日期
  • 截止时间: 用户可以选择清理的结束日期
  • 范围验证: 起始时间不能晚于截止时间,系统应提示或自动调整
  • 与快捷选择互斥: 当用户选择自定义时间时,快捷选择的单选框应取消选中

3.1.3 清理执行

  • 确认对话框: 点击"开始清理"前应弹出确认对话框,明确告知将清理的日志范围和大致数量
  • 风险提示: 提示用户清理操作不可恢复,建议先备份重要日志
  • 进度显示: 清理过程中显示进度条和当前清理状态
  • 结果反馈: 清理完成后显示清理的日志数量、释放的磁盘空间
  • 日志记录: 记录清理操作到操作记录中

3.1.4 上一次清理时间显示

  • 时间格式: 显示格式为"YYYY.MM.DD HH:mm"
  • 首次使用: 如果从未清理过,显示"尚未清理"或不显示此提示
  • 数据来源: 从系统配置或数据库中读取上次清理时间

3.2 自动清理功能

3.2.1 按照时间周期清理

  • 功能开关: 用户可以启用或禁用此功能
  • 清理周期: 设置每隔多少天自动执行一次清理
    • 有效范围: 1-999天
    • 默认值: 建议1天(每天清理一次)
    • 输入限制: 仅允许输入正整数
  • 保存周期: 设置保留多少天内的日志
    • 有效范围: 1-999天
    • 默认值: 建议10天
    • 输入限制: 仅允许输入正整数
  • 执行逻辑: 系统定时任务每天检查,如果距离上次清理超过"清理周期"天数,则自动清理超过"保存周期"天数的日志
  • 字符计数: 输入框右侧显示"已输入字符数/最大字符数",提示用户输入限制

3.2.2 按照磁盘空间清理

  • 功能开关: 用户可以启用或禁用此功能
  • 磁盘阈值: 设置触发清理的磁盘剩余空间阈值(单位:GB)
    • 有效范围: 1-999 GB
    • 默认值: 建议20 GB
    • 输入限制: 仅允许输入正整数
  • 保存周期: 设置清理时保留多少天内的日志
    • 有效范围: 1-999天
    • 默认值: 建议3天
    • 输入限制: 仅允许输入正整数
  • 执行逻辑: 系统定时任务定期检查磁盘空间,当剩余空间小于设定阈值时,自动清理超过"保存周期"天数的日志
  • 优先级: 如果同时启用时间周期和磁盘空间清理,两个条件任一满足即触发清理

3.2.3 配置保存

  • 表单验证: 保存前验证所有必填项和数值范围
  • 实时生效: 配置保存后立即生效,下次定时任务检查时使用新配置
  • 配置持久化: 配置保存到数据库或配置文件
  • 取消操作: 点击取消后,表单恢复到上次保存的值

3.2.4 开关联动

  • 禁用状态: 当开关关闭时,对应的输入框应禁用(disabled),呈现灰色不可编辑状态
  • 启用状态: 当开关打开时,输入框恢复可编辑状态
  • 保存验证: 如果两个开关都关闭,保存时应提示用户至少启用一种清理策略(或允许全部禁用,取决于产品需求)

4. 功能思考与需求描述

4.1 日志清理范围

4.1.1 日志类型分类

系统日志可能包含多种类型,需要明确哪些日志可以被清理:

  • 应用日志: 系统运行日志、错误日志、调试日志
  • 操作日志: 用户操作记录、系统配置变更记录
  • 审计日志: 安全相关的审计记录
  • 访问日志: API访问日志、文件访问日志
  • 硬件日志: 硬件设备的运行日志

需求建议:

  • 操作日志和审计日志可能有法规要求,不应随意清理,建议单独管理
  • 应用日志、调试日志可以根据时间或磁盘空间清理
  • 提供"日志类型选择"功能,让管理员明确选择要清理的日志类型

4.1.2 日志存储位置

  • 日志目录: 明确日志文件存储的目录路径
  • 多目录支持: 不同类型的日志可能存储在不同目录
  • 网络存储: 考虑日志可能存储在网络驱动器的情况

4.2 自动清理调度机制

4.2.1 定时任务设计

  • 调度频率: 建议每小时检查一次是否需要清理
  • 执行时间: 建议在系统空闲时段(如凌晨)执行清理,避免影响正常使用
  • 任务队列: 如果上次清理未完成,应等待完成后再启动新的清理任务
  • 失败重试: 如果清理失败,应有重试机制,但限制重试次数

4.2.2 触发条件判断

  • 时间周期判断:

    if (启用时间周期清理 && 当前时间 - 上次清理时间 >= 清理周期) {
    执行清理(保留周期内的日志);
    }
    
  • 磁盘空间判断:

    if (启用磁盘空间清理 && 磁盘剩余空间 < 磁盘阈值) {
    执行清理(保留周期内的日志);
    }
    
  • 组合条件: 两个条件是OR关系,任一满足即触发

4.2.3 清理策略优化

  • 渐进式清理: 如果磁盘空间仍然不足,可以逐步减少保存周期,先清理较旧的日志
  • 智能清理: 优先清理体积较大的日志文件
  • 保留机制: 即使超过保存周期,对于错误日志、异常日志可以考虑额外保留

4.3 用户体验优化

4.3.1 手动清理的交互改进

  • 预览功能: 在执行清理前,显示将要清理的日志文件列表和总大小
  • 部分清理: 允许用户从预览列表中排除某些不想清理的日志
  • 分批清理: 如果日志量很大,可以分批清理,避免长时间阻塞
  • 后台任务: 清理作为后台任务执行,用户可以继续其他操作

4.3.2 自动清理的通知机制

  • 清理通知: 自动清理执行后,应通知管理员清理结果
  • 空间告警: 即使启用自动清理,如果磁盘空间持续不足,应发出告警
  • 失败告警: 自动清理失败时应立即通知管理员

4.3.3 配置建议

  • 推荐配置: 在界面上提供推荐的配置值,引导新用户
  • 配置模板: 提供"标准模式"、"节省空间模式"、"保留详细日志模式"等预设模板
  • 智能建议: 根据磁盘空间大小和系统使用情况,智能推荐合理的配置

4.4 安全与合规考虑

4.4.1 权限控制

  • 操作权限: 只有系统管理员可以执行日志清理
  • 配置权限: 普通管理员可以查看配置,但不能修改
  • 审计要求: 所有清理操作(手动和自动)都应记录到审计日志

4.4.2 数据保护

  • 清理前备份: 提供选项,在清理前自动备份要删除的日志
  • 回收站机制: 清理的日志先移动到回收站,保留一段时间后再真正删除
  • 加密日志: 对于包含敏感信息的日志,清理时应安全删除(覆盖写入)

4.4.3 合规要求

  • 保留期限: 某些行业(如医疗、金融)对日志保留期限有法规要求
  • 不可篡改: 审计日志应保证不被清理或篡改
  • 法规提醒: 在界面上提示管理员注意法规要求

4.5 性能与稳定性

4.5.1 大数据量处理

  • 日志文件数量: 可能有数千个日志文件需要清理
  • 文件大小: 单个日志文件可能很大(几GB)
  • 清理时间: 清理过程可能需要几分钟甚至更长时间
  • 性能影响: 清理过程应避免占用过多系统资源

优化建议:

  • 使用异步任务处理清理操作
  • 批量删除文件,每批处理100个文件
  • 清理过程中定期休眠,避免CPU占用过高
  • 提供"暂停"和"恢复"功能

4.5.2 错误处理

  • 文件被占用: 某些日志文件可能正在被写入,无法删除
  • 权限不足: 某些日志文件可能由系统进程创建,权限不足
  • 磁盘错误: 磁盘故障可能导致删除失败
  • 部分成功: 部分文件清理成功,部分失败的情况

处理策略:

  • 记录失败的文件列表,下次清理时重试
  • 对于被占用的文件,先关闭日志写入,清理后再恢复
  • 提供"强制清理"选项,尝试强制删除被占用的文件
  • 清理失败时不回滚已清理的文件

5. 后续实现建议

5.1 状态管理 (Redux Slice)

建议创建 logCleanupSlice 来管理日志清理的状态。

5.1.1 Slice 结构

// src/store/slices/logCleanupSlice.ts

interface LogCleanupState {
  // 当前激活的Tab
  activeTab: 'auto' | 'manual';
  
  // 手动清理
  manual: {
    lastCleanupTime: string | null;  // 上次清理时间
    quickSelect: 'all' | 'recent3' | 'recent10' | 'custom';
    dateRange: {
      startDate: string | null;
      endDate: string | null;
    };
    status: 'idle' | 'confirming' | 'cleaning' | 'success' | 'failed';
    progress: {
      current: number;
      total: number;
      currentFile: string;
    } | null;
    result: {
      filesDeleted: number;
      spaceFreed: number;  // 单位:字节
      errorFiles: string[];
    } | null;
  };
  
  // 自动清理
  auto: {
    config: {
      timeBased: {
        enabled: boolean;
        cleanupInterval: number;  // 清理周期(天)
        retentionPeriod: number;  // 保存周期(天)
      };
      spaceBased: {
        enabled: boolean;
        threshold: number;  // 磁盘阈值(GB)
        retentionPeriod: number;  // 保存周期(天)
      };
    };
    originalConfig: any;  // 用于取消时恢复
    hasChanges: boolean;
    saving: boolean;
  };
}

5.1.2 Actions

// Tab切换
- setActiveTab: 设置当前激活的Tab

// 手动清理相关
- setQuickSelect: 设置快捷选择选项
- setDateRange: 设置自定义时间范围
- startManualCleanup: 开始手动清理
- updateCleanupProgress: 更新清理进度
- manualCleanupComplete: 手动清理完成
- manualCleanupFailed: 手动清理失败
- resetManualCleanup: 重置手动清理状态

// 自动清理相关
- fetchAutoCleanupConfig: 获取自动清理配置
- updateAutoCleanupConfig: 更新自动清理配置(本地状态)
- toggleTimeBased: 切换时间周期清理开关
- toggleSpaceBased: 切换磁盘空间清理开关
- saveAutoCleanupConfig: 保存自动清理配置到服务器
- cancelAutoCleanupConfig: 取消修改,恢复原始配置
- resetAutoCleanupConfig: 重置配置

5.2 API 接口设计

5.2.1 获取上次清理时间

// GET /api/system/log-cleanup/last-time
interface GetLastCleanupTimeResponse {
  lastCleanupTime: string | null;  // ISO 8601格式
}

5.2.2 手动清理接口

// POST /api/system/log-cleanup/manual
interface ManualCleanupRequest {
  mode: 'all' | 'recent3' | 'recent10' | 'custom';
  dateRange?: {
    startDate: string;  // YYYY-MM-DD
    endDate: string;    // YYYY-MM-DD
  };
}

interface ManualCleanupResponse {
  success: boolean;
  taskId: string;  // 用于WebSocket订阅进度
  errorMessage?: string;
}

// WebSocket: /ws/log-cleanup/progress/{taskId}
// 实时推送清理进度
interface CleanupProgressMessage {
  current: number;
  total: number;
  currentFile: string;
  status: 'cleaning' | 'success' | 'failed';
  result?: {
    filesDeleted: number;
    spaceFreed: number;
    errorFiles: string[];
  };
}

5.2.3 自动清理配置接口

// GET /api/system/log-cleanup/auto-config
interface GetAutoCleanupConfigResponse {
  timeBased: {
    enabled: boolean;
    cleanupInterval: number;
    retentionPeriod: number;
  };
  spaceBased: {
    enabled: boolean;
    threshold: number;
    retentionPeriod: number;
  };
}

// POST /api/system/log-cleanup/auto-config
interface SaveAutoCleanupConfigRequest {
  timeBased: {
    enabled: boolean;
    cleanupInterval: number;
    retentionPeriod: number;
  };
  spaceBased: {
    enabled: boolean;
    threshold: number;
    retentionPeriod: number;
  };
}

interface SaveAutoCleanupConfigResponse {
  success: boolean;
  errorMessage?: string;
}

5.3 组件实现建议

5.3.1 页面组件结构

src/pages/system/LogCleanup/
├── index.tsx                    // 主页面组件
├── ManualCleanup.tsx           // 手动清理Tab
├── AutoCleanup.tsx             // 自动清理Tab
└── components/
    ├── QuickSelectRadio.tsx    // 快捷选择单选组件
    ├── DateRangeSelector.tsx   // 时间范围选择组件
    ├── CleanupProgressModal.tsx // 清理进度弹窗组件
    └── AutoConfigForm.tsx      // 自动清理配置表单组件

5.3.2 主页面组件

// src/pages/system/LogCleanup/index.tsx
import { useState } from 'react';
import { Tabs } from 'antd';
import ManualCleanup from './ManualCleanup';
import AutoCleanup from './AutoCleanup';

const LogCleanup: React.FC = () => {
  const [activeTab, setActiveTab] = useState<'manual' | 'auto'>('manual');

  return (
    <div className="log-cleanup-page">
      <Tabs
        activeKey={activeTab}
        onChange={(key) => setActiveTab(key as 'manual' | 'auto')}
        items={[
          {
            key: 'manual',
            label: '手动清理',
            children: <ManualCleanup />
          },
          {
            key: 'auto',
            label: '自动清理',
            children: <AutoCleanup />
          }
        ]}
      />
    </div>
  );
};

export default LogCleanup;

5.3.3 手动清理组件

// src/pages/system/LogCleanup/ManualCleanup.tsx
import { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Radio, DatePicker, Button, Space, Typography, Modal } from 'antd';
import { ExclamationCircleOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import CleanupProgressModal from './components/CleanupProgressModal';

const { Text } = Typography;

const ManualCleanup: React.FC = () => {
  const dispatch = useDispatch();
  const { lastCleanupTime, quickSelect, dateRange, status } = useSelector(
    (state) => state.logCleanup.manual
  );

  const [localQuickSelect, setLocalQuickSelect] = useState(quickSelect);
  const [localDateRange, setLocalDateRange] = useState(dateRange);

  const handleQuickSelectChange = (value: string) => {
    setLocalQuickSelect(value);
    if (value !== 'custom') {
      setLocalDateRange({ startDate: null, endDate: null });
    }
  };

  const handleDateChange = (dates: any, field: 'startDate' | 'endDate') => {
    setLocalQuickSelect('custom');
    setLocalDateRange({
      ...localDateRange,
      [field]: dates ? dates.format('YYYY-MM-DD') : null
    });
  };

  const handleStartCleanup = () => {
    Modal.confirm({
      title: '确认清理',
      icon: <ExclamationCircleOutlined />,
      content: '日志清理操作不可恢复,确定要继续吗?',
      onOk: () => {
        dispatch(startManualCleanup({
          mode: localQuickSelect,
          dateRange: localQuickSelect === 'custom' ? localDateRange : undefined
        }));
      }
    });
  };

  const isCleanupDisabled = () => {
    if (localQuickSelect === 'custom') {
      return !localDateRange.startDate || !localDateRange.endDate;
    }
    return false;
  };

  return (
    <div className="manual-cleanup-tab">
      {/* 上次清理时间提示 */}
      {lastCleanupTime && (
        <div className="last-cleanup-info">
          <Text type="danger">
            提示: 上一次清理时间 {dayjs(lastCleanupTime).format('YYYY.MM.DD HH:mm')}
          </Text>
        </div>
      )}

      {/* 快捷选择 */}
      <div className="section">
        <div className="section-header">快捷选择</div>
        <Radio.Group
          value={localQuickSelect}
          onChange={(e) => handleQuickSelectChange(e.target.value)}
        >
          <Space direction="vertical">
            <Radio value="all">全部日志</Radio>
            <Radio value="recent3">保留近3天</Radio>
            <Radio value="recent10">保留近10天</Radio>
          </Space>
        </Radio.Group>
      </div>

      {/* 选择时间范围 */}
      <div className="section">
        <div className="section-header">选择时间范围</div>
        <Space size="large">
          <div className="form-item">
            <label>起始时间</label>
            <DatePicker
              value={localDateRange.startDate ? dayjs(localDateRange.startDate) : null}
              onChange={(date) => handleDateChange(date, 'startDate')}
              placeholder="请选择开始日期"
              format="YYYY-MM-DD"
              disabled={localQuickSelect !== 'custom'}
            />
          </div>
          <div className="form-item">
            <label>截止时间</label>
            <DatePicker
              value={localDateRange.endDate ? dayjs(localDateRange.endDate) : null}
              onChange={(date) => handleDateChange(date, 'endDate')}
              placeholder="请选择结束日期"
              format="YYYY-MM-DD"
              disabled={localQuickSelect !== 'custom'}
              disabledDate={(current) => {
                if (!localDateRange.startDate) return false;
                return current && current < dayjs(localDateRange.startDate);
              }}
            />
          </div>
        </Space>
      </div>

      {/* 操作按钮 */}
      <div className="footer-actions">
        <Button>取消</Button>
        <Button
          type="primary"
          onClick={handleStartCleanup}
          disabled={isCleanupDisabled()}
          loading={status === 'cleaning'}
        >
          开始清理
        </Button>
      </div>

      {/* 清理进度弹窗 */}
      {status !== 'idle' && <CleanupProgressModal />}
    </div>
  );
};

export default ManualCleanup;

5.3.4 自动清理配置组件

// src/pages/system/LogCleanup/AutoCleanup.tsx
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Form, Switch, InputNumber, Button, Divider, message } from 'antd';

const AutoCleanup: React.FC = () => {
  const dispatch = useDispatch();
  const { config, hasChanges, saving } = useSelector(
    (state) => state.logCleanup.auto
  );
  const [form] = Form.useForm();

  useEffect(() => {
    dispatch(fetchAutoCleanupConfig());
  }, [dispatch]);

  useEffect(() => {
    form.setFieldsValue(config);
  }, [config, form]);

  const handleSave = async () => {
    try {
      const values = await form.validateFields();
      dispatch(saveAutoCleanupConfig(values));
    } catch (error) {
      message.error('请检查表单输入');
    }
  };

  const handleCancel = () => {
    form.resetFields();
    dispatch(cancelAutoCleanupConfig());
  };

  const handleValuesChange = (changedValues: any, allValues: any) => {
    dispatch(updateAutoCleanupConfig(allValues));
  };

  return (
    <div className="auto-cleanup-tab">
      <Form
        form={form}
        layout="horizontal"
        labelCol={{ span: 8 }}
        wrapperCol={{ span: 16 }}
        onValuesChange={handleValuesChange}
      >
        {/* 按照时间周期清理 */}
        <div className="section">
          <div className="section-header">按照时间周期清理</div>
          
          <Form.Item
            label="按照时间周期清理"
            name={['timeBased', 'enabled']}
            valuePropName="checked"
          >
            <Switch />
          </Form.Item>

          <Form.Item
            label="清理周期(天)"
            name={['timeBased', 'cleanupInterval']}
            rules={[{ required: true, message: '请输入清理周期' }]}
          >
            <InputNumber
              min={1}
              max={999}
              precision={0}
              disabled={!form.getFieldValue(['timeBased', 'enabled'])}
              style={{ width: '100%' }}
            />
          </Form.Item>

          <Form.Item
            label="保存周期(天)"
            name={['timeBased', 'retentionPeriod']}
            rules={[{ required: true, message: '请输入保存周期' }]}
          >
            <InputNumber
              min={1}
              max={999}
              precision={0}
              disabled={!form.getFieldValue(['timeBased', 'enabled'])}
              style={{ width: '100%' }}
            />
          </Form.Item>
        </div>

        <Divider />

        {/* 按照磁盘空间清理 */}
        <div className="section">
          <div className="section-header">按照磁盘空间清理</div>
          
          <Form.Item
            label="按照磁盘空间清理"
            name={['spaceBased', 'enabled']}
            valuePropName="checked"
          >
            <Switch />
          </Form.Item>

          <Form.Item
            label="磁盘小于该值时进行清理(G)"
            name={['spaceBased', 'threshold']}
            rules={[{ required: true, message: '请输入磁盘阈值' }]}
          >
            <InputNumber
              min={1}
              max={999}
              precision={0}
              disabled={!form.getFieldValue(['spaceBased', 'enabled'])}
              style={{ width: '100%' }}
            />
          </Form.Item>

          <Form.Item
            label="保存周期(天)"
            name={['spaceBased', 'retentionPeriod']}
            rules={[{ required: true, message: '请输入保存周期' }]}
          >
            <InputNumber
              min={1}
              max={999}
              precision={0}
              disabled={!form.getFieldValue(['spaceBased', 'enabled'])}
              style={{ width: '100%' }}
            />
          </Form.Item>
        </div>
      </Form>

      {/* 操作按钮 */}
      <div className="footer-actions">
        <Button onClick={handleCancel} disabled={!hasChanges}>
          取消
        </Button>
        <Button
          type="primary"
          onClick={handleSave}
          loading={saving}
          disabled={!hasChanges}
        >
          保存
        </Button>
      </div>
    </div>
  );
};

export default AutoCleanup;

5.3.5 清理进度弹窗组件

// src/pages/system/LogCleanup/components/CleanupProgressModal.tsx
import { Modal, Progress, Typography, Alert } from 'antd';
import { useSelector, useDispatch } from 'react-redux';

const { Text } = Typography;

const CleanupProgressModal: React.FC = () => {
  const dispatch = useDispatch();
  const { status, progress, result } = useSelector(
    (state) => state.logCleanup.manual
  );

  const handleClose = () => {
    dispatch(resetManualCleanup());
  };

  const getTitle = () => {
    switch (status) {
      case 'cleaning': return '正在清理日志...';
      case 'success': return '清理完成';
      case 'failed': return '清理失败';
      default: return '日志清理';
    }
  };

  const formatSize = (bytes: number) => {
    if (bytes < 1024) return `${bytes} B`;
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
    if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
    return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
  };

  return (
    <Modal
      title={getTitle()}
      open={status !== 'idle'}
      onCancel={handleClose}
      footer={status === 'cleaning' ? null : undefined}
      closable={status !== 'cleaning'}
      maskClosable={false}
    >
      {status === 'cleaning' && progress && (
        <div>
          <Progress
            percent={Math.round((progress.current / progress.total) * 100)}
            status="active"
          />
          <Text type="secondary" style={{ marginTop: 8, display: 'block' }}>
            正在清理: {progress.currentFile} ({progress.current}/{progress.total})
          </Text>
        </div>
      )}

      {status === 'success' && result && (
        <Alert
          type="success"
          message="清理成功"
          description={
            <div>
              <p>已清理 {result.filesDeleted} 个日志文件</p>
              <p>释放磁盘空间: {formatSize(result.spaceFreed)}</p>
              {result.errorFiles.length > 0 && (
                <p style={{ color: '#ff4d4f' }}>
                  {result.errorFiles.length} 个文件清理失败
                </p>
              )}
            </div>
          }
          showIcon
        />
      )}

      {status === 'failed' && (
        <Alert
          type="error"
          message="清理失败"
          description="清理过程中发生错误,请稍后重试"
          showIcon
        />
      )}
    </Modal>
  );
};

export default CleanupProgressModal;

5.4 关键交互流程

5.4.1 手动清理流程

1. 用户选择清理范围
   ├─> 选择快捷选项(全部/近3天/近10天)
   └─> 或自定义时间范围

2. 用户点击"开始清理"
   ├─> 验证选择是否有效(自定义模式需要起止时间都填写)
   ├─> 弹出确认对话框
   └─> 用户确认后开始清理

3. 执行清理
   ├─> dispatch(startManualCleanup({ mode, dateRange }))
   ├─> 调用 API: POST /api/system/log-cleanup/manual
   ├─> 获取taskId,建立WebSocket连接
   ├─> 显示进度弹窗
   └─> 实时更新清理进度

4. 清理完成
   ├─> 显示清理结果(文件数、释放空间)
   ├─> 更新上次清理时间
   ├─> 记录到操作日志
   └─> 用户关闭弹窗

5.4.2 自动清理配置流程

1. 页面加载
   ├─> dispatch(fetchAutoCleanupConfig())
   ├─> 调用 API: GET /api/system/log-cleanup/auto-config
   └─> 填充表单默认值

2. 用户修改配置
   ├─> 切换开关/修改输入框
   ├─> dispatch(updateAutoCleanupConfig(newConfig))
   ├─> 设置hasChanges=true
   └─> 启用保存按钮

3. 用户保存配置
   ├─> 验证表单
   ├─> dispatch(saveAutoCleanupConfig(config))
   ├─> 调用 API: POST /api/system/log-cleanup/auto-config
   ├─> 显示保存成功消息
   └─> 设置hasChanges=false

4. 用户取消修改
   ├─> dispatch(cancelAutoCleanupConfig())
   ├─> form.resetFields()
   └─> 恢复到原始配置

5. 开关联动
   ├─> 监听开关值变化
   ├─> 开关关闭时禁用相关输入框
   └─> 开关打开时启用相关输入框

5.5 样式建议

// src/pages/system/LogCleanup/styles.scss

.log-cleanup-page {
  height: 100%;
  padding: 16px;
  
  .ant-tabs {
    height: 100%;
    
    .ant-tabs-content {
      height: calc(100% - 46px);
    }
  }
}

// 手动清理Tab
.manual-cleanup-tab {
  display: flex;
  flex-direction: column;
  height: 100%;
  gap: 24px;
  
  .last-cleanup-info {
    padding: 12px;
    background: #fff7e6;
    border: 1px solid #ffd591;
    border-radius: 4px;
  }
  
  .section {
    background: #fff;
    padding: 16px;
    border-radius: 4px;
    
    .section-header {
      font-size: 16px;
      font-weight: 500;
      margin-bottom: 16px;
      color: #262626;
    }
  }
  
  .form-item {
    display: flex;
    flex-direction: column;
    gap: 8px;
    
    label {
      font-size: 14px;
      color: #595959;
    }
  }
  
  .footer-actions {
    margin-top: auto;
    display: flex;
    justify-content: flex-end;
    gap: 12px;
    padding: 16px;
    background: #fff;
    border-top: 1px solid #f0f0f0;
  }
}

// 自动清理Tab
.auto-cleanup-tab {
  display: flex;
  flex-direction: column;
  height: 100%;
  
  .section {
    background: #fff;
    padding: 16px;
    border-radius: 4px;
    margin-bottom: 16px;
    
    .section-header {
      font-size: 16px;
      font-weight: 500;
      margin-bottom: 16px;
      color: #262626;
    }
  }
  
  .ant-divider {
    margin: 24px 0;
  }
  
  .footer-actions {
    margin-top: auto;
    display: flex;
    justify-content: flex-end;
    gap: 12px;
    padding: 16px;
    background: #fff;
    border-top: 1px solid #f0f0f0;
  }
}

5.6 WebSocket 连接管理

// src/services/logCleanupWebSocket.ts
import { store } from '@/store';
import { updateCleanupProgress, manualCleanupComplete, manualCleanupFailed } from '@/store/slices/logCleanupSlice';

class LogCleanupWebSocket {
  private ws: WebSocket | null = null;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 3;

  connect(taskId: string) {
    const wsUrl = `ws://localhost:8080/ws/log-cleanup/progress/${taskId}`;
    this.ws = new WebSocket(wsUrl);

    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      
      if (message.status === 'cleaning') {
        store.dispatch(updateCleanupProgress({
          current: message.current,
          total: message.total,
          currentFile: message.currentFile
        }));
      } else if (message.status === 'success') {
        store.dispatch(manualCleanupComplete(message.result));
        this.close();
      } else if (message.status === 'failed') {
        store.dispatch(manualCleanupFailed());
        this.close();
      }
    };

    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error);
      this.reconnect(taskId);
    };

    this.ws.onclose = () => {
      console.log('WebSocket closed');
    };
  }

  reconnect(taskId: string) {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++;
      setTimeout(() => this.connect(taskId), 2000 * this.reconnectAttempts);
    }
  }

  close() {
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
    this.reconnectAttempts = 0;
  }
}

export default new LogCleanupWebSocket();

5.7 错误处理建议

// 表单验证错误
- 自定义时间范围: 起始时间晚于截止时间
- 输入值超出范围: min=1, max=999
- 必填项未填写: 保存配置时检查

// API调用错误
- 网络错误: 显示"网络连接失败,请检查网络"
- 服务器错误: 显示"服务器错误,请稍后重试"
- 权限错误: 显示"权限不足,无法执行操作"

// 清理操作错误
- 文件占用: 记录失败文件,显示在结果中
- 磁盘错误: 中止清理,显示错误信息
- 部分成功: 显示成功和失败的文件数

// WebSocket错误
- 连接失败: 自动重连(最多3次)
- 消息丢失: 使用轮询API补充进度
- 连接断开: 提示用户刷新页面或重试

5.8 性能优化建议

1. 配置表单防抖
   - 输入框onChange事件使用防抖(300ms)
   - 减少频繁的状态更新

2. WebSocket消息节流
   - 进度更新消息限制频率(最多500ms一次)
   - 避免UI频繁重绘

3. 大文件清理优化
   - 批量删除文件(每批100个)
   - 在批次间添加延迟,避免阻塞

4. 配置缓存
   - 自动清理配置缓存5分钟
   - 减少重复请求

5. 惰性加载
   - Tab内容按需渲染
   - 减少初始加载时间

6. 总结

日志清理页面是系统维护的重要功能,设计时重点考虑:

  1. 双模式设计: 手动清理和自动清理分离,满足不同场景需求
  2. 快捷操作: 预设常用时间范围,提升手动清理效率
  3. 灵活配置: 支持时间周期和磁盘空间双重自动清理策略
  4. 实时反馈: WebSocket实时推送清理进度,提升用户体验
  5. 安全保障: 清理前确认、进度可视化、结果详细反馈
  6. 配置管理: 自动清理配置可取消、可重置,防止误操作

通过合理的表单设计、状态管理和WebSocket通信,可以实现一个功能完善、交互流畅的日志清理系统。