Files
workutils/docs/audit/plugin-ui-code-audit.md

18 KiB
Raw Permalink Blame History

WorkUtils 插件 UI 与代码可维护性审查

审查日期:2026-07-30

1. 结论

当前产品已经形成统一的左侧导航、颜色和基础按钮风格,但各插件仍是“每个 Window 自己搭 UI、自己开线程、自己管理 loading、自己处理错误”的实现方式。视觉统一停留在颜色层,交互状态和代码结构没有统一。

主要风险:

  1. Window 类过大:8 个窗口平均约 824 行,MobileUiAutomationWindow 达 1702 行。
  2. 异步状态没有统一模型:存在 new Thread、Task、CompletableFuture、布尔字段和手动 Platform.runLater 等多套实现。
  3. loading 行为不一致:有的只禁用按钮,有的改文字,有的显示旋转图标,有的同时显示停止按钮;异常分支容易漏恢复状态。
  4. UI 直接暴露敏感配置:Git 远程 URL 显示账号,数据库生成器显示完整 JDBC URL 和用户名。
  5. 操作层级不清:刷新、执行、交换、终止、删除等按钮经常处于同一视觉层级。
  6. 空状态普遍只是大片空白,用户无法判断是“尚未执行”“没有数据”还是“执行失败”。
  7. UI、业务编排、文件操作、历史记录和线程控制混在同一个类里,AI 修改局部功能时需要理解整个窗口,误改概率高。

建议先建设公共 UI/异步基础设施,再逐插件迁移。不要同时重写所有插件。

2. 统一设计与代码基线

2.1 页面结构

所有插件统一为:

  1. PageHeader:标题、简短说明、帮助入口。
  2. ConfigCard:输入与选项。
  3. ActionBar:一个主操作、零到两个次操作、一个条件显示的取消操作。
  4. OperationStatus:阶段、进度、耗时、当前处理对象。
  5. ResultArea:结果、空状态、失败状态。
  6. HistoryTable:历史记录放在独立 Tab 或折叠区,不抢占主任务空间。

2.2 按钮状态

按钮不应由业务代码直接反复调用 setText、setDisable、setGraphic。统一使用状态:

public enum OperationState {
    IDLE, VALIDATING, RUNNING, CANCELLING, SUCCEEDED, FAILED
}

统一按钮规范:

状态 主按钮 取消按钮 输入区
IDLE 可用,显示动作名称 隐藏 可编辑
VALIDATING spinner +“校验中” 隐藏 禁用
RUNNING spinner + 阶段名称 显示 禁用
CANCELLING “正在停止…” 禁用 禁用
SUCCEEDED 恢复动作名称 隐藏 可编辑
FAILED 恢复动作名称 隐藏 可编辑

建议增加公共组件:

public final class AsyncAction {
    private final ReadOnlyObjectWrapper<OperationState> state =
            new ReadOnlyObjectWrapper<>(OperationState.IDLE);
    private final StringProperty message = new SimpleStringProperty("");
    private final DoubleProperty progress = new SimpleDoubleProperty(-1);

    public <T> void run(
            Supplier<T> work,
            Consumer<T> onSuccess,
            Consumer<Throwable> onFailure) {
        if (state.get() == OperationState.RUNNING) return;
        state.set(OperationState.RUNNING);

        Task<T> task = new Task<>() {
            @Override protected T call() { return work.get(); }
        };
        task.setOnSucceeded(e -> finish(() -> onSuccess.accept(task.getValue())));
        task.setOnFailed(e -> finish(() -> onFailure.accept(task.getException())));
        AppExecutors.io().execute(task);
    }

    private void finish(Runnable callback) {
        try {
            callback.run();
        } finally {
            state.set(OperationState.IDLE);
        }
    }
}

LoadingButtonBinder 只负责展示:

LoadingButtonBinder.bind(
        exportButton,
        exportAction.stateProperty(),
        "开始导出",
        "导出中…"
);

这样异常、提前返回和取消都不会遗漏恢复按钮。

2.3 推荐的插件目录边界

biz/<plugin>/
  <Plugin>.java                 # 插件元数据和装配
  application/
    <UseCase>.java              # 一次用户任务的编排
    <Request>.java
    <Result>.java
  domain/
    ...                         # 纯业务模型和规则
  infrastructure/
    ...                         # Git、DB、文件、进程等外部访问
  ui/
    <Plugin>Pane.java           # 只组装区域
    <Plugin>ViewModel.java      # 可观察状态与命令
    components/
      ...

限制规则:

  • Pane 不直接创建线程、访问文件、调用 Git/DB 或拼接业务结果。
  • ViewModel 不创建 JavaFX 节点。
  • UseCase 不依赖 JavaFX。
  • 表格 cell 不直接执行文件操作,调用明确的 command。
  • 单文件建议不超过 350 行;超过 500 行必须拆分。
  • 每个异步操作只有一个拥有者,禁止窗口和 Service 同时管理线程生命周期。

3. 主框架

证据:screenshots/00-home.png

UI 问题

  • 欢迎页信息价值低,大片空白;没有最近任务、常用插件或配置健康状态。
  • 顶部只有重复的插件图标和名称,无法承载状态、帮助或全局通知。
  • 导航图标来源不统一,部分图标显示风格和宽度不一致。
  • 插件说明只存在元数据中,导航和页面内没有有效呈现。

重构

  • 欢迎页改为“最近使用 + 配置状态 + 快捷入口”,不要重复罗列功能名称。
  • MainWindow 只负责路由与壳层;拆出 NavigationPane、WorkspaceHeader、WelcomePane。
  • 增加统一 NotificationCenter,插件错误使用页内 banner 或 toast,不要各自弹 Alert。
  • 用图标库统一图标,不再把 Emoji 当 UI 图标。

4. Git 仓库同步

证据:screenshots/01-git-sync.png

UI 问题

  • 项目表直接展示含账号的远程 URL,既敏感又占据大量宽度。
  • “Git …”列标题被截断,列宽策略失效。
  • 69 个项目没有搜索、状态筛选和批量选择反馈。
  • “同步执行”和“损坏仓库处理”以 Tab 分割,但用户无法看到同步任务与项目选择的关系。
  • loading 主要靠禁用“同步”和启用“停止”,缺少总进度、当前项目、成功/失败计数。

设计

  • 项目列表顶部:搜索、状态筛选、已选择 N 项。
  • URL 默认显示 host/group/repo,账号信息掩码;完整值仅通过“复制地址”获得。
  • 同步操作采用固定底部 ActionBar:同步选中项目 + 条件显示 停止。
  • 运行时展示:总进度、当前仓库、成功/跳过/失败计数;失败项留在结果列表。
  • “损坏仓库处理”作为失败结果的后续动作,而不是并列一级 Tab。

代码重构

  • GitSyncWindow 拆为 ProjectTablePane、SyncProgressPane、DamagedRepoPane。
  • 从 GitSyncPlugin 中迁出 Git 命令、进程输出和 URL 解析,形成 GitRepositoryGateway。
  • SyncRepositoriesUseCase 返回结构化事件,不向 UI 传原始日志字符串:
sealed interface SyncEvent {
    record Started(int total) implements SyncEvent {}
    record RepositoryStarted(String repository, int index) implements SyncEvent {}
    record RepositoryFinished(String repository, SyncOutcome outcome) implements SyncEvent {}
    record Completed(SyncSummary summary) implements SyncEvent {}
}

5. Git 分支评审

证据:screenshots/02-git-review.png

UI 问题

  • 页面信息密度过高,“粘贴识别”“手动字段”“IDEA 命令”“执行日志”同时出现。
  • 主按钮“准备评审”含义不明确,实际上会拉取、切分支并打开 IDEA,副作用较大。
  • 来源/目标依靠 A、B 解释,用户仍容易反向操作。
  • 运行时 runButton 和 historyOpenButton 被同一 running 状态控制,两个无关动作耦合。
  • 日志区默认空白,缺少阶段式反馈。

设计

  • 改为三步:①识别评审信息;②确认仓库和分支方向;③执行并查看结果。
  • 主按钮文案明确为“切换到目标分支并打开评审”。
  • 在执行前展示摘要:来源 A → 目标 B、将执行的 Git 操作、工作区是否干净。
  • 每个操作拥有独立状态:识别、刷新、执行、打开历史互不锁死。

代码重构

  • ReviewTextParser 独立且纯函数化。
  • GitReviewViewModel 持有表单和 reviewAction。
  • GitReviewService 拆成 GitStatusReader、BranchPreparationService、IdeaLauncher。
  • 用 ReviewPlan 表达执行前计划,使 UI 可预览,也便于单测。

6. 文本对比

证据:screenshots/03-diff.png

UI 问题

  • 空状态完全空白,没有示例或输入提示。
  • 结果区在未比较时仍占据大半页面。
  • “开始对比”对于本地 CPU 操作没有 loading;大文本可能冻结 JavaFX 线程。
  • 上一处/下一处在无结果时只是灰色,未解释差异数量。
  • 原文/对比文缺少文件导入、编码和换行符信息。

设计

  • 输入区增加拖入文件/粘贴入口和字符、行数统计。
  • 首次使用显示简短示例;比较后将结果区扩展。
  • ActionBar 显示 发现 N 处差异,导航按钮显示当前位置 3/18。
  • 大文本比较进入后台任务,按钮显示“比较中…”,并支持取消。

代码重构

  • 当前 DiffWindow 同时包含 LCS、字符分段、HTML 渲染和 UI,应拆为:
    • DiffEngine
    • DiffResult
    • DiffHtmlRenderer
    • DiffViewModel
    • DiffPane
  • DiffResult 保留结构化行和字符片段,UI 不再解析 HTML 来导航。

7. MyBatis SQL 导出

证据:screenshots/04-mybatis-export.png

UI 问题

  • “执行日志”在未执行时是空白大框;历史表反而成为页面视觉重点。
  • “导出/终止”始终并列,终止按钮即使禁用也持续占位。
  • 缺少项目范围、目标目录和预估数量摘要。
  • 历史表列标题被截断,文件名过长,核心信息难扫描。
  • loading 有进度文本,但没有进度条和错误计数。

设计

  • 配置卡显示过滤条件、项目范围、输出目录。
  • 主按钮为“导出 SQL”;运行后才出现“停止”。
  • 状态区显示 12/69、当前项目、已导出 SQL 数、错误数。
  • 历史记录放第二 Tab,文件名单行省略,Tooltip 展示完整路径。

代码重构

  • MyBatisExportWindow 和 ScannerWindow 的历史表、打开/目录菜单高度重复,提取 ExportHistoryPane<T>。
  • 用 ExportRequest 替代多个散落参数。
  • MyBatisExportService 通过 ProgressSink<ExportProgress> 上报结构化进度。

8. MyBatis 逆向工程

证据:screenshots/05-mybatis-generator.png

UI 问题

  • 顶部展示完整 JDBC URL 和用户名,存在泄露风险。
  • 左侧项目树与右侧表查询、输出目录并排,用户任务顺序不清。
  • 三个输出路径通常相关,却要求逐项浏览/填充。
  • “取消”实际是关闭窗口,在嵌入模式下语义不成立。
  • 查询表和生成操作没有统一 loading,也缺少连接失败后的就地恢复。

设计

  • 分成 ①数据源 ②选择表 ③生成配置 ④确认生成 四个区域或步骤。
  • 数据源仅显示别名、数据库名、主机掩码和连接状态,完整连接串只在设置页维护。
  • 输出目录选择一个项目根目录,再实时预览三个派生目录。
  • “生成”前显示将覆盖/创建的文件摘要;完成后展示文件列表和打开目录。
  • 嵌入模式去掉“取消”,改为“重置配置”。

代码重构

  • GeneratorFormState 统一持有表选择与目录规则。
  • DatasourceSummary 是脱敏 DTO,UI 禁止接触密码和原始 URL。
  • QueryTablesUseCase 与 GenerateArtifactsUseCase 使用两个独立 AsyncAction。
  • 路径推导放入 OutputLayoutPolicy,避免三个按钮各自维护逻辑。

9. 扫描功能

证据:screenshots/06-scanner.png

UI 问题

  • 页面写“扫描类型:关键词扫描”,但描述称支持关键词/Redis/Dubbo,入口和能力不一致。
  • 正则匹配只是一个 Checkbox,缺少风险提示和表达式校验。
  • 当前状态只显示“就绪”,位置和视觉权重过低。
  • 历史表抢占主页面,空白结果/当前任务没有清晰区分。
  • 开始扫描只禁用按钮,不展示 spinner、进度或取消能力。

设计

  • 扫描类型使用单选卡或下拉,并根据类型切换专属字段。
  • 正则模式提供即时校验和示例。
  • 执行区域展示扫描目录、项目数、匹配数和当前项目。
  • 当前结果与历史记录分 Tab。
  • 主按钮 loading 文案为“扫描中 12/69”,提供取消。

代码重构

  • 将 ScanConfig 升级为按类型区分的 sealed request,避免大量可空字段。
  • ScannerService 不使用默认 CompletableFuture 线程池,统一注入受控 Executor。
  • ScannerWindow 与历史文件操作解耦,复用公共历史组件。

10. API 接口文档导出

证据:screenshots/07-api-doc.png

UI 问题

  • 三张模式卡占据大量空间,但下方关键输入没有标签,依赖 placeholder。
  • “全限定名”“批量导出”的说明文字对非作者用户不够明确。
  • 主按钮横跨整个页面,视觉重量过大。
  • 导出配置、导出历史、基类配置同属一级 Tab,但基类配置是低频系统设置。
  • 文件/目录选择也使用 loading,操作通常瞬时完成,产生假的忙碌感。
  • loading 通过节点属性保存旧文字,且部分代码再次直接覆盖文字,状态来源不唯一。

设计

  • 模式切换改为紧凑 Segmented Control。
  • 每种模式只显示相关字段,并有明确 Label、示例和校验错误。
  • 主按钮右对齐,显示导出范围摘要。
  • 基类配置移入系统设置或高级设置。
  • 只有解析/生成过程进入 loading;文件选择不显示 spinner。
  • 结果页显示生成文件、接口数、方法数、耗时和“打开/复制路径”。

代码重构

  • ApiDocWindow 拆成 ExportConfigPane、ExportHistoryPane、BaseClassConfigPane。
  • ExportMode 各自实现 request 构造,避免 startExport 中按模式分支。
  • ApiDocExportUseCase 负责 parse/generate/history,UI 只提交 ApiDocExportRequest。
  • 删除窗口内 setBtnLoading,接入统一 AsyncAction。
  • HTML 调试代理作为单独能力开关,不在生成 HTML 时隐式启动。

11. 手机 UI 自动化

运行时当前禁用,结论基于源码;不能作为截图可验证的 UI 结论。

主要问题

  • MobileUiAutomationWindow 1702 行、67 个方法、30 个 action、36 处线程/回调相关代码,是最高风险文件。
  • 环境安装、设备管理、用例运行、录制、截图轮询、步骤编辑、报告历史全部集中在一个类。
  • 使用 isRunning、按钮文字和内联 style 共同表达状态,容易不一致。
  • 截图轮询拥有独立线程,窗口生命周期与线程停止逻辑难验证。
  • “运行/停止”和“开始录制/停止并生成用例”通过改文字复用按钮,状态机不明确。

设计

  • 一级拆为“运行用例”“录制用例”“环境与设备”三个页面。
  • 顶部设备条只显示当前设备和健康状态;安装工具放入环境页。
  • 运行页显示用例、参数、实时步骤和报告;录制页聚焦截图与步骤列表。
  • 运行和录制使用显式状态机,停止按钮独立出现。

代码重构

至少拆成:

mobileuiauto/ui/
  MobileAutomationPane.java
  DeviceBar.java
  run/RunCasePane.java
  run/RunCaseViewModel.java
  record/RecordCasePane.java
  record/RecordCaseViewModel.java
  environment/EnvironmentPane.java
  environment/EnvironmentViewModel.java

新增两个状态机:

enum RunState { IDLE, STARTING, RUNNING, STOPPING, SUCCEEDED, FAILED }
enum RecordState { IDLE, STARTING, RECORDING, GENERATING, FAILED }

截图轮询封装为 ScreenshotStream extends AutoCloseable,由录制 ViewModel 持有并在页面离开时关闭。

12. 系统设置

UI 问题

  • Tab 数量多但缺少分类,配置文件编辑依赖外部编辑器。
  • “重新加载”的反馈不明确。
  • 数据源、项目路径、插件管理本质是结构化配置,不应主要依赖文本文件。

重构

  • 分类为“常规 / 项目与数据源 / 插件 / 外观”。
  • 配置项使用表单并提供校验、保存成功状态和恢复默认。
  • 所有敏感字段支持显示/隐藏;列表页面只使用脱敏 DTO。

13. 建议实施顺序

  1. 建立 OperationState、AsyncAction、LoadingButtonBinder、OperationStatusPane。
  2. 提取 ExportHistoryPane、EmptyStatePane、NotificationCenter。
  3. 先迁移 Scanner 和 MyBatis SQL 导出,验证公共组件。
  4. 迁移 API 文档导出和 MyBatis 逆向工程。
  5. 拆分 Git 同步和 Git 评审。
  6. 抽离 DiffEngine。
  7. 最后单独重构手机 UI 自动化,不与其他插件并行大改。

14. AI 友好编码约束

  • 每个需求先定位到一个 UseCase 和一个 ViewModel,避免直接修改 Window。
  • 不允许新增裸 new Thread。
  • 不允许在业务代码直接调用 Button#setText/setDisable/setGraphic。
  • 不允许使用 Node#getProperties() 保存业务状态。
  • 不允许在 Window 中出现 Git、SQL、HTTP、进程命令或文件遍历。
  • 所有跨线程结果必须是不可变 DTO。
  • 所有状态使用 enum,不使用多个 boolean 拼状态。
  • 每个 ViewModel 为每个 action 提供独立状态,禁止一个 running 锁住无关按钮。
  • 为 Request 校验、状态迁移、UseCase 成功/失败/取消路径编写单元测试。
  • UI 测试只验证绑定和可见性,不重复测试业务逻辑。

15. 证据范围

已实际捕获并复核:主界面、Git 同步、Git 分支评审、文本对比、MyBatis SQL 导出、MyBatis 逆向工程、扫描、API 文档导出。

未实际触发具有副作用的同步、切分支、数据库查询、生成、导出和删除操作,因此 loading、取消、错误恢复的结论同时参考了源码。手机 UI 自动化因当前配置禁用,仅完成源码审查。截图不能证明完整键盘访问、焦点顺序、屏幕阅读器语义或颜色对比合规,这些需要专项测试。