- Halo Plugin 后端(Java/Gradle),含 DarkModePlugin 主类和测试 - Vue 3 + TypeScript 前端 UI,包含主题切换组件和设置页面 - 暗色模式 CSS 变量和覆盖样式(布局/编辑器/表单/滚动条等) - 设计文档和调查文档 - Halo 插件/主题开发 Agent Skills Co-Authored-By: Claude <noreply@anthropic.com>
20 KiB
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:
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.yamlmanifest 格式(apiVersion: theme.halo.run/v1alpha1)settings.yaml用于控制台表单配置- Agent Skills 嵌入在
.agents/skills/目录
0.4 对设计文档的影响
基于以上新发现,设计文档需要做以下修正:
- 脚手架工具:使用
pnpm create halo-plugin而非手动克隆 plugin-starter definePlugin导入:从@halo-dev/ui-shared导入(非@halo-dev/console-shared)- 版本目标:BOM
2.25.0,DevTools0.8.0,Halo2.25 - UI 产物路径:
resources/main/ui/(非resources/main/console/) - 路由懒加载:使用
() => import(...)动态导入 - 不需要 SNAPSHOT 仓库:仅
mavenCentral()即可 - 开发时参考:善用
halo-plugin-devskill 的参考文档
1. Halo 项目概览
1.1 基本信息
| 属性 | 值 |
|---|---|
| 仓库 | https://github.com/halo-dev/halo |
| 许可 | GPL-3.0 |
| 类型 | 全栈 monorepo |
| 最新稳定版 | 2.25 |
| 当前开发版 | 2.23.0-beta.2(plugin-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 插件生命周期
// 每个插件只有一个主类继承 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 清单
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 构建系统
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 插件前端入口系统
// 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 配置
// 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 分析
// 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 变量方案 */
[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 自定义组件定义变量名
技术要点:
: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 推荐方案:方案 C(CSS 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 后发现可选位置:
- 侧边栏底部(推荐) —
sidebar__profile区域,与用户头像并列 - 顶部导航栏 — 但目前 Halo 没有顶部导航栏
- 插件自有设置页面 — 可添加一个设置页面
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 关键发现总结
- Halo 后台完全没有黑暗模式支持 — 这既是挑战也是机会,意味着插件是第一无二的解决方案
- 插件 CSS 最后加载 — 为样式覆盖提供了天然优势
definePlugin是纯透传函数 — 简单但缺少类型安全,需要自行保证配置正确性tailwindcss-themer基础设施存在 — 但只配置了 light 主题且运行时无法修改extensionPoints机制尚不成熟 — 不推荐作为主要实现手段- Halo 使用
pnpmworkspace — 插件前端开发需匹配此工具链
6.3 技术风险
| 风险 | 严重程度 | 缓解措施 |
|---|---|---|
| Halo 升级导致选择器失效 | 中 | 使用语义化 CSS 变量名;定期跟进 Halo changelog |
@halo-dev/components 组件内部样式无法覆盖 |
中 | 利用 CSS 变量穿透(该组件库可能使用 CSS 变量或 Tailwind 类) |
| 第三方插件不受暗色主题影响 | 低 | 在设计文档中说明插件的暗色化范围 |
| FormKit/编辑器样式复杂 | 中 | 优先保证核心布局暗色化,表单和编辑器作为第二阶段 |
7. 下一步建议
- 审阅此调查文档,确认技术方向
- 编写设计文档,详细定义:
- CSS 变量命名体系
- 组件覆盖策略
- 开关 UI 设计
- 路由/菜单结构
- 版本兼容性承诺
- 使用
create-halo-plugin创建插件骨架 - TDD 开发流程(先写测试,后实现)
本文档基于 2026-08-06 时 Halo v2.25 的代码分析。