10 Commits

Author SHA1 Message Date
Serendipity 4a83d05f27 chore: 移除 docs 原版 logo 跟踪 2026-08-08 21:40:26 +08:00
Serendipity 3e9630fc91 docs: README 增加截图预览并停止跟踪内部文档
- README 新增截图预览:插件设置界面与后台深色模式效果
- docs 下内部审查/计划文档从仓库移除,本地保留并加入 gitignore
- 新增 docs/screenshots 4 张截图
2026-08-08 21:36:59 +08:00
Serendipity c4d99ec041 chore: 更新插件 logo 2026-08-08 21:14:44 +08:00
Serendipity 30b2ae2f83 chore: 提交 gradle wrapper jar 并修复忽略规则 2026-08-08 20:51:16 +08:00
Serendipity cffbe572bf feat: 设置页官方化改造并升级 1.0.8
- 设置页改用 Halo 官方 VPageHeader / VCard / VDescription / VTag 组件
- 精简 variables.css 为设置页实际使用的变量
- 同步 README 更新日志与本地说明文档
2026-08-08 20:36:52 +08:00
Serendipity 96dd1ef4d3 refactor: 移除设置页键盘操作支持并升级 1.0.7
- SettingsView 删除方向键切换、roving tabindex、ARIA 单选组语义与焦点环,仅保留原生按钮点击切换
- README 清理过时描述并补充 v1.0.7 更新日志
- 版本升级 1.0.7
2026-08-08 20:11:28 +08:00
Serendipity df9cd52de6 chore: 更新插件清单 GitHub 仓库地址 2026-08-08 19:32:23 +08:00
Serendipity 394613f516 fix: 深色模式入口移至外观分组并移除侧边栏按钮(1.0.6)
- 设置入口 group 改为官方 interface(外观)分组
- 删除侧边栏 ThemeToggle 注入器及组件,设置页成为唯一入口
- verify-toggle.py 改为直接驱动主题状态并校验翻转
- 同步 README 与脚本文档,版本升级 1.0.6
2026-08-08 19:25:27 +08:00
Serendipity 5cbce86e03 chore: 收口复查遗留项并补 radiogroup 键盘导航 2026-08-08 18:46:43 +08:00
Serendipity 45451c0d49 chore: 提交工作区剩余变更与复查文档 2026-08-08 18:38:42 +08:00
28 changed files with 176 additions and 658 deletions
+6 -1
View File
@@ -1,8 +1,8 @@
.gradle/
build/
!gradle/wrapper/gradle-wrapper.jar
*.class
*.jar
!gradle/wrapper/gradle-wrapper.jar
*.war
*.log
*.tmp
@@ -35,3 +35,8 @@ third-party/darkreader/*
!third-party/darkreader/SHA256SUMS
__pycache__/
*.pyc
# 内部审查/计划文档不入库,仅保留本地
docs/*.md
docs/**/*.md
docs/原版logo.png
+34 -10
View File
@@ -8,19 +8,19 @@
- **偏好持久化**:自动记忆用户选择(localStorage),刷新不丢失
- **系统偏好跟随**:切换系统外观时自动响应
- **Dark Reader 引擎**:自动分析页面 CSS 与 DOM,覆盖 Halo 核心页面和第三方插件页面
- **侧边栏一键切换**:切换按钮自动出现在侧边栏 UserProfileBanner 上方
- **设置页面**:在「偏好设置 → 深色模式」中详细选择显示模式
- **官方外观分组**:设置入口位于 Halo「外观」分组,与主题、菜单、插件同组
- **设置页面**:在「外观 → 深色模式」中详细选择显示模式
- **零后端依赖**:纯前端实现,不需要额外后端 API
## 安装
1. 从 Releases 下载 `plugin-dark-mode-<version>.jar`
2. 在 Halo 后台的「插件管理」中上传并安装。
3. 启用插件后,侧边栏底部会出现深色模式切换按钮
3. 启用插件后,进入「外观 → 深色模式」设置页面调整主题
## 使用
安装并启用后,可以直接点击侧边栏按钮在深色和浅色模式之间切换,也可以进入设置页面选择固定模式:
安装并启用后,进入「外观 → 深色模式」设置页面选择显示模式:
| 配置项 | 可选值 | 说明 |
| -------- | --------- | -------------------- |
@@ -28,12 +28,24 @@
| 主题模式 | `dark` | 始终使用深色模式 |
| 主题模式 | `auto` | 跟随系统外观自动切换 |
## 截图预览
以下截图展示插件在 Halo 后台的实际效果。
![插件设置界面](docs/screenshots/插件界面.png)
![后台深色模式效果 1](docs/screenshots/photo1.png)
![后台深色模式效果 2](docs/screenshots/photo2.png)
![后台深色模式效果 3](docs/screenshots/photo3.png)
## 技术原理
- 插件通过 `useDarkMode()` 管理 `light` / `dark` / `auto` 三种状态。
- 深色模式下调用 Dark Reader 的 `enable()`,浅色模式下调用 `disable()`
- Dark Reader 会持续监听页面 DOM 变化,因此第三方插件动态渲染的内容也能自动转换。
- 插件自身只保留切换器与设置页所需的最小 UI 变量,不再维护逐页手工 CSS 覆盖。
- 插件自身只保留设置页所需的最小 UI 变量,不再维护逐页手工 CSS 覆盖。
> 说明:Dark Reader 的样式注入是异步的,刷新瞬间仍可能存在极短闪白;插件通过同步设置 `color-scheme` 缓解,但无法完全消除。
@@ -108,12 +120,9 @@ pnpm build # 生产构建
└── src/
├── index.ts # definePlugin 入口
├── darkreader-engine.ts # Dark Reader 通用暗色引擎
├── injector.ts # 侧边栏切换器注入
├── composables/
│ ├── useDarkMode.ts # 主题状态管理(模块级单例)
│ └── useSystemPreference.ts # 系统偏好监听
├── components/
│ └── ThemeToggle.vue # 侧边栏切换按钮
├── views/
│ └── SettingsView.vue # 设置页面
└── styles/
@@ -127,15 +136,30 @@ pnpm build # 生产构建
./gradlew test
```
运行时验证脚本位于 `scripts/verify-toggle.py`,用于检查主题切换、localStorage 持久化以及 Dark Reader 注入状态
运行时验证脚本位于 `scripts/verify-toggle.py`,用于直接驱动主题状态,并检查 data-halo-theme、localStorageDark Reader 注入与 color-scheme 的翻转
## 更新日志
### v1.0.8
- 设置页改用 Halo 官方 VPageHeader / VCard / VDescription / VTag 组件,与官方页面风格统一
- 精简插件自身 CSS 变量,仅保留设置页所需变量
### v1.0.7
- 移除设置页方向键切换、roving tabindex 与 ARIA 单选组语义,保留原生按钮点击切换
- 同步清理 README 中已过时的「设置选项支持键盘操作」描述
### v1.0.6
- 深色模式设置入口移至 Halo 官方「外观」分组
- 移除侧边栏底部注入的切换按钮,设置页成为唯一入口
- 更新运行时验证脚本:直接驱动主题状态,不再依赖侧边栏按钮
### v1.0.5
- 同步设置 `color-scheme`,缓解深色模式刷新闪烁
- 增加多标签页主题同步与 `useDarkMode` 单元测试
- 侧边栏切换按钮与设置选项支持键盘操作
- 后端日志改用 Lombok `@Slf4j`
- 裁剪 vendored `package.json`,新增 Dark Reader `SHA256SUMS` 完整性校验
- `plugin.yaml` 改为安装后由用户手动启用
-151
View File
@@ -1,151 +0,0 @@
# 修复计划 — halo-dark-mode-plugin2026-08-08
> 依据:`docs/review-2026-08-08.md`
> 状态:待用户审批
> 审批通过后按 P2 → P3 顺序执行
## 一、修复总览
| ID | 优先级 | 问题 | 修复方式 | 主要涉及文件 |
| --- | --- | --- | --- | --- |
| P2-1 | P2 | 深色用户刷新 console 会浅色闪烁 | 同步设置 `color-scheme`,在 `enable()` 前减少闪烁 | `ui/src/composables/useDarkMode.ts``ui/src/darkreader-engine.ts` |
| P2-2 | P2 | 本地项目文档仍描述旧的手工 CSS / Monaco 架构 | 同步 AGENTS.md、CLAUDE.md、scripts/README.md 到纯 Dark Reader 现状 | `AGENTS.md``CLAUDE.md``scripts/README.md` |
| P2-3 | P2 | `scripts/``workplace/` 脚本双份 | 删除重复脚本;专用分析脚本保留在 `workplace/` 或迁移,待确认 | `workplace/login_wait.py``workplace/scan_dark.py` 等 |
| P2-4 | P2 | 前端无单元测试 | 为 `useDarkMode` 补测试,覆盖 light/dark/auto、持久化与非法回退 | `ui/src/composables/__tests__/useDarkMode.spec.ts` |
| P2-5 | P2 | 多标签页主题不同步 | 监听 `storage` 事件同步主题 | `ui/src/composables/useDarkMode.ts` |
| P2-6 | P2 | 切换器/设置选项不可键盘操作 | 改为原生 `button` + ARIA,或原生 radio 语义 | `ui/src/components/ThemeToggle.vue``ui/src/views/SettingsView.vue` |
| P3-1 | P3 | 后端使用 `System.out.println` | 使用 Lombok `@Slf4j` 输出日志 | `src/main/java/run/halo/darkmode/DarkModePlugin.java` |
| P3-2 | P3 | vendored package.json 保留完整 devDependencies | 裁剪为最小字段,只保留构建所需元数据 | `third-party/darkreader/package.json` |
| P3-3 | P3 | 无 Dark Reader 升级/完整性机制 | 增加 `SHA256SUMS` 并写入 README 校验说明 | `third-party/darkreader/SHA256SUMS``README.md``.gitignore` |
| P3-4 | P3 | `data-halo-theme` 属性为历史遗留 | 保留属性,但在代码注释中说明兼容性遗留 | `ui/src/composables/useDarkMode.ts``ui/src/styles/variables.css` |
| P3-6 | P3 | `plugin.yaml``spec.enabled: true` | 建议改为 `false`,安装后由用户启用 | `src/main/resources/plugin.yaml` |
## 二、详细修复说明
### P2-1 FOUC 闪烁
-`useDarkMode.ts``applyHtmlAttribute()` 中同步设置:
`document.documentElement.style.colorScheme = isDark ? 'dark' : 'light'`
- `darkreader-engine.ts` 保持现有 `enable()/disable()` 逻辑。
- `color-scheme` 只能缓解浏览器控件/滚动条闪烁,无法完全消除 DR 异步注入间隙;
该限制会写进注释和 README。
### P2-2 本地项目文档
- `AGENTS.md` / `CLAUDE.md`
- 删除 `overrides/``halo-core.css`、Monaco 同步、FOUC 同步脚本等旧描述
- 统一为“纯 Dark Reader 策略”
- `scripts/README.md`
- 把“Monaco 日志查看器主题同步”改为检查 `data-darkreader-mode`
- 补充 `verify-toggle.py` 的新三向断言说明
### P2-3 脚本收口
- 明确 `scripts/` 为正式工具目录。
- 计划删除的重复文件(需审批):
- `workplace/login_wait.py`(重复 `scripts/login-wait.py`
- `workplace/scan_dark.py`(重复 `scripts/scan-dark.py`
- `workplace/fetch_bundle.py``probe_conflict.py``aggregate.py` 属于专用分析工具,
默认保留在 `workplace/`;如需一并迁移到 `scripts/`,请在审批时注明。
### P2-4 单元测试
新增 `ui/src/composables/__tests__/useDarkMode.spec.ts`,覆盖:
1. 默认 `auto` + 系统深色 → `isDark = true`
2. `auto` 下点击切换 → 变成显式深色/浅色
3. `dark ↔ light` 往返切换
4. `setTheme()` 持久化到 localStorage
5. localStorage 非法值回退到 `auto`
6. `storage` 事件跨标签页同步
同时移除 `pnpm test:unit``--passWithNoTests`,防止测试静默通过。
### P2-5 多标签页同步
- `useDarkMode.ts` 增加:
`window.addEventListener('storage', handler)`
- 仅当 `newValue` 是合法 `light/dark/auto` 时更新 `theme`
### P2-6 可访问性
- `ThemeToggle.vue`
- `div` 改为 `<button type="button">`
- 增加 `:aria-pressed="isDark"`
- 保留现有样式并补按钮 reset
- `SettingsView.vue`
- 选项容器加 `role="radiogroup"`
- 每个选项改为 `<button type="button" role="radio" :aria-checked="...">`
- 键盘 Tab / Enter / Space 原生可用
### P3-1 后端日志
- `DarkModePlugin.java` 增加 `@Slf4j`
- `System.out.println` 改为 `log.info(...)`
- 保留 start/stop 语义不变
### P3-2 vendored package.json
- 裁剪为:
`name / version / description / main / module / types / license`
- 删除 scripts、devDependencies、optionalDependencies 等构建无关字段
- 更新后执行 `pnpm install` 同步 lockfile
### P3-3 完整性校验
- 新增 `third-party/darkreader/SHA256SUMS`,记录:
`darkreader.js``darkreader.mjs``package.json``index.d.ts``LICENSE`
- `.gitignore` 白名单增加 `SHA256SUMS`
- README 增加“升级 Dark Reader 后校验 SHA256SUMS”说明
### P3-4 历史遗留属性
- 保留 `data-halo-theme`verify-toggle.py 仍使用)
-`useDarkMode.ts``variables.css` 增加注释:
该属性当前无 CSS 消费方,仅为兼容性遗留标记
### P3-6 plugin.yaml
- 建议将 `spec.enabled` 改为 `false`
- 理由:官方 manifest 文档建议生产环境由用户安装后手动启用
## 三、暂缓项(本计划不做)
- P3-5 i18n:目标用户为中文,暂不引入国际化
- Dark Reader 完整源码保留在本地但不入库:维持现状
- 不新增界面截图:README 已可用,后续上架前再补
## 四、回归验证
```bash
cd ui
pnpm install
pnpm type-check
pnpm lint
pnpm test:unit
pnpm build
cd ..
./gradlew test
./gradlew build
```
运行时验证:
```bash
D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\verify-toggle.py
```
验收标准:
- 单测至少覆盖 P2-4 中列出的 6 个场景且全部通过
- `pnpm test:unit` 不再 `--passWithNoTests`
- 深色刷新时 `color-scheme` 已同步
- 键盘可操作侧边栏按钮与设置选项
- `SHA256SUMS` 可校验,README 有说明
- 后端日志不再输出到 stdout
## 五、版本与提交
- 修复内容涉及运行时代码,后续打 JAR 时按项目约定递增版本(1.0.4 → 1.0.5
- 本次审批通过后,先提交代码修复,再按需打包
-81
View File
@@ -1,81 +0,0 @@
# 代码审查交接单 — halo-dark-mode-plugin2026-08-08
> 由审查窗口产出,供开发窗口执行。结论:**可合入,无 P1 阻断问题**。
> 审查基线:`HEAD = 363f30d`(工作区干净,已与 origin/main 同步)。
> 审查范围:Dark Reader 迁移相关 4 个提交(202ab53 / 0bb552b / 88a071f / 363f30d)。
## 一、开发窗口无需重复验证(审查窗口已实测)
- `vite build` 通过:main.js 109.96KBgzip 37.79KB)、style.css 3.76KBDark Reader 已打入 bundle`enable/disable` 具名导出解析正常
- `vue-tsc --build` 通过(exit 0
- `pnpm-lock.yaml``file:../third-party/darkreader` 依赖一致(含 malevic 0.20.2
- 构建产物 style.css 已无 `[data-halo-theme=dark]` 规则(属性属历史遗留)
- DR 产物头 `v4.9.129` 与 README 一致
## 二、待办清单(建议按 P2 → P3 顺序执行)
### P2-1 FOUC:深色用户刷新 console 会浅色闪烁
- 位置:`ui/src/darkreader-engine.ts`watch immediate 块,33-46 行)
- 原因:bundle 在 console 渲染后才执行,DR `enable()` 是异步分析+注入,无同步兜底
- 建议:`enable()` 前同步设 `document.documentElement.style.colorScheme = 'dark'`;或作为已知限制写进文档
### P2-2 本地项目文档滞后(与"纯 DR 策略"不符)
- `AGENTS.md` / `CLAUDE.md`:仍描述 `halo-core.css``overrides/`、Monaco 同步、FOUC 同步脚本——这些已不存在(`ui/src/styles/` 只剩 `index.css` + `variables.css`
- `scripts/README.md` 45-52 行:仍写"Monaco 日志查看器主题同步",实际已改为检查 `data-darkreader-mode`
- 建议:同步到纯 DR 现状
### P2-3 验证工具双份、命名不一致
- `scripts/`login-wait.py、scan-dark.py、verify-toggle.py
- `workplace/`login_wait.py、scan_dark.py、fetch_bundle.py、probe_conflict.py、aggregate.py
- 建议:统一收口到 `scripts/`,旧文件删除需用户书面确认
### P2-4 前端零单元测试
- `pnpm test:unit``vitest --passWithNoTests`,恒绿
- 建议:为 `ui/src/composables/useDarkMode.ts` 补 3-4 个用例,覆盖 `toggle()` 的 light/dark/auto 三种迁移与持久化回退
### P2-5 多标签页主题不同步
- 位置:`ui/src/composables/useDarkMode.ts`
- 建议:监听 `storage` 事件同步主题
### P2-6 可访问性
- `ui/src/components/ThemeToggle.vue` 10-15 行:div+@click,无 role/tabindex/键盘/aria-pressed
- `ui/src/views/SettingsView.vue` 36-41 行:选项 div+@click,同上
- 建议:补键盘与 ARIA(或改原生 radio 语义)
### P3-1 后端日志
- 位置:`src/main/java/run/halo/darkmode/DarkModePlugin.java` 24、29 行 `System.out.println`
- 建议:build.gradle 已有 Lombok,改 `@Slf4j` + `log.info`
### P3-2 vendored package.json 保留完整 devDependencies
- 位置:`third-party/darkreader/package.json`
- 建议:裁剪为 name/version/main/module/types/license 最小字段(`file:` 依赖不会安装 devDeps,但保持干净)
### P3-3 无 DR 升级/完整性机制
- 建议:README 或脚本记录 DR 产物 SHA-256,固定版本
### P3-4 data-halo-theme 属性 + variables.css 已成历史遗留
- 构建产物已无 `[data-halo-theme=dark]` 规则,属性无 CSS 消费方
- 建议:保留可以,但注释说明是兼容性遗留
### P3-5 文案硬编码中文,无 i18n
- 低优先级(目标用户中文),可暂缓
### P3-6 plugin.yaml `spec.enabled: true`
- 确认是否有意(Halo 通常安装后由用户启用)
## 三、完成后回归验证清单
```bash
cd ui
pnpm build # 构建通过
pnpm type-check # 类型检查通过
pnpm lint # 通过
# 真实环境验证(AGENTS.md 规定流程)
python scripts\verify-toggle.py # 三向断言全 PASS(属性/存储/Dark Reader 翻转)
```
## 四、无需改动的亮点(避免开发窗口误改)
- third-party/darkreader 只跟踪 5 个构建必需文件 + gitignore 白名单,LICENSE/版权头保留,合规 ✅
- style.css 从 ~500KB 降到 3.76KB,纯 DR 策略成立 ✅
- verify-toggle.py 的 darkreader 断言与策略一致 ✅
Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

-157
View File
@@ -1,157 +0,0 @@
# Halo 暗色模式残留排查报告
> 排查时间:2026-08-07
> 排查环境:腾讯云生产站 `blog.liuhangyv.top`Halo Pro 2.24.2
> 排查方式:Playwright 自动化全路由扫描,共 92 个后台路由
## 一、结论摘要
1. **部署版本没有问题**。服务端实际加载的 `bundle.css` 与本仓库 `ui/build/dist/style.css` 的 194 条暗色规则完全一致,不存在"部署了旧包"的情况。
2. **73 个后台路由存在深浅色残留**,19 个路由完全干净(主要是 Shop 插件全家桶、快照页、权限页)。
3. 残留可归纳为 **3 类根因**
- 插件 CSS 加载顺序在最前,同特异性规则被后加载的 Halo 核心样式覆盖;
- UnoCSS 哈希类前缀已从 `i-*` 变为 `uno-*`,现有兜底规则匹配不到;
- 第三方插件页面(AstraHub、RSS 订阅、私密文章、AI 评论等)基本没有覆盖。
## 二、扫描方法与产物
### 方法
1. Playwright 启动持久化 Edge 上下文,复用已登录会话;
2. 从 Vue Router 读取全部注册路由(92 个),逐页加载;
3. 每页强制 `data-halo-theme="dark"`,等待网络空闲后扫描;
4. 扫描规则:可见且尺寸大于 50×20 的元素,背景 RGB 均 >235 记为浅色背景残留,直接文本颜色 RGB 均 <70 记为深色文字残留;
5. 每页截图存档,结果按页面 + 元素分组去重。
### 产物(均在 `workplace/`
| 文件 | 说明 |
|------|------|
| `scan_dark.py` | 全路由扫描脚本 |
| `scan-results.json` | 原始扫描结果(92 页 × 元素级) |
| `aggregate.txt` | 按元素/类名/问题聚合后的明细 |
| `shots/*.png` | 92 页深色模式截图 |
| `deployed-bundle.css` | 服务端实际部署的 CSS 快照 |
| `probe_conflict.py` / `fetch_bundle.py` | 样式表加载顺序与版本对比脚本 |
## 三、总体数据
| 指标 | 数值 |
|------|------|
| 扫描路由数 | 92 |
| 存在残留的路由 | 73 |
| 完全干净的路由 | 19 |
| 去重后的残留组 | 134 |
| 页面 × 元素残留条目 | 256 |
| 影响页面最多的残留 | 侧边栏 `.menu-item-title.active` 高亮背景(68 页) |
| 次多 | `.empty-title` 深色文字(16 页) |
无残留的 19 个路由:`/console/403`、Shop 插件全部 11 个页面、`/console/app-store/privacy-policy``/console/posts/snapshots``/console/single-pages/snapshots``/console/users/auth-providers``/console/users/roles`
## 四、根因分析
### 根因 1(P0):插件 CSS 加载顺序在最前,同特异性规则被 Halo 核心样式覆盖
**证据**(来自 `probe_conflict.py`):
- 插件 `bundle.css``document.styleSheets` 的第 0 张表;
- Halo 核心样式 `process-bar-DsZ2lvXx.css` 是第 9 张表,加载更晚;
- 双方存在同特异性规则,CSS 级联规则让"后加载者"胜出:
| 规则 | 特异性 | 结果 |
|------|--------|------|
| Halo`.description-item-wrapper .description-item__label { color: rgb(17 24 39) }` | (0,2,0) | 胜出 |
| 插件:`[data-halo-theme="dark"] .description-item__label { color: var(--halo-text-secondary) }` | (0,2,0) | 被覆盖 |
**受影响页面**`/console/overview``/console/schedule-calendar``/console/theme` 的详情描述项(名称、Halo、站点地址等文字仍是深色)。
**修复建议**
- 对这类核心语义类统一加 `!important`
- 或把前缀从 `[data-halo-theme="dark"]` 提升为 `html[data-halo-theme="dark"]`,把特异性从 (0,2,0) 提高到 (0,2,1)。
推荐两者并用(提高特异性 + `!important` 双保险),因为 Halo 核心样式后续仍可能继续调整。
### 根因 2(P0):UnoCSS 哈希类前缀已变化,现有兜底规则失效
**证据**
- 大量残留元素的类名是 `uno-*` 前缀(如 `uno-ga61as``uno-529526``uno-b60d0h`);
- `plugin-pages.css` 中的兜底规则只覆盖 `[class*="i-"]`,这是旧版 UnoCSS 的哈希前缀;
- 部署 CSS 探针确认:`menu-item-title``empty-title``alert-wrapper` 三个关键字在暗色规则中完全不存在。
**受影响页面**AI Foundation 全家桶(每页 3–24 处)、数据迁移、装备/关注/照片/时间线/投票的卡片头部"新增"栏、SummaraidGPT 等。
**修复建议**
- 新增 `[class*="uno-"]` 的兜底覆盖(背景、文字、边框三件套),并加 `!important`
- 保留 `i-*` 规则,因为扫描中仍有少量 `i-*` 哈希(如 `.i-p4hnaq`)在真实页面出现,两个前缀并存。
### 根因 3(P0):核心语义类缺覆盖
以下类在部署 CSS 中**完全没有暗色规则**,属于新增覆盖而非覆盖失效:
| 类名 | 残留内容 | 影响页面 |
|------|----------|----------|
| `.menu-item-title.active` | 高亮背景 `rgb(243,244,246)` | 68 页 |
| `.empty-title` | 文字 `rgb(17,24,39)` | 16 页 |
| `.alert-wrapper` / `.alert-default` | 背景 `rgb(249,250,251)` | 3 页(概览、许可证、导入) |
| `.description-item__label/content` | 文字 `rgb(17,24,39)` | 3 页(同特异性失效,见根因 1) |
**修复建议**
- 侧边栏高亮:覆盖 `.menu-item-title.active`(含 `:hover`),用 `--halo-menu-item-active` 变量;
- 空状态:覆盖 `.empty-title`,用 `--halo-text-secondary`
- 提示条:覆盖 `.alert-wrapper` 及各变体的背景/边框/文字。
### 根因 4(P1):第三方插件页面几乎未覆盖
| 插件/页面 | 残留类名 | 说明 |
|-----------|----------|------|
| AstraHub + 心愿便签 | `.ah-card``.ah-topbar``.ah-float-nav``.ah-topbar-brand` | 两插件共用同一套 `ah-*` UI,覆盖一次可同时生效 |
| RSS 订阅 | `.subscription-panel``.feed-toolbar``.feed-status-tabs``.feed-stream``.feed-brief__title` | 整个页面几乎整片浅色 |
| 私密文章 | `.focus-card``.overview-card``.list-card``.empty-state` | 半透明白背景叠在深色上,文字也是深色 |
| AI 评论自动处理 | `.setting-panel``.form-row``.sidebar-card``.tabs-wrap``.list-col``.reply-card` | 设置页和日志页整片浅色 |
| 数据工坊 | `.modal-content``.uno-*` 卡片 | 弹窗与主内容区均残留 |
| 代码注入器 | `.uno-*` 侧栏/编辑区 | 整页浅色 |
| Meilisearch 概览 | `.uno-*` | 卡片与标题残留 |
| 日志查看器 | `.monaco-editor``.margin``.lines-content` | **Monaco Editor 自带独立主题系统**,仅靠 CSS 覆盖很难稳定 |
| 导入/存储工具 | `.uno-*` | 提示卡片、分页栏残留 |
**修复建议**
- `ah-*``feed-*``focus-card/overview-card/list-card``setting-panel/form-row/sidebar-card` 等语义类按插件页面补 `!important` 规则;
- Monaco 不要硬啃 CSS,优先在 JS 里监听主题切换并调用 `monaco.editor.setTheme('vs-dark')`,或加载官方 dark 主题 CSS;
- 每个插件补完后再跑一次 `scan_dark.py` 验证零残留。
### 根因 5(P2):历史遗留规则与自相矛盾的选择器
- `components.css` 中大量 `.v-*`Vuetify 类)在 Halo 2.25 的真实 DOM 中不存在,属无效规则(CLAUDE.md 已记录);
- `[class*="tag-"]``[class*="modal"]` 等宽泛匹配容易误伤,建议逐步收紧;
- `utilities.css``halo-core.css` 对链接色的规则互相覆盖,最终生效结果不明确;
- `halo-core.css``html, body` 编译后为 `[data-halo-theme="dark"] html`,该选择器**永远不匹配**(html 元素不会是其自身的后代),根背景兜底实际只靠 `body` 一条,页面在特殊滚动场景下仍有露白风险。
## 五、修复优先级清单
### P0(一次重构后全站收益最大)
1. 全局暗色规则前缀从 `[data-halo-theme="dark"]` 升级为 `html[data-halo-theme="dark"]`
2. 核心语义类(`.page-header``.card-wrapper``.description-item-*``.table` 等)加 `!important`
3. 新增 `.menu-item-title.active``.empty-title``.alert-wrapper` 覆盖;
4. 新增 `[class*="uno-"]` 兜底三件套;
5. 修正根背景规则,让 `html` 本身也参与暗色。
### P1(按插件逐个补齐并复扫)
6. `ah-*``feed-*`、私密文章、AI 评论插件语义类覆盖;
7. Monaco 主题切换接入;
8. 数据工坊、代码注入器、Meilisearch、导入/存储工具等 `uno-*` 页面复查。
### P2(清理)
9. 删除失效的 Vuetify 类规则,收敛宽泛匹配;
10. 统一链接色规则,消除重复/冲突。
## 六、完整残留明细
`workplace/aggregate.txt`(134 组,含影响页面列表),原始数据见 `workplace/scan-results.json`
+1 -1
View File
@@ -1,3 +1,3 @@
version=1.0.5
version=1.0.8
org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8
org.gradle.java.home=D:/settings/Language/Java/jdk-25.0.1
Binary file not shown.
+4 -2
View File
@@ -42,13 +42,15 @@ D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\scan-dark.py --mode
D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\verify-toggle.py
```
部署新版插件后运行,会检查:
部署新版插件后运行,会直接驱动 localStorage 中的主题状态,并检查:
- `data-halo-theme` 属性翻转
- localStorage 持久化翻转
- `data-darkreader-mode`Dark Reader 注入状态)翻转
- `color-scheme` 翻转
- 侧边栏已不再注入 `.theme-toggle` / `.plugin-dark-mode-toggle` 按钮
三项全部 PASS 时返回 0。
状态翻转全部 PASS 且按钮已移除时返回 0。
## 输出
+9
View File
@@ -0,0 +1,9 @@
# scripts/legacy
保留的旧版分析工具,来自早期手工 CSS 工作流,当前纯 Dark Reader 架构下不再参与日常流程。
- `aggregate.py` — 聚合多路由扫描结果
- `fetch_bundle.py` — 抓取线上部署 CSS bundle
- `probe_conflict.py` — 探测 CSS 覆盖冲突
如需恢复手工 CSS 扫描工作流,可参考这些脚本;否则可在一段时间后删除。
+62 -10
View File
@@ -1,4 +1,4 @@
"""验证侧边栏主题切换:点击后检查 data-halo-theme、localStorage 与 Dark Reader 模式"""
"""验证主题切换:直接驱动 localStorage检查 data-halo-theme、Dark Reader 与 color-scheme 翻转"""
import argparse
import pathlib
import sys
@@ -8,6 +8,7 @@ from playwright.sync_api import sync_playwright
DEFAULT_PROFILE = pathlib.Path(__file__).parent / ".browser-profile"
BASE = "https://blog.liuhangyv.top"
STORAGE_KEY = "halo-dark-mode-theme"
def read_state(page) -> dict:
@@ -16,10 +17,40 @@ def read_state(page) -> dict:
attr: document.documentElement.getAttribute('data-halo-theme'),
darkreader: document.documentElement.getAttribute('data-darkreader-mode'),
stored: localStorage.getItem('halo-dark-mode-theme'),
colorScheme: document.documentElement.style.colorScheme,
})"""
)
def set_mode(page, mode: str) -> None:
"""写入持久化偏好并派发 storage 事件,让当前页面 useDarkMode 单例即时响应。"""
page.evaluate(
"""([key, mode]) => {
localStorage.setItem(key, mode)
window.dispatchEvent(
new StorageEvent('storage', { key: key, newValue: mode })
)
}""",
[STORAGE_KEY, mode],
)
def wait_mode(page, expect_dark: bool) -> None:
page.wait_for_function(
"""(expectDark) => {
const attr = document.documentElement.getAttribute('data-halo-theme')
const darkreader = document.documentElement.getAttribute('data-darkreader-mode')
const scheme = document.documentElement.style.colorScheme
if (expectDark) {
return attr === 'dark' && darkreader === 'dynamic' && scheme === 'dark'
}
return attr === null && darkreader === null && scheme !== 'dark'
}""",
arg=expect_dark,
timeout=15000,
)
def main() -> int:
parser = argparse.ArgumentParser(description="验证主题切换运行时行为")
parser.add_argument("--base", default=BASE)
@@ -45,33 +76,54 @@ def main() -> int:
ctx.close()
return 3
toggle = page.locator(".theme-toggle")
toggle.wait_for(state="visible", timeout=15000)
toggle_absent = page.locator(".theme-toggle, .plugin-dark-mode-toggle").count() == 0
print(f"侧边栏注入按钮已移除: {'PASS' if toggle_absent else 'FAIL'}")
# 先归一化到浅色,保证后续翻转判定与当前会话状态无关
set_mode(page, "light")
wait_mode(page, expect_dark=False)
before = read_state(page)
toggle.click()
time.sleep(1)
set_mode(page, "dark")
wait_mode(page, expect_dark=True)
after = read_state(page)
toggle.click()
time.sleep(1)
set_mode(page, "light")
wait_mode(page, expect_dark=False)
restored = read_state(page)
attr_flipped = before["attr"] != after["attr"] and before["attr"] == restored["attr"]
stored_flipped = before["stored"] != after["stored"] and before["stored"] == restored["stored"]
stored_flipped = (
before["stored"] != after["stored"] and before["stored"] == restored["stored"]
)
darkreader_flipped = (
before["darkreader"] != after["darkreader"]
and before["darkreader"] == restored["darkreader"]
)
scheme_flipped = (
before["colorScheme"] != after["colorScheme"]
and before["colorScheme"] == restored["colorScheme"]
)
print(f"初始: {before}")
print(f"切换: {after}")
print(f"还原: {restored}")
print(
f"属性翻转: {'PASS' if attr_flipped else 'FAIL'} "
f"存储翻转: {'PASS' if stored_flipped else 'FAIL'} "
f"Dark Reader 翻转: {'PASS' if darkreader_flipped else 'FAIL'}"
f"Dark Reader 翻转: {'PASS' if darkreader_flipped else 'FAIL'} "
f"color-scheme 翻转: {'PASS' if scheme_flipped else 'FAIL'}"
)
ctx.close()
return 0 if attr_flipped and stored_flipped and darkreader_flipped else 1
return (
0
if toggle_absent
and attr_flipped
and stored_flipped
and darkreader_flipped
and scheme_flipped
else 1
)
if __name__ == "__main__":
@@ -11,7 +11,7 @@ import run.halo.app.plugin.PluginContext;
* <p>Only one main class extending {@link BasePlugin} is allowed per plugin.</p>
*
* @author LHY
* @since 1.0.5
* @since 1.0.8
*/
@Slf4j
@Component
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 77 KiB

+5 -5
View File
@@ -10,13 +10,13 @@ spec:
requires: ">=2.25.0"
author:
name: LHY
website: https://github.com/LHY
website: https://github.com/LHY0125
logo: logo.png
homepage: https://github.com/LHY/dark-mode#readme
repo: https://github.com/LHY/dark-mode
issues: https://github.com/LHY/dark-mode/issues
homepage: https://github.com/LHY0125/halo-dark-mode-plugin#readme
repo: https://github.com/LHY0125/halo-dark-mode-plugin
issues: https://github.com/LHY0125/halo-dark-mode-plugin/issues
displayName: "深色模式"
description: "为 Halo 后台管理面板提供深色/浅色模式切换,支持跟随系统、手动切换和偏好记忆"
license:
- name: "GPL-3.0"
url: "https://github.com/LHY/dark-mode/blob/main/LICENSE"
url: "https://github.com/LHY0125/halo-dark-mode-plugin/blob/main/LICENSE"
-62
View File
@@ -1,62 +0,0 @@
<script setup lang="ts">
import IconSunLine from '~icons/ri/sun-line'
import IconMoonLine from '~icons/ri/moon-line'
import { useDarkMode } from '../composables/useDarkMode'
const { isDark, toggle } = useDarkMode()
</script>
<template>
<button
type="button"
class="theme-toggle"
:aria-pressed="isDark"
:aria-label="isDark ? '切换到浅色模式' : '切换到深色模式'"
@click="toggle"
>
<IconSunLine v-if="isDark" class="theme-toggle__icon" />
<IconMoonLine v-else class="theme-toggle__icon" />
<span class="theme-toggle__label">
{{ isDark ? '浅色模式' : '深色模式' }}
</span>
</button>
</template>
<style scoped>
.theme-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.5rem 0.75rem;
border: 0;
background: transparent;
color: var(--halo-text-secondary);
font: inherit;
cursor: pointer;
border-radius: 0.25rem;
transition: background-color 0.15s, color 0.15s;
user-select: none;
}
.theme-toggle:hover {
background-color: var(--halo-bg-hover);
color: var(--halo-text-primary);
}
.theme-toggle:focus-visible {
outline: 2px solid var(--halo-accent-primary);
outline-offset: 2px;
}
.theme-toggle__icon {
width: 1.125rem;
height: 1.125rem;
flex-shrink: 0;
}
.theme-toggle__label {
font-size: 0.8125rem;
white-space: nowrap;
}
</style>
+2 -4
View File
@@ -3,10 +3,7 @@ import { IconPalette } from '@halo-dev/components'
import { markRaw } from 'vue'
import './styles/index.css'
import { initDarkReaderEngine } from './darkreader-engine'
import { injectThemeToggle } from './injector'
// 在插件加载后将切换器注入到侧边栏,暗色转换完全交给 Dark Reader
injectThemeToggle()
initDarkReaderEngine()
export default definePlugin({
@@ -23,7 +20,8 @@ export default definePlugin({
searchable: true,
menu: {
name: '深色模式',
group: '偏好设置',
// Halo 官方「外观」分组
group: 'interface',
icon: markRaw(IconPalette),
priority: 50,
},
-46
View File
@@ -1,46 +0,0 @@
import { createVNode, render } from 'vue'
import ThemeToggle from './components/ThemeToggle.vue'
const CONTAINER_CLASS = 'plugin-dark-mode-toggle'
const TARGET_SELECTOR = '.sidebar__profile'
let mounted = false
/**
* 将 ThemeToggle 注入到侧边栏中 UserProfileBanner 上方。
* 使用 MutationObserver 等待侧边栏 DOM 渲染完成。
*/
export function injectThemeToggle(): void {
if (mounted) return
// 先尝试直接查找(侧边栏可能已经渲染)
tryMount()
// 如果还没渲染,等待 DOM 变化
if (!mounted) {
const observer = new MutationObserver(() => {
tryMount()
if (mounted) observer.disconnect()
})
observer.observe(document.body, { childList: true, subtree: true })
}
}
function tryMount(): void {
// 避免重复挂载
if (document.querySelector(`.${CONTAINER_CLASS}`)) {
mounted = true
return
}
const profileEl = document.querySelector(TARGET_SELECTOR)
if (!profileEl?.parentNode) return
const container = document.createElement('div')
container.className = CONTAINER_CLASS
profileEl.parentNode.insertBefore(container, profileEl)
const vnode = createVNode(ThemeToggle)
render(vnode, container)
mounted = true
}
+6 -48
View File
@@ -6,56 +6,14 @@
/* 说明:data-halo-theme 为兼容性遗留标记,当前已无 CSS 消费方,保留给外部脚本与验证工具。 */
/* ===== 浅色模式(Halo 默认,此处定义为显式回退) ===== */
:root {
--halo-bg-body: oklch(97% 0.005 250);
--halo-bg-sidebar: oklch(100% 0 0);
--halo-bg-content: oklch(97% 0.005 250);
--halo-bg-card: oklch(100% 0 0);
--halo-bg-input: oklch(100% 0 0);
--halo-bg-hover: oklch(95% 0.01 250);
--halo-bg-active: oklch(90% 0.02 160);
--halo-bg-disabled: oklch(95% 0.005 250);
--halo-bg-tooltip: oklch(20% 0.01 250);
--halo-bg-modal: oklch(0% 0 0 / 60%);
--halo-bg-dropdown: oklch(100% 0 0);
--halo-text-primary: oklch(20% 0.01 250);
--halo-text-tertiary: oklch(60% 0.01 250);
--halo-text-primary: oklch(20% 0.01 250);
--halo-text-secondary: oklch(45% 0.01 250);
--halo-text-tertiary: oklch(60% 0.01 250);
--halo-text-link: oklch(45% 0.15 160);
--halo-text-inverse: oklch(100% 0 0);
--halo-bg-hover: oklch(95% 0.01 250);
--halo-border-base: oklch(88% 0.01 250);
--halo-border-light: oklch(93% 0.005 250);
--halo-border-input: oklch(80% 0.01 250);
--halo-border-focus: oklch(55% 0.15 160);
--halo-border-base: oklch(88% 0.01 250);
--halo-accent-primary: oklch(55% 0.14 160);
--halo-accent-primary-hover: oklch(48% 0.15 160);
--halo-accent-primary-text: oklch(100% 0 0);
--halo-accent-danger: oklch(45% 0.18 25);
--halo-accent-danger-hover: oklch(40% 0.19 25);
--halo-accent-success: oklch(50% 0.16 150);
--halo-accent-warning: oklch(60% 0.16 85);
--halo-accent-primary: oklch(55% 0.14 160);
--halo-shadow-sm: 0 1px 2px oklch(0% 0 0 / 6%);
--halo-shadow-base: 0 2px 8px oklch(0% 0 0 / 10%);
--halo-shadow-lg: 0 4px 16px oklch(0% 0 0 / 14%);
--halo-scrollbar-thumb: oklch(80% 0.01 250);
--halo-scrollbar-track: oklch(95% 0.005 250);
--halo-search-bg: oklch(95% 0.01 250);
--halo-search-text: oklch(45% 0.01 250);
--halo-search-placeholder: oklch(60% 0.01 250);
--halo-menu-item-hover: oklch(93% 0.02 160);
--halo-menu-item-active: oklch(88% 0.03 160);
--halo-menu-group-title: oklch(55% 0.01 250);
--halo-table-header-bg: oklch(96% 0.005 250);
--halo-table-row-hover: oklch(93% 0.015 250);
--halo-table-border: oklch(88% 0.01 250);
--halo-tag-bg: oklch(90% 0.04 160);
--halo-tag-text: oklch(40% 0.1 160);
--halo-menu-item-active: oklch(88% 0.03 160);
}
+41 -74
View File
@@ -1,6 +1,14 @@
<script setup lang="ts">
import type { ThemeMode } from '../composables/useDarkMode'
import { useDarkMode } from '../composables/useDarkMode'
import {
IconPalette,
VCard,
VDescription,
VDescriptionItem,
VPageHeader,
VTag,
} from '@halo-dev/components'
import { computed } from 'vue'
const { theme, isDark, setTheme } = useDarkMode()
@@ -20,85 +28,47 @@ const modeOptions: { value: ThemeMode; label: string; description: string }[] =
</script>
<template>
<div class="dark-mode-settings">
<div class="dark-mode-settings__header">
<h1 class="dark-mode-settings__title">深色模式设置</h1>
<p class="dark-mode-settings__desc">选择后台管理面板的显示模式</p>
</div>
<div>
<VPageHeader title="深色模式设置">
<template #icon>
<IconPalette />
</template>
</VPageHeader>
<div class="dark-mode-settings__card">
<div class="dark-mode-settings__current">
当前生效<strong>{{ currentEffectiveMode }}</strong>
</div>
<div class="m-0 md:m-4">
<VCard :body-class="['!p-0']">
<div class="p-4">
<VDescription>
<VDescriptionItem label="当前生效">
<VTag>{{ currentEffectiveMode }}</VTag>
</VDescriptionItem>
</VDescription>
<div
class="dark-mode-settings__options"
role="radiogroup"
aria-label="主题模式"
>
<button
v-for="option in modeOptions"
:key="option.value"
type="button"
role="radio"
class="dark-mode-settings__option"
:class="{ 'is-active': theme === option.value }"
:aria-checked="theme === option.value"
@click="setTheme(option.value)"
>
<div class="dark-mode-settings__option-label">{{ option.label }}</div>
<div class="dark-mode-settings__option-desc">{{ option.description }}</div>
</button>
</div>
<div class="dark-mode-settings__options">
<button
v-for="option in modeOptions"
:key="option.value"
type="button"
class="dark-mode-settings__option"
:class="{ 'is-active': theme === option.value }"
@click="setTheme(option.value)"
>
<div class="dark-mode-settings__option-label">{{ option.label }}</div>
<div class="dark-mode-settings__option-desc">{{ option.description }}</div>
</button>
</div>
</div>
</VCard>
</div>
</div>
</template>
<style scoped>
.dark-mode-settings {
max-width: 640px;
padding: 1.5rem;
}
.dark-mode-settings__header {
margin-bottom: 1.5rem;
}
.dark-mode-settings__title {
font-size: 1.25rem;
font-weight: 600;
color: var(--halo-text-primary);
margin-bottom: 0.25rem;
}
.dark-mode-settings__desc {
font-size: 0.875rem;
color: var(--halo-text-secondary);
}
.dark-mode-settings__card {
padding: 1.25rem;
background-color: var(--halo-bg-card);
border: 1px solid var(--halo-border-base);
border-radius: 0.25rem;
}
.dark-mode-settings__current {
font-size: 0.875rem;
color: var(--halo-text-secondary);
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--halo-border-light);
}
.dark-mode-settings__current strong {
color: var(--halo-accent-primary);
}
.dark-mode-settings__options {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 1rem;
}
.dark-mode-settings__option {
@@ -111,18 +81,15 @@ const modeOptions: { value: ThemeMode; label: string; description: string }[] =
text-align: left;
border-radius: 0.375rem;
cursor: pointer;
transition: background-color 0.15s, border-color 0.15s;
transition:
background-color 0.15s,
border-color 0.15s;
}
.dark-mode-settings__option:hover {
background-color: var(--halo-bg-hover);
}
.dark-mode-settings__option:focus-visible {
outline: 2px solid var(--halo-accent-primary);
outline-offset: 2px;
}
.dark-mode-settings__option.is-active {
background-color: var(--halo-menu-item-active);
border-color: var(--halo-accent-primary);