fix: 按审查报告修复 P2/P3 问题并升级 1.0.5
This commit is contained in:
@@ -32,5 +32,6 @@ third-party/darkreader/*
|
|||||||
!third-party/darkreader/darkreader.mjs
|
!third-party/darkreader/darkreader.mjs
|
||||||
!third-party/darkreader/index.d.ts
|
!third-party/darkreader/index.d.ts
|
||||||
!third-party/darkreader/LICENSE
|
!third-party/darkreader/LICENSE
|
||||||
|
!third-party/darkreader/SHA256SUMS
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|||||||
@@ -22,11 +22,11 @@
|
|||||||
|
|
||||||
安装并启用后,可以直接点击侧边栏按钮在深色和浅色模式之间切换,也可以进入设置页面选择固定模式:
|
安装并启用后,可以直接点击侧边栏按钮在深色和浅色模式之间切换,也可以进入设置页面选择固定模式:
|
||||||
|
|
||||||
| 配置项 | 可选值 | 说明 |
|
| 配置项 | 可选值 | 说明 |
|
||||||
| --- | --- | --- |
|
| -------- | --------- | -------------------- |
|
||||||
| 主题模式 | `light` | 始终使用浅色模式 |
|
| 主题模式 | `light` | 始终使用浅色模式 |
|
||||||
| 主题模式 | `dark` | 始终使用深色模式 |
|
| 主题模式 | `dark` | 始终使用深色模式 |
|
||||||
| 主题模式 | `auto` | 跟随系统外观自动切换 |
|
| 主题模式 | `auto` | 跟随系统外观自动切换 |
|
||||||
|
|
||||||
## 技术原理
|
## 技术原理
|
||||||
|
|
||||||
@@ -35,10 +35,14 @@
|
|||||||
- Dark Reader 会持续监听页面 DOM 变化,因此第三方插件动态渲染的内容也能自动转换。
|
- Dark Reader 会持续监听页面 DOM 变化,因此第三方插件动态渲染的内容也能自动转换。
|
||||||
- 插件自身只保留切换器与设置页所需的最小 UI 变量,不再维护逐页手工 CSS 覆盖。
|
- 插件自身只保留切换器与设置页所需的最小 UI 变量,不再维护逐页手工 CSS 覆盖。
|
||||||
|
|
||||||
|
> 说明:Dark Reader 的样式注入是异步的,刷新瞬间仍可能存在极短闪白;插件通过同步设置 `color-scheme` 缓解,但无法完全消除。
|
||||||
|
|
||||||
## 第三方依赖
|
## 第三方依赖
|
||||||
|
|
||||||
- [Dark Reader](https://github.com/darkreader/darkreader) `4.9.129`,[MIT License](https://github.com/darkreader/darkreader/blob/main/LICENSE)。
|
- [Dark Reader](https://github.com/darkreader/darkreader) `4.9.129`,[MIT License](https://github.com/darkreader/darkreader/blob/main/LICENSE)。
|
||||||
- 构建所需文件位于 `third-party/darkreader/`,由 `ui/package.json` 通过本地文件依赖引用。
|
- 构建所需文件位于 `third-party/darkreader/`,由 `ui/package.json` 通过本地文件依赖引用。
|
||||||
|
- 升级或替换 Dark Reader 构建文件后,请校验 `third-party/darkreader/SHA256SUMS`:
|
||||||
|
Linux / macOS 使用 `sha256sum -c SHA256SUMS`,Windows 使用 `Get-FileHash -Algorithm SHA256` 对比。
|
||||||
|
|
||||||
## 开发环境
|
## 开发环境
|
||||||
|
|
||||||
@@ -127,6 +131,15 @@ pnpm build # 生产构建
|
|||||||
|
|
||||||
## 更新日志
|
## 更新日志
|
||||||
|
|
||||||
|
### v1.0.5
|
||||||
|
|
||||||
|
- 同步设置 `color-scheme`,缓解深色模式刷新闪烁
|
||||||
|
- 增加多标签页主题同步与 `useDarkMode` 单元测试
|
||||||
|
- 侧边栏切换按钮与设置选项支持键盘操作
|
||||||
|
- 后端日志改用 Lombok `@Slf4j`
|
||||||
|
- 裁剪 vendored `package.json`,新增 Dark Reader `SHA256SUMS` 完整性校验
|
||||||
|
- `plugin.yaml` 改为安装后由用户手动启用
|
||||||
|
|
||||||
### v1.0.4
|
### v1.0.4
|
||||||
|
|
||||||
- 改为纯 Dark Reader 引擎,移除手工 CSS 覆盖
|
- 改为纯 Dark Reader 引擎,移除手工 CSS 覆盖
|
||||||
@@ -141,4 +154,4 @@ pnpm build # 生产构建
|
|||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
[GPL-3.0](./LICENSE) © LHY
|
[GPL-3.0](./LICENSE) © LHY
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# 修复计划 — 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)
|
||||||
|
- 本次审批通过后,先提交代码修复,再按需打包
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# 代码审查交接单 — 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 断言与策略一致 ✅
|
||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
version=1.0.4
|
version=1.0.5
|
||||||
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
|
||||||
|
|||||||
+19
-14
@@ -1,6 +1,6 @@
|
|||||||
# 暗色模式回归扫描工具
|
# 暗色模式回归扫描与运行时验证工具
|
||||||
|
|
||||||
用 Playwright 驱动 Edge 对 Halo 后台做全路由深浅色残留扫描,验证每次 CSS 改动。
|
用 Playwright 驱动 Edge 对 Halo 后台做全路由深浅色残留扫描与运行时切换验证。
|
||||||
|
|
||||||
## 依赖
|
## 依赖
|
||||||
|
|
||||||
@@ -20,13 +20,13 @@ D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\login-wait.py
|
|||||||
已有登录态想复用,可显式指定 profile:
|
已有登录态想复用,可显式指定 profile:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\scan-dark.py --profile workplace\pw-profile
|
D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\scan-dark.py --profile scripts\.browser-profile
|
||||||
```
|
```
|
||||||
|
|
||||||
## 常用命令
|
## 常用命令
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# 全路由深色扫描,注入本地构建产物做预部署验证,存在残留时返回非 0
|
# 全路由深色扫描(历史 CSS 工作流保留)
|
||||||
D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\scan-dark.py --local-css ui\build\dist\style.css --assert-zero
|
D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\scan-dark.py --local-css ui\build\dist\style.css --assert-zero
|
||||||
|
|
||||||
# 只扫指定页面
|
# 只扫指定页面
|
||||||
@@ -36,25 +36,30 @@ D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\scan-dark.py --page
|
|||||||
D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\scan-dark.py --mode light --pages /console/dashboard,/console/posts
|
D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\scan-dark.py --mode light --pages /console/dashboard,/console/posts
|
||||||
```
|
```
|
||||||
|
|
||||||
## 输出
|
|
||||||
|
|
||||||
- `scripts/output/scan-results.json`:按路由分组的结果
|
|
||||||
- `scripts/output/shots/*.png`:每页截图
|
|
||||||
- 均已被 gitignore,不入库。
|
|
||||||
|
|
||||||
## 切换行为验证
|
## 切换行为验证
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
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
|
||||||
```
|
```
|
||||||
|
|
||||||
部署新版插件后运行,会检查侧边栏按钮切换、`data-halo-theme` 属性、
|
部署新版插件后运行,会检查:
|
||||||
localStorage 持久化以及 Monaco 日志查看器主题是否同步。
|
|
||||||
|
- `data-halo-theme` 属性翻转
|
||||||
|
- localStorage 持久化翻转
|
||||||
|
- `data-darkreader-mode`(Dark Reader 注入状态)翻转
|
||||||
|
|
||||||
|
三项全部 PASS 时返回 0。
|
||||||
|
|
||||||
|
## 输出
|
||||||
|
|
||||||
|
- `scripts/output/scan-results.json`:按路由分组的结果
|
||||||
|
- `scripts/output/shots/*.png`:每页截图
|
||||||
|
- 均已被 gitignore,不入库。
|
||||||
|
|
||||||
## 注意事项
|
## 注意事项
|
||||||
|
|
||||||
- 扫描会忽略 `img/canvas/video/svg/iframe`,避免把文章图片、编辑器画布等内容性
|
- 扫描会忽略 `img/canvas/video/svg/iframe`,避免把文章图片、编辑器画布等内容性
|
||||||
元素误判为界面残留。
|
元素误判为界面残留。
|
||||||
- 路由从 Vue Router 动态发现,Halo 或插件升级新增页面后无需维护清单。
|
- 路由从 Vue Router 动态发现,Halo 或插件升级新增页面后无需维护清单。
|
||||||
- `--local-css` 用 `add_style_tag` 追加本地样式(不替换线上 bundle),可安全验证
|
- `scan-dark.py` 主要为历史手工 CSS 工作流保留;当前暗色转换由 Dark Reader 负责,
|
||||||
CSS 修复;JS 改动(如 Monaco 主题同步)需要重新构建并部署后再跑 `verify-toggle.py`。
|
运行时行为验证以 `verify-toggle.py` 为准。
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package run.halo.darkmode;
|
package run.halo.darkmode;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import run.halo.app.plugin.BasePlugin;
|
import run.halo.app.plugin.BasePlugin;
|
||||||
import run.halo.app.plugin.PluginContext;
|
import run.halo.app.plugin.PluginContext;
|
||||||
@@ -10,8 +11,9 @@ 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.4
|
* @since 1.0.5
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class DarkModePlugin extends BasePlugin {
|
public class DarkModePlugin extends BasePlugin {
|
||||||
|
|
||||||
@@ -21,11 +23,11 @@ public class DarkModePlugin extends BasePlugin {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start() {
|
public void start() {
|
||||||
System.out.println("插件启动成功!");
|
log.info("插件启动成功!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stop() {
|
public void stop() {
|
||||||
System.out.println("插件停止!");
|
log.info("插件停止!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ metadata:
|
|||||||
# The name defines how the plugin is invoked, A unique name
|
# The name defines how the plugin is invoked, A unique name
|
||||||
name: dark-mode
|
name: dark-mode
|
||||||
spec:
|
spec:
|
||||||
enabled: true
|
enabled: false
|
||||||
requires: ">=2.25.0"
|
requires: ">=2.25.0"
|
||||||
author:
|
author:
|
||||||
name: LHY
|
name: LHY
|
||||||
|
|||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
0b126b96a0d76a1ff1b1c7cc7088c3ee1903dea2119abded2fb3913b340b63b1 darkreader.js
|
||||||
|
fbacd711bfd26b33b160881b50028a3e22e8f083b62ec727baebc0c4ad14b052 darkreader.mjs
|
||||||
|
5f7ebb5de2d74e011d87b86c2cc12048d667981599bdc9c527f98de9e0503a26 package.json
|
||||||
|
ab1ba9694b242323d15ed4839457c14fe04891966568413d5cd5eb78156dae1f index.d.ts
|
||||||
|
3f018657498dc4a805af22cc872688f1cd699bbe0045c82d29aeb0c8e65843e5 LICENSE
|
||||||
Vendored
+3
-111
@@ -2,116 +2,8 @@
|
|||||||
"name": "darkreader",
|
"name": "darkreader",
|
||||||
"version": "4.9.129",
|
"version": "4.9.129",
|
||||||
"description": "Dark mode for every website",
|
"description": "Dark mode for every website",
|
||||||
"scripts": {
|
|
||||||
"api": "node --max-old-space-size=3072 tasks/cli.js build --api",
|
|
||||||
"build": "node --max-old-space-size=3072 tasks/cli.js build --release",
|
|
||||||
"build:all": "node --max-old-space-size=3072 tasks/cli.js build --debug --release --api",
|
|
||||||
"build:firefox": "node --max-old-space-size=3072 tasks/cli.js build --release --firefox",
|
|
||||||
"build:plus": "node --max-old-space-size=3072 tasks/cli.js build --plus --release",
|
|
||||||
"code-style": "npm run lint",
|
|
||||||
"config-cleanup": "node --max-http-header-size=131072 tasks/ping.js dark-sites detector-hints dynamic-theme-fixes inversion-fixes",
|
|
||||||
"debug": "node --max-old-space-size=3072 tasks/cli.js build --debug",
|
|
||||||
"debug:watch": "node --max-old-space-size=3072 tasks/cli.js build --debug --watch --chrome --firefox",
|
|
||||||
"debug:watch:mv3": "node --max-old-space-size=3072 tasks/cli.js build --debug --watch --chrome-mv3",
|
|
||||||
"debug:watch:plus": "node --max-old-space-size=3072 tasks/cli.js build --debug --watch --plus --chrome",
|
|
||||||
"dependencies:upgrade": "node tasks/dependencies.js",
|
|
||||||
"lint": "eslint -- 'src/**/*.ts' 'src/**/*.tsx' 'tasks/**/*.js' 'tests/[!coverage]**/*.js' 'tests/**/*.ts' eslint.config.js index.d.ts",
|
|
||||||
"lint:bundle": "(node ./tasks/check-exists.js ./build/debug/chrome || node tasks/build.js --debug --api --chrome) && eslint -- 'build/debug/chrome/**/*.js' 'darkreader.js' 'darkreader.mjs'",
|
|
||||||
"plus-link": "cd src && ln -s ../../darkreader-plus plus",
|
|
||||||
"prepublishOnly": "npm test && npm run api",
|
|
||||||
"release": "npm test && npm run lint && node tasks/cli.js build --release",
|
|
||||||
"test": "npm run test:unit",
|
|
||||||
"test:all": "npm run test:unit; npm run test:browser; npm run test:inject",
|
|
||||||
"test:browser": "npm run test:chrome && npm run test:chrome-mv3 && npm run test:firefox",
|
|
||||||
"test:chrome": "node tasks/cli.js build --debug --test --chrome-mv2 && jest --config=tests/browser/jest.config.mjs --runInBand",
|
|
||||||
"test:chrome-mv3": "node tasks/cli.js build --debug --test --chrome-mv3 && jest --config=tests/browser/jest.config.chrome-mv3.mjs --runInBand",
|
|
||||||
"test:ci": "npm run test:unit",
|
|
||||||
"test:coverage": "jest --config=tests/unit/jest.config.mjs --coverage",
|
|
||||||
"test:edge": "node tasks/cli.js build --debug --test --plus && jest --config=tests/browser/jest.config.mjs --runInBand",
|
|
||||||
"test:firefox": "node tasks/cli.js build --debug --test --firefox-mv2 && jest --config=tests/browser/jest.config.firefox.mjs --runInBand",
|
|
||||||
"test:inject": "node --max-old-space-size=3072 node_modules/.bin/karma start ./tests/inject/karma.conf.cjs",
|
|
||||||
"test:inject:debug": "node --max-old-space-size=3072 node_modules/.bin/karma start ./tests/inject/karma.conf.cjs --debug",
|
|
||||||
"test:unit": "jest --config=tests/unit/jest.config.mjs",
|
|
||||||
"test:unit:debug": "node --inspect-brk ./node_modules/jest/bin/jest --config=tests/unit/jest.config.mjs --runInBand --no-cache --watch",
|
|
||||||
"translate-en-message": "node ./tasks/translate.js --message",
|
|
||||||
"translate-new-en-messages": "node ./tasks/translate.js --new-messages",
|
|
||||||
"deno:bootstrap": "deno run --allow-read=./ --allow-sys=uid --allow-write=deno.json tasks/deno.js"
|
|
||||||
},
|
|
||||||
"main": "darkreader.js",
|
"main": "darkreader.js",
|
||||||
"module": "darkreader.mjs",
|
"module": "darkreader.mjs",
|
||||||
"type": "module",
|
"types": "index.d.ts",
|
||||||
"repository": {
|
"license": "MIT"
|
||||||
"type": "git",
|
}
|
||||||
"url": "git+https://github.com/darkreader/darkreader.git"
|
|
||||||
},
|
|
||||||
"author": "Alexander Shutau <darkreaderapp@gmail.com> (https://darkreader.org/)",
|
|
||||||
"license": "MIT",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/darkreader/darkreader/issues"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"dark-theme",
|
|
||||||
"dark-mode",
|
|
||||||
"night-mode",
|
|
||||||
"darkmode",
|
|
||||||
"nightmode",
|
|
||||||
"accessibility",
|
|
||||||
"eye-care"
|
|
||||||
],
|
|
||||||
"homepage": "https://darkreader.org/",
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/darkreader/donate"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"malevic": "0.20.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@eslint/compat": "2.1.0",
|
|
||||||
"@eslint/eslintrc": "3.3.6",
|
|
||||||
"@eslint/js": "10.0.0",
|
|
||||||
"@rollup/plugin-node-resolve": "16.0.3",
|
|
||||||
"@rollup/plugin-replace": "6.0.3",
|
|
||||||
"@rollup/plugin-typescript": "12.3.0",
|
|
||||||
"@stylistic/eslint-plugin": "5.10.0",
|
|
||||||
"@types/chrome": "0.2.5",
|
|
||||||
"@types/eslint": "9.6.1",
|
|
||||||
"@types/jasmine": "6.0.0",
|
|
||||||
"@types/jest": "30.0.0",
|
|
||||||
"@types/karma": "6.3.9",
|
|
||||||
"@types/karma-coverage": "2.0.3",
|
|
||||||
"@types/node": "26.1.2",
|
|
||||||
"@types/ws": "8.18.1",
|
|
||||||
"chokidar": "5.0.0",
|
|
||||||
"eslint-plugin-compat": "7.0.2",
|
|
||||||
"eslint-plugin-import": "2.32.0",
|
|
||||||
"globals": "17.9.0",
|
|
||||||
"globby": "16.2.2",
|
|
||||||
"jasmine-core": "6.3.0",
|
|
||||||
"jest": "30.4.2",
|
|
||||||
"jest-extended": "7.0.0",
|
|
||||||
"karma": "6.4.4",
|
|
||||||
"karma-chrome-launcher": "3.2.0",
|
|
||||||
"karma-coverage": "2.2.1",
|
|
||||||
"karma-firefox-launcher": "2.1.3",
|
|
||||||
"karma-jasmine": "5.1.0",
|
|
||||||
"karma-rollup-preprocessor": "7.0.8",
|
|
||||||
"karma-safari-launcher": "1.0.0",
|
|
||||||
"karma-spec-reporter": "0.0.36",
|
|
||||||
"less": "4.8.1",
|
|
||||||
"prettier": "3.9.6",
|
|
||||||
"puppeteer-core": "25.4.0",
|
|
||||||
"rollup": "4.62.4",
|
|
||||||
"rollup-plugin-istanbul": "5.0.0",
|
|
||||||
"ts-jest": "29.4.12",
|
|
||||||
"tslib": "2.8.1",
|
|
||||||
"typescript": "6.0.3",
|
|
||||||
"typescript-eslint": "8.65.0",
|
|
||||||
"ws": "8.21.1",
|
|
||||||
"yazl": "3.3.1"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.62.4",
|
|
||||||
"@rollup/rollup-win32-x64-msvc": "4.62.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
"lint:eslint": "eslint . --fix",
|
"lint:eslint": "eslint . --fix",
|
||||||
"lint": "run-s lint:*",
|
"lint": "run-s lint:*",
|
||||||
"prettier": "prettier --write src/",
|
"prettier": "prettier --write src/",
|
||||||
"test:unit": "vitest --passWithNoTests",
|
"test:unit": "vitest",
|
||||||
"type-check": "vue-tsc --build"
|
"type-check": "vue-tsc --build"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
|
|||||||
Generated
+1
-28
@@ -562,17 +562,6 @@ packages:
|
|||||||
'@rolldown/pluginutils@1.0.1':
|
'@rolldown/pluginutils@1.0.1':
|
||||||
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
|
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-gnu@4.62.4':
|
|
||||||
resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [linux]
|
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-win32-x64-msvc@4.62.4':
|
|
||||||
resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
'@rsbuild/core@2.1.10':
|
'@rsbuild/core@2.1.10':
|
||||||
resolution: {integrity: sha512-lwxC5w88U2AMv6aNwG3VH7+AV33N4JJQjD//egVWFsMbV+OE/bnfCsurSpX8s3lGyRYkJMwUVN+YXMbmmYZFfw==}
|
resolution: {integrity: sha512-lwxC5w88U2AMv6aNwG3VH7+AV33N4JJQjD//egVWFsMbV+OE/bnfCsurSpX8s3lGyRYkJMwUVN+YXMbmmYZFfw==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -1627,9 +1616,6 @@ packages:
|
|||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
malevic@0.20.2:
|
|
||||||
resolution: {integrity: sha512-s44yEUyfDaONt7nPT7NDQ+Z2oAswErG70ok2Q95bJFh1Bdcn4dZQVMrLE02ZIjTtYfQ/LFOVxF+yB3bdGw/GtQ==}
|
|
||||||
|
|
||||||
math-intrinsics@1.1.0:
|
math-intrinsics@1.1.0:
|
||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2755,12 +2741,6 @@ snapshots:
|
|||||||
|
|
||||||
'@rolldown/pluginutils@1.0.1': {}
|
'@rolldown/pluginutils@1.0.1': {}
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-gnu@4.62.4':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@rollup/rollup-win32-x64-msvc@4.62.4':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@rsbuild/core@2.1.10':
|
'@rsbuild/core@2.1.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rspack/core': 2.1.8(@swc/helpers@0.5.23)
|
'@rspack/core': 2.1.8(@swc/helpers@0.5.23)
|
||||||
@@ -3340,12 +3320,7 @@ snapshots:
|
|||||||
|
|
||||||
csstype@3.2.3: {}
|
csstype@3.2.3: {}
|
||||||
|
|
||||||
darkreader@file:../third-party/darkreader:
|
darkreader@file:../third-party/darkreader: {}
|
||||||
dependencies:
|
|
||||||
malevic: 0.20.2
|
|
||||||
optionalDependencies:
|
|
||||||
'@rollup/rollup-linux-x64-gnu': 4.62.4
|
|
||||||
'@rollup/rollup-win32-x64-msvc': 4.62.4
|
|
||||||
|
|
||||||
data-urls@5.0.0:
|
data-urls@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3855,8 +3830,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
malevic@0.20.2: {}
|
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
memorystream@0.3.1: {}
|
memorystream@0.3.1: {}
|
||||||
|
|||||||
@@ -7,13 +7,19 @@ const { isDark, toggle } = useDarkMode()
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="theme-toggle" @click="toggle">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="theme-toggle"
|
||||||
|
:aria-pressed="isDark"
|
||||||
|
:aria-label="isDark ? '切换到浅色模式' : '切换到深色模式'"
|
||||||
|
@click="toggle"
|
||||||
|
>
|
||||||
<IconSunLine v-if="isDark" class="theme-toggle__icon" />
|
<IconSunLine v-if="isDark" class="theme-toggle__icon" />
|
||||||
<IconMoonLine v-else class="theme-toggle__icon" />
|
<IconMoonLine v-else class="theme-toggle__icon" />
|
||||||
<span class="theme-toggle__label">
|
<span class="theme-toggle__label">
|
||||||
{{ isDark ? '浅色模式' : '深色模式' }}
|
{{ isDark ? '浅色模式' : '深色模式' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -21,9 +27,13 @@ const { isDark, toggle } = useDarkMode()
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
cursor: pointer;
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
color: var(--halo-text-secondary);
|
color: var(--halo-text-secondary);
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
transition: background-color 0.15s, color 0.15s;
|
transition: background-color 0.15s, color 0.15s;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@@ -34,6 +44,11 @@ const { isDark, toggle } = useDarkMode()
|
|||||||
color: var(--halo-text-primary);
|
color: var(--halo-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.theme-toggle:focus-visible {
|
||||||
|
outline: 2px solid var(--halo-accent-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.theme-toggle__icon {
|
.theme-toggle__icon {
|
||||||
width: 1.125rem;
|
width: 1.125rem;
|
||||||
height: 1.125rem;
|
height: 1.125rem;
|
||||||
@@ -44,4 +59,4 @@ const { isDark, toggle } = useDarkMode()
|
|||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { nextTick } from 'vue'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'halo-dark-mode-theme'
|
||||||
|
|
||||||
|
function mockMatchMedia(matches: boolean): void {
|
||||||
|
window.matchMedia = vi.fn().mockImplementation((query: string) => ({
|
||||||
|
matches,
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
addListener: vi.fn(),
|
||||||
|
removeListener: vi.fn(),
|
||||||
|
dispatchEvent: vi.fn(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useDarkMode', () => {
|
||||||
|
let useDarkMode: typeof import('../useDarkMode').useDarkMode
|
||||||
|
|
||||||
|
async function load(systemDark = false): Promise<void> {
|
||||||
|
vi.resetModules()
|
||||||
|
localStorage.clear()
|
||||||
|
mockMatchMedia(systemDark)
|
||||||
|
const mod = await import('../useDarkMode')
|
||||||
|
useDarkMode = mod.useDarkMode
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await load(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('默认 auto 且系统为深色时 isDark 为 true', async () => {
|
||||||
|
await load(true)
|
||||||
|
const { theme, isDark } = useDarkMode()
|
||||||
|
expect(theme.value).toBe('auto')
|
||||||
|
expect(isDark.value).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('auto 模式下点击切换变为显式深色并持久化', async () => {
|
||||||
|
const { theme, toggle } = useDarkMode()
|
||||||
|
toggle()
|
||||||
|
expect(theme.value).toBe('dark')
|
||||||
|
await nextTick()
|
||||||
|
expect(localStorage.getItem(STORAGE_KEY)).toBe('dark')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('auto 且系统为深色时点击切换变为显式浅色', async () => {
|
||||||
|
await load(true)
|
||||||
|
const { theme, toggle } = useDarkMode()
|
||||||
|
toggle()
|
||||||
|
expect(theme.value).toBe('light')
|
||||||
|
await nextTick()
|
||||||
|
expect(localStorage.getItem(STORAGE_KEY)).toBe('light')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('dark 与 light 之间往返切换', async () => {
|
||||||
|
const { theme, toggle } = useDarkMode()
|
||||||
|
theme.value = 'dark'
|
||||||
|
toggle()
|
||||||
|
expect(theme.value).toBe('light')
|
||||||
|
toggle()
|
||||||
|
expect(theme.value).toBe('dark')
|
||||||
|
await nextTick()
|
||||||
|
expect(localStorage.getItem(STORAGE_KEY)).toBe('dark')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('setTheme 会持久化到 localStorage', async () => {
|
||||||
|
const { setTheme } = useDarkMode()
|
||||||
|
setTheme('dark')
|
||||||
|
await nextTick()
|
||||||
|
expect(localStorage.getItem(STORAGE_KEY)).toBe('dark')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('localStorage 非法值回退到 auto', async () => {
|
||||||
|
localStorage.setItem(STORAGE_KEY, 'not-a-theme')
|
||||||
|
await load(false)
|
||||||
|
const { theme } = useDarkMode()
|
||||||
|
expect(theme.value).toBe('auto')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('storage 事件会同步其他标签页的主题', () => {
|
||||||
|
const { theme, setTheme } = useDarkMode()
|
||||||
|
setTheme('light')
|
||||||
|
window.dispatchEvent(
|
||||||
|
new StorageEvent('storage', {
|
||||||
|
key: STORAGE_KEY,
|
||||||
|
newValue: 'dark',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(theme.value).toBe('dark')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('storage 事件忽略非法值', () => {
|
||||||
|
const { theme, setTheme } = useDarkMode()
|
||||||
|
setTheme('light')
|
||||||
|
window.dispatchEvent(
|
||||||
|
new StorageEvent('storage', {
|
||||||
|
key: STORAGE_KEY,
|
||||||
|
newValue: 'invalid',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(theme.value).toBe('light')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -26,13 +26,19 @@ function persistTheme(theme: ThemeMode): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将 data-halo-theme 属性应用到 <html> 元素 */
|
/**
|
||||||
|
* 将主题状态同步到 <html>。
|
||||||
|
* data-halo-theme 为兼容性遗留标记,当前已无 CSS 消费方,保留给外部脚本与验证工具;
|
||||||
|
* color-scheme 用于在 Dark Reader 异步注入前同步浏览器控件外观,缓解 FOUC。
|
||||||
|
*/
|
||||||
function applyHtmlAttribute(isDark: boolean): void {
|
function applyHtmlAttribute(isDark: boolean): void {
|
||||||
|
const root = document.documentElement
|
||||||
if (isDark) {
|
if (isDark) {
|
||||||
document.documentElement.setAttribute('data-halo-theme', 'dark')
|
root.setAttribute('data-halo-theme', 'dark')
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.removeAttribute('data-halo-theme')
|
root.removeAttribute('data-halo-theme')
|
||||||
}
|
}
|
||||||
|
root.style.colorScheme = isDark ? 'dark' : 'light'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模块级单例 — 所有组件共享同一个状态
|
// 模块级单例 — 所有组件共享同一个状态
|
||||||
@@ -52,6 +58,15 @@ watch(isDark, (dark) => applyHtmlAttribute(dark), { immediate: true })
|
|||||||
// 监听 theme 变化 → 持久化
|
// 监听 theme 变化 → 持久化
|
||||||
watch(theme, (t) => persistTheme(t))
|
watch(theme, (t) => persistTheme(t))
|
||||||
|
|
||||||
|
// 多标签页同步:其他标签页修改主题时跟随更新
|
||||||
|
window.addEventListener('storage', (event) => {
|
||||||
|
if (event.key !== STORAGE_KEY) return
|
||||||
|
const value = event.newValue
|
||||||
|
if (value === 'light' || value === 'dark' || value === 'auto') {
|
||||||
|
theme.value = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 监听系统偏好变化 → 仅在 'auto' 模式下响应
|
// 监听系统偏好变化 → 仅在 'auto' 模式下响应
|
||||||
onChange((systemDark) => {
|
onChange((systemDark) => {
|
||||||
if (theme.value === 'auto') {
|
if (theme.value === 'auto') {
|
||||||
@@ -83,4 +98,4 @@ export function useDarkMode() {
|
|||||||
setTheme,
|
setTheme,
|
||||||
toggle,
|
toggle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import { useDarkMode } from './composables/useDarkMode'
|
|||||||
* Dark Reader 引擎参数。
|
* Dark Reader 引擎参数。
|
||||||
* 纯 Dark Reader 策略下不再维护手工 CSS 覆盖;
|
* 纯 Dark Reader 策略下不再维护手工 CSS 覆盖;
|
||||||
* 这里只调整通用观感参数,让核心与第三方插件页面转换后风格统一。
|
* 这里只调整通用观感参数,让核心与第三方插件页面转换后风格统一。
|
||||||
|
* 注意:DR 注入为异步过程,刷新闪烁只能由 useDarkMode 的 color-scheme 同步缓解。
|
||||||
*/
|
*/
|
||||||
const DARK_READER_THEME = {
|
const DARK_READER_THEME = {
|
||||||
brightness: 100,
|
brightness: 100,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
配色策略: OKLCH 颜色空间 | 中性色微蓝着色调 | 亮度层次替代阴影
|
配色策略: OKLCH 颜色空间 | 中性色微蓝着色调 | 亮度层次替代阴影
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
|
/* 说明:data-halo-theme 为兼容性遗留标记,当前已无 CSS 消费方,保留给外部脚本与验证工具。 */
|
||||||
/* ===== 浅色模式(Halo 默认,此处定义为显式回退) ===== */
|
/* ===== 浅色模式(Halo 默认,此处定义为显式回退) ===== */
|
||||||
:root {
|
:root {
|
||||||
--halo-bg-body: oklch(97% 0.005 250);
|
--halo-bg-body: oklch(97% 0.005 250);
|
||||||
|
|||||||
@@ -31,17 +31,24 @@ const modeOptions: { value: ThemeMode; label: string; description: string }[] =
|
|||||||
当前生效:<strong>{{ currentEffectiveMode }}</strong>
|
当前生效:<strong>{{ currentEffectiveMode }}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dark-mode-settings__options">
|
<div
|
||||||
<div
|
class="dark-mode-settings__options"
|
||||||
|
role="radiogroup"
|
||||||
|
aria-label="主题模式"
|
||||||
|
>
|
||||||
|
<button
|
||||||
v-for="option in modeOptions"
|
v-for="option in modeOptions"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
class="dark-mode-settings__option"
|
class="dark-mode-settings__option"
|
||||||
:class="{ 'is-active': theme === option.value }"
|
:class="{ 'is-active': theme === option.value }"
|
||||||
|
:aria-checked="theme === option.value"
|
||||||
@click="setTheme(option.value)"
|
@click="setTheme(option.value)"
|
||||||
>
|
>
|
||||||
<div class="dark-mode-settings__option-label">{{ option.label }}</div>
|
<div class="dark-mode-settings__option-label">{{ option.label }}</div>
|
||||||
<div class="dark-mode-settings__option-desc">{{ option.description }}</div>
|
<div class="dark-mode-settings__option-desc">{{ option.description }}</div>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,10 +102,15 @@ const modeOptions: { value: ThemeMode; label: string; description: string }[] =
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dark-mode-settings__option {
|
.dark-mode-settings__option {
|
||||||
|
width: 100%;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
|
border: 1px solid var(--halo-border-base);
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid var(--halo-border-base);
|
|
||||||
transition: background-color 0.15s, border-color 0.15s;
|
transition: background-color 0.15s, border-color 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +118,11 @@ const modeOptions: { value: ThemeMode; label: string; description: string }[] =
|
|||||||
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);
|
||||||
@@ -122,4 +139,4 @@ const modeOptions: { value: ThemeMode; label: string; description: string }[] =
|
|||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
color: var(--halo-text-tertiary);
|
color: var(--halo-text-tertiary);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# 启动带持久化配置的 Edge 窗口,等待用户登录 Halo 后台。
|
|
||||||
# 登录成功后脚本自动退出,会话会保留在 pw-profile 目录里供后续扫描使用。
|
|
||||||
import pathlib
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
from playwright.sync_api import sync_playwright
|
|
||||||
|
|
||||||
PROFILE = pathlib.Path(__file__).parent / "pw-profile"
|
|
||||||
LOGIN_URL = "https://blog.liuhangyv.top/console/login"
|
|
||||||
TIMEOUT_S = 280
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
with sync_playwright() as p:
|
|
||||||
ctx = p.chromium.launch_persistent_context(
|
|
||||||
str(PROFILE),
|
|
||||||
channel="msedge",
|
|
||||||
headless=False,
|
|
||||||
viewport={"width": 1600, "height": 950},
|
|
||||||
)
|
|
||||||
page = ctx.pages[0] if ctx.pages else ctx.new_page()
|
|
||||||
page.goto(LOGIN_URL)
|
|
||||||
print("浏览器窗口已打开,请在其中登录 Halo 后台...", flush=True)
|
|
||||||
|
|
||||||
deadline = time.time() + TIMEOUT_S
|
|
||||||
while time.time() < deadline:
|
|
||||||
try:
|
|
||||||
url = page.url
|
|
||||||
except Exception:
|
|
||||||
print("检测到窗口被关闭", flush=True)
|
|
||||||
return 2
|
|
||||||
if "/console" in url and "/login" not in url:
|
|
||||||
time.sleep(3) # 等待会话 cookie 写入磁盘
|
|
||||||
print(f"检测到登录成功: {url}", flush=True)
|
|
||||||
ctx.close()
|
|
||||||
return 0
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
print("等待超时,未检测到登录", flush=True)
|
|
||||||
ctx.close()
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
# Halo 后台暗色模式残留扫描器
|
|
||||||
# 自动发现侧边栏全部 /console 路由,逐页扫描浅色背景 / 深色文字残留并截图。
|
|
||||||
import json
|
|
||||||
import pathlib
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
from playwright.sync_api import sync_playwright
|
|
||||||
|
|
||||||
BASE = "https://blog.liuhangyv.top"
|
|
||||||
ROOT = pathlib.Path(__file__).parent
|
|
||||||
PROFILE = ROOT / "pw-profile"
|
|
||||||
SHOTS = ROOT / "shots"
|
|
||||||
OUT = ROOT / "scan-results.json"
|
|
||||||
|
|
||||||
# 在每个页面加载前强制插件进入深色模式
|
|
||||||
INIT_JS = """
|
|
||||||
try { localStorage.setItem('halo-dark-mode-theme', 'dark'); } catch(e) {}
|
|
||||||
document.documentElement.setAttribute('data-halo-theme', 'dark');
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 页面内扫描:浅色背景(RGB 均 >235 且不透明)、深色文字(RGB 均 <70)
|
|
||||||
SCAN_JS = r"""
|
|
||||||
() => {
|
|
||||||
const results = [];
|
|
||||||
const seen = new Set();
|
|
||||||
const isVisible = (el) => {
|
|
||||||
const cs = getComputedStyle(el);
|
|
||||||
return cs.display !== 'none' && cs.visibility !== 'hidden' && +cs.opacity > 0.05;
|
|
||||||
};
|
|
||||||
const shortPath = (el) => {
|
|
||||||
const parts = [];
|
|
||||||
let cur = el;
|
|
||||||
for (let i = 0; i < 5 && cur && cur !== document.body; i++) {
|
|
||||||
let p = cur.tagName.toLowerCase();
|
|
||||||
if (cur.id) p += '#' + cur.id;
|
|
||||||
else if (typeof cur.className === 'string' && cur.className.trim()) {
|
|
||||||
p += '.' + cur.className.trim().split(/\s+/).slice(0, 2).join('.');
|
|
||||||
}
|
|
||||||
parts.unshift(p);
|
|
||||||
cur = cur.parentElement;
|
|
||||||
}
|
|
||||||
return parts.join(' > ');
|
|
||||||
};
|
|
||||||
document.querySelectorAll('body *').forEach(el => {
|
|
||||||
if (!isVisible(el)) return;
|
|
||||||
const r = el.getBoundingClientRect();
|
|
||||||
if (r.width < 50 || r.height < 20) return;
|
|
||||||
const cs = getComputedStyle(el);
|
|
||||||
const issues = [];
|
|
||||||
const bg = cs.backgroundColor.match(/rgba?\(([\d.]+),\s*([\d.]+),\s*([\d.]+)(?:,\s*([\d.]+))?\)/);
|
|
||||||
if (bg && bg[4] !== '0' && +bg[1] > 235 && +bg[2] > 235 && +bg[3] > 235) {
|
|
||||||
issues.push('light-bg ' + cs.backgroundColor);
|
|
||||||
}
|
|
||||||
const hasText = [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim());
|
|
||||||
const c = cs.color.match(/rgba?\(([\d.]+),\s*([\d.]+),\s*([\d.]+)/);
|
|
||||||
if (hasText && c && +c[1] < 70 && +c[2] < 70 && +c[3] < 70) {
|
|
||||||
issues.push('dark-text ' + cs.color);
|
|
||||||
}
|
|
||||||
if (!issues.length) return;
|
|
||||||
const cls = (typeof el.className === 'string' ? el.className : '').trim().replace(/\s+/g, ' ').slice(0, 150);
|
|
||||||
const key = el.tagName + '|' + cls + '|' + issues.join(',');
|
|
||||||
if (seen.has(key)) return;
|
|
||||||
seen.add(key);
|
|
||||||
results.push({
|
|
||||||
tag: el.tagName.toLowerCase(),
|
|
||||||
cls,
|
|
||||||
path: shortPath(el),
|
|
||||||
issues,
|
|
||||||
size: Math.round(r.width) + 'x' + Math.round(r.height),
|
|
||||||
text: (el.textContent || '').trim().slice(0, 40),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def slug(route: str) -> str:
|
|
||||||
return re.sub(r"[^a-z0-9]+", "-", route.lower()).strip("-") or "root"
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
SHOTS.mkdir(exist_ok=True)
|
|
||||||
with sync_playwright() as p:
|
|
||||||
ctx = p.chromium.launch_persistent_context(
|
|
||||||
str(PROFILE),
|
|
||||||
channel="msedge",
|
|
||||||
headless=True,
|
|
||||||
viewport={"width": 1600, "height": 950},
|
|
||||||
)
|
|
||||||
ctx.add_init_script(INIT_JS)
|
|
||||||
page = ctx.pages[0] if ctx.pages else ctx.new_page()
|
|
||||||
page.goto(BASE + "/console/dashboard", wait_until="domcontentloaded")
|
|
||||||
try:
|
|
||||||
page.wait_for_load_state("networkidle", timeout=10000)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
time.sleep(3)
|
|
||||||
if "/login" in page.url:
|
|
||||||
print("SESSION_EXPIRED 登录态失效,需要重新登录", flush=True)
|
|
||||||
ctx.close()
|
|
||||||
return 3
|
|
||||||
|
|
||||||
dark = page.evaluate("document.documentElement.getAttribute('data-halo-theme')")
|
|
||||||
print(f"data-halo-theme = {dark}", flush=True)
|
|
||||||
|
|
||||||
# 普查样式表:确认服务端实际部署的插件 CSS 覆盖了哪些内容
|
|
||||||
census = page.evaluate(
|
|
||||||
"""() => {
|
|
||||||
const out = [];
|
|
||||||
for (const sheet of document.styleSheets) {
|
|
||||||
let rules;
|
|
||||||
try { rules = sheet.cssRules; } catch (e) { continue; }
|
|
||||||
let darkRules = 0;
|
|
||||||
let samples = [];
|
|
||||||
for (const r of rules) {
|
|
||||||
const t = r.cssText || '';
|
|
||||||
if (t.includes('data-halo-theme')) {
|
|
||||||
darkRules++;
|
|
||||||
if (samples.length < 3) samples.push(t.slice(0, 100));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (darkRules > 0) {
|
|
||||||
out.push({ href: sheet.href || '(inline)', total: rules.length, darkRules, samples });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}"""
|
|
||||||
)
|
|
||||||
print("=== 包含暗色规则的样式表 ===", flush=True)
|
|
||||||
for c in census:
|
|
||||||
print(f" {c['href']} dark规则数={c['darkRules']}", flush=True)
|
|
||||||
|
|
||||||
# 关键字探针:确认部署的 CSS 是否包含关键覆盖(判断部署版本新旧)
|
|
||||||
keywords = page.evaluate(
|
|
||||||
"""() => {
|
|
||||||
const kws = ['description-item', 'bytemd', 'week-picker', 'menu-item-title',
|
|
||||||
'alert-wrapper', 'entity-field-title', 'sidebar__profile', 'card-wrapper'];
|
|
||||||
const found = {};
|
|
||||||
for (const kw of kws) found[kw] = false;
|
|
||||||
for (const sheet of document.styleSheets) {
|
|
||||||
let rules;
|
|
||||||
try { rules = sheet.cssRules; } catch (e) { continue; }
|
|
||||||
for (const r of rules) {
|
|
||||||
const t = r.cssText || '';
|
|
||||||
if (!t.includes('data-halo-theme')) continue;
|
|
||||||
for (const kw of kws) if (t.includes(kw)) found[kw] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return found;
|
|
||||||
}"""
|
|
||||||
)
|
|
||||||
print(f"=== 部署 CSS 关键字探针 === {keywords}", flush=True)
|
|
||||||
|
|
||||||
# 从 Vue Router 读取全部已注册路由(含插件注册的菜单页)
|
|
||||||
try:
|
|
||||||
routes = page.evaluate(
|
|
||||||
"""() => {
|
|
||||||
const app = document.querySelector('#app').__vue_app__;
|
|
||||||
const router = app.config.globalProperties.$router;
|
|
||||||
return router.getRoutes().map(r => r.path);
|
|
||||||
}"""
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Router 读取失败,回退到锚点抓取: {e}", flush=True)
|
|
||||||
routes = page.evaluate(
|
|
||||||
"""() => [...new Set([...document.querySelectorAll('a[href]')]
|
|
||||||
.map(a => a.getAttribute('href'))
|
|
||||||
.filter(h => h && h.startsWith('/console')))]"""
|
|
||||||
)
|
|
||||||
# Halo Console 的 router base 是 /console/,getRoutes() 返回的路径不带 base
|
|
||||||
routes = sorted(
|
|
||||||
{
|
|
||||||
r if r.startswith("/console") else "/console" + r
|
|
||||||
for r in routes
|
|
||||||
if r.startswith("/") and ":" not in r and r not in ("/", "/console")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
# 编辑器路由只保留一个样本,避免重复扫描
|
|
||||||
editor = [r for r in routes if "editor" in r]
|
|
||||||
routes = [r for r in routes if "editor" not in r] + editor[:1]
|
|
||||||
print(f"发现 {len(routes)} 个后台路由: {routes}", flush=True)
|
|
||||||
|
|
||||||
all_results = {}
|
|
||||||
all_results["__stylesheet_census__"] = census
|
|
||||||
for route in routes:
|
|
||||||
try:
|
|
||||||
page.goto(BASE + route, wait_until="domcontentloaded")
|
|
||||||
try:
|
|
||||||
page.wait_for_load_state("networkidle", timeout=6000)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
time.sleep(1.5)
|
|
||||||
# 兜底:再设一次暗色属性,防止插件脚本时序问题
|
|
||||||
page.evaluate("document.documentElement.setAttribute('data-halo-theme','dark')")
|
|
||||||
time.sleep(0.3)
|
|
||||||
items = page.evaluate(SCAN_JS)
|
|
||||||
all_results[route] = items
|
|
||||||
page.screenshot(path=str(SHOTS / (slug(route) + ".png")))
|
|
||||||
print(f"{route}: {len(items)} 处疑似残留", flush=True)
|
|
||||||
except Exception as e: # noqa: BLE001
|
|
||||||
all_results[route] = {"error": str(e)[:200]}
|
|
||||||
print(f"{route}: 扫描失败 {e}", flush=True)
|
|
||||||
|
|
||||||
OUT.write_text(json.dumps(all_results, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
||||||
ctx.close()
|
|
||||||
print(f"DONE -> {OUT}", flush=True)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
Reference in New Issue
Block a user