# 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` | 注入脚本/样式到主题 `` | | `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.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 插件生命周期 ```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= /apis/api.console.halo.run/v1alpha1/ui-plugins/-/bundle.css?t= 2. setupModules.ts 执行流程: ├─ loadEnabledUiPluginModules() │ ├─ loadUiPluginBundle() → 加载 JS bundle │ └─ 读取 window["enabledUiPlugins"] → 获取已启用插件列表 ├─ setupComponents() → 注册 FormKit 输入组件 ├─ setupPluginModules() → 注册路由 + 全局组件 └─ setupPluginStyles() → 加载插件 CSS bundle └─ loadStyle(url) → 动态创建 标签 ``` **关键发现:** - **插件 CSS 会自动加载**:`setupPluginStyles()` 函数通过 `loadStyle()` 动态注入 `` 标签到 `` - **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(); 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 推荐方案:方案 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` 后发现可选位置: 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 的代码分析。*