Files
halo-dark-mode-plugin/设计文档.md
T
Serendipity edbf78f236 feat: 初始化 Halo 暗色模式插件
- Halo Plugin 后端(Java/Gradle),含 DarkModePlugin 主类和测试
- Vue 3 + TypeScript 前端 UI,包含主题切换组件和设置页面
- 暗色模式 CSS 变量和覆盖样式(布局/编辑器/表单/滚动条等)
- 设计文档和调查文档
- Halo 插件/主题开发 Agent Skills

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 21:03:00 +08:00

33 KiB
Raw Blame History

Halo 黑暗模式插件 — 设计文档

版本:v0.2.0-draft(基于 create-halo-plugin + dev-skills 调查更新) 日期:2026-08-06 状态:待审阅 上一步:调查文档(含 0.4 节补充调查更新)


目录

  1. 设计目标与范围
  2. 技术架构
  3. CSS 变量体系设计
  4. 黑暗模式调色板
  5. 组件覆盖策略
  6. 切换器 UI 设计
  7. 路由与菜单
  8. 偏好持久化
  9. 项目文件结构
  10. 实现阶段划分
  11. 测试策略
  12. 兼容性矩阵

1. 设计目标与范围

1.1 核心目标

将 Halo 后台管理面板(Console)从纯浅色模式改造为支持浅色/黑暗双模式,不修改 Halo 核心代码,完全通过插件机制实现。

1.2 范围界定

范围 包含 不包含
页面 Halo Console(后台管理)全体页面 用户中心 (uc-src)、前台主题
组件 Halo 核心组件 + @halo-dev/components 组件库 第三方插件自有 UI
编辑器 FormKit 表单 + 富文本编辑器 编辑器内容区自定义样式
模式 浅色 ↔ 黑暗手动切换 + 跟随系统 定时切换、多主题

1.3 非功能性目标

  • 性能CSS 变量切换应 < 50ms,无可见闪烁(FOUC
  • 可访问性:黑暗模式下所有文本满足 WCAG AA 对比度要求(≥ 4.5:1
  • 兼容性:支持 Halo ≥ 2.23.0(对应 plugin-starter 的版本约束)
  • 可维护性:CSS 变量体系命名清晰,一个语义变量对应一个视觉属性

1.4 反目标(明确不做)

  • 不美化 UI(不改变布局、圆角、间距、字体等)
  • 不添加任何视觉装饰效果
  • 不修改 Halo 组件库源码
  • 不支持前台主题的暗色化

2. 技术架构

2.1 整体架构图

┌──────────────────────────────────────────────────────────┐
│                    插件边界                                │
│                                                          │
│  ┌─────────────┐    ┌──────────────────────────────────┐ │
│  │  Java 后端   │    │        前端 (ui/)                 │ │
│  │             │    │                                  │ │
│  │ BasePlugin  │    │  ┌────────────────────────────┐  │ │
│  │  ├ start()  │    │  │  index.ts (definePlugin)    │  │ │
│  │  └ stop()   │    │  │  ├ components: {           │  │ │
│  │             │    │  │  │    ThemeToggle           │  │ │
│  │ (极简骨架)   │    │  │  │  }                      │  │ │
│  └─────────────┘    │  │  ├ routes: [设置页面]       │  │ │
│                     │  │  └ extensionPoints: {}     │  │ │
│                     │  └────────────────────────────┘  │ │
│                     │                                  │ │
│                     │  ┌────────────────────────────┐  │ │
│                     │  │  composables/              │  │ │
│                     │  │  ├ useDarkMode.ts          │  │ │
│                     │  │  └ useSystemPreference.ts  │  │ │
│                     │  └────────────────────────────┘  │ │
│                     │                                  │ │
│                     │  ┌────────────────────────────┐  │ │
│                     │  │  styles/                   │  │ │
│                     │  │  ├ variables.css           │  │ │
│                     │  │  ├ dark-theme.css          │  │ │
│                     │  │  ├ overrides/              │  │ │
│                     │  │  │  ├ layout.css           │  │ │
│                     │  │  │  ├ components.css       │  │ │
│                     │  │  │  ├ formkit.css          │  │ │
│                     │  │  │  ├ editor.css           │  │ │
│                     │  │  │  └ scrollbar.css        │  │ │
│                     │  │  └ index.css               │  │ │
│                     │  └────────────────────────────┘  │ │
│                     └──────────────────────────────────┘ │
│                                                          │
│  ┌──────────────────────────────────────────────────┐    │
│  │  注入方式(Halo 插件加载机制自动处理)               │    │
│  │  CSS → /apis/.../ui-plugins/-/bundle.css          │    │
│  │  JS  → /apis/.../ui-plugins/-/bundle.js           │    │
│  └──────────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────────┘

2.2 运行时数据流

                    ┌──────────────┐
                    │  App 启动     │
                    └──────┬───────┘
                           │
                    ┌──────▼───────┐
                    │ 读取持久化偏好 │
                    │ localStorage │
                    │ (默认: system)│
                    └──────┬───────┘
                           │
              ┌────────────┼────────────┐
              ▼            ▼            ▼
        ┌─────────┐  ┌─────────┐  ┌─────────┐
        │ 浅色    │  │ 黑暗    │  │ 跟随系统 │
        │ theme=  │  │ theme=  │  │ theme=  │
        │ "light" │  │ "dark"  │  │ "auto"  │
        └────┬────┘  └────┬────┘  └────┬────┘
             │            │            │
             │            │      ┌─────▼──────┐
             │            │      │ 监听 match │
             │            │      │ Media query│
             │            │      └─────┬──────┘
             │            │            │
             └────────────┼────────────┘
                          │
                   ┌──────▼───────┐
                   │ 设置         │
                   │ document     │
                   │ .documentEl  │
                   │ 的 data attr │
                   │ data-halo-   │
                   │ theme="dark" │
                   │ 或移除该属性  │
                   └──────┬───────┘
                          │
                   ┌──────▼───────┐
                   │ CSS 变量切换  │
                   │ :root vs     │
                   │ [data-halo-  │
                   │ theme="dark"]│
                   └──────────────┘

2.3 关键技术决策

决策点 选择 理由
主题切换方式 data-halo-theme 属性 前缀避免冲突,属性选择器高效
颜色系统 CSS Variables + OKLCH 颜色空间 感知均匀,暗色模式天然适配
切换状态管理 Vue composable (useDarkMode) 轻量,无 Pinia 依赖,方便跨组件复用
持久化存储 localStorage 极简,零后端依赖,立即可用
系统偏好监听 matchMedia('prefers-color-scheme: dark') 标准 API,所有现代浏览器支持
初始加载防闪烁 <script> 阻塞渲染提前设置属性 避免 FOUC
样式注入方式 依赖 Halo 插件 CSS bundle 自动加载 无需额外代码
脚手架工具 pnpm create halo-plugin 官方推荐,替代 plugin-starter
definePlugin 导入 @halo-dev/ui-shared create-halo-plugin 模板使用的正确源
BOM 版本 2.25.0 最新稳定版(非 SNAPSHOT
DevTools 0.8.0 create-halo-plugin 模板版本
UI 产物路径 resources/main/ui/ create-halo-plugin 模板约定
路由加载 () => import(...) 懒加载 代码分割,提升首屏性能
开发参考 halo-plugin-dev skill 官方 AI Agent 开发文档

3. CSS 变量体系设计

3.1 设计原则

  1. 语义优先:变量名表达意图(--halo-bg-sidebar),而非颜色值(--halo-gray-900
  2. 完全覆盖:每个被 Halo 使用的视觉属性都应有对应变量
  3. 按区域分层:布局 → 组件 → 表单 → 编辑器 → 滚动条
  4. OKLCH 颜色空间:所有颜色值使用 oklch(L C H) 格式,保证感知均匀

3.2 变量命名规范

--halo-{category}-{property}

category:
  bg       背景色
  text     文字色
  border   边框色
  accent   强调色(primary/danger/warning/success
  shadow   阴影
  scroll   滚动条

property:
  primary / secondary / tertiary   主要/次要/三级
  hover / active / disabled        交互状态
  sidebar / header / content       区域限定

3.3 完整变量清单

/* ============================================================
   Halo Dark Mode — CSS Variables Definition
   ============================================================ */

/* ---------- 基础背景 ---------- */
--halo-bg-body            /* 页面底色 */
--halo-bg-sidebar         /* 侧边栏背景 */
--halo-bg-content         /* 内容区背景 */
--halo-bg-card            /* 卡片/面板背景 */
--halo-bg-input           /* 输入框背景 */
--halo-bg-hover           /* 通用悬停背景 */
--halo-bg-active          /* 通用激活背景 */
--halo-bg-disabled        /* 禁用态背景 */
--halo-bg-tooltip         /* 提示框背景 */
--halo-bg-modal           /* 弹窗遮罩 */
--halo-bg-dropdown        /* 下拉菜单背景 */

/* ---------- 文字颜色 ---------- */
--halo-text-primary       /* 主要文字 */
--halo-text-secondary     /* 次要文字(描述、元信息) */
--halo-text-tertiary      /* 三级文字(占位符、禁用文字) */
--halo-text-link          /* 链接文字 */
--halo-text-inverse       /* 反色文字(深色背景上) */

/* ---------- 边框 ---------- */
--halo-border-base        /* 默认边框 */
--halo-border-light       /* 浅边框(分割线) */
--halo-border-input       /* 输入框边框 */
--halo-border-focus       /* 聚焦边框 */

/* ---------- 强调色 ---------- */
--halo-accent-primary           /* 主色 */
--halo-accent-primary-hover     /* 主色悬停 */
--halo-accent-primary-text      /* 主色上的文字 */
--halo-accent-danger            /* 危险色 */
--halo-accent-danger-hover      /* 危险色悬停 */
--halo-accent-success           /* 成功色 */
--halo-accent-warning           /* 警告色 */

/* ---------- 阴影 ---------- */
--halo-shadow-sm           /* 小阴影 */
--halo-shadow-base         /* 默认阴影 */
--halo-shadow-lg           /* 大阴影 */

/* ---------- 滚动条 ---------- */
--halo-scrollbar-thumb     /* 滚动条滑块 */
--halo-scrollbar-track     /* 滚动条轨道 */

/* ---------- 搜索框(独立区域) ---------- */
--halo-search-bg           /* 搜索框背景 */
--halo-search-text         /* 搜索框文字 */
--halo-search-placeholder  /* 搜索框占位符 */

/* ---------- 菜单 ---------- */
--halo-menu-item-hover     /* 菜单项悬停 */
--halo-menu-item-active    /* 菜单项激活 */
--halo-menu-group-title    /* 菜单组标题 */

/* ---------- 表格 ---------- */
--halo-table-header-bg     /* 表头背景 */
--halo-table-row-hover     /* 行悬停 */
--halo-table-border        /* 表格边框 */

/* ---------- 标签/徽章 ---------- */
--halo-tag-bg              /* 标签背景 */
--halo-tag-text            /* 标签文字 */

/* 总计: ~40 个语义变量 */

4. 黑暗模式调色板

4.1 色彩策略

采用 OKLCH 颜色空间,遵循以下原则:

  • 背景层次:越"高"的层(卡片 > 内容区 > 页面)越亮,用亮度区分层次替代阴影
  • 文字层次:通过亮度和不透明度区分主/次/三级文字
  • 强调色去饱和:暗色下饱和度略降,保持视觉舒适
  • 中性色着色:所有中性色含微量蓝色调(Halo 主色的补色方向)

4.2 完整调色板(OKLCH 值)

[data-halo-theme="dark"] {
  /* ===== 背景层次 ===== */
  --halo-bg-body:              oklch(14% 0.01 250);
  --halo-bg-sidebar:           oklch(16% 0.015 250);
  --halo-bg-content:           oklch(14% 0.01 250);
  --halo-bg-card:              oklch(18% 0.015 250);
  --halo-bg-input:             oklch(20% 0.015 250);
  --halo-bg-hover:             oklch(24% 0.02 250);
  --halo-bg-active:            oklch(28% 0.025 250);
  --halo-bg-disabled:          oklch(16% 0.005 250);
  --halo-bg-tooltip:           oklch(25% 0.01 250);
  --halo-bg-modal:             oklch(0% 0 0 / 60%);  /* 遮罩 */
  --halo-bg-dropdown:          oklch(20% 0.015 250);

  /* ===== 文字颜色 ===== */
  --halo-text-primary:         oklch(92% 0.005 250);
  --halo-text-secondary:       oklch(70% 0.01 250);
  --halo-text-tertiary:        oklch(50% 0.01 250);
  --halo-text-link:            oklch(72% 0.14 160);   /* 绿色链接,保持与主色关联 */
  --halo-text-inverse:         oklch(14% 0.01 250);

  /* ===== 边框 ===== */
  --halo-border-base:          oklch(28% 0.015 250);
  --halo-border-light:         oklch(22% 0.01 250);
  --halo-border-input:         oklch(30% 0.015 250);
  --halo-border-focus:         oklch(65% 0.14 160);

  /* ===== 强调色 ===== */
  --halo-accent-primary:           oklch(60% 0.13 160);
  --halo-accent-primary-hover:     oklch(66% 0.12 160);
  --halo-accent-primary-text:      oklch(14% 0.02 160);
  --halo-accent-danger:            oklch(50% 0.18 25);
  --halo-accent-danger-hover:      oklch(56% 0.17 25);
  --halo-accent-success:           oklch(58% 0.16 150);
  --halo-accent-warning:           oklch(65% 0.16 85);

  /* ===== 阴影(暗色模式下很微妙) ===== */
  --halo-shadow-sm:            0 1px 2px oklch(0% 0 0 / 30%);
  --halo-shadow-base:          0 2px 8px oklch(0% 0 0 / 40%);
  --halo-shadow-lg:            0 4px 16px oklch(0% 0 0 / 50%);

  /* ===== 滚动条 ===== */
  --halo-scrollbar-thumb:      oklch(35% 0.02 250);
  --halo-scrollbar-track:      oklch(18% 0.01 250);

  /* ===== 搜索框 ===== */
  --halo-search-bg:            oklch(20% 0.015 250);
  --halo-search-text:          oklch(70% 0.01 250);
  --halo-search-placeholder:   oklch(50% 0.01 250);

  /* ===== 菜单 ===== */
  --halo-menu-item-hover:      oklch(22% 0.02 160);
  --halo-menu-item-active:     oklch(28% 0.03 160);
  --halo-menu-group-title:     oklch(55% 0.01 250);

  /* ===== 表格 ===== */
  --halo-table-header-bg:      oklch(18% 0.01 250);
  --halo-table-row-hover:      oklch(22% 0.015 250);
  --halo-table-border:         oklch(26% 0.015 250);

  /* ===== 标签/徽章 ===== */
  --halo-tag-bg:               oklch(22% 0.02 160);
  --halo-tag-text:             oklch(80% 0.08 160);
}

4.3 浅色模式参考值(不注入,仅作对照)

以下为 Halo 当前浅色模式对应的大致 OKLCH 值,用于设计阶段对照,不会作为 CSS 注入(浅色模式是 Halo 默认行为):

变量                      浅色 ≈ oklch 值          暗色 ≈ oklch 值         对比度
--halo-bg-body             oklch(97% 0.01 250)      oklch(14% 0.01 250)    ✓
--halo-bg-sidebar          oklch(100% 0 0)          oklch(16% 0.015 250)   ✓
--halo-bg-card             oklch(100% 0 0)          oklch(18% 0.015 250)   ✓
--halo-text-primary        oklch(20% 0.01 250)      oklch(92% 0.005 250)   ✓
--halo-text-secondary      oklch(45% 0.01 250)      oklch(70% 0.01 250)    ✓

4.4 WCAG 对比度保证

文字类型 暗色模式组合 预估对比度 WCAG AA
主要文字 oklch(92%) on oklch(14%) ~15:1 AAA
次要文字 oklch(70%) on oklch(14%) ~8:1 AAA
三级文字 oklch(50%) on oklch(14%) ~4.8:1 AA
主色按钮文字 oklch(14%) on oklch(60% 0.13 160) ~5:1 AA

5. 组件覆盖策略

5.1 覆盖层级

优先级(低 → 高):
1. TailwindCSS 默认样式     ← Halo 核心
2. @halo-dev/components      ← Halo 组件库
3. Halo console-src 样式     ← 后台自定义样式
4. 插件 CSS 变量定义          ← 本插件注入
5. 插件 CSS 覆盖规则          ← 本插件注入(最高优先级)

5.2 覆盖方法

采用三阶段渐进覆盖

阶段 A:CSS 变量注入(覆盖 ~80% 场景)
    ↓ 适用:使用了 Tailwind 颜色类的元素
    ↓ 方法:用 CSS 变量重新定义 Tailwind 的语义类

阶段 B:选择器覆盖(覆盖 ~17% 场景)
    ↓ 适用:有硬编码颜色的 Halo 自定义样式
    ↓ 方法:高特异性 [data-halo-theme="dark"] 前缀选择器

阶段 C:组件穿透(覆盖 ~3% 场景)
    ↓ 适用:FormKit / 编辑器等第三方组件
    ↓ 方法:利用其自身的 CSS 变量接口

5.3 具体覆盖清单

阶段 A — CSS 变量注入

/* 重新定义 Tailwind 颜色变量的语义映射 */
[data-halo-theme="dark"] .layout {
  /* 侧边栏 */
  .sidebar {
    background-color: var(--halo-bg-sidebar);
  }
}

[data-halo-theme="dark"] .main-content {
  background-color: var(--halo-bg-content);
}

/* 覆盖 Tailwind gray 色系的使用(通过变量值覆盖) */
[data-halo-theme="dark"] {
  /* 凡是用 bg-gray-* 的地方,统一改为暗色变量 */
  /* 凡是用 text-gray-* 的地方,统一改为暗色文字变量 */
}

阶段 B — 关键选择器覆盖

目标 选择器 覆盖属性
侧边栏搜索框 .sidebar__search background-color, color
侧边栏 Logo .sidebar__logo filter(亮度调整)
菜单项 .routes-menu__item background-color, color
页面卡片 .card, [class*="card"] background-color, border-color, box-shadow
模态框 .modal, .dialog background-color
页脚 .main-content__footer color
表格 table, .table background-color, border-color
输入框 input, textarea, select background-color, color, border-color
下拉菜单 .dropdown, .v-dropdown background-color, border-color

阶段 C — 第三方组件穿透

/* FormKit — 使用 FormKit 自身的 CSS 变量接口 */
[data-halo-theme="dark"] {
  --fk-bg-input: var(--halo-bg-input);
  --fk-color-text: var(--halo-text-primary);
  --fk-border-color: var(--halo-border-input);
  /* ... */
}

/* OverlayScrollbars */
[data-halo-theme="dark"] .os-scrollbar {
  --os-handle-bg: var(--halo-scrollbar-thumb);
  --os-track-bg: var(--halo-scrollbar-track);
}

/* 富文本编辑器(@halo-dev/richtext-editor */
[data-halo-theme="dark"] .ProseMirror {
  color: var(--halo-text-primary);
  background: var(--halo-bg-input);
}

6. 切换器 UI 设计

6.1 放置位置

选择侧边栏底部 UserProfileBanner 上方作为切换器位置:

┌──────────────────┐
│                  │
│    Logo          │
│                  │
│  ┌────────────┐  │
│  │ 🔍 搜索... │  │
│  └────────────┘  │
│                  │
│  菜单项...       │
│                  │
│                  │
├──────────────────┤
│  ☀️ 浅色 | 🌙 深色│ ← 主题切换器
├──────────────────┤
│  👤 用户头像      │ ← UserProfileBanner(现有)
└──────────────────┘

选择此位置的理由:

  • 与用户偏好设置相邻,符合认知模型
  • 不干扰主导航菜单
  • 始终可见(侧边栏固定)
  • 不需要额外开辟路由

6.2 切换器设计

类型:双态图标按钮(非 Toggle Switch

浅色模式时显示:  🌙 深色模式     (点击切换到深色)
深色模式时显示:  ☀️ 浅色模式     (点击切换到浅色)

采用图标按钮而非 Toggle Switch,因为:

  1. 只有两个状态,不需要 Switch 的"开/关"隐喻
  2. 图标 + 文字 = 当前点击将去往的状态(而非当前状态),更直观
  3. 占用空间小,符合侧边栏底部的紧凑布局

6.3 交互规格

属性
触发方式 点击
过渡效果 无(瞬间切换,暗色模式切换不宜有过渡动画)
图标切换 点击后立即切换图标
快捷键 无(不设置全局快捷键,避免与其他插件冲突)
状态反馈 图标 + 文字变化即为反馈
首次加载 默认跟随系统(prefers-color-scheme),无偏好时使用浅色

6.4 组件接口

// ThemeToggle.vue 的公开接口
interface ThemeToggleProps {
  // 当前无 props — 组件自主从 useDarkMode() 读取状态
}

// useDarkMode composable 的接口
interface UseDarkModeReturn {
  theme: Ref<'light' | 'dark' | 'auto'>;
  isDark: ComputedRef<boolean>;     // 当前是否实际为深色
  setTheme: (t: 'light' | 'dark' | 'auto') => void;
  toggle: () => void;               // 在 light/dark 间切换
}

7. 路由与菜单

7.1 路由注册

插件需要在 Halo Console 中注册一个设置页面用于主题偏好的详细配置:

// index.ts
routes: [
  {
    parentName: 'Root',
    route: {
      path: '/dark-mode-settings',
      name: 'DarkModeSettings',
      component: SettingsView,
      meta: {
        title: '深色模式设置',
        menu: {
          name: '深色模式',
          group: '偏好设置',
          icon: markRaw(IconMoon),
          priority: 100,   // 较低优先级,排在菜单靠后位置
        },
      },
    },
  },
],

7.2 设置页面功能

设置页面提供比侧边栏切换器更细粒度的控制:

  • 模式选择:浅色 / 深色 / 跟随系统(Radio Group
  • 当前生效模式:只读显示
  • 关于:插件版本、GitHub 链接

8. 偏好持久化

8.1 存储方案

localStorage key: "halo-dark-mode-theme"
value: "light" | "dark" | "auto"
默认值: "auto"

8.2 初始化时序(防闪烁 FOUC 策略)

console.html 中无法注入代码(插件仅在 Console 加载后才激活),因此采用以下防闪烁策略:

<!-- 在插件 JS 入口的最顶部,同步执行 -->
<script>
  // 这段代码会在插件的 bundle.js 最顶部执行
  // 此时 DOM 尚未渲染,设置属性不会引起闪烁
  (function() {
    var theme = localStorage.getItem('halo-dark-mode-theme') || 'auto';
    if (theme === 'dark') {
      document.documentElement.setAttribute('data-halo-theme', 'dark');
    } else if (theme === 'auto') {
      if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
        document.documentElement.setAttribute('data-halo-theme', 'dark');
      }
    }
  })();
</script>

注意:由于插件 bundle 是异步加载的(useScriptTag),FOUC 风险需要通过以下方式缓解:

  • CSS 变量切换是瞬时的(< 1 帧),即使有短暂浅色闪现,用户体感为"页面加载完成"
  • 后续可通过向 Halo 提交 PR 在 console.html 添加 <script> 插槽来彻底解决

8.3 系统偏好监听

// useSystemPreference.ts
export function useSystemPreference() {
  const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

  function onChange(callback: (isDark: boolean) => void) {
    mediaQuery.addEventListener('change', (e) => {
      callback(e.matches);
    });
  }

  return {
    isSystemDark: () => mediaQuery.matches,
    onChange,
  };
}

9. 项目文件结构

halo-dark-mode-plugin/
│
├── build.gradle                        # Gradle 构建(BOM 2.25.0, DevTools 0.8.0
├── settings.gradle                     # 包含 :ui 子项目
├── gradle.properties                   # Gradle 属性
├── gradle/                             # Gradle wrapper
├── gradlew / gradlew.bat
│
├── src/
│   └── main/
│       ├── java/run/halo/darkmode/
│       │   └── DarkModePlugin.java     # 插件主类(极简骨架)
│       │
│       └── resources/
│           ├── plugin.yaml             # 插件清单
│           └── logo.png                # 插件图标
│
├── ui/
│   ├── package.json                    # 前端依赖
│   ├── tsconfig.json
│   ├── vite.config.ts                  # Vite 配置(ui-plugin-bundler-kit
│   ├── build.gradle                    # 前端构建 Gradle 任务
│   │
│   └── src/
│       ├── index.ts                    # definePlugin 入口(从 @halo-dev/ui-shared 导入)
│       │
│       ├── composables/
│       │   ├── useDarkMode.ts          # 核心状态管理
│       │   └── useSystemPreference.ts  # 系统偏好匹配
│       │
│       ├── components/
│       │   └── ThemeToggle.vue         # 侧边栏切换器组件
│       │
│       ├── views/
│       │   └── SettingsView.vue        # 设置页面
│       │
│       ├── styles/
│       │   ├── index.css               # 样式入口
│       │   ├── variables.css           # CSS 变量定义(浅色 + 深色)
│       │   └── overrides/
│       │       ├── layout.css          # BasicLayout 覆盖
│       │       ├── components.css      # @halo-dev/components 覆盖
│       │       ├── forms.css           # FormKit 输入组件覆盖
│       │       ├── editor.css          # 富文本编辑器覆盖
│       │       ├── scrollbar.css       # 滚动条覆盖
│       │       └── utilities.css       # Tailwind 工具类覆盖
│       │
│       └── assets/
│           └── icons/                  # 月/日 图标 SVG
│
└── README.md

9.1 关键配置速查

// build.gradle (根)
plugins {
    id "run.halo.plugin.devtools" version "0.8.0"
}
dependencies {
    implementation platform('run.halo.tools.platform:plugin:2.25.0')
    compileOnly 'run.halo.app:api'
}
halo { version = '2.25' }

// settings.gradle
rootProject.name = 'dark-mode'
include ':ui'

// 关键:UI 产物复制到 resources/main/ui/
tasks.register('processUiResources', Copy) {
    from project(':ui').layout.buildDirectory.dir('dist')
    into layout.buildDirectory.dir('resources/main/ui')
}
// ui/src/index.ts
import { definePlugin } from '@halo-dev/ui-shared'  // ← 注意导入源

export default definePlugin({
  components: {},
  routes: [
    {
      parentName: 'Root',
      route: {
        path: '/dark-mode-settings',
        component: () => import('./views/SettingsView.vue'),  // 懒加载
        meta: { /* ... */ },
      },
    },
  ],
  extensionPoints: {},
})

10. 实现阶段划分

Phase 1:骨架搭建(约 150 行)

  • 使用 create-halo-plugin 或从 plugin-starter 创建项目
  • Java DarkModePlugin extends BasePlugin 基本骨架
  • plugin.yaml 元数据填写
  • 前端 index.ts 最小 definePlugin(空路由 + 空组件)
  • 验证插件能成功加载

Phase 2:CSS 变量体系 + 核心布局暗色化(约 400 行 CSS)

  • variables.css:定义全部 40+ CSS 变量(:root 浅色 + [data-halo-theme="dark"] 深色)
  • overrides/layout.cssBasicLayout 覆盖(侧边栏 + 内容区 + 页脚)
  • overrides/scrollbar.css:滚动条覆盖
  • overrides/utilities.cssTailwind 通用类覆盖
  • 视觉验收:侧边栏、主内容区、菜单在暗色下正常

Phase 3:切换器 + 状态管理(约 200 行 TS/Vue

  • useDarkMode.ts composable
  • useSystemPreference.ts composable
  • ThemeToggle.vue 组件
  • FOUC 防护脚本(bundle 顶部内联)
  • 设置页面 SettingsView.vue + 路由注册

Phase 4:组件 + 表单暗色化(约 300 行 CSS)

  • overrides/components.cssHalo UI 组件库覆盖
  • overrides/forms.cssFormKit 输入组件覆盖
  • 覆盖表格、标签、徽章、下拉菜单、模态框、提示框
  • 视觉验收:所有表单控件、弹窗、提示在暗色下正常

Phase 5:编辑器 + 打磨(约 150 行 CSS + 测试)

  • overrides/editor.css:富文本编辑器 + Markdown 编辑器覆盖
  • WCAG 对比度验证
  • 图片/Logo 暗色模式适配(亮度降低)
  • 完整的视觉回归检查

Phase 6:文档 + 发布

  • README.md(安装说明 + 使用指南 + 截图)
  • 版本号确定(v0.1.0
  • 构建产物验证

预估总规模

类型 预估行数
Java ~30 行
TypeScript/Vue ~250 行
CSS ~850 行
配置(Gradle/JSON ~150 行
总计 ~1,280 行

11. 测试策略

11.1 自动化测试

层级 工具 覆盖目标 示例
composable 单元测试 Vitest useDarkMode 状态转换逻辑 切换主题 → isDark 变化正确
composable 单元测试 Vitest useSystemPreference matchMedia mock 系统偏好变化 → 回调触发
组件测试 Vitest + vue-test-utils ThemeToggle 渲染与事件 点击 → setTheme 被调用

11.2 手动视觉验收清单

□ 浅色模式:所有页面与未安装插件时一致(无回归)
□ 深色模式:侧边栏背景为深色
□ 深色模式:菜单项清晰可读
□ 深色模式:卡片/面板背景与页面有明确分层
□ 深色模式:输入框背景与文字对比度足够
□ 深色模式:表格行可区分(斑马纹或边框)
□ 深色模式:按钮主色/危险色/默认色分明
□ 深色模式:模态框遮罩 + 内容清晰
□ 深色模式:下拉菜单不"漂浮"
□ 深色模式:Toast 通知可读
□ 深色模式:富文本编辑器内容可编辑
□ 深色模式:代码块有合适的暗色主题
□ 切换器:点击即时切换,无闪烁
□ 持久化:刷新页面后偏好保持
□ 跟随系统:切换系统暗色 → 页面跟随
□ 设置页面:三种模式可切换
□ 所有页面无可见的浅色残余

12. 兼容性矩阵

12.1 Halo 版本兼容

Halo 版本 支持状态 说明
2.23.x ~ 2.25.x 主要支持 对应 plugin-starter 的 BOM 版本
2.26+ ⚠️ 预期兼容 插件 API 向后兼容;新版本发布后验证
< 2.23 不支持 requires: ">=2.23.0"

12.2 浏览器兼容

浏览器 最低版本 备注
Chrome 111+ OKLCH 支持
Firefox 113+ OKLCH 支持
Safari 15.4+ OKLCH 支持
Edge 111+ 与 Chrome 内核相同

关键依赖oklch() CSS 颜色函数。所有目标浏览器均原生支持(2023 年后发布版本)。

12.3 与其他插件的兼容性

  • 主题插件:如果有其他插件也定义了暗色模式,后加载的 CSS 可能产生冲突。通过 data-halo-theme 属性选择器命名空间隔离。
  • UI 自定义插件:不冲突,CSS 变量覆盖不影响自定义插件的样式。

附录 A:插件 ID 与命名

属性
Plugin Name (ID) dark-mode
Display Name 深色模式
Description (zh) 为 Halo 后台管理面板提供深色/浅色模式切换功能
Description (en) Dark mode toggle for Halo admin console
Author (用户自定义)
License GPL-3.0

附录 B:待确认事项

以下是需要在开始实现前与用户确认的决策点:

  1. 插件名称确认:dark-mode 是否合适?
  2. 是否需要国际化(中文为主 + 英文备选)? → 需求仅中文,可选加英文
  3. 切换器图标风格Material Icons(与 Halo 一致)还是自定义 SVG?
  4. 是否需要后端 Setting API 持久化作为 Phase 2 特性?
  5. 插件 Logo 设计方向

本文档基于 调查文档 的发现,面向首次实现。