Compare commits
7 Commits
1.0.5
...
30b2ae2f83
| Author | SHA1 | Date | |
|---|---|---|---|
| 30b2ae2f83 | |||
| cffbe572bf | |||
| 96dd1ef4d3 | |||
| df9cd52de6 | |||
| 394613f516 | |||
| 5cbce86e03 | |||
| 45451c0d49 |
+1
-1
@@ -1,8 +1,8 @@
|
||||
.gradle/
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
*.class
|
||||
*.jar
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
*.war
|
||||
*.log
|
||||
*.tmp
|
||||
|
||||
@@ -8,19 +8,19 @@
|
||||
- **偏好持久化**:自动记忆用户选择(localStorage),刷新不丢失
|
||||
- **系统偏好跟随**:切换系统外观时自动响应
|
||||
- **Dark Reader 引擎**:自动分析页面 CSS 与 DOM,覆盖 Halo 核心页面和第三方插件页面
|
||||
- **侧边栏一键切换**:切换按钮自动出现在侧边栏 UserProfileBanner 上方
|
||||
- **设置页面**:在「偏好设置 → 深色模式」中详细选择显示模式
|
||||
- **官方外观分组**:设置入口位于 Halo「外观」分组,与主题、菜单、插件同组
|
||||
- **设置页面**:在「外观 → 深色模式」中详细选择显示模式
|
||||
- **零后端依赖**:纯前端实现,不需要额外后端 API
|
||||
|
||||
## 安装
|
||||
|
||||
1. 从 Releases 下载 `plugin-dark-mode-<version>.jar`。
|
||||
2. 在 Halo 后台的「插件管理」中上传并安装。
|
||||
3. 启用插件后,侧边栏底部会出现深色模式切换按钮。
|
||||
3. 启用插件后,进入「外观 → 深色模式」设置页面调整主题。
|
||||
|
||||
## 使用
|
||||
|
||||
安装并启用后,可以直接点击侧边栏按钮在深色和浅色模式之间切换,也可以进入设置页面选择固定模式:
|
||||
安装并启用后,进入「外观 → 深色模式」设置页面选择显示模式:
|
||||
|
||||
| 配置项 | 可选值 | 说明 |
|
||||
| -------- | --------- | -------------------- |
|
||||
@@ -33,7 +33,7 @@
|
||||
- 插件通过 `useDarkMode()` 管理 `light` / `dark` / `auto` 三种状态。
|
||||
- 深色模式下调用 Dark Reader 的 `enable()`,浅色模式下调用 `disable()`。
|
||||
- Dark Reader 会持续监听页面 DOM 变化,因此第三方插件动态渲染的内容也能自动转换。
|
||||
- 插件自身只保留切换器与设置页所需的最小 UI 变量,不再维护逐页手工 CSS 覆盖。
|
||||
- 插件自身只保留设置页所需的最小 UI 变量,不再维护逐页手工 CSS 覆盖。
|
||||
|
||||
> 说明:Dark Reader 的样式注入是异步的,刷新瞬间仍可能存在极短闪白;插件通过同步设置 `color-scheme` 缓解,但无法完全消除。
|
||||
|
||||
@@ -108,12 +108,9 @@ pnpm build # 生产构建
|
||||
└── src/
|
||||
├── index.ts # definePlugin 入口
|
||||
├── darkreader-engine.ts # Dark Reader 通用暗色引擎
|
||||
├── injector.ts # 侧边栏切换器注入
|
||||
├── composables/
|
||||
│ ├── useDarkMode.ts # 主题状态管理(模块级单例)
|
||||
│ └── useSystemPreference.ts # 系统偏好监听
|
||||
├── components/
|
||||
│ └── ThemeToggle.vue # 侧边栏切换按钮
|
||||
├── views/
|
||||
│ └── SettingsView.vue # 设置页面
|
||||
└── styles/
|
||||
@@ -127,15 +124,30 @@ pnpm build # 生产构建
|
||||
./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
|
||||
|
||||
- 同步设置 `color-scheme`,缓解深色模式刷新闪烁
|
||||
- 增加多标签页主题同步与 `useDarkMode` 单元测试
|
||||
- 侧边栏切换按钮与设置选项支持键盘操作
|
||||
- 后端日志改用 Lombok `@Slf4j`
|
||||
- 裁剪 vendored `package.json`,新增 Dark Reader `SHA256SUMS` 完整性校验
|
||||
- `plugin.yaml` 改为安装后由用户手动启用
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# 问题交接单:深色模式入口去重 + 菜单分组调整(2026-08-08)
|
||||
|
||||
> 审查窗口产出,**交付开发窗口执行**;审查窗口不修改代码。
|
||||
> 用户需求(已确认):① 深色模式设置入口从"偏好设置"移到 Halo 官方"外观"分组;② 移除侧边栏底部注入的切换按钮(与菜单入口重复,用户判定多余)。
|
||||
> 前置说明:真实环境取证于 `blog.liuhangyv.top/console`(已登录),见"问题描述"。
|
||||
|
||||
## 一、问题描述
|
||||
|
||||
### 问题 1:深色模式存在两个入口,功能重复
|
||||
|
||||
当前 Halo 后台对"深色模式"有两个入口:
|
||||
|
||||
- **入口 A(侧边栏底部切换按钮)**:插件注入的 `ThemeToggle` 按钮(显示"浅色模式 / 深色模式",点击一键切换)。
|
||||
- **入口 B(侧边栏菜单项)**:「偏好设置 → 深色模式」设置页(浅色 / 深色 / 跟随系统三选一)。
|
||||
|
||||
用户观察到侧边栏底部有两个"切换"类入口,底部注入的按钮闲置不用,判定没有必要,要求移除。
|
||||
|
||||
**真实环境取证结果**(消除歧义):
|
||||
- 侧边栏底部**只有 1 个**主题切换按钮:插件注入的 `plugin-dark-mode-toggle`(位于 `sidebar__profile` 上方),工作正常。
|
||||
- 用户信息区(`user-profile__actions`)另外两个图标按钮是 Halo 官方「个人资料(跳 /uc)」和「退出登录」——已核对官方源码 `ui/src/layouts/UserProfileBanner.vue`,**不是**主题切换。
|
||||
- 因此用户所说的"两个切换按钮" = 入口 A(底部注入按钮)+ 入口 B(菜单项),二者功能重复,保留其一即可。
|
||||
|
||||
### 问题 2:设置页入口挂在自建分组"偏好设置"下
|
||||
|
||||
- 现状:菜单 `group` 为字符串 `'偏好设置'`。这不是 Halo 标准分组 key,Halo 找不到对应 i18n 翻译,直接将该字符串作为分组标题显示,于是在侧边栏多出"偏好设置"分组(其中只有"深色模式"一项)。
|
||||
- 期望:放入 Halo 官方**「外观」**分组(主题 / 菜单 / 插件所在)。
|
||||
- 背景:Halo 2.25 官方分组 key(`halo-dev/halo` `ui/console-src/router/constant.ts`):`dashboard`、`content`、`interface`、`system`、`tool`。其中 **`interface` 即"外观"**(实测该分组内已有主题 / 菜单 / 插件)。
|
||||
|
||||
## 二、问题在代码中的具体体现
|
||||
|
||||
| # | 文件 | 位置 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `ui/src/index.ts` | L5 | `import { injectThemeToggle } from './injector'` |
|
||||
| 2 | `ui/src/index.ts` | L7-L8 | 模块加载时执行 `injectThemeToggle()` → 注入入口 A |
|
||||
| 3 | `ui/src/index.ts` | L26 | `group: '偏好设置'` → 自建字面量分组(入口 B 位置错误) |
|
||||
| 4 | `ui/src/injector.ts` | 全文 | 入口 A 的注入器:`CONTAINER_CLASS = 'plugin-dark-mode-toggle'`(L4)、`TARGET_SELECTOR = '.sidebar__profile'`(L5)、`injectThemeToggle()` 用 `MutationObserver` 等待侧边栏渲染(L13-L31)、`tryMount()` 将容器插入 profile 上方并 `render(ThemeToggle)`(L33-L48) |
|
||||
| 5 | `ui/src/components/ThemeToggle.vue` | 全文 | 入口 A 的按钮本体(class `theme-toggle`,文案"浅色模式 / 深色模式") |
|
||||
|
||||
**连带影响(改动入口 A 时必须同步处理):**
|
||||
|
||||
| # | 文件 | 位置 | 影响 |
|
||||
| --- | --- | --- | --- |
|
||||
| 6 | `scripts/verify-toggle.py` | L40-L43 | 依赖 `.theme-toggle` 定位点击验证,移除按钮后脚本失效 |
|
||||
| 7 | `README.md` | L11 / L19 / L23 / L111 / L116 / L130 / L138 | 多处描述"侧边栏一键切换 / UserProfileBanner / 注入器" |
|
||||
| 8 | `CLAUDE.md` / `AGENTS.md` | L54 / L59 / L86-L88 | 架构树与"ThemeToggle 侧边栏注入"章节(gitignored,本地维护) |
|
||||
|
||||
**不涉及**:`useDarkMode.ts`、`darkreader-engine.ts`、`SettingsView.vue` 均无需改动(入口 B 及其状态管理完全独立)。
|
||||
|
||||
## 三、修改建议(供开发窗口执行)
|
||||
|
||||
### 推荐方案:移分组 + 移除侧边栏注入按钮
|
||||
|
||||
1. **`ui/src/index.ts`**
|
||||
- L26:`group: '偏好设置'` → `group: 'interface'`(进入"外观"分组)
|
||||
- L5:删除 `import { injectThemeToggle } from './injector'`
|
||||
- L7-L8:删除调用与注释
|
||||
2. **删除文件(需用户书面确认,AGENTS.md 约定)**
|
||||
- `ui/src/injector.ts`
|
||||
- `ui/src/components/ThemeToggle.vue`
|
||||
3. **`scripts/verify-toggle.py`**
|
||||
- 移除 L40-L43 对 `.theme-toggle` 的点击依赖
|
||||
- 改为直接驱动状态 + 断言,例如:`localStorage.setItem('halo-dark-mode-theme', 'dark'/'light')` 后断言 `data-halo-theme`、`data-darkreader-mode`、`color-scheme` 翻转;保留现有三向 PASS 判定逻辑
|
||||
4. **文档同步**
|
||||
- `README.md`:功能特性去掉"侧边栏一键切换"、使用说明改为"进入「外观 → 深色模式」"、项目结构删除 injector/ThemeToggle 两行、验证脚本描述更新
|
||||
- `CLAUDE.md` / `AGENTS.md`:删除"ThemeToggle 侧边栏注入"章节,更新架构树
|
||||
5. **构建与回归**
|
||||
- `cd ui && pnpm build`、`pnpm type-check`、`pnpm prettier`、`pnpm lint`、`pnpm test:unit`
|
||||
- 根目录 `gradlew build`
|
||||
- 部署后跑 `scripts/verify-toggle.py`(改版后)
|
||||
|
||||
### 备选方案:仅移分组(保留侧边栏按钮)
|
||||
|
||||
- 只改 `ui/src/index.ts` L26 为 `'interface'`;验证脚本与文档均不用动。
|
||||
- 适合希望保留"一键切换"快捷方式的场景;但无法满足用户"移除多余按钮"的诉求。
|
||||
|
||||
## 四、注意事项
|
||||
|
||||
- **删除文件必须先获得用户书面同意**(项目 AGENTS.md 明确"未经书面同意不得删除任何文件");用户本意是"没必要保留入口 A",可据此向用户确认后执行。
|
||||
- 移除入口 A 后,设置页(`SettingsView.vue`)成为唯一入口,属预期结果,保留即可。
|
||||
- `useDarkMode` 的 localStorage 持久化、storage 跨标签同步、Dark Reader 引擎均与入口 A 无关,不受影响。
|
||||
- 版本号建议随本次改动递增(如 1.0.6),并同步 `plugin.yaml`(如需)与 `@since`。
|
||||
@@ -0,0 +1,175 @@
|
||||
# 改进交接单:设置页官方化改造(2026-08-08)
|
||||
|
||||
> 审查窗口调研产出,**交付开发窗口执行**;审查窗口不修改代码。
|
||||
> 调研来源:Halo 官方仓库 `halo-dev/halo`(commit `815292f`)+ 本地代码 + 线上 `blog.liuhangyv.top/console` 实证(已登录)。
|
||||
> 本文档为**唯一交付文件**(已合并此前两份调研文档)。
|
||||
|
||||
## 〇、需求与结论总览
|
||||
|
||||
| # | 需求 | 结论 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 侧边栏「深色模式」入口状态反馈 | **不做改动**(依据见第一节) |
|
||||
| 2 | 设置页顶部缺官方风格标题栏 | **要改**:使用官方 `VPageHeader`(第二节) |
|
||||
| 3 | 尽量用官方组件实现 | 组件全景与复用方案见第三、四节 |
|
||||
|
||||
## 一、侧边栏「深色模式」入口状态反馈(结论:保持现状,不动)
|
||||
|
||||
### 1.1 现象
|
||||
|
||||
点击「深色模式」后界面切换为深色,但侧边栏「深色模式」入口(菜单项)文案没有随之变化;用户曾疑问是否应显示"白天模式 / 深色模式 / 跟随系统模式"。
|
||||
|
||||
### 1.2 调研结论(证据)
|
||||
|
||||
1. **Halo 菜单是静态导航**:`use-route-menu-generator.ts` 一次性从 `router.getRoutes()` 按 `meta.menu` 生成菜单,无业务状态联动。
|
||||
2. **菜单名是静态字符串**:`RoutesMenu.tsx` 中 `title={t(item.name, item.name)}`,官方机制不支持动态文案;所有官方/插件菜单项均为固定名称。
|
||||
3. **激活高亮已生效**:`active={route.matched.includes(item.path)}`;线上实证进入 `/console/dark-mode-settings` 后,菜单项 `class="active menu-item-title"`。
|
||||
4. **平台惯例**:窗口内改设置、侧边栏入口不变化,是 Halo 全站一致行为(菜单是入口,不是状态开关)。
|
||||
|
||||
### 1.3 结论
|
||||
|
||||
- 动态菜单文案不可直接实现(需 DOM hack,不推荐)。
|
||||
- 保持静态「深色模式」菜单名,激活高亮现状即符合平台标准。
|
||||
- 当前模式信息继续由设置页内「当前生效」展示(第四节中会优化展示形式)。
|
||||
|
||||
## 二、设置页顶部标题栏(需改造)
|
||||
|
||||
### 2.1 现象
|
||||
|
||||
`/console/dark-mode-settings` 顶部为自定义 `h1 + p`;官方页面(`/console/theme`、`/console/plugins`)顶部有统一白底标题栏。线上实证:设置页 `document.querySelectorAll('.page-header').length === 0`。
|
||||
|
||||
### 2.2 官方 `VPageHeader` 组件(`@halo-dev/components` 已导出)
|
||||
|
||||
结构(源码 `ui/packages/components/src/components/header/PageHeader.vue`):
|
||||
|
||||
```html
|
||||
<div class="page-header">
|
||||
<h2 class="page-header__title">
|
||||
<slot name="icon" />
|
||||
<span class="page-header__title-text">{{ title }}</span>
|
||||
</h2>
|
||||
<div class="page-header__actions"><slot name="actions" /></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
官方用法(`PluginList.vue`):
|
||||
|
||||
```html
|
||||
<VPageHeader :title="…">
|
||||
<template #icon><IconPlug /></template>
|
||||
<template #actions>…按钮…</template>
|
||||
</VPageHeader>
|
||||
<div class="m-0 md:m-4"><VCard>…</VCard></div>
|
||||
```
|
||||
|
||||
## 三、官方组件库全景(21 类)与插件对照
|
||||
|
||||
### 3.1 组件清单
|
||||
|
||||
| 目录 | 导出名 | 用途 | 设置页可用 |
|
||||
| --- | --- | --- | --- |
|
||||
| header | `VPageHeader` | 页面标题栏 | ✅ |
|
||||
| card | `VCard`(title / bodyClass;slot header) | 内容卡片 | ✅ |
|
||||
| description | `VDescription` / `VDescriptionItem`(label / content / verticalCenter) | 键值展示 | ✅ |
|
||||
| tag | `VTag`(theme / rounded;slot leftIcon) | 标签/徽标 | ✅ |
|
||||
| status | `VStatusDot`(state / text / animate) | 状态点 | ✅ |
|
||||
| button | `VButton`(type / size / block / ghost / loading / route;slot icon) | 按钮 | ✅ |
|
||||
| space | `VSpace` | 间距布局 | ✅ |
|
||||
| alert | `VAlert` | 提示条 | 可选 |
|
||||
| switch | `VSwitch` | 开关 | 未来扩展 |
|
||||
| menu | `VMenu` / `VMenuItem` / `VMenuLabel` | 菜单 | — |
|
||||
| entity | `VEntity` / `VEntityContainer` / `VEntityField` | 列表实体 | — |
|
||||
| modal / dialog | `VModal` / `VDialog` | 模态/确认 | — |
|
||||
| dropdown | `VDropdownItem` / `VDropdownDivider` | 下拉 | — |
|
||||
| tabs | `VTabs` / `VTabItem` / `VTabbar` | 标签页 | — |
|
||||
| pagination / empty / loading | `VPagination` / `VEmpty` / `VLoading` | 分页/空态/加载 | — |
|
||||
| avatar | `VAvatar` / `VAvatarGroup` | 头像 | — |
|
||||
| toast / tooltip | `toast` 函数 / `vTooltip` 指令(非 V 组件) | 提示 | 可选 |
|
||||
|
||||
> **注意:官方组件库没有 radio / radio group 单选组件** —— 三个模式选项保持语义化 `<button>` 是合理方案。
|
||||
|
||||
### 3.2 插件现状 vs 官方组件
|
||||
|
||||
| 插件现状 | 建议 |
|
||||
| --- | --- |
|
||||
| 标题:自定义 `.dark-mode-settings__header`(h1+p) | **`VPageHeader`** |
|
||||
| 卡片:`.dark-mode-settings__card` | **`VCard`** |
|
||||
| 「当前生效」:自定义 div + strong | **`VDescription` + `VDescriptionItem`**,模式值用 **`VTag`** / `VStatusDot` |
|
||||
| 三个模式选项:自定义 `<button>` + `is-active` | 保持 `<button>`(无官方单选组件) |
|
||||
| 图标:`IconPalette` / ri 图标 | 已是官方体系 ✅ |
|
||||
|
||||
## 四、设置页改造方案(用官方组件实现)
|
||||
|
||||
`ui/src/views/SettingsView.vue` 重构骨架:
|
||||
|
||||
```html
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
IconPalette,
|
||||
VCard,
|
||||
VDescription,
|
||||
VDescriptionItem,
|
||||
VPageHeader,
|
||||
VTag,
|
||||
} from '@halo-dev/components'
|
||||
// …现有 useDarkMode / currentEffectiveMode / modeOptions 逻辑保留…
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VPageHeader title="深色模式设置">
|
||||
<template #icon><IconPalette /></template>
|
||||
<!-- 可选:<template #actions><VButton size="sm" @click="…">…</VButton></template> -->
|
||||
</VPageHeader>
|
||||
|
||||
<div class="m-0 md:m-4">
|
||||
<VCard :body-class="['!p-0']">
|
||||
<div class="p-4">
|
||||
<VDescription>
|
||||
<VDescriptionItem label="当前生效">
|
||||
<VTag>{{ currentEffectiveMode }}</VTag>
|
||||
</VDescriptionItem>
|
||||
</VDescription>
|
||||
|
||||
<div class="dark-mode-settings__options">
|
||||
<!-- 三个模式按钮保持现有实现(含 is-active 高亮) -->
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
要点:
|
||||
- 外层 `m-0 md:m-4` + `VCard` 是官方列表页通行布局(同 `PluginList.vue`)。
|
||||
- 顶部标题栏白底在深色模式下由 Dark Reader 自动转换,无需额外适配。
|
||||
- 三个模式选项维持 `<button>`(官方无单选组件),保留 `is-active` 选中态。
|
||||
- 样式清理:移除 `.dark-mode-settings__header` / `__title` / `__desc` 相关 CSS;卡片/选项样式按需精简。
|
||||
|
||||
## 五、涉及文件与验证
|
||||
|
||||
| 文件 | 改动 |
|
||||
| --- | --- |
|
||||
| `ui/src/views/SettingsView.vue` | 用 `VPageHeader` / `VCard` / `VDescription` / `VTag` 重构 |
|
||||
| `ui/src/styles/variables.css` | 可精简(自定义 header/card 样式移除后) |
|
||||
| `README.md` / `CLAUDE.md` / `AGENTS.md` | 可选:同步设置页结构描述 |
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
pnpm build && pnpm type-check && pnpm prettier && pnpm lint && pnpm test:unit
|
||||
cd .. && gradlew build
|
||||
```
|
||||
|
||||
部署后线上确认:
|
||||
- `/console/dark-mode-settings` 顶部出现与官方一致的 `.page-header` 标题栏
|
||||
- 侧边栏「深色模式」菜单项进入页面后保持高亮(现状回归确认,未改动)
|
||||
- 深色模式下标题栏/卡片与整体协调
|
||||
|
||||
## 六、注意事项
|
||||
|
||||
- 问题 1(侧边栏入口)**明确不做改动**。
|
||||
- 官方组件无单选组件,三选项保持 `<button>`,勿强行套用不存在的组件。
|
||||
- 官方组件样式依赖 Halo Console 的 UnoCSS/主题变量,插件内直接使用即可。
|
||||
- i18n(可选):插件文案目前硬编码中文,未来可接入 Halo `locales` 国际化,非本次必做。
|
||||
@@ -0,0 +1,70 @@
|
||||
# 复查交接单 — halo-dark-mode-plugin(2026-08-08 第二版)
|
||||
|
||||
> 由审查窗口对开发窗口修复结果(`d95d172 fix: 按审查报告修复 P2/P3 问题并升级 1.0.5`、`b6403b7 docs: 按 Halo 官方插件 README 风格重写`)进行复查。
|
||||
> 初审交接单见 `docs/review-2026-08-08.md`(本文件为其复查结果,独立文档)。
|
||||
> 复查基线:`HEAD = d95d172`,工作区有未提交改动(见"遗留问题")。
|
||||
|
||||
## 一、总体结论
|
||||
|
||||
**P2/P3 绝大部分已修复,构建链全绿,可收尾。** 无新增阻断问题。剩余 3 个遗留项均为"收尾/决策"性质。
|
||||
|
||||
## 二、逐项复查结果(对照初审交接单)
|
||||
|
||||
| 清单项 | 状态 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| P2-1 FOUC | ✅ | `useDarkMode.applyHtmlAttribute` 同步设置 `color-scheme`;`darkreader-engine.ts` 与 README 均注明 DR 异步注入、闪白无法完全消除 |
|
||||
| P2-2 文档 | ✅ | `AGENTS.md` / `CLAUDE.md` 已无旧策略残留(halo-core/overrides/Monaco/Tailwind 零命中);`scripts/README.md` 重写;README 按 Halo 官方风格重写 |
|
||||
| P2-3 工具重复 | ⚠️ 部分 | 已删 `workplace/login_wait.py`、`workplace/scan_dark.py`;`fetch_bundle.py`、`probe_conflict.py`、`aggregate.py` 仍在(见遗留-3) |
|
||||
| P2-4 前端单测 | ✅ | 新增 `ui/src/composables/__tests__/useDarkMode.spec.ts` 8 个用例,实测 8/8 通过 |
|
||||
| P2-5 多标签同步 | ✅ | `storage` 事件监听已实现,并有 2 个测试覆盖(合法值同步 / 非法值忽略) |
|
||||
| P2-6 可访问性 | ✅ | `ThemeToggle` 改 `<button>` + `aria-pressed` + `:focus-visible`;设置页改 `radiogroup` + `role=radio` + `aria-checked` |
|
||||
| P3-1 后端日志 | ✅ | `@Slf4j` + `log.info`,替换 `System.out.println` |
|
||||
| P3-2 vendored package.json | ✅ | 裁剪为 name/version/description/main/module/types/license 最小字段 |
|
||||
| P3-3 完整性校验 | ✅ | `third-party/darkreader/SHA256SUMS` 新增,5 个文件哈希**全部实测匹配** |
|
||||
| P3-4 遗留属性注释 | ✅ | `variables.css`、`useDarkMode.ts` 已注明 `data-halo-theme` 为兼容性遗留 |
|
||||
| P3-5 i18n | ➖ 未处理 | 初审标注低优先级可暂缓,符合预期 |
|
||||
| P3-6 plugin.yaml | ✅ | `spec.enabled: true → false`(合理默认) |
|
||||
| 版本号 | ✅ | `gradle.properties` 1.0.5 + `DarkModePlugin @since 1.0.5` 同步 |
|
||||
|
||||
## 三、审查窗口实测验证(开发窗口无需重复)
|
||||
|
||||
- `vitest run`:8/8 通过
|
||||
- `vue-tsc --build`:通过
|
||||
- `vite build`:通过(main.js 110.40KB / gzip 37.96KB,style.css 4.11KB)
|
||||
- `prettier --check src`:通过(审查窗口已代跑 `prettier --write` 修复,见遗留-1)
|
||||
- `gradlew test`:BUILD SUCCESSFUL(含 `:ui:assemble` + `:processUiResources` 全链路;沙箱外联网执行)
|
||||
|
||||
## 四、遗留问题(待开发窗口收尾)
|
||||
|
||||
### 遗留-1:prettier 格式化已修,改动未提交
|
||||
- `d95d172` 中 5 个文件缺末尾换行、`variables.css` 列对齐不规范
|
||||
- 审查窗口已执行 `prettier --write src`(纯格式化,无逻辑变化),涉及文件:
|
||||
- `ui/src/components/ThemeToggle.vue`
|
||||
- `ui/src/composables/__tests__/useDarkMode.spec.ts`
|
||||
- `ui/src/composables/useDarkMode.ts`
|
||||
- `ui/src/styles/index.css`
|
||||
- `ui/src/styles/variables.css`
|
||||
- `ui/src/views/SettingsView.vue`
|
||||
- 当前这些改动在工作区**未提交**,需开发窗口提交
|
||||
|
||||
### 遗留-2:logo 改动未提交(需用户决策)
|
||||
- `src/main/resources/logo.png`:被替换(35KB → 65KB)但未提交
|
||||
- `src/main/resources/原版.png`:新增未跟踪文件(旧 logo 备份)
|
||||
- 待决策:新 logo 是否随 1.0.5 入库?`原版.png` 删除 / 保留 / gitignore?
|
||||
|
||||
### 遗留-3:workplace/ 旧工具未清完(需用户确认删除)
|
||||
- 仍保留:`fetch_bundle.py`、`probe_conflict.py`、`aggregate.py`
|
||||
- 均为旧手工 CSS 工作流配套(抓线上 bundle / 探测覆盖冲突 / 聚合扫描结果),`scan_dark.py` 删除后基本失效
|
||||
- 建议连同 `deployed-bundle.css`、`aggregate.txt`、`scan-results.json` 等产物一并清理收口到 `scripts/`
|
||||
|
||||
## 五、可选改进(非必须)
|
||||
|
||||
- `SettingsView.vue` radiogroup:各按钮可 Tab 聚焦,但未实现 ArrowUp/ArrowDown 方向键切换焦点(语义上 radiogroup 通常只保留一个 tab stop);不影响当前可用性
|
||||
|
||||
## 六、收尾动作清单
|
||||
|
||||
1. 提交 prettier 格式化改动(遗留-1)
|
||||
2. 与用户确认 logo / 原版.png 处置(遗留-2)
|
||||
3. 与用户确认 workplace/ 剩余工具删除(遗留-3)
|
||||
4. 可选:radiogroup 方向键导航(五)
|
||||
5. 可选:推送后跑一次 `scripts/verify-toggle.py` 做真实环境回归(部署 1.0.5 后)
|
||||
@@ -0,0 +1,70 @@
|
||||
# 修改交接单:移除设置页键盘操作支持(2026-08-08)
|
||||
|
||||
> 审查窗口产出,**交付开发窗口执行**;审查窗口不修改代码。
|
||||
> 本文档为**唯一交接文件**(已合并 `recheck-entry-2026-08-08.md` 的内容)。
|
||||
> 用户决定:设置页的键盘操作支持"没必要",要求移除。
|
||||
|
||||
## 零、背景:1.0.6 已落实项(复查结论,无需重复处理)
|
||||
|
||||
提交 `394613f fix: 深色模式入口移至外观分组并移除侧边栏按钮(1.0.6)` 已复查通过:
|
||||
|
||||
- 菜单移入「外观」分组(`ui/src/index.ts` `group: 'interface'`),线上已生效
|
||||
- 侧边栏注入按钮已移除(`injector.ts`、`ThemeToggle.vue` 已删除),线上 `.theme-toggle` 数量 = 0
|
||||
- `verify-toggle.py` 已改为 localStorage + `StorageEvent` 驱动 + color-scheme 断言
|
||||
- 文档已同步、版本号 1.0.6
|
||||
- 构建验证:vitest 8/8 · vue-tsc · prettier · vite build · `gradlew build` 全绿
|
||||
|
||||
唯一遗留的 R-1(README 过时描述)由本文档第四节第 4 点一并处理。
|
||||
|
||||
## 一、需求描述
|
||||
|
||||
移除 `SettingsView.vue`(深色模式设置页)中为"键盘操作"特意实现的逻辑:
|
||||
方向键切换选项、roving tabindex、ARIA 单选组语义、焦点环等。保留最简交互:点击三个选项按钮切换主题模式。
|
||||
|
||||
## 二、决策依据(用户补充)
|
||||
|
||||
- Halo 后台的官方页面与第三方插件页面均**没有方向键特殊交互的惯例**:按方向键上下键只用于页面滚动/翻页,没有其他特殊功能。
|
||||
- 设置页单独实现"方向键切换主题选项"(radiogroup 键盘导航)与平台整体交互习惯不一致,用户群体也不会预期方向键会改变主题选择,因此判定该键盘操作支持**不必要**,予以移除。
|
||||
- 保留原生 `<button>` 即可:点击可用,浏览器内置的 Tab 聚焦 + Enter/空格 激活属基础可用性,非刻意实现,无需额外维护。
|
||||
|
||||
## 三、现状代码体现(`ui/src/views/SettingsView.vue`)
|
||||
|
||||
| 行号 | 内容 | 作用 |
|
||||
| --- | --- | --- |
|
||||
| L10 | `import { computed, nextTick, ref } from 'vue'` | `nextTick`/`ref` 仅键盘导航使用 |
|
||||
| L27 | `const optionEls = ref<HTMLButtonElement[]>([])` | 选项按钮 DOM 引用数组 |
|
||||
| L29-L35 | `activeIndex` computed | 当前选中索引 |
|
||||
| L37-L39 | `setOptionRef(el, index)` | 收集按钮 ref |
|
||||
| L41-L45 | `focusOption(index)` | 方向键切换选中并聚焦 |
|
||||
| L47-L61 | `onKeydown(event)` | ArrowUp/Down/Left/Right/Home/End 处理 |
|
||||
| L81-L84 | `role="radiogroup"` `aria-label` `@keydown="onKeydown"` | 单选组容器语义 |
|
||||
| L88-L104 | button 上的 `role="radio"` / `:aria-checked` / `:tabindex`(roving)/ `:ref` / `@click` | 单选按钮语义与焦点管理 |
|
||||
| L162-L165 | `.dark-mode-settings__option:focus-visible` | 键盘焦点环(可选移除) |
|
||||
|
||||
连带:`README.md` L141 `- 侧边栏切换按钮与设置选项支持键盘操作`(R-1,侧边栏按钮已删,"键盘操作"整体废弃)。
|
||||
|
||||
## 四、修改建议
|
||||
|
||||
### 方案 A(推荐):移除键盘导航实现,保留简单按钮
|
||||
|
||||
`ui/src/views/SettingsView.vue`:
|
||||
1. **script**:
|
||||
- 删除 L27 `optionEls`、L29-L35 `activeIndex`、L37-L39 `setOptionRef`、L41-L45 `focusOption`、L47-L61 `onKeydown`
|
||||
- L10 import 精简为 `import { computed } from 'vue'`(`nextTick`、`ref` 不再使用)
|
||||
2. **template**:
|
||||
- L81-L84 删除 `role="radiogroup"`、`aria-label="主题模式"`、`@keydown="onKeydown"`
|
||||
- L88-L104 按钮上删除 `role="radio"`、`:aria-checked`、`:tabindex`、`:ref`,仅保留 `type="button"`、`:class="{ 'is-active': ... }"`、`@click="setTheme(option.value)"`(`v-for` 中 `index` 不再需要,可去掉)
|
||||
3. **style**(可选):删除 L162-L165 `:focus-visible` 规则;也可保留(无害,仅鼠标点击时不会触发)
|
||||
4. **README.md**:删除 L141 整行(R-1 一并处理)
|
||||
5. 构建回归:`cd ui && pnpm build`、`pnpm type-check`、`pnpm prettier`、`pnpm lint`、`pnpm test:unit`;根目录 `gradlew build`
|
||||
|
||||
### 方案 B(备选,不推荐):彻底无键盘语义
|
||||
|
||||
- 三个选项改回 `<div>` + `@click`(删除 button 全部键盘/语义属性)
|
||||
- 需在样式补 `cursor: pointer`;可访问性退化,仅当明确要求时使用
|
||||
|
||||
## 五、注意
|
||||
|
||||
- 删除逻辑不涉及 `useDarkMode` / `darkreader-engine`,状态管理与主题切换不受影响。
|
||||
- vitest 仅覆盖 `useDarkMode`,不受本次改动影响。
|
||||
- 本次为前端 UI 变更,需重新构建部署后验证。
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 34 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.java.home=D:/settings/Language/Java/jdk-25.0.1
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+5
-3
@@ -42,13 +42,15 @@ D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\scan-dark.py --mode
|
||||
D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\verify-toggle.py
|
||||
```
|
||||
|
||||
部署新版插件后运行,会检查:
|
||||
部署新版插件后运行,会直接驱动 localStorage 中的主题状态,并检查:
|
||||
|
||||
- `data-halo-theme` 属性翻转
|
||||
- localStorage 持久化翻转
|
||||
- `data-darkreader-mode`(Dark Reader 注入状态)翻转
|
||||
- `color-scheme` 翻转
|
||||
- 侧边栏已不再注入 `.theme-toggle` / `.plugin-dark-mode-toggle` 按钮
|
||||
|
||||
三项全部 PASS 时返回 0。
|
||||
状态翻转全部 PASS 且按钮已移除时返回 0。
|
||||
|
||||
## 输出
|
||||
|
||||
@@ -62,4 +64,4 @@ D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\verify-toggle.py
|
||||
元素误判为界面残留。
|
||||
- 路由从 Vue Router 动态发现,Halo 或插件升级新增页面后无需维护清单。
|
||||
- `scan-dark.py` 主要为历史手工 CSS 工作流保留;当前暗色转换由 Dark Reader 负责,
|
||||
运行时行为验证以 `verify-toggle.py` 为准。
|
||||
运行时行为验证以 `verify-toggle.py` 为准。
|
||||
|
||||
@@ -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 pathlib
|
||||
import sys
|
||||
@@ -8,6 +8,7 @@ from playwright.sync_api import sync_playwright
|
||||
|
||||
DEFAULT_PROFILE = pathlib.Path(__file__).parent / ".browser-profile"
|
||||
BASE = "https://blog.liuhangyv.top"
|
||||
STORAGE_KEY = "halo-dark-mode-theme"
|
||||
|
||||
|
||||
def read_state(page) -> dict:
|
||||
@@ -16,10 +17,40 @@ def read_state(page) -> dict:
|
||||
attr: document.documentElement.getAttribute('data-halo-theme'),
|
||||
darkreader: document.documentElement.getAttribute('data-darkreader-mode'),
|
||||
stored: localStorage.getItem('halo-dark-mode-theme'),
|
||||
colorScheme: document.documentElement.style.colorScheme,
|
||||
})"""
|
||||
)
|
||||
|
||||
|
||||
def set_mode(page, mode: str) -> None:
|
||||
"""写入持久化偏好并派发 storage 事件,让当前页面 useDarkMode 单例即时响应。"""
|
||||
page.evaluate(
|
||||
"""([key, mode]) => {
|
||||
localStorage.setItem(key, mode)
|
||||
window.dispatchEvent(
|
||||
new StorageEvent('storage', { key: key, newValue: mode })
|
||||
)
|
||||
}""",
|
||||
[STORAGE_KEY, mode],
|
||||
)
|
||||
|
||||
|
||||
def wait_mode(page, expect_dark: bool) -> None:
|
||||
page.wait_for_function(
|
||||
"""(expectDark) => {
|
||||
const attr = document.documentElement.getAttribute('data-halo-theme')
|
||||
const darkreader = document.documentElement.getAttribute('data-darkreader-mode')
|
||||
const scheme = document.documentElement.style.colorScheme
|
||||
if (expectDark) {
|
||||
return attr === 'dark' && darkreader === 'dynamic' && scheme === 'dark'
|
||||
}
|
||||
return attr === null && darkreader === null && scheme !== 'dark'
|
||||
}""",
|
||||
arg=expect_dark,
|
||||
timeout=15000,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="验证主题切换运行时行为")
|
||||
parser.add_argument("--base", default=BASE)
|
||||
@@ -45,33 +76,54 @@ def main() -> int:
|
||||
ctx.close()
|
||||
return 3
|
||||
|
||||
toggle = page.locator(".theme-toggle")
|
||||
toggle.wait_for(state="visible", timeout=15000)
|
||||
toggle_absent = page.locator(".theme-toggle, .plugin-dark-mode-toggle").count() == 0
|
||||
print(f"侧边栏注入按钮已移除: {'PASS' if toggle_absent else 'FAIL'}")
|
||||
|
||||
# 先归一化到浅色,保证后续翻转判定与当前会话状态无关
|
||||
set_mode(page, "light")
|
||||
wait_mode(page, expect_dark=False)
|
||||
before = read_state(page)
|
||||
toggle.click()
|
||||
time.sleep(1)
|
||||
|
||||
set_mode(page, "dark")
|
||||
wait_mode(page, expect_dark=True)
|
||||
after = read_state(page)
|
||||
toggle.click()
|
||||
time.sleep(1)
|
||||
|
||||
set_mode(page, "light")
|
||||
wait_mode(page, expect_dark=False)
|
||||
restored = read_state(page)
|
||||
|
||||
attr_flipped = before["attr"] != after["attr"] and before["attr"] == restored["attr"]
|
||||
stored_flipped = before["stored"] != after["stored"] and before["stored"] == restored["stored"]
|
||||
stored_flipped = (
|
||||
before["stored"] != after["stored"] and before["stored"] == restored["stored"]
|
||||
)
|
||||
darkreader_flipped = (
|
||||
before["darkreader"] != after["darkreader"]
|
||||
and before["darkreader"] == restored["darkreader"]
|
||||
)
|
||||
scheme_flipped = (
|
||||
before["colorScheme"] != after["colorScheme"]
|
||||
and before["colorScheme"] == restored["colorScheme"]
|
||||
)
|
||||
print(f"初始: {before}")
|
||||
print(f"切换: {after}")
|
||||
print(f"还原: {restored}")
|
||||
print(
|
||||
f"属性翻转: {'PASS' if attr_flipped else 'FAIL'} "
|
||||
f"存储翻转: {'PASS' if stored_flipped else 'FAIL'} "
|
||||
f"Dark Reader 翻转: {'PASS' if darkreader_flipped else 'FAIL'}"
|
||||
f"Dark Reader 翻转: {'PASS' if darkreader_flipped else 'FAIL'} "
|
||||
f"color-scheme 翻转: {'PASS' if scheme_flipped else 'FAIL'}"
|
||||
)
|
||||
|
||||
ctx.close()
|
||||
return 0 if attr_flipped and stored_flipped and darkreader_flipped else 1
|
||||
return (
|
||||
0
|
||||
if toggle_absent
|
||||
and attr_flipped
|
||||
and stored_flipped
|
||||
and darkreader_flipped
|
||||
and scheme_flipped
|
||||
else 1
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -11,7 +11,7 @@ import run.halo.app.plugin.PluginContext;
|
||||
* <p>Only one main class extending {@link BasePlugin} is allowed per plugin.</p>
|
||||
*
|
||||
* @author LHY
|
||||
* @since 1.0.5
|
||||
* @since 1.0.8
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 64 KiB |
@@ -10,13 +10,13 @@ spec:
|
||||
requires: ">=2.25.0"
|
||||
author:
|
||||
name: LHY
|
||||
website: https://github.com/LHY
|
||||
website: https://github.com/LHY0125
|
||||
logo: logo.png
|
||||
homepage: https://github.com/LHY/dark-mode#readme
|
||||
repo: https://github.com/LHY/dark-mode
|
||||
issues: https://github.com/LHY/dark-mode/issues
|
||||
homepage: https://github.com/LHY0125/halo-dark-mode-plugin#readme
|
||||
repo: https://github.com/LHY0125/halo-dark-mode-plugin
|
||||
issues: https://github.com/LHY0125/halo-dark-mode-plugin/issues
|
||||
displayName: "深色模式"
|
||||
description: "为 Halo 后台管理面板提供深色/浅色模式切换,支持跟随系统、手动切换和偏好记忆"
|
||||
license:
|
||||
- name: "GPL-3.0"
|
||||
url: "https://github.com/LHY/dark-mode/blob/main/LICENSE"
|
||||
url: "https://github.com/LHY0125/halo-dark-mode-plugin/blob/main/LICENSE"
|
||||
|
||||
@@ -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>
|
||||
@@ -103,4 +103,4 @@ describe('useDarkMode', () => {
|
||||
)
|
||||
expect(theme.value).toBe('light')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -98,4 +98,4 @@ export function useDarkMode() {
|
||||
setTheme,
|
||||
toggle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-4
@@ -3,10 +3,7 @@ import { IconPalette } from '@halo-dev/components'
|
||||
import { markRaw } from 'vue'
|
||||
import './styles/index.css'
|
||||
import { initDarkReaderEngine } from './darkreader-engine'
|
||||
import { injectThemeToggle } from './injector'
|
||||
|
||||
// 在插件加载后将切换器注入到侧边栏,暗色转换完全交给 Dark Reader
|
||||
injectThemeToggle()
|
||||
initDarkReaderEngine()
|
||||
|
||||
export default definePlugin({
|
||||
@@ -23,7 +20,8 @@ export default definePlugin({
|
||||
searchable: true,
|
||||
menu: {
|
||||
name: '深色模式',
|
||||
group: '偏好设置',
|
||||
// Halo 官方「外观」分组
|
||||
group: 'interface',
|
||||
icon: markRaw(IconPalette),
|
||||
priority: 50,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -3,4 +3,4 @@
|
||||
仅保留插件自身 UI 变量;页面暗色转换完全交给 Dark Reader。
|
||||
============================================================ */
|
||||
|
||||
@import './variables.css';
|
||||
@import './variables.css';
|
||||
|
||||
@@ -6,56 +6,14 @@
|
||||
/* 说明:data-halo-theme 为兼容性遗留标记,当前已无 CSS 消费方,保留给外部脚本与验证工具。 */
|
||||
/* ===== 浅色模式(Halo 默认,此处定义为显式回退) ===== */
|
||||
:root {
|
||||
--halo-bg-body: oklch(97% 0.005 250);
|
||||
--halo-bg-sidebar: oklch(100% 0 0);
|
||||
--halo-bg-content: oklch(97% 0.005 250);
|
||||
--halo-bg-card: oklch(100% 0 0);
|
||||
--halo-bg-input: oklch(100% 0 0);
|
||||
--halo-bg-hover: oklch(95% 0.01 250);
|
||||
--halo-bg-active: oklch(90% 0.02 160);
|
||||
--halo-bg-disabled: oklch(95% 0.005 250);
|
||||
--halo-bg-tooltip: oklch(20% 0.01 250);
|
||||
--halo-bg-modal: oklch(0% 0 0 / 60%);
|
||||
--halo-bg-dropdown: oklch(100% 0 0);
|
||||
--halo-text-primary: oklch(20% 0.01 250);
|
||||
--halo-text-tertiary: oklch(60% 0.01 250);
|
||||
|
||||
--halo-text-primary: oklch(20% 0.01 250);
|
||||
--halo-text-secondary: oklch(45% 0.01 250);
|
||||
--halo-text-tertiary: oklch(60% 0.01 250);
|
||||
--halo-text-link: oklch(45% 0.15 160);
|
||||
--halo-text-inverse: oklch(100% 0 0);
|
||||
--halo-bg-hover: oklch(95% 0.01 250);
|
||||
|
||||
--halo-border-base: oklch(88% 0.01 250);
|
||||
--halo-border-light: oklch(93% 0.005 250);
|
||||
--halo-border-input: oklch(80% 0.01 250);
|
||||
--halo-border-focus: oklch(55% 0.15 160);
|
||||
--halo-border-base: oklch(88% 0.01 250);
|
||||
|
||||
--halo-accent-primary: oklch(55% 0.14 160);
|
||||
--halo-accent-primary-hover: oklch(48% 0.15 160);
|
||||
--halo-accent-primary-text: oklch(100% 0 0);
|
||||
--halo-accent-danger: oklch(45% 0.18 25);
|
||||
--halo-accent-danger-hover: oklch(40% 0.19 25);
|
||||
--halo-accent-success: oklch(50% 0.16 150);
|
||||
--halo-accent-warning: oklch(60% 0.16 85);
|
||||
--halo-accent-primary: oklch(55% 0.14 160);
|
||||
|
||||
--halo-shadow-sm: 0 1px 2px oklch(0% 0 0 / 6%);
|
||||
--halo-shadow-base: 0 2px 8px oklch(0% 0 0 / 10%);
|
||||
--halo-shadow-lg: 0 4px 16px oklch(0% 0 0 / 14%);
|
||||
|
||||
--halo-scrollbar-thumb: oklch(80% 0.01 250);
|
||||
--halo-scrollbar-track: oklch(95% 0.005 250);
|
||||
|
||||
--halo-search-bg: oklch(95% 0.01 250);
|
||||
--halo-search-text: oklch(45% 0.01 250);
|
||||
--halo-search-placeholder: oklch(60% 0.01 250);
|
||||
|
||||
--halo-menu-item-hover: oklch(93% 0.02 160);
|
||||
--halo-menu-item-active: oklch(88% 0.03 160);
|
||||
--halo-menu-group-title: oklch(55% 0.01 250);
|
||||
|
||||
--halo-table-header-bg: oklch(96% 0.005 250);
|
||||
--halo-table-row-hover: oklch(93% 0.015 250);
|
||||
--halo-table-border: oklch(88% 0.01 250);
|
||||
|
||||
--halo-tag-bg: oklch(90% 0.04 160);
|
||||
--halo-tag-text: oklch(40% 0.1 160);
|
||||
--halo-menu-item-active: oklch(88% 0.03 160);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { ThemeMode } from '../composables/useDarkMode'
|
||||
import { useDarkMode } from '../composables/useDarkMode'
|
||||
import {
|
||||
IconPalette,
|
||||
VCard,
|
||||
VDescription,
|
||||
VDescriptionItem,
|
||||
VPageHeader,
|
||||
VTag,
|
||||
} from '@halo-dev/components'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { theme, isDark, setTheme } = useDarkMode()
|
||||
@@ -20,85 +28,47 @@ const modeOptions: { value: ThemeMode; label: string; description: string }[] =
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dark-mode-settings">
|
||||
<div class="dark-mode-settings__header">
|
||||
<h1 class="dark-mode-settings__title">深色模式设置</h1>
|
||||
<p class="dark-mode-settings__desc">选择后台管理面板的显示模式</p>
|
||||
</div>
|
||||
<div>
|
||||
<VPageHeader title="深色模式设置">
|
||||
<template #icon>
|
||||
<IconPalette />
|
||||
</template>
|
||||
</VPageHeader>
|
||||
|
||||
<div class="dark-mode-settings__card">
|
||||
<div class="dark-mode-settings__current">
|
||||
当前生效:<strong>{{ currentEffectiveMode }}</strong>
|
||||
</div>
|
||||
<div class="m-0 md:m-4">
|
||||
<VCard :body-class="['!p-0']">
|
||||
<div class="p-4">
|
||||
<VDescription>
|
||||
<VDescriptionItem label="当前生效">
|
||||
<VTag>{{ currentEffectiveMode }}</VTag>
|
||||
</VDescriptionItem>
|
||||
</VDescription>
|
||||
|
||||
<div
|
||||
class="dark-mode-settings__options"
|
||||
role="radiogroup"
|
||||
aria-label="主题模式"
|
||||
>
|
||||
<button
|
||||
v-for="option in modeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
role="radio"
|
||||
class="dark-mode-settings__option"
|
||||
:class="{ 'is-active': theme === option.value }"
|
||||
:aria-checked="theme === option.value"
|
||||
@click="setTheme(option.value)"
|
||||
>
|
||||
<div class="dark-mode-settings__option-label">{{ option.label }}</div>
|
||||
<div class="dark-mode-settings__option-desc">{{ option.description }}</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="dark-mode-settings__options">
|
||||
<button
|
||||
v-for="option in modeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="dark-mode-settings__option"
|
||||
:class="{ 'is-active': theme === option.value }"
|
||||
@click="setTheme(option.value)"
|
||||
>
|
||||
<div class="dark-mode-settings__option-label">{{ option.label }}</div>
|
||||
<div class="dark-mode-settings__option-desc">{{ option.description }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dark-mode-settings {
|
||||
max-width: 640px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.dark-mode-settings__header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.dark-mode-settings__title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--halo-text-primary);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.dark-mode-settings__desc {
|
||||
font-size: 0.875rem;
|
||||
color: var(--halo-text-secondary);
|
||||
}
|
||||
|
||||
.dark-mode-settings__card {
|
||||
padding: 1.25rem;
|
||||
background-color: var(--halo-bg-card);
|
||||
border: 1px solid var(--halo-border-base);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.dark-mode-settings__current {
|
||||
font-size: 0.875rem;
|
||||
color: var(--halo-text-secondary);
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--halo-border-light);
|
||||
}
|
||||
|
||||
.dark-mode-settings__current strong {
|
||||
color: var(--halo-accent-primary);
|
||||
}
|
||||
|
||||
.dark-mode-settings__options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.dark-mode-settings__option {
|
||||
@@ -111,18 +81,15 @@ const modeOptions: { value: ThemeMode; label: string; description: string }[] =
|
||||
text-align: left;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s, border-color 0.15s;
|
||||
transition:
|
||||
background-color 0.15s,
|
||||
border-color 0.15s;
|
||||
}
|
||||
|
||||
.dark-mode-settings__option:hover {
|
||||
background-color: var(--halo-bg-hover);
|
||||
}
|
||||
|
||||
.dark-mode-settings__option:focus-visible {
|
||||
outline: 2px solid var(--halo-accent-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.dark-mode-settings__option.is-active {
|
||||
background-color: var(--halo-menu-item-active);
|
||||
border-color: var(--halo-accent-primary);
|
||||
@@ -139,4 +106,4 @@ const modeOptions: { value: ThemeMode; label: string; description: string }[] =
|
||||
font-size: 0.8125rem;
|
||||
color: var(--halo-text-tertiary);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user