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

557 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Halo 黑暗模式插件 — 调查文档
> 调查日期:2026-08-06
> 调查范围:Halo 核心仓库、plugin-starter、create-halo-plugin、dev-skills、theme-vite-starter、插件生态系统
> 目的:为开发 Halo 后台管理面板黑暗模式插件提供技术基础
---
## 0. 补充调查:create-halo-plugin / dev-skills / theme-vite-starter
> 本节为第二轮调查补充(2026-08-06),基于对三个额外仓库的分析。**本节内容优先于第 1-6 节中过时的信息。**
### 0.1 create-halo-plugin — 官方推荐的脚手架工具
**仓库**https://github.com/halo-dev/create-halo-plugin
这是 Halo 官方推荐的插件创建方式,取代了旧的 `plugin-starter`
```bash
pnpm create halo-plugin
pnpm create halo-plugin my-plugin
```
**交互式 CLI 特性**
- 插件名称(遵循 `[a-z0-9][a-z0-9.-]*[a-z0-9]` 规则)
- 域名 → 自动生成 Java 包名
- 作者姓名
- 是否包含 UI 项目
- **UI 构建工具选择:Vite 或 Rsbuild**
**与 plugin-starter 的关键差异**
| 项目 | plugin-starter (旧) | create-halo-plugin (新) |
|------|---------------------|------------------------|
| `definePlugin` 导入源 | `@halo-dev/console-shared` | `@halo-dev/ui-shared` |
| UI 构建产物目录 | `resources/main/console/` | `resources/main/ui/` |
| BOM 版本 | `2.23.0-SNAPSHOT` | `2.25.0` |
| DevTools 版本 | `0.6.2` | `0.8.0` |
| `halo.version` | `2.23.0-beta.2` | `2.25` |
| Maven 仓库 | 含 `Central Portal Snapshots` | 仅 `mavenCentral()` |
| 路由导入 | 静态 import | 懒加载 `() => import(...)` |
| 代码格式化 | Prettier | ESLint |
| SNAPSHOT 依赖 | 是 | **否(仅稳定版)** |
### 0.2 dev-skills — AI Agent 开发技能库
**仓库**https://github.com/halo-dev/dev-skills
这是 Halo 官方的 Agent Skills 集合,为 AI Agent 提供结构化的领域知识。
**`halo-plugin-dev` 技能** — 涵盖 20+ 参考文档:
| 参考文档 | 用途 |
|----------|------|
| `plugin-structure.md` | 目录结构、前后端布局 |
| `plugin-manifest.md` | plugin.yaml 字段、版本约束、依赖 |
| `server-lifecycle.md` | BasePlugin start/stop/delete 生命周期 |
| `server-extension.md` | 自定义 GVK 扩展、CRUD API |
| `server-api.md` | CustomEndpoint、Controller |
| `server-shared-beans.md` | 注入 Halo 核心服务 |
| `server-security.md` | RBAC 角色模板 |
| `ui-entry.md` | definePlugin、routes、菜单配置 |
| `ui-build.md` | bundler-kit (Vite/Rsbuild)、产物目录 |
| `ui-shared.md` | stores (currentUser, globalInfo)、utils |
| `ui-extension-points.md` | Console UI 扩展点(编辑器、附件选择器等) |
| `ui-components.md` | @halo-dev/components 组件库 |
| `ui-forms.md` | FormKit schema、自定义输入组件 |
| `ui-api-request.md` | API client 调用、OpenAPI 客户端生成 |
| `ui-tooling.md` | unplugin-icons、UnoCSS 工具链 |
| `devtools.md` | haloServer、reload、watch、调试 |
| `api-changelog.md` | 各版本 API 变更记录 |
| `theme-head-processor.md` | 注入脚本/样式到主题 `<head>` |
| `theme-content-handler.md` | 修改渲染后的 HTML |
| `theme-integration.md` | 主题 Finder API、反向代理 |
> **实施时**:开发过程中应加载 `halo-plugin-dev` 技能以获取精确的 API 参考。
### 0.3 theme-vite-starter — 主题开发参考
**仓库**https://github.com/halo-dev/theme-vite-starter
这是 Halo 主题脚手架,与插件开发不直接相关,但展示了 Halo 生态的以下约定:
- 使用 `vite-plus`Vite 超集,集成 format/lint
- 使用 `@halo-dev/vite-plugin-halo-theme` 构建主题
- `theme.yaml` manifest 格式(`apiVersion: theme.halo.run/v1alpha1`
- `settings.yaml` 用于控制台表单配置
- Agent Skills 嵌入在 `.agents/skills/` 目录
### 0.4 对设计文档的影响
基于以上新发现,设计文档需要做以下修正:
1. **脚手架工具**:使用 `pnpm create halo-plugin` 而非手动克隆 plugin-starter
2. **`definePlugin` 导入**:从 `@halo-dev/ui-shared` 导入(非 `@halo-dev/console-shared`
3. **版本目标**BOM `2.25.0`DevTools `0.8.0`Halo `2.25`
4. **UI 产物路径**`resources/main/ui/`(非 `resources/main/console/`
5. **路由懒加载**:使用 `() => import(...)` 动态导入
6. **不需要 SNAPSHOT 仓库**:仅 `mavenCentral()` 即可
7. **开发时参考**:善用 `halo-plugin-dev` skill 的参考文档
---
## 1. Halo 项目概览
### 1.1 基本信息
| 属性 | 值 |
|------|-----|
| 仓库 | https://github.com/halo-dev/halo |
| 许可 | GPL-3.0 |
| 类型 | 全栈 monorepo |
| 最新稳定版 | 2.25 |
| 当前开发版 | 2.23.0-beta.2plugin-starter 依赖版本) |
### 1.2 技术栈总览
```
┌─────────────────────────────────────────────────────┐
│ HALO 架构图 │
├─────────────────────────────────────────────────────┤
│ 前端 (ui/) │
│ ├─ console-src/ → 后台管理面板 (Vue 3 + TS) │
│ ├─ uc-src/ → 用户中心 (Vue 3 + TS) │
│ ├─ src/ → 共享代码 (locales/styles/setup) │
│ └─ packages/ → 共享工作空间包 │
│ ├─ api-client → 后端 API 客户端 (生成) │
│ ├─ components → UI 组件库 (Storybook) │
│ ├─ console-shared → definePlugin API │
│ ├─ editor → 富文本编辑器 │
│ ├─ shared → 共享工具 │
│ └─ ui-plugin-bundler-kit → 插件打包工具链 │
├─────────────────────────────────────────────────────┤
│ 后端 │
│ ├─ api/ → 扩展模型/合约/安全 API │
│ ├─ application/ → 服务/路由/数据迁移 │
│ └─ platform/ → BOM (依赖版本约束) │
│ ├─ application/ → 应用 BOM │
│ └─ plugin/ → 插件 BOM (插件可用的依赖) │
└─────────────────────────────────────────────────────┘
```
**核心依赖版本:**
| 层 | 技术 | 版本 |
|----|------|------|
| 后端 | Java | 21 |
| 后端 | Spring Boot (WebFlux) | 3.x |
| 后端 | Gradle | 8.x |
| 前端 | Vue | 3.x |
| 前端 | TypeScript | 5.x |
| 前端 | TailwindCSS | 3.x (含 tailwindcss-themer) |
| 前端 | Vite | 6.x |
| 前端 | pnpm | 9.x |
| 前端 | Pinia | 2.x (状态管理) |
| 前端 | Vue Router | 4.x |
| 前端 | FormKit | 1.x (表单) |
---
## 2. 插件系统深度分析
### 2.1 插件生命周期
```java
// 每个插件只有一个主类继承 BasePlugin
@Component
public class PluginMain extends BasePlugin {
public PluginMain(PluginContext pluginContext) {
super(pluginContext);
}
@Override
public void start() {
// 插件启动 → 注册扩展点、创建资源
}
@Override
public void stop() {
// 插件停止 → 清理资源、取消注册
}
}
```
**关键点:**
- 插件以 Spring `@Component` 的形式存在
- 通过 `PluginContext` 可以访问 Halo 运行时上下文
- `start()` / `stop()` 是主要的生命周期钩子
- 只允许一个类继承 `BasePlugin`
### 2.2 plugin.yaml 清单
```yaml
apiVersion: plugin.halo.run/v1alpha1
kind: Plugin
metadata:
name: PluginName # 唯一标识符,后续无法修改
spec:
enabled: true
requires: ">=2.23.0" # Halo 版本要求
author:
name: 作者名
website: https://example.com
logo: logo.png
homepage: https://github.com/...
repo: https://github.com/...
issues: https://github.com/.../issues
displayName: "插件显示名称"
description: "插件描述"
license:
- name: "GPL-3.0"
```
**关键约束:**
- `metadata.name` 设置后不能修改(作为标识符绑定到 API 路由和资源路径)
- `spec.requires` 定义最低 Halo 版本
- 版本约束由 `platform/plugin/` BOM 管理
### 2.3 插件 Gradle 构建系统
```groovy
plugins {
id 'java'
id "io.freefair.lombok" version "9.2.0"
id "run.halo.plugin.devtools" version "0.6.2" // 热重载开发工具
}
dependencies {
implementation platform('run.halo.tools.platform:plugin:2.23.0-SNAPSHOT')
compileOnly 'run.halo.app:api' // 编译时 API
testImplementation 'run.halo.app:api' // 测试时可用
}
// 前端资源复制到后端 classpath
tasks.register('processUiResources', Copy) {
from project(':ui').layout.buildDirectory.dir('dist')
into layout.buildDirectory.dir('resources/main/console')
dependsOn project(':ui').tasks.named('assemble')
}
```
**关键点:**
- `run.halo.app:api``compileOnly` — 运行时由 Halo 提供
- 前端构建产物(JS/CSS)被复制到 `resources/main/console/`
- DevTools 插件支持 Docker 热重载开发
### 2.4 插件前端入口系统
```typescript
// ui/src/index.ts — 插件前端唯一入口
import { definePlugin } from '@halo-dev/console-shared'
import HomeView from './views/HomeView.vue'
import { IconPlug } from '@halo-dev/components'
import { markRaw } from 'vue'
export default definePlugin({
components: {}, // { [name: string]: Component } — 全局注册组件
routes: [...], // 路由定义(可追加到父路由)
extensionPoints: {}, // 扩展点(当前生态尚不成熟)
ucRoutes: [...], // 用户中心路由(可选)
})
```
**`definePlugin` 的本质:** 源码显示它是一个**身份函数**identity function),即 `(x) => x`。这意味着插件配置对象直接传递给 Halo 运行时,没有额外的转换层。这简化了调试但也意味着没有类型强制。
### 2.5 插件资源加载机制(核心发现)
```
浏览器加载流程:
1. Halo 后端生成 bundle URL
/apis/api.console.halo.run/v1alpha1/ui-plugins/-/bundle.js?t=<timestamp>
/apis/api.console.halo.run/v1alpha1/ui-plugins/-/bundle.css?t=<timestamp>
2. setupModules.ts 执行流程:
├─ loadEnabledUiPluginModules()
│ ├─ loadUiPluginBundle() → 加载 JS bundle
│ └─ 读取 window["enabledUiPlugins"] → 获取已启用插件列表
├─ setupComponents() → 注册 FormKit 输入组件
├─ setupPluginModules() → 注册路由 + 全局组件
└─ setupPluginStyles() → 加载插件 CSS bundle
└─ loadStyle(url) → 动态创建 <link> 标签
```
**关键发现:**
- **插件 CSS 会自动加载**`setupPluginStyles()` 函数通过 `loadStyle()` 动态注入 `<link>` 标签到 `<head>`
- **CSS 在所有插件 JS 初始化完成后加载**:这确保了样式加载有正确的优先级
- **无需 Java 端干预 CSS 注入**:只要插件包含前端资源,Halo 会自动处理
- `window["enabledUiPlugins"]` 由后端填充,包含所有已启用插件的元数据
---
## 3. 当前 UI 主题系统分析
### 3.1 TailwindCSS 配置
```typescript
// ui/tailwind.config.ts
plugins: [
themer({
defaultTheme: {
extend: {
colors: {
primary: "#4CCBA0", // 绿色主色调
secondary: "#0E1731", // 深蓝色辅助色
danger: "#D71D1D", // 红色危险色
},
borderRadius: {
base: "4px",
},
},
},
// 注意:只有 defaultTheme,没有 dark 主题
}),
]
```
**核心问题:**
- `tailwindcss-themer` 支持多主题但只配置了一个 `defaultTheme`
- **没有定义 dark 主题变量** — 这是黑暗模式缺失的根本原因
- 所有颜色值硬编码为浅色模式值
### 3.2 BasicLayout 颜色扫描
通过分析 `BasicLayout.vue`(后台主布局),发现以下颜色使用模式:
| 元素 | 当前颜色 | 来源 |
|------|---------|------|
| 侧边栏背景 | `white` | `theme("colors.white")` |
| 搜索框背景 | `gray.100` | `theme("colors.gray.100")` |
| 搜索框图标色 | `gray.400` | `theme("colors.gray.400")` |
| 页脚文字 | `gray.600` | `theme("colors.gray.600")` |
| 悬停链接色 | `gray.600` | `theme("colors.gray.600")` |
| 搜索框悬停文字 | `gray.900` | `theme("colors.gray.900")` |
这些全部是 Tailwind 默认的浅色模式 gray 色调。
### 3.3 主题 Store 分析
```typescript
// ui/console-src/stores/theme.ts
export const useThemeStore = defineStore("theme", () => {
const activatedTheme = ref<Theme>();
async function fetchActivatedTheme() {
// 注意:这里获取的是站点前台主题(博客主题),不是 UI 主题
const { data } = await consoleApiClient.theme.theme.fetchActivatedTheme(...);
activatedTheme.value = data;
}
return { activatedTheme, fetchActivatedTheme };
});
```
**关键发现:**
- `useThemeStore` 管理的是**站点前台主题**(用于访客页面渲染)
- **Halo 后台完全没有 UI 主题/黑暗模式的概念**
- 不存在 `uiTheme``darkMode` 相关的 store、API 或配置项
### 3.4 样式加载架构
```
setupStyles.ts 加载顺序:
1. @halo-dev/richtext-editor/dist/style.css → 编辑器样式
2. @halo-dev/components/dist/style.css → 组件库样式
3. @/styles/tailwind.css → Tailwind 基础 + 组件样式
4. @/styles/index.css → 全局样式覆盖
5. overlayscrollbars/overlayscrollbars.css → 滚动条样式
6. 插件 CSS (最后加载) → setupPluginStyles()
```
**重要发现:插件 CSS 最后加载**,这意味着插件可以通过更高特异性的选择器覆盖前面的样式。这为黑暗模式插件的 CSS 覆盖策略提供了天然的优先级优势。
---
## 4. 黑暗模式实现可行性分析
### 4.1 Halo 现有的"主题"相关 API
| API / Store | 用途 | 是否可用于 UI 暗色模式 |
|-------------|------|----------------------|
| `useThemeStore` | 站点前台主题管理 | ❌ 不相关 |
| `tailwindcss-themer` | Tailwind 多主题支持 | ✅ 基础设施存在但未使用 |
| 插件 CSS bundle | 自动加载插件样式 | ✅ 天然支持 |
| 插件 JS bundle | 自动加载插件 JS | ✅ 天然支持 |
### 4.2 实现黑暗模式的可行路径
#### 方案 A:纯 CSS 覆盖(推荐 — 最简可行方案)
**原理:** 利用插件 CSS 最后加载的优先级,用高特异性 CSS 规则覆盖所有浅色模式样式。
**优点:**
- 无需修改 Halo 核心代码
- 开发成本最低
- 不依赖不成熟的 extensionPoints API
- 兼容所有 Halo 2.x 版本
**缺点:**
- 需要精准定位所有颜色元素
- Halo 版本升级可能导致选择器失效
- 无法利用 Tailwind 的 `dark:` 前缀体系
**技术要点:**
```css
/* 通过属性选择器 + CSS 变量方案 */
[data-color-scheme="dark"] {
--color-bg: #1a1a2e;
--color-text: #e0e0e0;
/* ... 覆盖所有相关的 CSS 变量/类 */
}
```
#### 方案 B:注入 Tailwind dark 主题(中等复杂度)
**原理:** 通过插件在运行时修改 `tailwindcss-themer` 的配置,注入 dark 主题。
**优点:**
- 可以复用 Tailwind 的 `dark:` 类名前缀
- 更系统化的颜色管理
**缺点:**
- 需要访问 Tailwind 运行时配置(不确定是否可行)
- `tailwindcss-themer` 的主题在构建时确定,运行时修改不标准
- 需要修改 Halo 前端构建流程,超出插件范畴
**可行性评估:不可行** — Tailwind 主题是编译时确定的,插件无法在运行时修改。
#### 方案 C:自定义 CSS Properties 体系(推荐 — 稳健方案)
**原理:** 在插件 CSS 中定义一套完整的 CSS 自定义属性(CSS Variables),覆盖 Tailwind 和 Halo 组件的颜色。
**优点:**
- CSS Variables 是标准 Web 技术,浏览器原生支持
- 切换时只需修改 `:root``[data-theme]` 属性
- 渐进式,可以分批覆盖
- 易于维护和扩展
- 可以使用 `prefers-color-scheme` 媒体查询自动跟随系统
**缺点:**
- 需要全面覆盖 Tailwind 颜色类
- 需要为 Halo 自定义组件定义变量名
**技术要点:**
```css
:root {
--halo-bg-primary: #ffffff;
--halo-bg-secondary: #f3f4f6;
--halo-text-primary: #111827;
--halo-text-secondary: #6b7280;
--halo-border: #e5e7eb;
/* ... */
}
[data-halo-theme="dark"] {
--halo-bg-primary: #1a1a2e;
--halo-bg-secondary: #16213e;
--halo-text-primary: #e0e0e0;
--halo-text-secondary: #a0a0a0;
--halo-border: #2a2a4a;
/* ... */
}
```
### 4.3 推荐方案:方案 CCSS Variables+ 方案 A 作为补充
采用 **CSS 自定义属性** 作为主体策略,对少量无法通过变量覆盖的硬编码颜色采用 **选择器覆盖** 作为补充。
---
## 5. 插件架构设计关键决策点
### 5.1 需要改动的内容
| 层级 | 内容 | 必要性 |
|------|------|--------|
| 后端 Java | 插件入口类(基本骨架) | ✅ 必须 |
| 后端 Java | 可选的 Setting API 端点 | ⬜ 可选(存储用户偏好) |
| plugin.yaml | 插件元数据 | ✅ 必须 |
| 前端 JS | 开关按钮 UI 组件 | ✅ 必须 |
| 前端 JS | 路由注册(设置页) | ✅ 必须 |
| 前端 JS | 主题切换逻辑 | ✅ 必须 |
| 前端 CSS | 暗色变量定义 (~200-500 变量) | ✅ 必须 |
| 前端 CSS | 组件覆盖样式 | ✅ 必须 |
### 5.2 不需要改动的内容
- ❌ Halo 核心代码 — 插件机制完全支持
- ❌ Tailwind 配置 — 通过 CSS 变量覆盖
- ❌ Halo 组件库源码 — 通过 CSS 变量穿透
- ❌ 后端路由 — 使用现有 API
### 5.3 开关按钮放置位置
分析 `BasicLayout.vue` 后发现可选位置:
1. **侧边栏底部(推荐)**`sidebar__profile` 区域,与用户头像并列
2. **顶部导航栏** — 但目前 Halo 没有顶部导航栏
3. **插件自有设置页面** — 可添加一个设置页面
### 5.4 偏好持久化策略
| 方案 | 复杂度 | 跨设备 | 持久性 |
|------|--------|--------|--------|
| `localStorage` | 极低 | ❌ | 浏览器级 |
| 后端 Plugin Setting API | 中 | ✅ | 服务器级 |
| Cookie | 低 | ❌ | 浏览器级 |
**推荐:`localStorage` + `prefers-color-scheme` 作为初始版本,后续可选升级到后端持久化。**
---
## 6. 现有参考资源
### 6.1 Halo 生态相关仓库
| 仓库 | 用途 | 关键信息 |
|------|------|---------|
| halo-dev/halo | 核心仓库 | 前端 Vue 3 + Tailwind,后端 Spring Boot |
| halo-dev/plugin-starter | 插件模板 | 完整 Gradle + Vue 3 插件骨架 |
| halo-dev/create-halo-plugin | 交互式创建工具 | 推荐替代 plugin-starter |
### 6.2 关键发现总结
1. **Halo 后台完全没有黑暗模式支持** — 这既是挑战也是机会,意味着插件是第一无二的解决方案
2. **插件 CSS 最后加载** — 为样式覆盖提供了天然优势
3. **`definePlugin` 是纯透传函数** — 简单但缺少类型安全,需要自行保证配置正确性
4. **`tailwindcss-themer` 基础设施存在** — 但只配置了 light 主题且运行时无法修改
5. **`extensionPoints` 机制尚不成熟** — 不推荐作为主要实现手段
6. **Halo 使用 `pnpm` workspace** — 插件前端开发需匹配此工具链
### 6.3 技术风险
| 风险 | 严重程度 | 缓解措施 |
|------|---------|---------|
| Halo 升级导致选择器失效 | 中 | 使用语义化 CSS 变量名;定期跟进 Halo changelog |
| `@halo-dev/components` 组件内部样式无法覆盖 | 中 | 利用 CSS 变量穿透(该组件库可能使用 CSS 变量或 Tailwind 类) |
| 第三方插件不受暗色主题影响 | 低 | 在设计文档中说明插件的暗色化范围 |
| FormKit/编辑器样式复杂 | 中 | 优先保证核心布局暗色化,表单和编辑器作为第二阶段 |
---
## 7. 下一步建议
1. **审阅此调查文档**,确认技术方向
2. **编写设计文档**,详细定义:
- CSS 变量命名体系
- 组件覆盖策略
- 开关 UI 设计
- 路由/菜单结构
- 版本兼容性承诺
3. **使用 `create-halo-plugin` 创建插件骨架**
4. **TDD 开发流程**(先写测试,后实现)
---
*本文档基于 2026-08-06 时 Halo v2.25 的代码分析。*