本文档面向想要为 plugin-ui-beautify 插件创建自定义主题的开发者。
style.css(全局结构,主题无关)
|
+-- theme-base-light.css(浅色公共样式 + CSS 变量)
| +-- theme-default.css(薄荷)
| +-- theme-ocean.css(海洋)
| +-- theme-sakura.css(樱花)
| +-- theme-minimal.css(极简)
| +-- patch-plugin-pages-light.css(插件页补丁层)
| +-- patch-app-store-light.css(应用市场补丁层)
| +-- patch-dashboard-light.css(仪表盘补丁层)
|
+-- theme-base-dark.css(暗色公共样式 + CSS 变量)
+-- theme-dark.css(暗夜)
+-- theme-deepblue.css(深邃蓝)
+-- theme-aurora.css(极光)
+-- theme-neon.css(霓虹)
+-- patch-plugin-pages-dark.css(插件页补丁层)
+-- patch-app-store-dark.css(应用市场补丁层)
+-- patch-dashboard-dark.css(仪表盘补丁层)
加载顺序:style.css 始终加载 -> 根据主题类型加载对应 base -> 加载 theme-{name}.css 覆盖 -> 加载页面级 patch CSS。
所有组件样式通过 CSS 变量(--ui-*)引用颜色。base 文件定义默认值,主题文件通过 :root 覆盖变量即可改变全局配色,无需重写组件样式。
main.js
|
+-- App(核心): 配置加载、主题切换、路由监听、模块管理、patch CSS 加载
+-- FX(粒子引擎): Canvas 粒子系统,每个主题定义独立的粒子配置
+-- 功能模块: 通过 App.register() 注册,支持生命周期回调
+-- pageTransition(页面过渡动画)
+-- listAnimation(列表入场动画)
+-- cursorGlow(光标光晕)
+-- macOSCards(macOS 窗口风格)
+-- wallpaper(动态壁纸)
+-- welcomeBanner(欢迎横幅)
+-- routeFlags(路由语义类)
+-- semanticPagePatches(页面语义补丁标记)
+-- shadowThemeBridge(shadow DOM 主题桥接)
网关页面(登录、注册等)使用独立的样式和效果系统:
gateway-beautify.css— 公共样式gateway-{name}.css— 主题特定样式gateway-effects.js— 独立的粒子效果引擎- 通过
GatewayStyleInjector.java在服务端注入 HTML
每个主题必须基于一个 base 文件。base 文件提供了完整的组件样式(侧边栏、菜单、卡片、表单、弹窗等),你的主题只需覆盖颜色变量和添加特色样式。
- 浅色主题 -> 继承
theme-base-light.css - 暗色主题 -> 继承
theme-base-dark.css
base 的加载由 main.js 中的 DARK_THEMES 数组决定:
var DARK_THEMES = ["dark", "deepblue", "aurora", "neon"];如果你的主题名在 DARK_THEMES 中,系统会加载 theme-base-dark.css;否则加载 theme-base-light.css。
加载逻辑位于 App.loadThemeCSS() 方法中:
var isDark = DARK_THEMES.includes(theme);
var baseName = isDark ? "theme-base-dark" : "theme-base-light";文件命名规则:theme-{name}.css,其中 {name} 是你的主题标识符(小写英文,无空格)。
放置位置:src/main/resources/console/
主题 CSS 文件的职责:
- 覆盖
:root中的 CSS 变量来定制颜色 - 添加主题特有的组件样式(渐变背景、装饰元素等)
以下是一个最小的浅色主题模板:
/* ============================================
Theme: MyTheme — 简短描述
Primary: #主色调
============================================ */
/* 覆盖颜色变量(可选,不覆盖则使用 base 默认值) */
:root {
--ui-primary: #你的主色调;
--ui-primary-soft: rgba(你的主色调, 0.08);
}
/* === 侧边栏定制 === */
.sidebar {
background: linear-gradient(180deg, #颜色1 0%, #颜色2 100%) !important;
border-right: 1px solid rgba(主色调, 0.15) !important;
}
/* === 主内容区背景 === */
.main-content {
background: linear-gradient(160deg, #颜色1 0%, #颜色2 100%) !important;
}
/* === 菜单激活项 === */
.menu-item-title.active {
background: linear-gradient(135deg, rgba(主色调, 0.15), rgba(主色调, 0.08)) !important;
}
.menu-item-title.active::before {
background: linear-gradient(180deg, #主色调, #深色调) !important;
}注意事项:
- 所有样式需要
!important来覆盖 Halo 默认样式 - 参考
theme-default.css(薄荷主题)了解完整的组件覆盖方式 - 不需要重写 base 中已有的通用样式(圆角、过渡动画等),只需覆盖颜色相关的部分
打开 src/main/resources/console/main.js,修改文件顶部的两个常量数组:
// 1. 在 VALID_THEMES 中添加你的主题名
var VALID_THEMES = ["default", "ocean", "deepblue", "dark", "sakura", "minimal", "aurora", "neon", "mytheme"];
// 2. 如果是暗色主题,还需添加到 DARK_THEMES
var DARK_THEMES = ["dark", "deepblue", "aurora", "neon", "mytheme"];VALID_THEMES 用于校验主题名是否合法。如果用户配置了不在列表中的主题名,系统会回退到 "minimal"。
粒子效果是可选的。如果你的主题不需要粒子效果,跳过此步骤即可(系统不会为没有 FX.THEMES 配置的主题显示粒子)。
在 main.js 的 FX.THEMES 对象中添加你的主题配置:
FX.THEMES.mytheme = {
count: 20, // 粒子数量
color: function() { // 返回粒子颜色字符串
return "rgba(100,200,150,0.15)";
},
size: function() { // 返回粒子大小(像素)
return 3 + Math.random() * 5;
},
speed: function() { // 返回垂直移动速度(vy)
return 0.2 + Math.random() * 0.4;
},
draw: function(ctx, p) { // 绘制单个粒子
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fillStyle = p.color;
ctx.fill();
},
update: function(p, w, h) { // 每帧更新粒子位置
p.y -= p.vy;
p.x += Math.sin(p.wobble) * 0.3;
p.wobble += 0.02;
if (p.y < -20) { p.y = h + 10; p.x = Math.random() * w; }
},
// 可选属性
shootingStars: false, // 是否启用流星效果
shootingStarColor: "#a78bfa", // 流星颜色
shootingStarGlow: "#c4b5fd", // 流星光晕颜色
wind: false // 是否启用风效果(影响 vx)
};各属性详细说明见第 4 节。
打开 src/main/resources/extensions/setting.yaml,在 consoleTheme 和 gatewayTheme 的 options 列表中添加你的主题选项:
- label: "🌲 森林 — 自然深邃"
value: "mytheme"如果是暗色主题且部分页面未完全适配,建议在 label 中标注:
- label: "🌲 森林 — 自然深邃(⚠️ 部分页面未适配)"
value: "mytheme"注意:consoleTheme 和 gatewayTheme 是两个独立的选项列表,如果你的主题同时支持 Console 和网关页面,两处都需要添加。
打开 src/main/java/run/halo/ui/beautify/GatewayStyleInjector.java,在 VALID_THEMES 集合中添加你的主题名:
private static final Set<String> VALID_THEMES = Set.of(
"default", "ocean", "deepblue", "dark", "sakura", "minimal", "aurora", "neon", "mytheme"
);这个集合用于校验网关页面的主题配置。如果主题名不在集合中,系统会回退到 "minimal"。
同时,你需要创建对应的网关样式文件 src/main/resources/static/gateway-mytheme.css(详见第 6 节)。
以下是 theme-base-light.css 和 theme-base-dark.css 中 :root 定义的所有 CSS 变量:
| 变量名 | 用途 | 浅色默认值 | 暗色默认值 |
|---|---|---|---|
--ui-bg |
页面背景色 | #f8fafc |
#1a1a2e |
--ui-surface |
卡片/面板表面色 | #ffffff |
rgba(30,30,50,0.8) |
--ui-surface-hover |
表面悬停色 | #f1f5f9 |
rgba(50,50,70,0.8) |
--ui-border |
边框颜色 | rgba(0,0,0,0.06) |
rgba(255,255,255,0.06) |
--ui-border-hover |
边框悬停色 | rgba(0,0,0,0.1) |
rgba(255,255,255,0.12) |
--ui-text |
主文本颜色 | #1e293b |
#e0e0f0 |
--ui-text-secondary |
次要文本颜色 | #64748b |
#a0a0b8 |
--ui-text-muted |
弱化文本颜色 | #94a3b8 |
#666680 |
--ui-primary |
主色调(强调色) | #6366f1 |
#8b5cf6 |
--ui-primary-soft |
主色调柔和版(背景用) | rgba(99,102,241,0.08) |
rgba(139,92,246,0.1) |
--ui-shadow |
阴影颜色 | rgba(0,0,0,0.04) |
rgba(0,0,0,0.2) |
--ui-shadow-hover |
悬停阴影颜色 | rgba(0,0,0,0.08) |
rgba(0,0,0,0.3) |
--ui-scrollbar |
滚动条颜色 | rgba(148,163,184,0.3) |
rgba(255,255,255,0.15) |
--ui-scrollbar-hover |
滚动条悬停色 | rgba(148,163,184,0.5) |
rgba(255,255,255,0.25) |
--ui-overlay |
遮罩层颜色 | rgba(255,255,255,0.8) |
rgba(0,0,0,0.6) |
这些变量被 base 文件中的所有组件样式引用。你只需在主题 CSS 的 :root 中覆盖需要修改的变量,未覆盖的变量会使用 base 的默认值。
示例 — 覆盖主色调为绿色:
:root {
--ui-primary: #22c55e;
--ui-primary-soft: rgba(34, 197, 94, 0.1);
}Console 端的粒子效果定义在 main.js 的 FX.THEMES 对象中。每个主题的配置是一个对象,包含以下属性:
FX.THEMES.themeName = {
// --- 必需属性 ---
count: Number, // 粒子数量(建议 4~35)
color: function() { // 返回颜色字符串(每个粒子创建时调用一次)
return "rgba(r,g,b,a)";
},
size: function() { // 返回粒子大小,单位像素
return Number;
},
speed: function() { // 返回垂直移动速度(赋值给 vy)
return Number; // 返回 0 表示粒子不做垂直移动
},
draw: function(ctx, p) { // 绘制单个粒子
// ctx: CanvasRenderingContext2D
// p: 粒子对象(见下方属性列表)
},
update: function(p, w, h) { // 每帧更新粒子状态
// p: 粒子对象
// w: 画布宽度
// h: 画布高度
},
// --- 可选属性 ---
shootingStars: Boolean, // 是否启用流星效果(默认 false)
shootingStarColor: String, // 流星颜色,如 "#a78bfa"
shootingStarGlow: String, // 流星光晕颜色,如 "#c4b5fd"
wind: Boolean // 是否启用风效果(默认 false)
// 启用后粒子会有水平漂移(影响 vx)
};各内置主题的粒子效果参考:
| 主题 | count | 效果类型 | 特殊功能 |
|---|---|---|---|
| default | 12 | 径向渐变光斑,缓慢漂移 | - |
| sakura | 22 | 五瓣花瓣,旋转飘落 | wind |
| ocean | 18 | 气泡,向上浮动 | - |
| deepblue | 15 | 发光粒子,缓慢上浮 | - |
| dark | 35 | 闪烁星点 | shootingStars |
| aurora | 4 | 大型径向渐变光团 | - |
| neon | 30 | 霓虹发光点,缓慢漂移 | shootingStars |
FX.createParticle(cfg, w, h) 方法为每个粒子创建一个对象,包含以下属性:
| 属性 | 类型 | 说明 |
|---|---|---|
x |
Number | 水平位置(随机 0~w) |
y |
Number | 垂直位置(随机 0~h) |
size |
Number | 粒子大小(由 cfg.size() 返回) |
color |
String | 粒子颜色(由 cfg.color() 返回) |
vy |
Number | 垂直速度(由 cfg.speed() 返回) |
vx |
Number | 水平速度(初始为 0,wind 模式下会被修改) |
rot |
Number | 旋转角度(随机 0~2PI) |
rotSpeed |
Number | 旋转速度(随机 -0.01~0.01) |
wobble |
Number | 摆动相位(随机 0~2PI,用于 Math.sin 产生波动) |
phase |
Number | 闪烁相位(随机 0~2PI) |
phaseSpeed |
Number | 闪烁速度(0.01~0.03) |
alpha |
Number | 当前透明度(初始为 1) |
baseAlpha |
Number | 基础透明度(0.15~0.5) |
你可以在 update 函数中修改这些属性来实现各种动画效果。也可以在 draw 函数中读取这些属性来控制绘制方式。
常见的动画模式:
// 1. 向上浮动(气泡效果)
update: function(p, w, h) {
p.y -= p.vy;
p.x += Math.sin(p.wobble) * 0.3;
p.wobble += 0.02;
if (p.y < -20) { p.y = h + 10; p.x = Math.random() * w; }
}
// 2. 向下飘落(花瓣/雪花效果)
update: function(p, w, h) {
p.y += p.vy;
p.x += Math.sin(p.wobble) * 0.5;
p.wobble += 0.015;
p.rot += p.rotSpeed;
if (p.y > h + 20) { p.y = -20; p.x = Math.random() * w; }
}
// 3. 闪烁(星空效果)
update: function(p) {
p.phase += p.phaseSpeed;
p.alpha = 0.3 + Math.sin(p.phase) * 0.7;
}
// 4. 缓慢漂移(光斑效果)
update: function(p, w, h) {
p.x += Math.sin(p.wobble) * 0.2;
p.y += Math.cos(p.wobble * 0.8) * 0.15;
p.wobble += 0.003;
if (p.x < -p.size) p.x = w + p.size;
if (p.x > w + p.size) p.x = -p.size;
if (p.y < -p.size) p.y = h + p.size;
if (p.y > h + p.size) p.y = -p.size;
}通过 App.register() 注册功能模块。每个模块是一个对象,支持以下生命周期回调:
App.register({
id: "myModule", // 模块唯一标识(必需)
toggle: "enableMyModule", // 对应的功能开关名(可选)
// 对应 setting.yaml 中 features 组的字段名
skipIfReducedMotion: true, // 是否在用户开启"减少动画"时跳过初始化(可选)
init: function(app) { // 初始化回调(必需)
// app: App 对象引用
// 在这里创建 DOM 元素、绑定事件等
},
onThemeChange: function(theme) { // 主题变更回调(可选)
// theme: 新的主题名(已解析,不会是 "auto")
},
onRouteChange: function(path) { // 路由变更回调(可选)
// path: 当前路径(pathname + hash)
},
onToggle: function(on) { // 开关状态变更回调(可选)
// on: Boolean,当前开关状态
},
destroy: function() { // 销毁清理回调(可选)
// 移除 DOM 元素、解绑事件、取消 RAF 等
}
});模块初始化流程:
App._initModules()遍历所有已注册模块- 如果模块设置了
skipIfReducedMotion: true且用户开启了"减少动画"偏好,跳过该模块 - 如果模块设置了
toggle且对应开关为关闭状态,跳过该模块 - 调用
mod.init(App)初始化模块
- 在
main.js中编写模块代码,放在/* ========== BOOTSTRAP ========== */注释之前 - 如果模块需要功能开关,在
setting.yaml的features组中添加对应的 checkbox - 在
App.fetchConfig()方法中添加对应的readToggle调用
示例 — 添加一个简单的打字机效果模块:
App.register({
id: "typewriter",
toggle: "enableTypewriter",
skipIfReducedMotion: true,
_styleEl: null,
init: function() {
this._styleEl = document.createElement("style");
this._styleEl.id = "ui-beautify-typewriter";
this._styleEl.textContent =
"@keyframes _ui_typewriter{from{width:0}to{width:100%}}" +
".page-header__title{overflow:hidden;white-space:nowrap;" +
"animation:_ui_typewriter 0.5s steps(20) forwards}";
document.head.appendChild(this._styleEl);
},
onToggle: function(on) {
if (this._styleEl) this._styleEl.disabled = !on;
},
destroy: function() {
if (this._styleEl && this._styleEl.parentNode) {
this._styleEl.parentNode.removeChild(this._styleEl);
}
}
});对应的 setting.yaml 配置:
- $formkit: checkbox
label: "启用打字机效果"
name: enableTypewriter
value: true
help: "页面标题以打字机动画方式显示"网关页面(登录、注册、密码重置等)有独立的样式和效果系统,与 Console 端分开管理。
src/main/resources/static/
+-- gateway-beautify.css # 公共样式(所有网关主题共享)
+-- gateway-default.css # 薄荷主题
+-- gateway-ocean.css # 海洋主题
+-- gateway-sakura.css # 樱花主题
+-- gateway-dark.css # 暗夜主题
+-- gateway-deepblue.css # 深邃蓝主题
+-- gateway-aurora.css # 极光主题
+-- gateway-neon.css # 霓虹主题
+-- gateway-minimal.css # 极简主题
+-- gateway-effects.js # 粒子效果引擎
GatewayStyleInjector.java 通过 WebFilter 拦截网关页面的 HTML 响应,在 </head> 前注入:
<link rel="stylesheet" href="/plugins/plugin-ui-beautify/assets/static/gateway-beautify.css" />
<link rel="stylesheet" href="/plugins/plugin-ui-beautify/assets/static/gateway-{theme}.css" />
<script src="/plugins/plugin-ui-beautify/assets/static/gateway-effects.js" defer></script>
<script>window.__UI_BEAUTIFY_THEME__="{theme}";</script>拦截的路径包括:
- 精确匹配:
/login、/signup、/logout、/setup、/binding - 前缀匹配:
/challenges/、/password-reset/
网关端的粒子效果接口与 Console 端略有不同,使用 create 方法代替 color/size/speed:
var THEMES = {
mytheme: {
count: 15, // 粒子数量
create: function() { // 创建单个粒子(返回粒子对象)
return {
x: Math.random() * w, // w, h 是闭包中的画布尺寸
y: Math.random() * h,
size: 3 + Math.random() * 5,
color: "rgba(100,200,150,0.15)",
vy: 0.3 + Math.random() * 0.5,
wobble: Math.random() * Math.PI * 2
// 可以添加任意自定义属性
};
},
draw: function(p) { // 绘制粒子(ctx 在闭包中)
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fillStyle = p.color;
ctx.fill();
},
update: function(p) { // 更新粒子(w, h 在闭包中)
p.y -= p.vy;
p.x += Math.sin(p.wobble) * 0.3;
p.wobble += 0.02;
if (p.y < -20) { p.y = h + 10; p.x = Math.random() * w; }
}
}
};注意与 Console 端 FX.THEMES 的区别:
- 网关端使用
create()方法直接返回完整的粒子对象 - Console 端使用
color()、size()、speed()分别返回属性值,由FX.createParticle()组装 - 网关端的
draw和update不接收ctx/w/h参数(它们在闭包中) - Console 端的
draw(ctx, p)和update(p, w, h)通过参数传递
- 创建
src/main/resources/static/gateway-mytheme.css - 在
gateway-effects.js的THEMES对象中添加粒子配置 - 在
setting.yaml的gatewayThemeoptions 中添加选项 - 在
GatewayStyleInjector.java的VALID_THEMES中添加主题名
以下演示从零创建一个名为 "forest" 的暗色主题,主色调为翠绿色,粒子效果为萤火虫。
文件路径:src/main/resources/console/theme-forest.css
/* ============================================
Theme: Forest — 深林幽境
Primary: #22c55e
Accent gradient: emerald → lime
============================================ */
/* 覆盖暗色 base 的颜色变量 */
:root {
--ui-bg: #0f1a0f;
--ui-surface: rgba(20, 40, 20, 0.85);
--ui-surface-hover: rgba(30, 60, 30, 0.8);
--ui-border: rgba(34, 197, 94, 0.08);
--ui-border-hover: rgba(34, 197, 94, 0.15);
--ui-text: #d4e8d4;
--ui-text-secondary: #8faa8f;
--ui-text-muted: #4a6a4a;
--ui-primary: #22c55e;
--ui-primary-soft: rgba(34, 197, 94, 0.12);
--ui-shadow: rgba(0, 0, 0, 0.3);
--ui-shadow-hover: rgba(0, 0, 0, 0.4);
--ui-scrollbar: rgba(34, 197, 94, 0.2);
--ui-scrollbar-hover: rgba(34, 197, 94, 0.35);
--ui-overlay: rgba(0, 0, 0, 0.7);
}
/* === 侧边栏 === */
.sidebar {
background: linear-gradient(180deg, #0f1a0f 0%, #132613 50%, #0f1a0f 100%) !important;
border-right: 1px solid rgba(34, 197, 94, 0.1) !important;
}
.sidebar__search {
background-color: rgba(34, 197, 94, 0.06) !important;
border: 1px solid rgba(34, 197, 94, 0.1) !important;
}
.sidebar__search:hover {
background-color: rgba(34, 197, 94, 0.1) !important;
border-color: rgba(34, 197, 94, 0.2) !important;
}
/* === 菜单 === */
.menu-item:hover {
background-color: rgba(34, 197, 94, 0.08) !important;
}
.menu-item-title.active {
background: linear-gradient(135deg, rgba(34, 197, 94, 0.15), rgba(34, 197, 94, 0.06)) !important;
color: #d4e8d4 !important;
}
.menu-item-title.active::before {
background: linear-gradient(180deg, #22c55e, #16a34a) !important;
}
/* === 主内容区 === */
.main-content {
background:
radial-gradient(ellipse at 20% 20%, rgba(34, 197, 94, 0.04), transparent 50%),
radial-gradient(ellipse at 80% 80%, rgba(22, 163, 74, 0.03), transparent 50%),
#0f1a0f !important;
}
/* === 页面头部 === */
.page-header {
background: rgba(15, 26, 15, 0.9) !important;
border-bottom: 1px solid rgba(34, 197, 94, 0.08) !important;
}
/* === 卡片 === */
.card-wrapper {
background: rgba(20, 40, 20, 0.7) !important;
border: 1px solid rgba(34, 197, 94, 0.1) !important;
}
.card-wrapper:hover {
border-color: rgba(34, 197, 94, 0.2) !important;
box-shadow: 0 4px 16px rgba(34, 197, 94, 0.08) !important;
}在 main.js 的 FX.THEMES 对象中添加:
forest: {
count: 25,
color: function() {
var colors = [
"rgba(34,197,94,",
"rgba(74,222,128,",
"rgba(134,239,172,",
"rgba(187,247,208,"
];
return colors[Math.floor(Math.random() * colors.length)]
+ (0.1 + Math.random() * 0.25).toFixed(2) + ")";
},
size: function() { return 1.5 + Math.random() * 3; },
speed: function() { return 0; },
draw: function(ctx, p) {
/* 萤火虫核心 */
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
var alpha = (p.alpha * p.baseAlpha).toFixed(2);
var colorBase = p.color.slice(0, p.color.lastIndexOf(",") + 1);
ctx.fillStyle = colorBase + alpha + ")";
ctx.fill();
/* 萤火虫光晕 */
var g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.size * 4);
g.addColorStop(0, colorBase + (alpha * 0.3).toFixed(2) + ")");
g.addColorStop(1, "transparent");
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * 4, 0, Math.PI * 2);
ctx.fillStyle = g;
ctx.fill();
},
update: function(p, w, h) {
/* 缓慢随机漂移 + 闪烁 */
p.phase += p.phaseSpeed;
p.alpha = 0.2 + Math.sin(p.phase) * 0.8;
p.x += Math.sin(p.wobble) * 0.3;
p.y += Math.cos(p.wobble * 0.7) * 0.2;
p.wobble += 0.004 + Math.random() * 0.002;
/* 边界循环 */
if (p.x < -10) p.x = w + 10;
if (p.x > w + 10) p.x = -10;
if (p.y < -10) p.y = h + 10;
if (p.y > h + 10) p.y = -10;
}
},var VALID_THEMES = ["default", "ocean", "deepblue", "dark", "sakura", "minimal", "aurora", "neon", "forest"];
var DARK_THEMES = ["dark", "deepblue", "aurora", "neon", "forest"];光标光晕、极光背景和动态壁纸默认会从 CSS 变量 --ui-primary 自动回退生成颜色,因此不需要额外修改 _COLORS 映射也能正常工作。
如果你希望为新主题提供更精细的专属颜色(而不是使用 --ui-primary 的自动回退),可以可选地在光标光晕模块的 _COLORS 中添加:
_COLORS: {
// ... 已有主题 ...
"forest": "rgba(34,197,94,0.15)"
},在 consoleTheme 的 options 中添加:
- label: "🌲 森林 — 深林幽境(⚠️ 部分页面未适配)"
value: "forest"在 gatewayTheme 的 options 中添加:
- label: "🌲 森林 — 深林幽境(⚠️ 部分页面未适配)"
value: "forest"private static final Set<String> VALID_THEMES = Set.of(
"default", "ocean", "deepblue", "dark", "sakura", "minimal", "aurora", "neon", "forest"
);创建 src/main/resources/static/gateway-forest.css,参考已有的 gateway-dark.css 编写网关页面的暗色样式。
在 gateway-effects.js 的 THEMES 对象中添加:
forest: {
count: 20,
create: function() {
var colors = [
"rgba(34,197,94,0.2)",
"rgba(74,222,128,0.15)",
"rgba(134,239,172,0.18)"
];
return {
x: Math.random() * w,
y: Math.random() * h,
size: 1.5 + Math.random() * 2.5,
color: colors[Math.floor(Math.random() * colors.length)],
phase: Math.random() * Math.PI * 2,
phaseSpeed: 0.008 + Math.random() * 0.015,
alpha: 1,
baseAlpha: 0.2 + Math.random() * 0.4,
wobble: Math.random() * Math.PI * 2
};
},
draw: function(p) {
var a = (p.alpha * p.baseAlpha).toFixed(2);
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fillStyle = p.color.replace(/[\d.]+\)$/, a + ")");
ctx.fill();
var g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.size * 3);
g.addColorStop(0, p.color.replace(/[\d.]+\)$/, (a * 0.3).toFixed(2) + ")"));
g.addColorStop(1, "transparent");
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * 3, 0, Math.PI * 2);
ctx.fillStyle = g;
ctx.fill();
},
update: function(p) {
p.phase += p.phaseSpeed;
p.alpha = 0.2 + Math.sin(p.phase) * 0.8;
p.x += Math.sin(p.wobble) * 0.25;
p.y += Math.cos(p.wobble * 0.7) * 0.15;
p.wobble += 0.004;
if (p.x < -10) p.x = w + 10;
if (p.x > w + 10) p.x = -10;
if (p.y < -10) p.y = h + 10;
if (p.y > h + 10) p.y = -10;
}
},添加新主题时,确保以下文件都已正确修改:
-
src/main/resources/console/theme-{name}.css— Console 主题样式文件 -
src/main/resources/console/main.js—VALID_THEMES数组 -
src/main/resources/console/main.js—DARK_THEMES数组(仅暗色主题) -
src/main/resources/console/main.js—FX.THEMES.{name}粒子配置(可选) -
src/main/resources/console/main.js— 光标光晕_COLORS映射(可选) -
src/main/resources/extensions/setting.yaml—consoleThemeoptions -
src/main/resources/extensions/setting.yaml—gatewayThemeoptions(如果支持网关) -
src/main/java/run/halo/ui/beautify/GatewayStyleInjector.java—VALID_THEMES集合 -
src/main/resources/static/gateway-{name}.css— 网关主题样式文件(如果支持网关) -
src/main/resources/static/gateway-effects.js—THEMES.{name}粒子配置(如果支持网关)