Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a83d05f27 | |||
| 3e9630fc91 | |||
| c4d99ec041 | |||
| 30b2ae2f83 | |||
| cffbe572bf | |||
| 96dd1ef4d3 | |||
| df9cd52de6 | |||
| 394613f516 | |||
| 5cbce86e03 | |||
| 45451c0d49 |
+6
-1
@@ -1,8 +1,8 @@
|
|||||||
.gradle/
|
.gradle/
|
||||||
build/
|
build/
|
||||||
!gradle/wrapper/gradle-wrapper.jar
|
|
||||||
*.class
|
*.class
|
||||||
*.jar
|
*.jar
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
*.war
|
*.war
|
||||||
*.log
|
*.log
|
||||||
*.tmp
|
*.tmp
|
||||||
@@ -35,3 +35,8 @@ third-party/darkreader/*
|
|||||||
!third-party/darkreader/SHA256SUMS
|
!third-party/darkreader/SHA256SUMS
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
|
# 内部审查/计划文档不入库,仅保留本地
|
||||||
|
docs/*.md
|
||||||
|
docs/**/*.md
|
||||||
|
docs/原版logo.png
|
||||||
|
|||||||
@@ -8,19 +8,19 @@
|
|||||||
- **偏好持久化**:自动记忆用户选择(localStorage),刷新不丢失
|
- **偏好持久化**:自动记忆用户选择(localStorage),刷新不丢失
|
||||||
- **系统偏好跟随**:切换系统外观时自动响应
|
- **系统偏好跟随**:切换系统外观时自动响应
|
||||||
- **Dark Reader 引擎**:自动分析页面 CSS 与 DOM,覆盖 Halo 核心页面和第三方插件页面
|
- **Dark Reader 引擎**:自动分析页面 CSS 与 DOM,覆盖 Halo 核心页面和第三方插件页面
|
||||||
- **侧边栏一键切换**:切换按钮自动出现在侧边栏 UserProfileBanner 上方
|
- **官方外观分组**:设置入口位于 Halo「外观」分组,与主题、菜单、插件同组
|
||||||
- **设置页面**:在「偏好设置 → 深色模式」中详细选择显示模式
|
- **设置页面**:在「外观 → 深色模式」中详细选择显示模式
|
||||||
- **零后端依赖**:纯前端实现,不需要额外后端 API
|
- **零后端依赖**:纯前端实现,不需要额外后端 API
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
1. 从 Releases 下载 `plugin-dark-mode-<version>.jar`。
|
1. 从 Releases 下载 `plugin-dark-mode-<version>.jar`。
|
||||||
2. 在 Halo 后台的「插件管理」中上传并安装。
|
2. 在 Halo 后台的「插件管理」中上传并安装。
|
||||||
3. 启用插件后,侧边栏底部会出现深色模式切换按钮。
|
3. 启用插件后,进入「外观 → 深色模式」设置页面调整主题。
|
||||||
|
|
||||||
## 使用
|
## 使用
|
||||||
|
|
||||||
安装并启用后,可以直接点击侧边栏按钮在深色和浅色模式之间切换,也可以进入设置页面选择固定模式:
|
安装并启用后,进入「外观 → 深色模式」设置页面选择显示模式:
|
||||||
|
|
||||||
| 配置项 | 可选值 | 说明 |
|
| 配置项 | 可选值 | 说明 |
|
||||||
| -------- | --------- | -------------------- |
|
| -------- | --------- | -------------------- |
|
||||||
@@ -28,12 +28,24 @@
|
|||||||
| 主题模式 | `dark` | 始终使用深色模式 |
|
| 主题模式 | `dark` | 始终使用深色模式 |
|
||||||
| 主题模式 | `auto` | 跟随系统外观自动切换 |
|
| 主题模式 | `auto` | 跟随系统外观自动切换 |
|
||||||
|
|
||||||
|
## 截图预览
|
||||||
|
|
||||||
|
以下截图展示插件在 Halo 后台的实际效果。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## 技术原理
|
## 技术原理
|
||||||
|
|
||||||
- 插件通过 `useDarkMode()` 管理 `light` / `dark` / `auto` 三种状态。
|
- 插件通过 `useDarkMode()` 管理 `light` / `dark` / `auto` 三种状态。
|
||||||
- 深色模式下调用 Dark Reader 的 `enable()`,浅色模式下调用 `disable()`。
|
- 深色模式下调用 Dark Reader 的 `enable()`,浅色模式下调用 `disable()`。
|
||||||
- Dark Reader 会持续监听页面 DOM 变化,因此第三方插件动态渲染的内容也能自动转换。
|
- Dark Reader 会持续监听页面 DOM 变化,因此第三方插件动态渲染的内容也能自动转换。
|
||||||
- 插件自身只保留切换器与设置页所需的最小 UI 变量,不再维护逐页手工 CSS 覆盖。
|
- 插件自身只保留设置页所需的最小 UI 变量,不再维护逐页手工 CSS 覆盖。
|
||||||
|
|
||||||
> 说明:Dark Reader 的样式注入是异步的,刷新瞬间仍可能存在极短闪白;插件通过同步设置 `color-scheme` 缓解,但无法完全消除。
|
> 说明:Dark Reader 的样式注入是异步的,刷新瞬间仍可能存在极短闪白;插件通过同步设置 `color-scheme` 缓解,但无法完全消除。
|
||||||
|
|
||||||
@@ -108,12 +120,9 @@ pnpm build # 生产构建
|
|||||||
└── src/
|
└── src/
|
||||||
├── index.ts # definePlugin 入口
|
├── index.ts # definePlugin 入口
|
||||||
├── darkreader-engine.ts # Dark Reader 通用暗色引擎
|
├── darkreader-engine.ts # Dark Reader 通用暗色引擎
|
||||||
├── injector.ts # 侧边栏切换器注入
|
|
||||||
├── composables/
|
├── composables/
|
||||||
│ ├── useDarkMode.ts # 主题状态管理(模块级单例)
|
│ ├── useDarkMode.ts # 主题状态管理(模块级单例)
|
||||||
│ └── useSystemPreference.ts # 系统偏好监听
|
│ └── useSystemPreference.ts # 系统偏好监听
|
||||||
├── components/
|
|
||||||
│ └── ThemeToggle.vue # 侧边栏切换按钮
|
|
||||||
├── views/
|
├── views/
|
||||||
│ └── SettingsView.vue # 设置页面
|
│ └── SettingsView.vue # 设置页面
|
||||||
└── styles/
|
└── styles/
|
||||||
@@ -127,15 +136,30 @@ pnpm build # 生产构建
|
|||||||
./gradlew test
|
./gradlew test
|
||||||
```
|
```
|
||||||
|
|
||||||
运行时验证脚本位于 `scripts/verify-toggle.py`,用于检查主题切换、localStorage 持久化以及 Dark Reader 注入状态。
|
运行时验证脚本位于 `scripts/verify-toggle.py`,用于直接驱动主题状态,并检查 data-halo-theme、localStorage、Dark 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
|
### v1.0.5
|
||||||
|
|
||||||
- 同步设置 `color-scheme`,缓解深色模式刷新闪烁
|
- 同步设置 `color-scheme`,缓解深色模式刷新闪烁
|
||||||
- 增加多标签页主题同步与 `useDarkMode` 单元测试
|
- 增加多标签页主题同步与 `useDarkMode` 单元测试
|
||||||
- 侧边栏切换按钮与设置选项支持键盘操作
|
|
||||||
- 后端日志改用 Lombok `@Slf4j`
|
- 后端日志改用 Lombok `@Slf4j`
|
||||||
- 裁剪 vendored `package.json`,新增 Dark Reader `SHA256SUMS` 完整性校验
|
- 裁剪 vendored `package.json`,新增 Dark Reader `SHA256SUMS` 完整性校验
|
||||||
- `plugin.yaml` 改为安装后由用户手动启用
|
- `plugin.yaml` 改为安装后由用户手动启用
|
||||||
|
|||||||
@@ -1,151 +0,0 @@
|
|||||||
# 修复计划 — halo-dark-mode-plugin(2026-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)
|
|
||||||
- 本次审批通过后,先提交代码修复,再按需打包
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# 代码审查交接单 — halo-dark-mode-plugin(2026-08-08)
|
|
||||||
|
|
||||||
> 由审查窗口产出,供开发窗口执行。结论:**可合入,无 P1 阻断问题**。
|
|
||||||
> 审查基线:`HEAD = 363f30d`(工作区干净,已与 origin/main 同步)。
|
|
||||||
> 审查范围:Dark Reader 迁移相关 4 个提交(202ab53 / 0bb552b / 88a071f / 363f30d)。
|
|
||||||
|
|
||||||
## 一、开发窗口无需重复验证(审查窗口已实测)
|
|
||||||
|
|
||||||
- `vite build` 通过:main.js 109.96KB(gzip 37.79KB)、style.css 3.76KB,Dark 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 |
@@ -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
@@ -1,3 +1,3 @@
|
|||||||
version=1.0.5
|
version=1.0.8
|
||||||
org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8
|
||||||
org.gradle.java.home=D:/settings/Language/Java/jdk-25.0.1
|
org.gradle.java.home=D:/settings/Language/Java/jdk-25.0.1
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+4
-2
@@ -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
|
D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\verify-toggle.py
|
||||||
```
|
```
|
||||||
|
|
||||||
部署新版插件后运行,会检查:
|
部署新版插件后运行,会直接驱动 localStorage 中的主题状态,并检查:
|
||||||
|
|
||||||
- `data-halo-theme` 属性翻转
|
- `data-halo-theme` 属性翻转
|
||||||
- localStorage 持久化翻转
|
- localStorage 持久化翻转
|
||||||
- `data-darkreader-mode`(Dark Reader 注入状态)翻转
|
- `data-darkreader-mode`(Dark Reader 注入状态)翻转
|
||||||
|
- `color-scheme` 翻转
|
||||||
|
- 侧边栏已不再注入 `.theme-toggle` / `.plugin-dark-mode-toggle` 按钮
|
||||||
|
|
||||||
三项全部 PASS 时返回 0。
|
状态翻转全部 PASS 且按钮已移除时返回 0。
|
||||||
|
|
||||||
## 输出
|
## 输出
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# scripts/legacy
|
||||||
|
|
||||||
|
保留的旧版分析工具,来自早期手工 CSS 工作流,当前纯 Dark Reader 架构下不再参与日常流程。
|
||||||
|
|
||||||
|
- `aggregate.py` — 聚合多路由扫描结果
|
||||||
|
- `fetch_bundle.py` — 抓取线上部署 CSS bundle
|
||||||
|
- `probe_conflict.py` — 探测 CSS 覆盖冲突
|
||||||
|
|
||||||
|
如需恢复手工 CSS 扫描工作流,可参考这些脚本;否则可在一段时间后删除。
|
||||||
+62
-10
@@ -1,4 +1,4 @@
|
|||||||
"""验证侧边栏主题切换:点击后检查 data-halo-theme、localStorage 与 Dark Reader 模式。"""
|
"""验证主题切换:直接驱动 localStorage,检查 data-halo-theme、Dark Reader 与 color-scheme 翻转。"""
|
||||||
import argparse
|
import argparse
|
||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
import sys
|
||||||
@@ -8,6 +8,7 @@ from playwright.sync_api import sync_playwright
|
|||||||
|
|
||||||
DEFAULT_PROFILE = pathlib.Path(__file__).parent / ".browser-profile"
|
DEFAULT_PROFILE = pathlib.Path(__file__).parent / ".browser-profile"
|
||||||
BASE = "https://blog.liuhangyv.top"
|
BASE = "https://blog.liuhangyv.top"
|
||||||
|
STORAGE_KEY = "halo-dark-mode-theme"
|
||||||
|
|
||||||
|
|
||||||
def read_state(page) -> dict:
|
def read_state(page) -> dict:
|
||||||
@@ -16,10 +17,40 @@ def read_state(page) -> dict:
|
|||||||
attr: document.documentElement.getAttribute('data-halo-theme'),
|
attr: document.documentElement.getAttribute('data-halo-theme'),
|
||||||
darkreader: document.documentElement.getAttribute('data-darkreader-mode'),
|
darkreader: document.documentElement.getAttribute('data-darkreader-mode'),
|
||||||
stored: localStorage.getItem('halo-dark-mode-theme'),
|
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:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description="验证主题切换运行时行为")
|
parser = argparse.ArgumentParser(description="验证主题切换运行时行为")
|
||||||
parser.add_argument("--base", default=BASE)
|
parser.add_argument("--base", default=BASE)
|
||||||
@@ -45,33 +76,54 @@ def main() -> int:
|
|||||||
ctx.close()
|
ctx.close()
|
||||||
return 3
|
return 3
|
||||||
|
|
||||||
toggle = page.locator(".theme-toggle")
|
toggle_absent = page.locator(".theme-toggle, .plugin-dark-mode-toggle").count() == 0
|
||||||
toggle.wait_for(state="visible", timeout=15000)
|
print(f"侧边栏注入按钮已移除: {'PASS' if toggle_absent else 'FAIL'}")
|
||||||
|
|
||||||
|
# 先归一化到浅色,保证后续翻转判定与当前会话状态无关
|
||||||
|
set_mode(page, "light")
|
||||||
|
wait_mode(page, expect_dark=False)
|
||||||
before = read_state(page)
|
before = read_state(page)
|
||||||
toggle.click()
|
|
||||||
time.sleep(1)
|
set_mode(page, "dark")
|
||||||
|
wait_mode(page, expect_dark=True)
|
||||||
after = read_state(page)
|
after = read_state(page)
|
||||||
toggle.click()
|
|
||||||
time.sleep(1)
|
set_mode(page, "light")
|
||||||
|
wait_mode(page, expect_dark=False)
|
||||||
restored = read_state(page)
|
restored = read_state(page)
|
||||||
|
|
||||||
attr_flipped = before["attr"] != after["attr"] and before["attr"] == restored["attr"]
|
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 = (
|
darkreader_flipped = (
|
||||||
before["darkreader"] != after["darkreader"]
|
before["darkreader"] != after["darkreader"]
|
||||||
and before["darkreader"] == restored["darkreader"]
|
and before["darkreader"] == restored["darkreader"]
|
||||||
)
|
)
|
||||||
|
scheme_flipped = (
|
||||||
|
before["colorScheme"] != after["colorScheme"]
|
||||||
|
and before["colorScheme"] == restored["colorScheme"]
|
||||||
|
)
|
||||||
print(f"初始: {before}")
|
print(f"初始: {before}")
|
||||||
print(f"切换: {after}")
|
print(f"切换: {after}")
|
||||||
print(f"还原: {restored}")
|
print(f"还原: {restored}")
|
||||||
print(
|
print(
|
||||||
f"属性翻转: {'PASS' if attr_flipped else 'FAIL'} "
|
f"属性翻转: {'PASS' if attr_flipped else 'FAIL'} "
|
||||||
f"存储翻转: {'PASS' if stored_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()
|
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__":
|
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>
|
* <p>Only one main class extending {@link BasePlugin} is allowed per plugin.</p>
|
||||||
*
|
*
|
||||||
* @author LHY
|
* @author LHY
|
||||||
* @since 1.0.5
|
* @since 1.0.8
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 77 KiB |
@@ -10,13 +10,13 @@ spec:
|
|||||||
requires: ">=2.25.0"
|
requires: ">=2.25.0"
|
||||||
author:
|
author:
|
||||||
name: LHY
|
name: LHY
|
||||||
website: https://github.com/LHY
|
website: https://github.com/LHY0125
|
||||||
logo: logo.png
|
logo: logo.png
|
||||||
homepage: https://github.com/LHY/dark-mode#readme
|
homepage: https://github.com/LHY0125/halo-dark-mode-plugin#readme
|
||||||
repo: https://github.com/LHY/dark-mode
|
repo: https://github.com/LHY0125/halo-dark-mode-plugin
|
||||||
issues: https://github.com/LHY/dark-mode/issues
|
issues: https://github.com/LHY0125/halo-dark-mode-plugin/issues
|
||||||
displayName: "深色模式"
|
displayName: "深色模式"
|
||||||
description: "为 Halo 后台管理面板提供深色/浅色模式切换,支持跟随系统、手动切换和偏好记忆"
|
description: "为 Halo 后台管理面板提供深色/浅色模式切换,支持跟随系统、手动切换和偏好记忆"
|
||||||
license:
|
license:
|
||||||
- name: "GPL-3.0"
|
- 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"
|
||||||
|
|||||||
@@ -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
@@ -3,10 +3,7 @@ import { IconPalette } from '@halo-dev/components'
|
|||||||
import { markRaw } from 'vue'
|
import { markRaw } from 'vue'
|
||||||
import './styles/index.css'
|
import './styles/index.css'
|
||||||
import { initDarkReaderEngine } from './darkreader-engine'
|
import { initDarkReaderEngine } from './darkreader-engine'
|
||||||
import { injectThemeToggle } from './injector'
|
|
||||||
|
|
||||||
// 在插件加载后将切换器注入到侧边栏,暗色转换完全交给 Dark Reader
|
|
||||||
injectThemeToggle()
|
|
||||||
initDarkReaderEngine()
|
initDarkReaderEngine()
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
@@ -23,7 +20,8 @@ export default definePlugin({
|
|||||||
searchable: true,
|
searchable: true,
|
||||||
menu: {
|
menu: {
|
||||||
name: '深色模式',
|
name: '深色模式',
|
||||||
group: '偏好设置',
|
// Halo 官方「外观」分组
|
||||||
|
group: 'interface',
|
||||||
icon: markRaw(IconPalette),
|
icon: markRaw(IconPalette),
|
||||||
priority: 50,
|
priority: 50,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,56 +6,14 @@
|
|||||||
/* 说明:data-halo-theme 为兼容性遗留标记,当前已无 CSS 消费方,保留给外部脚本与验证工具。 */
|
/* 说明:data-halo-theme 为兼容性遗留标记,当前已无 CSS 消费方,保留给外部脚本与验证工具。 */
|
||||||
/* ===== 浅色模式(Halo 默认,此处定义为显式回退) ===== */
|
/* ===== 浅色模式(Halo 默认,此处定义为显式回退) ===== */
|
||||||
:root {
|
:root {
|
||||||
--halo-bg-body: oklch(97% 0.005 250);
|
--halo-text-primary: oklch(20% 0.01 250);
|
||||||
--halo-bg-sidebar: oklch(100% 0 0);
|
--halo-text-tertiary: oklch(60% 0.01 250);
|
||||||
--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-bg-hover: oklch(95% 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-border-base: oklch(88% 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-accent-primary: oklch(55% 0.14 160);
|
--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-shadow-sm: 0 1px 2px oklch(0% 0 0 / 6%);
|
--halo-menu-item-active: oklch(88% 0.03 160);
|
||||||
--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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ThemeMode } from '../composables/useDarkMode'
|
import type { ThemeMode } from '../composables/useDarkMode'
|
||||||
import { useDarkMode } from '../composables/useDarkMode'
|
import { useDarkMode } from '../composables/useDarkMode'
|
||||||
|
import {
|
||||||
|
IconPalette,
|
||||||
|
VCard,
|
||||||
|
VDescription,
|
||||||
|
VDescriptionItem,
|
||||||
|
VPageHeader,
|
||||||
|
VTag,
|
||||||
|
} from '@halo-dev/components'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const { theme, isDark, setTheme } = useDarkMode()
|
const { theme, isDark, setTheme } = useDarkMode()
|
||||||
@@ -20,85 +28,47 @@ const modeOptions: { value: ThemeMode; label: string; description: string }[] =
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="dark-mode-settings">
|
<div>
|
||||||
<div class="dark-mode-settings__header">
|
<VPageHeader title="深色模式设置">
|
||||||
<h1 class="dark-mode-settings__title">深色模式设置</h1>
|
<template #icon>
|
||||||
<p class="dark-mode-settings__desc">选择后台管理面板的显示模式</p>
|
<IconPalette />
|
||||||
</div>
|
</template>
|
||||||
|
</VPageHeader>
|
||||||
|
|
||||||
<div class="dark-mode-settings__card">
|
<div class="m-0 md:m-4">
|
||||||
<div class="dark-mode-settings__current">
|
<VCard :body-class="['!p-0']">
|
||||||
当前生效:<strong>{{ currentEffectiveMode }}</strong>
|
<div class="p-4">
|
||||||
</div>
|
<VDescription>
|
||||||
|
<VDescriptionItem label="当前生效">
|
||||||
|
<VTag>{{ currentEffectiveMode }}</VTag>
|
||||||
|
</VDescriptionItem>
|
||||||
|
</VDescription>
|
||||||
|
|
||||||
<div
|
<div class="dark-mode-settings__options">
|
||||||
class="dark-mode-settings__options"
|
<button
|
||||||
role="radiogroup"
|
v-for="option in modeOptions"
|
||||||
aria-label="主题模式"
|
:key="option.value"
|
||||||
>
|
type="button"
|
||||||
<button
|
class="dark-mode-settings__option"
|
||||||
v-for="option in modeOptions"
|
:class="{ 'is-active': theme === option.value }"
|
||||||
:key="option.value"
|
@click="setTheme(option.value)"
|
||||||
type="button"
|
>
|
||||||
role="radio"
|
<div class="dark-mode-settings__option-label">{{ option.label }}</div>
|
||||||
class="dark-mode-settings__option"
|
<div class="dark-mode-settings__option-desc">{{ option.description }}</div>
|
||||||
:class="{ 'is-active': theme === option.value }"
|
</button>
|
||||||
:aria-checked="theme === option.value"
|
</div>
|
||||||
@click="setTheme(option.value)"
|
</div>
|
||||||
>
|
</VCard>
|
||||||
<div class="dark-mode-settings__option-label">{{ option.label }}</div>
|
|
||||||
<div class="dark-mode-settings__option-desc">{{ option.description }}</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.dark-mode-settings__options {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-mode-settings__option {
|
.dark-mode-settings__option {
|
||||||
@@ -111,18 +81,15 @@ const modeOptions: { value: ThemeMode; label: string; description: string }[] =
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
cursor: pointer;
|
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 {
|
.dark-mode-settings__option:hover {
|
||||||
background-color: var(--halo-bg-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 {
|
.dark-mode-settings__option.is-active {
|
||||||
background-color: var(--halo-menu-item-active);
|
background-color: var(--halo-menu-item-active);
|
||||||
border-color: var(--halo-accent-primary);
|
border-color: var(--halo-accent-primary);
|
||||||
|
|||||||
Reference in New Issue
Block a user