edbf78f236
- Halo Plugin 后端(Java/Gradle),含 DarkModePlugin 主类和测试 - Vue 3 + TypeScript 前端 UI,包含主题切换组件和设置页面 - 暗色模式 CSS 变量和覆盖样式(布局/编辑器/表单/滚动条等) - 设计文档和调查文档 - Halo 插件/主题开发 Agent Skills Co-Authored-By: Claude <noreply@anthropic.com>
894 lines
33 KiB
Markdown
894 lines
33 KiB
Markdown
# Halo 黑暗模式插件 — 设计文档
|
||
|
||
> 版本:v0.2.0-draft(基于 create-halo-plugin + dev-skills 调查更新)
|
||
> 日期:2026-08-06
|
||
> 状态:待审阅
|
||
> 上一步:[调查文档](./调查文档.md)(含 0.4 节补充调查更新)
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [设计目标与范围](#1-设计目标与范围)
|
||
2. [技术架构](#2-技术架构)
|
||
3. [CSS 变量体系设计](#3-css-变量体系设计)
|
||
4. [黑暗模式调色板](#4-黑暗模式调色板)
|
||
5. [组件覆盖策略](#5-组件覆盖策略)
|
||
6. [切换器 UI 设计](#6-切换器-ui-设计)
|
||
7. [路由与菜单](#7-路由与菜单)
|
||
8. [偏好持久化](#8-偏好持久化)
|
||
9. [项目文件结构](#9-项目文件结构)
|
||
10. [实现阶段划分](#10-实现阶段划分)
|
||
11. [测试策略](#11-测试策略)
|
||
12. [兼容性矩阵](#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 完整变量清单
|
||
|
||
```css
|
||
/* ============================================================
|
||
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 值)
|
||
|
||
```css
|
||
[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 变量注入
|
||
|
||
```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 — 第三方组件穿透
|
||
|
||
```css
|
||
/* 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 组件接口
|
||
|
||
```typescript
|
||
// 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 中注册一个设置页面用于主题偏好的详细配置:
|
||
|
||
```typescript
|
||
// 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 加载后才激活),因此采用以下防闪烁策略:
|
||
|
||
```html
|
||
<!-- 在插件 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 系统偏好监听
|
||
|
||
```typescript
|
||
// 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 关键配置速查
|
||
|
||
```groovy
|
||
// 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')
|
||
}
|
||
```
|
||
|
||
```typescript
|
||
// 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.css`:BasicLayout 覆盖(侧边栏 + 内容区 + 页脚)
|
||
- [ ] `overrides/scrollbar.css`:滚动条覆盖
|
||
- [ ] `overrides/utilities.css`:Tailwind 通用类覆盖
|
||
- [ ] 视觉验收:侧边栏、主内容区、菜单在暗色下正常
|
||
|
||
### Phase 3:切换器 + 状态管理(约 200 行 TS/Vue)
|
||
|
||
- [ ] `useDarkMode.ts` composable
|
||
- [ ] `useSystemPreference.ts` composable
|
||
- [ ] `ThemeToggle.vue` 组件
|
||
- [ ] FOUC 防护脚本(bundle 顶部内联)
|
||
- [ ] 设置页面 `SettingsView.vue` + 路由注册
|
||
|
||
### Phase 4:组件 + 表单暗色化(约 300 行 CSS)
|
||
|
||
- [ ] `overrides/components.css`:Halo UI 组件库覆盖
|
||
- [ ] `overrides/forms.css`:FormKit 输入组件覆盖
|
||
- [ ] 覆盖表格、标签、徽章、下拉菜单、模态框、提示框
|
||
- [ ] 视觉验收:所有表单控件、弹窗、提示在暗色下正常
|
||
|
||
### 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 设计方向
|
||
|
||
---
|
||
|
||
*本文档基于 [调查文档](./调查文档.md) 的发现,面向首次实现。*
|