feat: 初始化 Halo 暗色模式插件

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-06 21:03:00 +08:00
commit edbf78f236
115 changed files with 14745 additions and 0 deletions
+556
View File
@@ -0,0 +1,556 @@
# 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 的代码分析。*