Compare commits
12 Commits
1.0.4
...
30b2ae2f83
| Author | SHA1 | Date | |
|---|---|---|---|
| 30b2ae2f83 | |||
| cffbe572bf | |||
| 96dd1ef4d3 | |||
| df9cd52de6 | |||
| 394613f516 | |||
| 5cbce86e03 | |||
| 45451c0d49 | |||
| d95d17234e | |||
| b6403b7536 | |||
| 363f30dcd6 | |||
| 88a071f673 | |||
| 0bb552b026 |
+15
-3
@@ -1,8 +1,8 @@
|
||||
.gradle/
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
*.class
|
||||
*.jar
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
*.war
|
||||
*.log
|
||||
*.tmp
|
||||
@@ -21,5 +21,17 @@ workplace/*
|
||||
scripts/output/
|
||||
scripts/.browser-profile/
|
||||
|
||||
# Dark Reader 源码下载目录(构建依赖已 vendor 到 ui/vendor/darkreader)
|
||||
docs/darkreader/
|
||||
# 本地项目说明文件,不入库
|
||||
AGENTS.md
|
||||
CLAUDE.md
|
||||
|
||||
# Dark Reader 仅跟踪构建所需文件,完整源码保留在本地
|
||||
third-party/darkreader/*
|
||||
!third-party/darkreader/package.json
|
||||
!third-party/darkreader/darkreader.js
|
||||
!third-party/darkreader/darkreader.mjs
|
||||
!third-party/darkreader/index.d.ts
|
||||
!third-party/darkreader/LICENSE
|
||||
!third-party/darkreader/SHA256SUMS
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## 项目概述
|
||||
|
||||
Halo 暗色模式插件 — 为 Halo 2.25 后台管理面板提供深色/浅色模式切换。不修改 Halo 核心代码,完全通过插件机制实现。
|
||||
|
||||
**核心技术栈**:Java(插件骨架,编译目标 21)、Vue 3 + TypeScript(前端 UI)、OKLCH 色彩空间(CSS 变量体系)、Gradle(构建)。
|
||||
|
||||
**⚠️ 构建环境**:本机没有 JDK 21,构建必须使用 JDK 25(路径已硬编码在 `gradle.properties` 的 `org.gradle.java.home`)。`options.release = 21` 保证字节码兼容 Halo API。不要改回 toolchain 方式——那会因找不到 JDK 21 而失败。
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
# 后端 — 启用插件并启动 Halo 开发服务器(需要 Docker)
|
||||
./gradlew haloServer
|
||||
|
||||
# 后端 — 构建插件 JAR(产物在 build/libs/)
|
||||
./gradlew build
|
||||
|
||||
# 前端 — 进入 ui/ 开发
|
||||
cd ui && pnpm install
|
||||
|
||||
# 前端 — 开发模式(watch 构建)
|
||||
pnpm dev
|
||||
|
||||
# 前端 — 生产构建
|
||||
pnpm build
|
||||
|
||||
# 前端 — 类型检查
|
||||
pnpm type-check
|
||||
|
||||
# 前端 — Lint(oxlint + eslint)
|
||||
pnpm lint
|
||||
|
||||
# 前端 — 格式化
|
||||
pnpm prettier
|
||||
|
||||
# 前端 — 单元测试
|
||||
pnpm test:unit
|
||||
|
||||
# 后端 — 仅运行 Java 测试
|
||||
./gradlew test
|
||||
```
|
||||
|
||||
## 架构概览
|
||||
|
||||
```
|
||||
halo-dark-mode-plugin/
|
||||
├── 后端 (Java/Gradle) ← 插件骨架,极简
|
||||
│ ├── DarkModePlugin.java ← 继承 BasePlugin,仅 start()/stop()
|
||||
│ └── plugin.yaml ← 插件清单(声明式元数据 + 版本约束)
|
||||
│
|
||||
├── 前端 (Vue 3 / TypeScript) ← 核心实现
|
||||
│ ├── index.ts ← definePlugin() 入口,注册路由+组件
|
||||
│ ├── darkreader-engine.ts ← Dark Reader 通用暗色引擎
|
||||
│ ├── injector.ts ← ThemeToggle 侧边栏注入器(MutationObserver 方案)
|
||||
│ ├── composables/
|
||||
│ │ ├── useDarkMode.ts ← 单例状态管理(light/dark/auto + localStorage 持久化)
|
||||
│ │ └── useSystemPreference.ts ← matchMedia 系统偏好监听
|
||||
│ ├── components/
|
||||
│ │ └── ThemeToggle.vue ← 侧边栏切换按钮
|
||||
│ ├── views/
|
||||
│ │ └── SettingsView.vue ← 设置页面(浅色/深色/跟随系统 三选一)
|
||||
│ └── styles/
|
||||
│ ├── index.css ← 样式入口(@import 聚合)
|
||||
│ ├── variables.css ← 40+ CSS 变量(:root 浅色 + [data-halo-theme="dark"] 深色)
|
||||
│ └── overrides/
|
||||
│ ├── halo-core.css ← ★ 核心:真实 DOM 类名覆盖(最重要)
|
||||
│ ├── layout.css ← 侧边栏/内容区/页脚
|
||||
│ ├── components.css ← 旧版组件类覆盖(部分已失效,见下)
|
||||
│ ├── forms.css / editor.css / scrollbar.css / utilities.css
|
||||
│
|
||||
└── 构建链
|
||||
└── processUiResources ← Gradle task: ui/dist/ → resources/main/ui/
|
||||
```
|
||||
|
||||
## 关键设计决策
|
||||
|
||||
### 主题切换机制
|
||||
|
||||
- **触发方式**:`document.documentElement` 上设置/移除 `data-halo-theme="dark"` 属性
|
||||
- **主引擎**:Dark Reader(本地 vendored 于 `ui/vendor/darkreader`,API 包由
|
||||
`npm run api` 构建),监听 `useDarkMode().isDark`,深色时 `enable()`,浅色时
|
||||
`disable()`,可自动分析 Halo 核心与第三方插件的动态 DOM
|
||||
- **CSS 变量体系**:所有颜色通过 `--halo-*` 前缀的 CSS 自定义属性控制,一个语义变量对应一个视觉属性
|
||||
- **颜色空间**:全部使用 OKLCH(感知均匀,暗色模式天然适配)
|
||||
- **暗色配色策略**:用亮度层次区分背景(越"高"的层越亮),中性色含微量蓝色调,强调色略降饱和
|
||||
- **CSS 覆盖定位**:现有 `overrides/` 仅作为 Dark Reader 的兼容层与兜底,
|
||||
不再逐页新增手工转换规则;新页面残留统一交给 Dark Reader 处理
|
||||
|
||||
### ⚠️ 最关键的教训:Halo 2.25 用 UnoCSS,不是 Tailwind
|
||||
|
||||
Halo 2.25 的 Console 实际使用 **UnoCSS**(hash 类如 `uno-*`)加 **BEM 语义aliyun "curl -sI --max-time 5 http://localhost/ 2>&1 |类**。**不要写 `.bg-white`、`.text-gray-900`、`.v-card` 这类 Tailwind/Vuetify 原子选择器**——它们在真实 DOM 中不存在,覆盖会静默失效。
|
||||
|
||||
真实 DOM 里的容器类名(已被 `halo-core.css` 覆盖):
|
||||
|
||||
| 语义 | 真实类名 |
|
||||
| ------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| 页面顶栏 | `.page-header` / `.page-header__title-text` |
|
||||
| 列表卡片容器 | `.card-wrapper` |
|
||||
| 文章/用户列表项标题 | `.entity-field-title` / `.entity-field-title-body` |
|
||||
| 分页 | `.pagination` / `.pagination__btn` |
|
||||
| 标签 | `.tag-default` / `.tag-content` |
|
||||
| 模态框 | `.modal-content` / `.modal-header` / `.modal-body` / `.modal-footer` |
|
||||
| 详情页描述项 | `.description-item-wrapper` / `.description-item__label` / `.description-item__content` |
|
||||
| Toast | `.toast-container .toast-body` |
|
||||
| 用户头像 | `.avatar-wrapper` / `.avatar-circle` |
|
||||
|
||||
新增覆盖时:**先到真实环境确认类名,不要凭经验写**。
|
||||
|
||||
### 覆盖策略(重写后)
|
||||
|
||||
`halo-core.css` 是主要覆盖文件,按语义类精准覆盖。它包含三类规则:
|
||||
|
||||
1. **BEM 语义类**(如 `.card-wrapper`)— 直接 `[data-halo-theme="dark"] .card-wrapper { background-color: var(--halo-bg-card) }`
|
||||
2. **通用 UnoCSS 工具类**(如 `.bg-gray-50`、`.hover:text-gray-600`)— 批量覆盖文字/背景/边框
|
||||
3. **根背景兜底** — `html, body { background-color: var(--halo-bg-body) !important }`(body 不设暗色会在溢出时露白)
|
||||
|
||||
`components.css` 等旧文件里的 Tailwind/Vuetify 选择器部分已失效,保留但优先维护 `halo-core.css`。
|
||||
|
||||
### 状态管理
|
||||
|
||||
- `useDarkMode()` 是**模块级单例** — 所有组件共享同一份 `theme` ref 和 `isDark` computed
|
||||
- 三个主题模式:`light` / `dark` / `auto`(跟随系统)
|
||||
- 持久化:`localStorage` key `halo-dark-mode-theme`,默认 `auto`
|
||||
- FOUC 防护:插件 bundle 顶部同步执行脚本,DOM 渲染前设置 `data-halo-theme`
|
||||
|
||||
### ThemeToggle 侧边栏注入
|
||||
|
||||
Halo 扩展点系统**没有侧边栏插槽**。`injector.ts` 用 `MutationObserver` 监听 `.sidebar__profile` 元素出现,在其上方插入容器并挂载 `ThemeToggle.vue` 组件。这是当前 Halo 插件体系下的合理变通方案。
|
||||
|
||||
### 前端入口
|
||||
|
||||
- `definePlugin()` 从 `@halo-dev/ui-shared` 导入(非旧版 `@halo-dev/console-shared`)
|
||||
- 路由使用 `() => import(...)` 懒加载
|
||||
- 构建使用 `@halo-dev/ui-plugin-bundler-kit` 的 `viteConfig()` 包装器
|
||||
|
||||
### 后端
|
||||
|
||||
后端极简 — `DarkModePlugin extends BasePlugin` 仅含 `start()`/`stop()` 生命周期钩子。所有核心逻辑在前端。插件不依赖后端 Setting API。
|
||||
|
||||
## 打包约定(必须)
|
||||
|
||||
每次执行 `./gradlew build` 打 JAR 前,**必须先递增版本标签**:
|
||||
|
||||
1. 修改 `gradle.properties` 中的 `version`(如 `1.0.2` → `1.0.3`)
|
||||
2. 同步更新 `src/main/java/run/halo/darkmode/DarkModePlugin.java` 的 `@since`
|
||||
3. 构建完成后核对 `build/libs/plugin-dark-mode-<新版本>.jar` 存在,且 JAR 内的
|
||||
`ui/style.css`、`ui/main.js` 包含本次改动(用关键字检查,例如新增类名)
|
||||
4. 向用户交付时明确给出新 JAR 路径和版本号,避免线上仍加载旧 bundle 的混淆
|
||||
## 验证工作流(必须)
|
||||
|
||||
对 CSS 覆盖的任何改动,都要在**真实 Halo 环境**验证,不能只靠 `vite build` 通过:
|
||||
|
||||
1. 打开官方 demo 站 `https://demo.halocms.site/console`,登录 `demo / P@ssw0rd123..`
|
||||
2. 用 Playwright 的 `page.addStyleTag({ path: 'ui/build/dist/style.css' })` 注入构建产物,并 `document.documentElement.setAttribute('data-halo-theme', 'dark')`
|
||||
3. 遍历页面,扫描 `main *` 中残留的白色背景(`rgb(255,255,255)` 且尺寸 >100×40)和深色文字(`rgb(17,24,39)` 等)元素,记录其真实类名
|
||||
4. 新增覆盖后重新构建、重新注入、重新扫描,直到零残留
|
||||
5. 最终 `./gradlew build` 打 JAR,确认 `ui/style.css` 已更新
|
||||
|
||||
第三方插件页面(链接/订阅/瞬间等)在 demo 站已装,可一并验证。
|
||||
|
||||
## Playwright 自动化验证(workplace/)
|
||||
|
||||
`workplace/` 目录包含基于 Playwright 的自动化验证工具(未提交到 git):
|
||||
|
||||
- **`login_wait.py`** — 启动带持久化配置的 Edge 窗口,打开 `https://blog.liuhangyv.top/console/login`,等待用户手动登录后将会话 cookie 保存到 `pw-profile/`。超时 280 秒。
|
||||
- **`pw-profile/`** — Edge 浏览器持久化用户数据目录,登录后会话保留供后续 Playwright 脚本复用。
|
||||
- **`roleTemplates.yaml`** — 预留的角色模板配置(当前为空)。
|
||||
|
||||
典型流程:先运行 `login_wait.py` 登录,再编写 Playwright 脚本复用 `pw-profile` 中的会话进行页面扫描。
|
||||
|
||||
## README.md
|
||||
|
||||
用户向 README,包含功能介绍、快速开始、构建命令和项目结构图。
|
||||
@@ -1,66 +1,97 @@
|
||||
# dark-mode
|
||||
# Halo 深色模式
|
||||
|
||||
Halo 2.25 暗色模式插件 — 为 Halo 后台管理面板提供深色/浅色模式切换,支持跟随系统、手动切换和偏好记忆。
|
||||
为 Halo 后台管理面板提供深色/浅色模式切换,内置 Dark Reader 通用暗色引擎,支持手动切换、跟随系统与偏好记忆。
|
||||
|
||||
## 功能
|
||||
## 功能特性
|
||||
|
||||
- ☀️/🌙 **三种模式**:浅色、深色、跟随系统
|
||||
- 💾 **偏好持久化**:自动记忆用户选择(localStorage),刷新不丢失
|
||||
- 🖥️ **系统偏好跟随**:切换系统外观时自动响应
|
||||
- ⚡ **瞬间切换**:CSS 变量瞬时生效,无可见闪烁
|
||||
- 🧩 **侧边栏注入**:切换按钮自动出现在侧边栏底部(UserProfileBanner 上方)
|
||||
- ⚙️ **设置页面**:提供详细的模式选择界面(菜单 → 偏好设置 → 深色模式)
|
||||
- 🔍 **Dark Reader 引擎**:内置成熟的开源暗色转换引擎,自动分析页面 CSS 与 DOM,
|
||||
第三方插件页面的黑字/白底也能自动转换,不再依赖逐页手工覆盖
|
||||
- 🎨 **OKLCH 色彩空间**:感知均匀,暗色模式天然适配,WCAG AA 对比度保证
|
||||
- 📦 **零后端依赖**:纯前端实现,不需要后端 API
|
||||
- ✅ **覆盖已验证**:仪表盘、内容管理(文章/页面/评论)、链接、订阅、瞬间、用户、主题、设置、编辑器、模态框等页面已针对 Halo 2.25 真实 DOM 逐一验证
|
||||
- **三种模式**:浅色、深色、跟随系统
|
||||
- **偏好持久化**:自动记忆用户选择(localStorage),刷新不丢失
|
||||
- **系统偏好跟随**:切换系统外观时自动响应
|
||||
- **Dark Reader 引擎**:自动分析页面 CSS 与 DOM,覆盖 Halo 核心页面和第三方插件页面
|
||||
- **官方外观分组**:设置入口位于 Halo「外观」分组,与主题、菜单、插件同组
|
||||
- **设置页面**:在「外观 → 深色模式」中详细选择显示模式
|
||||
- **零后端依赖**:纯前端实现,不需要额外后端 API
|
||||
|
||||
## 安装
|
||||
|
||||
1. 从 Releases 下载 `plugin-dark-mode-<version>.jar`。
|
||||
2. 在 Halo 后台的「插件管理」中上传并安装。
|
||||
3. 启用插件后,进入「外观 → 深色模式」设置页面调整主题。
|
||||
|
||||
## 使用
|
||||
|
||||
安装并启用后,进入「外观 → 深色模式」设置页面选择显示模式:
|
||||
|
||||
| 配置项 | 可选值 | 说明 |
|
||||
| -------- | --------- | -------------------- |
|
||||
| 主题模式 | `light` | 始终使用浅色模式 |
|
||||
| 主题模式 | `dark` | 始终使用深色模式 |
|
||||
| 主题模式 | `auto` | 跟随系统外观自动切换 |
|
||||
|
||||
## 技术原理
|
||||
|
||||
- 插件通过 `useDarkMode()` 管理 `light` / `dark` / `auto` 三种状态。
|
||||
- 深色模式下调用 Dark Reader 的 `enable()`,浅色模式下调用 `disable()`。
|
||||
- Dark Reader 会持续监听页面 DOM 变化,因此第三方插件动态渲染的内容也能自动转换。
|
||||
- 插件自身只保留设置页所需的最小 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)。
|
||||
- 构建所需文件位于 `third-party/darkreader/`,由 `ui/package.json` 通过本地文件依赖引用。
|
||||
- 升级或替换 Dark Reader 构建文件后,请校验 `third-party/darkreader/SHA256SUMS`:
|
||||
Linux / macOS 使用 `sha256sum -c SHA256SUMS`,Windows 使用 `Get-FileHash -Algorithm SHA256` 对比。
|
||||
|
||||
## 开发环境
|
||||
|
||||
- Java 21+(Halo 插件编译要求;本机开发使用 JDK 25 + `--release 21`)
|
||||
- Halo `>=2.25.0`
|
||||
- Java 21+(项目使用 `--release 21` 编译)
|
||||
- Node.js 18+
|
||||
- pnpm
|
||||
- Docker(`haloServer` 开发服务器需要)
|
||||
|
||||
## 快速开始
|
||||
## 从源码构建
|
||||
|
||||
仓库使用标准 Gradle 结构,并提交了 Gradle Wrapper。
|
||||
|
||||
Linux / macOS:
|
||||
|
||||
```bash
|
||||
# 启用插件并启动 Halo 开发服务器
|
||||
./gradlew haloServer
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
# 前端开发(watch 模式)
|
||||
Windows:
|
||||
|
||||
```powershell
|
||||
.\gradlew.bat clean build
|
||||
```
|
||||
|
||||
构建产物位于 `build/libs/plugin-dark-mode-<version>.jar`。
|
||||
|
||||
## 前端开发
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## 构建
|
||||
常用检查命令:
|
||||
|
||||
```bash
|
||||
# 完整构建(后端 + 前端)
|
||||
./gradlew build
|
||||
```
|
||||
|
||||
构建完成后,插件 JAR 文件位于 `build/libs/`,可直接在 Halo 后台安装。
|
||||
|
||||
## 前端命令
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
|
||||
pnpm dev # 开发构建(watch)
|
||||
pnpm build # 生产构建
|
||||
pnpm type-check # TypeScript 类型检查
|
||||
pnpm lint # Lint(oxlint + eslint)
|
||||
pnpm prettier # 代码格式化
|
||||
pnpm test:unit # 单元测试
|
||||
pnpm type-check # TypeScript 类型检查
|
||||
pnpm lint # Lint(oxlint + eslint)
|
||||
pnpm build # 生产构建
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```text
|
||||
├── build.gradle # 根构建(BOM 2.25.0, DevTools 0.8.0)
|
||||
├── build.gradle # 根构建配置
|
||||
├── settings.gradle # 包含 :ui 子项目
|
||||
├── gradle/wrapper/ # Gradle Wrapper
|
||||
├── src/
|
||||
│ └── main/
|
||||
│ ├── java/run/halo/darkmode/
|
||||
@@ -68,31 +99,71 @@ pnpm test:unit # 单元测试
|
||||
│ └── resources/
|
||||
│ └── plugin.yaml # 插件清单
|
||||
│
|
||||
├── third-party/
|
||||
│ └── darkreader/ # Dark Reader 构建产物(MIT)
|
||||
│
|
||||
└── ui/
|
||||
├── package.json # 前端依赖
|
||||
├── vite.config.ts # Vite 配置
|
||||
└── src/
|
||||
├── index.ts # definePlugin 入口
|
||||
├── darkreader-engine.ts # Dark Reader 通用暗色引擎
|
||||
├── injector.ts # ThemeToggle 侧边栏注入器
|
||||
├── composables/
|
||||
│ ├── useDarkMode.ts # 核心状态管理(模块级单例)
|
||||
│ ├── useDarkMode.ts # 主题状态管理(模块级单例)
|
||||
│ └── useSystemPreference.ts # 系统偏好监听
|
||||
├── components/
|
||||
│ └── ThemeToggle.vue # 侧边栏切换按钮
|
||||
├── views/
|
||||
│ └── SettingsView.vue # 设置页面
|
||||
└── styles/
|
||||
├── index.css # 样式入口
|
||||
├── variables.css # 40+ CSS 变量(浅色 + 深色)
|
||||
└── overrides/
|
||||
├── halo-core.css # ★ 核心:真实 DOM 类名覆盖
|
||||
├── layout.css # 侧边栏/内容区/页脚
|
||||
├── components.css # 组件类覆盖
|
||||
├── forms.css # FormKit 表单
|
||||
├── editor.css # 富文本编辑器
|
||||
├── scrollbar.css # 滚动条
|
||||
└── utilities.css # 通用工具类
|
||||
└── variables.css # 插件自身 UI 变量
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
./gradlew test
|
||||
```
|
||||
|
||||
运行时验证脚本位于 `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` 改为安装后由用户手动启用
|
||||
|
||||
### v1.0.4
|
||||
|
||||
- 改为纯 Dark Reader 引擎,移除手工 CSS 覆盖
|
||||
- 移除 Monaco 手动主题同步,暗色转换统一交给 Dark Reader
|
||||
- 整理 `third-party/darkreader` 目录,仅保留构建所需文件
|
||||
|
||||
### v1.0.3
|
||||
|
||||
- 引入 Dark Reader 通用暗色引擎
|
||||
- 保留原有手工 CSS 作为兼容层,覆盖核心页面与第三方插件页面
|
||||
- 增加 Monaco 日志查看器主题同步
|
||||
|
||||
## 许可证
|
||||
|
||||
[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 @@
|
||||
# 问题交接单:深色模式入口去重 + 菜单分组调整(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 变更,需重新构建部署后验证。
|
||||
@@ -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 断言与策略一致 ✅
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
+1
-1
@@ -1,3 +1,3 @@
|
||||
version=1.0.3
|
||||
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.
+21
-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:
|
||||
|
||||
```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
|
||||
# 全路由深色扫描,注入本地构建产物做预部署验证,存在残留时返回非 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
|
||||
|
||||
# 只扫指定页面
|
||||
@@ -36,25 +36,32 @@ 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
|
||||
```
|
||||
|
||||
## 输出
|
||||
|
||||
- `scripts/output/scan-results.json`:按路由分组的结果
|
||||
- `scripts/output/shots/*.png`:每页截图
|
||||
- 均已被 gitignore,不入库。
|
||||
|
||||
## 切换行为验证
|
||||
|
||||
```powershell
|
||||
D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\verify-toggle.py
|
||||
```
|
||||
|
||||
部署新版插件后运行,会检查侧边栏按钮切换、`data-halo-theme` 属性、
|
||||
localStorage 持久化以及 Monaco 日志查看器主题是否同步。
|
||||
部署新版插件后运行,会直接驱动 localStorage 中的主题状态,并检查:
|
||||
|
||||
- `data-halo-theme` 属性翻转
|
||||
- localStorage 持久化翻转
|
||||
- `data-darkreader-mode`(Dark Reader 注入状态)翻转
|
||||
- `color-scheme` 翻转
|
||||
- 侧边栏已不再注入 `.theme-toggle` / `.plugin-dark-mode-toggle` 按钮
|
||||
|
||||
状态翻转全部 PASS 且按钮已移除时返回 0。
|
||||
|
||||
## 输出
|
||||
|
||||
- `scripts/output/scan-results.json`:按路由分组的结果
|
||||
- `scripts/output/shots/*.png`:每页截图
|
||||
- 均已被 gitignore,不入库。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 扫描会忽略 `img/canvas/video/svg/iframe`,避免把文章图片、编辑器画布等内容性
|
||||
元素误判为界面残留。
|
||||
- 路由从 Vue Router 动态发现,Halo 或插件升级新增页面后无需维护清单。
|
||||
- `--local-css` 用 `add_style_tag` 追加本地样式(不替换线上 bundle),可安全验证
|
||||
CSS 修复;JS 改动(如 Monaco 主题同步)需要重新构建并部署后再跑 `verify-toggle.py`。
|
||||
- `scan-dark.py` 主要为历史手工 CSS 工作流保留;当前暗色转换由 Dark Reader 负责,
|
||||
运行时行为验证以 `verify-toggle.py` 为准。
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# scripts/legacy
|
||||
|
||||
保留的旧版分析工具,来自早期手工 CSS 工作流,当前纯 Dark Reader 架构下不再参与日常流程。
|
||||
|
||||
- `aggregate.py` — 聚合多路由扫描结果
|
||||
- `fetch_bundle.py` — 抓取线上部署 CSS bundle
|
||||
- `probe_conflict.py` — 探测 CSS 覆盖冲突
|
||||
|
||||
如需恢复手工 CSS 扫描工作流,可参考这些脚本;否则可在一段时间后删除。
|
||||
+86
-32
@@ -1,7 +1,4 @@
|
||||
"""验证侧边栏主题切换:点击后检查 data-halo-theme、localStorage 与 Monaco 主题同步。
|
||||
|
||||
部署新版插件后运行可获得完整结果;预部署阶段 Monaco 断言会提示跳过。
|
||||
"""
|
||||
"""验证主题切换:直接驱动 localStorage,检查 data-halo-theme、Dark Reader 与 color-scheme 翻转。"""
|
||||
import argparse
|
||||
import pathlib
|
||||
import sys
|
||||
@@ -11,6 +8,47 @@ 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:
|
||||
return page.evaluate(
|
||||
"""() => ({
|
||||
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:
|
||||
@@ -21,7 +59,9 @@ def main() -> int:
|
||||
|
||||
with sync_playwright() as p:
|
||||
ctx = p.chromium.launch_persistent_context(
|
||||
args.profile, channel="msedge", headless=True,
|
||||
args.profile,
|
||||
channel="msedge",
|
||||
headless=True,
|
||||
viewport={"width": 1600, "height": 950},
|
||||
)
|
||||
page = ctx.pages[0] if ctx.pages else ctx.new_page()
|
||||
@@ -36,40 +76,54 @@ def main() -> int:
|
||||
ctx.close()
|
||||
return 3
|
||||
|
||||
toggle = page.locator(".theme-toggle")
|
||||
toggle.wait_for(state="visible", timeout=15000)
|
||||
before = page.evaluate("() => ({ attr: document.documentElement.getAttribute('data-halo-theme'), stored: localStorage.getItem('halo-dark-mode-theme') })")
|
||||
toggle.click()
|
||||
time.sleep(0.5)
|
||||
after = page.evaluate("() => ({ attr: document.documentElement.getAttribute('data-halo-theme'), stored: localStorage.getItem('halo-dark-mode-theme') })")
|
||||
toggle.click()
|
||||
time.sleep(0.5)
|
||||
restored = page.evaluate("() => ({ attr: document.documentElement.getAttribute('data-halo-theme'), stored: localStorage.getItem('halo-dark-mode-theme') })")
|
||||
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)
|
||||
|
||||
set_mode(page, "dark")
|
||||
wait_mode(page, expect_dark=True)
|
||||
after = read_state(page)
|
||||
|
||||
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'} 存储翻转: {'PASS' if stored_flipped else 'FAIL'}")
|
||||
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"color-scheme 翻转: {'PASS' if scheme_flipped else 'FAIL'}"
|
||||
)
|
||||
|
||||
page.goto(args.base + "/console/log-viewer", wait_until="domcontentloaded")
|
||||
try:
|
||||
page.wait_for_load_state("networkidle", timeout=6000)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(2)
|
||||
has_monaco = page.locator(".monaco-editor").count() > 0
|
||||
if has_monaco:
|
||||
dark_attr = page.evaluate("document.documentElement.getAttribute('data-halo-theme')") == "dark"
|
||||
theme_cls = page.evaluate("() => { const el = document.querySelector('.monaco-editor'); return el ? el.className : '' }")
|
||||
monaco_dark = "vs-dark" in theme_cls
|
||||
print(f"Monaco 实例: 存在 | 属性dark={dark_attr} | 主题类含vs-dark={monaco_dark} | class={theme_cls[:60]}")
|
||||
print(f"Monaco 主题同步: {'PASS' if (dark_attr and monaco_dark) or (not dark_attr and not monaco_dark) else 'FAIL(可能部署的还是旧版 JS)'}")
|
||||
else:
|
||||
print("Monaco 实例: 未检测到,跳过主题断言(日志页面可能未加载或路由不同)")
|
||||
ctx.close()
|
||||
return 0 if attr_flipped and stored_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__":
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package run.halo.darkmode;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.plugin.BasePlugin;
|
||||
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>
|
||||
*
|
||||
* @author LHY
|
||||
* @since 1.0.3
|
||||
* @since 1.0.8
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DarkModePlugin extends BasePlugin {
|
||||
|
||||
@@ -21,11 +23,11 @@ public class DarkModePlugin extends BasePlugin {
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
System.out.println("插件启动成功!");
|
||||
log.info("插件启动成功!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
System.out.println("插件停止!");
|
||||
log.info("插件停止!");
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 64 KiB |
@@ -6,17 +6,17 @@ metadata:
|
||||
# The name defines how the plugin is invoked, A unique name
|
||||
name: dark-mode
|
||||
spec:
|
||||
enabled: true
|
||||
enabled: false
|
||||
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"
|
||||
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
0b126b96a0d76a1ff1b1c7cc7088c3ee1903dea2119abded2fb3913b340b63b1 darkreader.js
|
||||
fbacd711bfd26b33b160881b50028a3e22e8f083b62ec727baebc0c4ad14b052 darkreader.mjs
|
||||
5f7ebb5de2d74e011d87b86c2cc12048d667981599bdc9c527f98de9e0503a26 package.json
|
||||
ab1ba9694b242323d15ed4839457c14fe04891966568413d5cd5eb78156dae1f index.d.ts
|
||||
3f018657498dc4a805af22cc872688f1cd699bbe0045c82d29aeb0c8e65843e5 LICENSE
|
||||
+2
-2
@@ -8,7 +8,7 @@
|
||||
"lint:eslint": "eslint . --fix",
|
||||
"lint": "run-s lint:*",
|
||||
"prettier": "prettier --write src/",
|
||||
"test:unit": "vitest --passWithNoTests",
|
||||
"test:unit": "vitest",
|
||||
"type-check": "vue-tsc --build"
|
||||
},
|
||||
"prettier": {
|
||||
@@ -23,7 +23,7 @@
|
||||
"@halo-dev/ui-shared": "^2.25.1",
|
||||
"axios": "^1.13.5",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
"darkreader": "file:./vendor/darkreader",
|
||||
"darkreader": "file:../third-party/darkreader",
|
||||
"vue": "^3.5.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Generated
+5
-5
@@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^1.9.3
|
||||
version: 1.9.4
|
||||
darkreader:
|
||||
specifier: file:./vendor/darkreader
|
||||
version: file:vendor/darkreader
|
||||
specifier: file:../third-party/darkreader
|
||||
version: file:../third-party/darkreader
|
||||
vue:
|
||||
specifier: ^3.5.28
|
||||
version: 3.5.41(typescript@5.8.3)
|
||||
@@ -1085,8 +1085,8 @@ packages:
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
darkreader@file:vendor/darkreader:
|
||||
resolution: {directory: vendor/darkreader, type: directory}
|
||||
darkreader@file:../third-party/darkreader:
|
||||
resolution: {directory: ../third-party/darkreader, type: directory}
|
||||
|
||||
data-urls@5.0.0:
|
||||
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
|
||||
@@ -3320,7 +3320,7 @@ snapshots:
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
darkreader@file:vendor/darkreader: {}
|
||||
darkreader@file:../third-party/darkreader: {}
|
||||
|
||||
data-urls@5.0.0:
|
||||
dependencies:
|
||||
|
||||
@@ -1,47 +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>
|
||||
<div class="theme-toggle" @click="toggle">
|
||||
<IconSunLine v-if="isDark" class="theme-toggle__icon" />
|
||||
<IconMoonLine v-else class="theme-toggle__icon" />
|
||||
<span class="theme-toggle__label">
|
||||
{{ isDark ? '浅色模式' : '深色模式' }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.theme-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
color: var(--halo-text-secondary);
|
||||
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__icon {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.theme-toggle__label {
|
||||
font-size: 0.8125rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</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 {
|
||||
const root = document.documentElement
|
||||
if (isDark) {
|
||||
document.documentElement.setAttribute('data-halo-theme', 'dark')
|
||||
root.setAttribute('data-halo-theme', 'dark')
|
||||
} 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 变化 → 持久化
|
||||
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' 模式下响应
|
||||
onChange((systemDark) => {
|
||||
if (theme.value === 'auto') {
|
||||
|
||||
@@ -4,8 +4,9 @@ import { useDarkMode } from './composables/useDarkMode'
|
||||
|
||||
/**
|
||||
* Dark Reader 引擎参数。
|
||||
* brightness/contrast 保持默认观感,背景色与现有 Halo 暗色变量接近,
|
||||
* 让 Dark Reader 补全第三方插件页面时不会显得突兀。
|
||||
* 纯 Dark Reader 策略下不再维护手工 CSS 覆盖;
|
||||
* 这里只调整通用观感参数,让核心与第三方插件页面转换后风格统一。
|
||||
* 注意:DR 注入为异步过程,刷新闪烁只能由 useDarkMode 的 color-scheme 同步缓解。
|
||||
*/
|
||||
const DARK_READER_THEME = {
|
||||
brightness: 100,
|
||||
|
||||
+2
-6
@@ -3,12 +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'
|
||||
import { initMonacoThemeSync } from './monaco-theme'
|
||||
|
||||
// 在插件加载后将切换器注入到侧边栏,并让 Monaco 跟随主题
|
||||
injectThemeToggle()
|
||||
initMonacoThemeSync()
|
||||
initDarkReaderEngine()
|
||||
|
||||
export default definePlugin({
|
||||
@@ -25,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
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* 让 Monaco Editor(日志查看器)跟随 Halo 暗色模式。
|
||||
* 优先调用 Monaco 官方主题 API,CSS 兜底见 styles/overrides/editor.css。
|
||||
*/
|
||||
|
||||
type MonacoLike = {
|
||||
editor?: {
|
||||
setTheme: (name: string) => void
|
||||
}
|
||||
}
|
||||
|
||||
const DARK_THEME = 'vs-dark'
|
||||
const LIGHT_THEME = 'vs'
|
||||
|
||||
function getMonaco(): MonacoLike | undefined {
|
||||
return (window as unknown as { monaco?: MonacoLike }).monaco
|
||||
}
|
||||
|
||||
function applyMonacoTheme(): void {
|
||||
const monaco = getMonaco()
|
||||
if (!monaco?.editor?.setTheme) return
|
||||
const isDark = document.documentElement.getAttribute('data-halo-theme') === 'dark'
|
||||
monaco.editor.setTheme(isDark ? DARK_THEME : LIGHT_THEME)
|
||||
}
|
||||
|
||||
/** 初始化 Monaco 主题同步:立即应用,并监听属性变化与实例懒加载。 */
|
||||
export function initMonacoThemeSync(): void {
|
||||
applyMonacoTheme()
|
||||
|
||||
const attributeObserver = new MutationObserver(() => applyMonacoTheme())
|
||||
attributeObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-halo-theme'],
|
||||
})
|
||||
|
||||
// 日志页面可能懒加载 Monaco,实例渲染后再补一次
|
||||
const instanceObserver = new MutationObserver(() => {
|
||||
if (document.querySelector('.monaco-editor')) {
|
||||
applyMonacoTheme()
|
||||
}
|
||||
})
|
||||
instanceObserver.observe(document.body, { childList: true, subtree: true })
|
||||
}
|
||||
+1
-10
@@ -1,15 +1,6 @@
|
||||
/* ============================================================
|
||||
Halo Dark Mode — 样式入口
|
||||
按加载顺序导入所有样式文件
|
||||
仅保留插件自身 UI 变量;页面暗色转换完全交给 Dark Reader。
|
||||
============================================================ */
|
||||
|
||||
@import './variables.css';
|
||||
@import './overrides/halo-core.css';
|
||||
@import './overrides/plugin-pages.css';
|
||||
@import './overrides/uno-fallback.css';
|
||||
@import './overrides/utilities.css';
|
||||
@import './overrides/layout.css';
|
||||
@import './overrides/components.css';
|
||||
@import './overrides/forms.css';
|
||||
@import './overrides/editor.css';
|
||||
@import './overrides/scrollbar.css';
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
/* ============================================================
|
||||
Halo Dark Mode — Halo 组件库覆盖(已清理)
|
||||
已删除 Halo 2.25 中不存在的 Vuetify 类(.v-card 等),
|
||||
真实 DOM 类覆盖请优先维护 halo-core.css。
|
||||
============================================================ */
|
||||
|
||||
[data-halo-theme="dark"] {
|
||||
/* ===== 模态框(具体类 + !important,防被核心 scoped 样式覆盖) ===== */
|
||||
.modal-content,
|
||||
.modal-header,
|
||||
.modal-body,
|
||||
.modal-footer {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
|
||||
/* ===== 表格 VTable ===== */
|
||||
table {
|
||||
background-color: var(--halo-bg-card);
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
|
||||
thead,
|
||||
.table-header {
|
||||
background-color: var(--halo-table-header-bg);
|
||||
}
|
||||
|
||||
thead th,
|
||||
.table-header th {
|
||||
color: var(--halo-text-secondary);
|
||||
border-color: var(--halo-table-border);
|
||||
}
|
||||
|
||||
tbody td,
|
||||
.table-body td {
|
||||
border-color: var(--halo-table-border);
|
||||
}
|
||||
|
||||
tbody tr:hover,
|
||||
.table-row:hover {
|
||||
background-color: var(--halo-table-row-hover);
|
||||
}
|
||||
|
||||
/* ===== 分页 ===== */
|
||||
.pagination {
|
||||
color: var(--halo-text-secondary);
|
||||
}
|
||||
|
||||
.pagination .active {
|
||||
background-color: var(--halo-accent-primary);
|
||||
color: var(--halo-accent-primary-text);
|
||||
}
|
||||
|
||||
/* ===== 面包屑 ===== */
|
||||
.breadcrumb {
|
||||
color: var(--halo-text-secondary);
|
||||
}
|
||||
|
||||
.breadcrumb a {
|
||||
color: var(--halo-text-link);
|
||||
}
|
||||
|
||||
/* ===== Toast / 通知 ===== */
|
||||
.toast-notification,
|
||||
[class*="toast"] {
|
||||
background-color: var(--halo-bg-card);
|
||||
color: var(--halo-text-primary);
|
||||
border-color: var(--halo-border-base);
|
||||
}
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
/* ============================================================
|
||||
Halo Dark Mode — 富文本编辑器覆盖
|
||||
@halo-dev/richtext-editor + TipTap / ProseMirror
|
||||
+ Bytemd (Markdown 编辑器) + CodeMirror
|
||||
============================================================ */
|
||||
|
||||
[data-halo-theme="dark"] {
|
||||
/* ===== Bytemd Markdown 编辑器 ===== */
|
||||
.bytemd {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
border-color: var(--halo-border-base) !important;
|
||||
}
|
||||
.bytemd-toolbar {
|
||||
background-color: var(--halo-bg-content) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
border-color: var(--halo-border-light) !important;
|
||||
}
|
||||
.bytemd-toolbar-icon {
|
||||
color: var(--halo-text-secondary) !important;
|
||||
}
|
||||
.bytemd-toolbar-icon:hover {
|
||||
background-color: var(--halo-bg-hover) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
.bytemd-toolbar-tab-active,
|
||||
.bytemd-toolbar-icon-active {
|
||||
color: var(--halo-accent-primary) !important;
|
||||
}
|
||||
.bytemd-editor,
|
||||
.bytemd-editor .CodeMirror {
|
||||
background-color: var(--halo-bg-input) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
.bytemd-preview {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
.bytemd-status,
|
||||
.bytemd-status-right,
|
||||
.bytemd-status-left {
|
||||
background-color: var(--halo-bg-content) !important;
|
||||
color: var(--halo-text-secondary) !important;
|
||||
border-color: var(--halo-border-light) !important;
|
||||
}
|
||||
.bytemd-dropdown,
|
||||
.bytemd-dropdown-menu,
|
||||
[class*="bytemd-dropdown"] {
|
||||
background-color: var(--halo-bg-dropdown) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
border-color: var(--halo-border-base) !important;
|
||||
}
|
||||
.bytemd-dropdown-title {
|
||||
color: var(--halo-text-secondary) !important;
|
||||
}
|
||||
.bytemd-dropdown-item {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
.bytemd-dropdown-item:hover {
|
||||
background-color: var(--halo-bg-hover) !important;
|
||||
}
|
||||
.bytemd-sidebar,
|
||||
.bytemd-toc,
|
||||
.bytemd-toc ul,
|
||||
.bytemd-toc li {
|
||||
background-color: var(--halo-bg-content) !important;
|
||||
color: var(--halo-text-secondary) !important;
|
||||
}
|
||||
.bytemd-toc-active {
|
||||
background-color: var(--halo-bg-hover) !important;
|
||||
color: var(--halo-accent-primary) !important;
|
||||
}
|
||||
.bytemd-floating-btn {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
color: var(--halo-text-secondary) !important;
|
||||
border-color: var(--halo-border-base) !important;
|
||||
}
|
||||
.bytemd-floating-btn:hover {
|
||||
background-color: var(--halo-bg-hover) !important;
|
||||
}
|
||||
.bytemd-floating-btn.active {
|
||||
background-color: var(--halo-bg-active) !important;
|
||||
color: var(--halo-accent-primary) !important;
|
||||
}
|
||||
.bytemd-code-theme-toggle,
|
||||
.bytemd-copy-code-btn,
|
||||
[class*="bytemd"] button {
|
||||
color: var(--halo-text-secondary) !important;
|
||||
}
|
||||
.bytemd-copy-code-btn:hover,
|
||||
[class*="bytemd"] button:hover {
|
||||
color: var(--halo-text-primary) !important;
|
||||
background-color: var(--halo-bg-hover) !important;
|
||||
}
|
||||
/* 编辑器内 markdown 正文 */
|
||||
.bytemd-preview .markdown-body,
|
||||
.bytemd-editor .markdown-body {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
.bytemd-preview .markdown-body p,
|
||||
.bytemd-preview .markdown-body li,
|
||||
.bytemd-preview .markdown-body h1,
|
||||
.bytemd-preview .markdown-body h2,
|
||||
.bytemd-preview .markdown-body h3,
|
||||
.bytemd-preview .markdown-body h4,
|
||||
.bytemd-preview .markdown-body td,
|
||||
.bytemd-preview .markdown-body th,
|
||||
.bytemd-preview .markdown-body blockquote {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
.bytemd-preview .markdown-body code,
|
||||
.bytemd-preview .markdown-body pre,
|
||||
.bytemd-preview .markdown-body pre code {
|
||||
background-color: oklch(22% 0.02 250) !important;
|
||||
color: oklch(85% 0.05 160) !important;
|
||||
}
|
||||
.bytemd-preview .markdown-body table tr,
|
||||
.bytemd-preview .markdown-body table tr:nth-child(2n) {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
}
|
||||
.bytemd-preview .markdown-body table td,
|
||||
.bytemd-preview .markdown-body table th {
|
||||
border-color: var(--halo-border-base) !important;
|
||||
}
|
||||
|
||||
/* ===== CodeMirror 代码编辑器 ===== */
|
||||
.CodeMirror {
|
||||
background-color: var(--halo-bg-input) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
.CodeMirror-gutters {
|
||||
background-color: var(--halo-bg-content) !important;
|
||||
border-color: var(--halo-border-light) !important;
|
||||
}
|
||||
.CodeMirror-linenumber {
|
||||
color: var(--halo-text-tertiary) !important;
|
||||
}
|
||||
.CodeMirror-cursor {
|
||||
border-color: var(--halo-text-primary) !important;
|
||||
}
|
||||
.CodeMirror-selected {
|
||||
background-color: oklch(30% 0.04 160 / 50%) !important;
|
||||
}
|
||||
.cm-s-default .cm-header,
|
||||
.cm-s-default .cm-strong,
|
||||
.cm-s-default .cm-em,
|
||||
.cm-s-default .cm-link,
|
||||
.cm-s-default .cm-url {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
.cm-s-default .cm-comment {
|
||||
color: var(--halo-text-tertiary) !important;
|
||||
}
|
||||
.cm-s-default .cm-quote {
|
||||
color: var(--halo-accent-success) !important;
|
||||
}
|
||||
.cm-s-default .cm-tag {
|
||||
color: var(--halo-accent-primary) !important;
|
||||
}
|
||||
.CodeMirror pre.CodeMirror-placeholder {
|
||||
color: var(--halo-text-tertiary) !important;
|
||||
}
|
||||
.CodeMirror-activeline-background {
|
||||
background-color: var(--halo-bg-hover) !important;
|
||||
}
|
||||
.CodeMirror-lines {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
|
||||
/* ===== ProseMirror / TipTap 编辑器主体 ===== */
|
||||
.ProseMirror,
|
||||
.tiptap,
|
||||
[class*="editor-content"] {
|
||||
color: var(--halo-text-primary);
|
||||
background-color: var(--halo-bg-input);
|
||||
}
|
||||
|
||||
/* ===== 编辑器菜单栏 ===== */
|
||||
.editor-toolbar,
|
||||
.editor-menubar,
|
||||
[class*="editor-toolbar"],
|
||||
[class*="editor-menubar"] {
|
||||
background-color: var(--halo-bg-card);
|
||||
border-color: var(--halo-border-base);
|
||||
}
|
||||
|
||||
.editor-toolbar button,
|
||||
.editor-menubar button {
|
||||
color: var(--halo-text-secondary);
|
||||
}
|
||||
|
||||
.editor-toolbar button:hover,
|
||||
.editor-menubar button:hover,
|
||||
.editor-toolbar button.is-active,
|
||||
.editor-menubar button.is-active {
|
||||
background-color: var(--halo-bg-hover);
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
|
||||
/* ===== 代码块 ===== */
|
||||
.ProseMirror pre,
|
||||
.ProseMirror code,
|
||||
.tiptap pre,
|
||||
.tiptap code {
|
||||
background-color: oklch(20% 0.015 250);
|
||||
color: oklch(85% 0.03 160);
|
||||
}
|
||||
|
||||
/* ===== Bubble menu / floating menu ===== */
|
||||
.tippy-box,
|
||||
.tiptap-bubble-menu,
|
||||
.floating-menu {
|
||||
background-color: var(--halo-bg-dropdown);
|
||||
border-color: var(--halo-border-base);
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
|
||||
/* ===== 编辑器占位符 ===== */
|
||||
.ProseMirror p.is-editor-empty:first-child::before {
|
||||
color: var(--halo-text-tertiary);
|
||||
}
|
||||
|
||||
/* ===== 链接 ===== */
|
||||
.ProseMirror a,
|
||||
.tiptap a {
|
||||
color: var(--halo-text-link);
|
||||
}
|
||||
|
||||
/* ===== 选中文字 ===== */
|
||||
.ProseMirror ::selection,
|
||||
.tiptap ::selection {
|
||||
background-color: oklch(30% 0.04 160 / 50%);
|
||||
}
|
||||
}
|
||||
/* ===== Monaco Editor(日志查看器,JS 主题优先,此处仅兜底) ===== */
|
||||
html[data-halo-theme="dark"] .monaco-editor.vs,
|
||||
html[data-halo-theme="dark"] .monaco-editor.vs .margin,
|
||||
html[data-halo-theme="dark"] .monaco-editor.vs .lines-content,
|
||||
html[data-halo-theme="dark"] .monaco-editor.vs .monaco-scrollable-element {
|
||||
background-color: var(--halo-bg-input) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .monaco-editor.vs .line-numbers {
|
||||
color: var(--halo-text-tertiary) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .monaco-editor.vs .cursor {
|
||||
border-color: var(--halo-text-primary) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .monaco-editor.vs .selected-text,
|
||||
html[data-halo-theme="dark"] .monaco-editor.vs .view-overlays .selected-text {
|
||||
background-color: oklch(30% 0.04 160 / 50%) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .monaco-editor.vs .find-widget,
|
||||
html[data-halo-theme="dark"] .monaco-editor.vs .monaco-hover {
|
||||
background-color: var(--halo-bg-dropdown) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
|
||||
/* ===== 编辑器链接(覆盖通用链接色规则) ===== */
|
||||
html[data-halo-theme="dark"] .ProseMirror a,
|
||||
html[data-halo-theme="dark"] .tiptap a {
|
||||
color: var(--halo-text-link) !important;
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
/* ============================================================
|
||||
Halo Dark Mode — FormKit 表单组件覆盖
|
||||
覆盖输入框、选择器、开关、复选框等表单元素
|
||||
============================================================ */
|
||||
|
||||
[data-halo-theme="dark"] {
|
||||
/* ===== 输入框 ===== */
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
input[type="search"],
|
||||
input[type="url"],
|
||||
input[type="tel"],
|
||||
textarea,
|
||||
select,
|
||||
.formkit-input,
|
||||
[data-formkit-input] {
|
||||
background-color: var(--halo-bg-input);
|
||||
color: var(--halo-text-primary);
|
||||
border-color: var(--halo-border-input);
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
select:focus,
|
||||
.formkit-input:focus {
|
||||
border-color: var(--halo-border-focus);
|
||||
outline-color: var(--halo-border-focus);
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder,
|
||||
.formkit-input::placeholder {
|
||||
color: var(--halo-text-tertiary);
|
||||
}
|
||||
|
||||
input:disabled,
|
||||
textarea:disabled,
|
||||
select:disabled {
|
||||
background-color: var(--halo-bg-disabled);
|
||||
color: var(--halo-text-tertiary);
|
||||
}
|
||||
|
||||
/* ===== 复选框/单选框 ===== */
|
||||
input[type="checkbox"],
|
||||
input[type="radio"],
|
||||
.formkit-checkbox,
|
||||
.formkit-radio {
|
||||
accent-color: var(--halo-accent-primary);
|
||||
}
|
||||
|
||||
/* ===== FormKit 标签和帮助文本 ===== */
|
||||
.formkit-label,
|
||||
.formkit-legend {
|
||||
color: var(--halo-text-secondary);
|
||||
}
|
||||
|
||||
.formkit-help,
|
||||
.formkit-message {
|
||||
color: var(--halo-text-tertiary);
|
||||
}
|
||||
|
||||
/* ===== FormKit 外层 ===== */
|
||||
.formkit-outer {
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
|
||||
/* ===== FormKit 前缀/后缀 ===== */
|
||||
.formkit-prefix,
|
||||
.formkit-suffix {
|
||||
background-color: var(--halo-bg-hover);
|
||||
color: var(--halo-text-secondary);
|
||||
border-color: var(--halo-border-input);
|
||||
}
|
||||
|
||||
/* ===== 选择器下拉 ===== */
|
||||
select option {
|
||||
background-color: var(--halo-bg-dropdown);
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
|
||||
/* ===== 代码块 in forms ===== */
|
||||
.formkit-code {
|
||||
background-color: var(--halo-bg-hover);
|
||||
color: var(--halo-text-primary);
|
||||
border-color: var(--halo-border-base);
|
||||
}
|
||||
}
|
||||
/* ===== 强表单覆盖(scoped 哈希类特异性更高,需 html 前缀 + !important) ===== */
|
||||
html[data-halo-theme="dark"] input:not([type="checkbox"]):not([type="radio"]):not([type="color"]),
|
||||
html[data-halo-theme="dark"] textarea,
|
||||
html[data-halo-theme="dark"] select {
|
||||
background-color: var(--halo-bg-input) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
border-color: var(--halo-border-input) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] input:not([type="checkbox"]):not([type="radio"]):not([type="color"])::placeholder,
|
||||
html[data-halo-theme="dark"] textarea::placeholder,
|
||||
html[data-halo-theme="dark"] select::placeholder {
|
||||
color: var(--halo-text-tertiary) !important;
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
/* ===== 根元素背景(html 自身携带属性,需独立选择器) ===== */
|
||||
html[data-halo-theme="dark"] {
|
||||
background-color: var(--halo-bg-body) !important;
|
||||
}
|
||||
/* ============================================================
|
||||
Halo Dark Mode — Halo 核心 BEM 类覆盖
|
||||
基于 Halo 2.25 真实 DOM 扫描(demo.halocms.site)得出的选择器
|
||||
============================================================ */
|
||||
|
||||
[data-halo-theme="dark"] {
|
||||
/* ===== 页面根背景(body 溢出时兜底,html 见文件顶部独立规则) ===== */
|
||||
body {
|
||||
background-color: var(--halo-bg-body) !important;
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
|
||||
/* ===== Toast 通知 ===== */
|
||||
.toast-container .toast-body {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
border-color: var(--halo-border-base);
|
||||
}
|
||||
|
||||
/* ===== Markdown 正文(编辑器预览/瞬间预览) ===== */
|
||||
.markdown-body {
|
||||
background-color: transparent;
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
.markdown-body p,
|
||||
.markdown-body li,
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3,
|
||||
.markdown-body h4,
|
||||
.markdown-body h5,
|
||||
.markdown-body h6,
|
||||
.markdown-body td,
|
||||
.markdown-body th,
|
||||
.markdown-body blockquote,
|
||||
.markdown-body a {
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
.markdown-body a {
|
||||
color: var(--halo-text-link);
|
||||
}
|
||||
.markdown-body code,
|
||||
.markdown-body tt {
|
||||
background-color: oklch(28% 0.02 250);
|
||||
color: oklch(85% 0.05 160);
|
||||
}
|
||||
.markdown-body pre,
|
||||
.markdown-body .highlight pre {
|
||||
background-color: oklch(20% 0.015 250);
|
||||
color: oklch(85% 0.03 160);
|
||||
}
|
||||
.markdown-body table tr,
|
||||
.markdown-body table tr:nth-child(2n) {
|
||||
background-color: var(--halo-bg-card);
|
||||
}
|
||||
.markdown-body table td,
|
||||
.markdown-body table th {
|
||||
border-color: var(--halo-border-base);
|
||||
}
|
||||
.markdown-body hr {
|
||||
background-color: var(--halo-border-base);
|
||||
}
|
||||
|
||||
/* ===== 页面顶栏 ===== */
|
||||
.page-header {
|
||||
background-color: var(--halo-bg-card);
|
||||
}
|
||||
.page-header__title-text {
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
.page-header__actions .btn,
|
||||
.page-header__actions button {
|
||||
background-color: var(--halo-bg-card);
|
||||
color: var(--halo-text-primary);
|
||||
border-color: var(--halo-border-base);
|
||||
}
|
||||
.page-header__actions .btn:hover,
|
||||
.page-header__actions button:hover {
|
||||
background-color: var(--halo-bg-hover);
|
||||
}
|
||||
.page-header__actions .btn-content {
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
|
||||
/* ===== 卡片容器(列表页、详情页主容器) ===== */
|
||||
.card-wrapper {
|
||||
background-color: var(--halo-bg-card);
|
||||
border-color: var(--halo-border-base);
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
|
||||
/* ===== 实体字段列表(文章/页面/评论列表项) ===== */
|
||||
.entity-field-title {
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
.entity-field-title-body {
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
.entity-field-description {
|
||||
color: var(--halo-text-secondary);
|
||||
}
|
||||
|
||||
/* ===== 分页 ===== */
|
||||
.pagination {
|
||||
background-color: var(--halo-bg-card);
|
||||
color: var(--halo-text-secondary);
|
||||
}
|
||||
.pagination__btn {
|
||||
background-color: var(--halo-bg-hover);
|
||||
color: var(--halo-text-secondary);
|
||||
}
|
||||
.pagination__btn:hover {
|
||||
background-color: var(--halo-bg-active);
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
.pagination__select {
|
||||
background-color: var(--halo-bg-input);
|
||||
color: var(--halo-text-primary);
|
||||
border-color: var(--halo-border-input);
|
||||
}
|
||||
|
||||
/* ===== 标签/徽章(BEM) ===== */
|
||||
.tag-wrapper,
|
||||
.tag-default,
|
||||
.tag-pill {
|
||||
background-color: var(--halo-bg-hover);
|
||||
color: var(--halo-text-secondary);
|
||||
}
|
||||
.tag-content,
|
||||
.tag-left-icon,
|
||||
.tag-right-icon {
|
||||
color: var(--halo-text-secondary);
|
||||
}
|
||||
|
||||
/* ===== 头像 ===== */
|
||||
.avatar-wrapper,
|
||||
.avatar-circle {
|
||||
background-color: var(--halo-bg-hover);
|
||||
}
|
||||
|
||||
/* ===== 按钮(默认态) ===== */
|
||||
.btn-default,
|
||||
.btn-secondary,
|
||||
.btn-sm,
|
||||
.btn-md,
|
||||
.btn-lg,
|
||||
.btn[class*="btn"]:not(.btn-primary):not(.btn-danger) {
|
||||
background-color: var(--halo-bg-card);
|
||||
color: var(--halo-text-primary);
|
||||
border-color: var(--halo-border-base);
|
||||
}
|
||||
.btn-default:hover,
|
||||
.btn-secondary:hover,
|
||||
.btn-sm:hover,
|
||||
.btn-md:hover,
|
||||
.btn-lg:hover {
|
||||
background-color: var(--halo-bg-hover);
|
||||
}
|
||||
.btn-content {
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
|
||||
/* ===== 表单容器(FormKit) ===== */
|
||||
.formkit-inner {
|
||||
background-color: var(--halo-bg-input);
|
||||
}
|
||||
.formkit-inner input,
|
||||
.formkit-inner textarea,
|
||||
.formkit-inner select {
|
||||
background-color: transparent;
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
.formkit-inner .border,
|
||||
.formkit-inner [class*="border-gray"] {
|
||||
border-color: var(--halo-border-input);
|
||||
}
|
||||
|
||||
/* ===== 下拉菜单 ===== */
|
||||
.entity-dropdown {
|
||||
background-color: var(--halo-bg-dropdown);
|
||||
color: var(--halo-text-primary);
|
||||
border-color: var(--halo-border-base);
|
||||
}
|
||||
.entity-dropdown-trigger {
|
||||
background-color: var(--halo-bg-hover);
|
||||
color: var(--halo-text-secondary);
|
||||
}
|
||||
.entity-dropdown-trigger:hover {
|
||||
background-color: var(--halo-bg-active);
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
|
||||
/* ===== 描述项(详情页列表,如用户详情) ===== */
|
||||
.description-item-wrapper {
|
||||
background-color: var(--halo-bg-card);
|
||||
border-color: var(--halo-border-light);
|
||||
}
|
||||
.description-item__label {
|
||||
color: var(--halo-text-secondary) !important;
|
||||
}
|
||||
.description-item__content {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
|
||||
/* ===== 已选指示器 ===== */
|
||||
.entity-selected-indicator {
|
||||
color: var(--halo-accent-primary);
|
||||
}
|
||||
|
||||
/* ===== 表格 ===== */
|
||||
.table,
|
||||
[class*="table-"] td,
|
||||
[class*="table-"] th {
|
||||
border-color: var(--halo-table-border);
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
|
||||
/* ===== 通用文字(UnoCSS hash 类包含 Tailwind 语义) ===== */
|
||||
.hover\:text-gray-600:hover,
|
||||
.hover\:text-gray-700:hover,
|
||||
.hover\:text-gray-500:hover,
|
||||
.text-gray-600,
|
||||
.text-gray-700,
|
||||
.text-gray-500 {
|
||||
color: var(--halo-text-secondary);
|
||||
}
|
||||
.text-gray-900,
|
||||
.text-gray-800 {
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
.text-gray-400,
|
||||
.text-gray-300 {
|
||||
color: var(--halo-text-tertiary);
|
||||
}
|
||||
|
||||
/* ===== 通用背景 ===== */
|
||||
.bg-gray-50,
|
||||
.bg-gray-100,
|
||||
.bg-white {
|
||||
background-color: var(--halo-bg-card);
|
||||
}
|
||||
.bg-gray-200 {
|
||||
background-color: var(--halo-bg-hover);
|
||||
}
|
||||
.hover\:bg-gray-50:hover,
|
||||
.hover\:bg-gray-100:hover,
|
||||
.hover\:bg-gray-200:hover {
|
||||
background-color: var(--halo-bg-hover);
|
||||
}
|
||||
|
||||
/* ===== 边框 ===== */
|
||||
.border,
|
||||
.border-gray-200,
|
||||
.border-gray-300 {
|
||||
border-color: var(--halo-border-base);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
/* ===== 侧边栏激活菜单(Halo 核心 .menu-item-title.active) ===== */
|
||||
html[data-halo-theme="dark"] .menu-item-title.active,
|
||||
html[data-halo-theme="dark"] .menu-item-title.active:hover {
|
||||
background-color: var(--halo-menu-item-active) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .menu-item-title.active::after {
|
||||
background: var(--halo-accent-primary) !important;
|
||||
}
|
||||
|
||||
/* ===== 空状态标题 ===== */
|
||||
html[data-halo-theme="dark"] .empty-wrapper .empty-title {
|
||||
color: var(--halo-text-secondary) !important;
|
||||
}
|
||||
|
||||
/* ===== 提示条(Alert) ===== */
|
||||
html[data-halo-theme="dark"] .alert-wrapper {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
border-color: var(--halo-border-base) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .alert-wrapper .alert-title,
|
||||
html[data-halo-theme="dark"] .alert-wrapper .alert-description,
|
||||
html[data-halo-theme="dark"] .alert-wrapper .alert-icon {
|
||||
color: var(--halo-text-secondary) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .alert-wrapper.alert-warning {
|
||||
border-color: oklch(40% 0.12 85) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .alert-wrapper.alert-error {
|
||||
border-color: oklch(40% 0.17 25) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .alert-wrapper.alert-success {
|
||||
border-color: oklch(40% 0.14 150) !important;
|
||||
}
|
||||
|
||||
/* ===== 实体列表标题链接(覆盖通用链接色,hover 变链接色) ===== */
|
||||
html[data-halo-theme="dark"] .entity-field-title a,
|
||||
html[data-halo-theme="dark"] .entity-field-title-body a {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .entity-field-title a:hover,
|
||||
html[data-halo-theme="dark"] .entity-field-title-body a:hover {
|
||||
color: var(--halo-text-link) !important;
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/* ============================================================
|
||||
Halo Dark Mode — 核心布局覆盖
|
||||
覆盖 BasicLayout.vue 的硬编码颜色
|
||||
============================================================ */
|
||||
|
||||
[data-halo-theme="dark"] {
|
||||
/* ===== 页面整体 ===== */
|
||||
.layout {
|
||||
background-color: var(--halo-bg-body);
|
||||
}
|
||||
|
||||
/* ===== 侧边栏 ===== */
|
||||
.sidebar {
|
||||
background-color: var(--halo-bg-sidebar);
|
||||
box-shadow: var(--halo-shadow-sm);
|
||||
}
|
||||
|
||||
.sidebar__search {
|
||||
background-color: var(--halo-search-bg);
|
||||
color: var(--halo-search-placeholder);
|
||||
}
|
||||
|
||||
.sidebar__search:hover {
|
||||
color: var(--halo-search-text);
|
||||
}
|
||||
|
||||
.sidebar__search-icon {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.sidebar__search-shortcut {
|
||||
color: var(--halo-text-tertiary);
|
||||
}
|
||||
|
||||
.sidebar__logo {
|
||||
filter: brightness(0.9) invert(0);
|
||||
}
|
||||
|
||||
.sidebar__profile {
|
||||
border-top-color: var(--halo-border-light);
|
||||
}
|
||||
|
||||
/* ===== 内容区 ===== */
|
||||
.main-content {
|
||||
background-color: var(--halo-bg-content);
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
|
||||
.main-content__footer-text {
|
||||
color: var(--halo-text-tertiary);
|
||||
}
|
||||
|
||||
.main-content__footer-link {
|
||||
color: var(--halo-text-link);
|
||||
}
|
||||
|
||||
/* ===== 页面标题 ===== */
|
||||
.page-title {
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
}
|
||||
@@ -1,383 +0,0 @@
|
||||
/* ============================================================
|
||||
Halo Dark Mode — 第三方插件页面覆盖
|
||||
针对用户服务器上已安装插件的内部 UI 残留。
|
||||
⚠️ 这些插件使用 Vue scoped 样式 ([data-v-xxx]),
|
||||
需用 !important 才能稳定覆盖。
|
||||
============================================================ */
|
||||
|
||||
[data-halo-theme="dark"] {
|
||||
/* ===== 装备管理插件 (halo-plugin-equipments) ===== */
|
||||
.equipments .group,
|
||||
.equipment-group .group,
|
||||
main .group {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
border-color: var(--halo-border-base) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
.equipment-card,
|
||||
.equipment-card__name,
|
||||
.equipment-card__desc,
|
||||
[class*="equipment"] {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
|
||||
/* ===== 日程日历插件 (plugin-calendar) ===== */
|
||||
.week-picker {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
border-color: var(--halo-border-base) !important;
|
||||
}
|
||||
.week-picker select,
|
||||
.week-picker input,
|
||||
.week-picker button {
|
||||
background-color: var(--halo-bg-input) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
border-color: var(--halo-border-input) !important;
|
||||
}
|
||||
.day-column__body {
|
||||
background-color: var(--halo-bg-content) !important;
|
||||
border-color: var(--halo-border-light) !important;
|
||||
}
|
||||
.day-column,
|
||||
.calendar-column,
|
||||
[class*="day-column"] {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
[class*="calendar"] th,
|
||||
[class*="calendar"] td {
|
||||
color: var(--halo-text-primary) !important;
|
||||
border-color: var(--halo-border-base) !important;
|
||||
}
|
||||
|
||||
/* ===== 心愿便签插件 (plugin-wishboard) ===== */
|
||||
.ah-body {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
.wb-s-card {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
border-color: var(--halo-border-base) !important;
|
||||
}
|
||||
.wb-s-card-title {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
.wb-s-card-count {
|
||||
color: var(--halo-accent-primary) !important;
|
||||
}
|
||||
.wb-s-list,
|
||||
.wb-s-list-item,
|
||||
[class*="wb-s-"] {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
|
||||
/* ===== 瞬间插件 (plugin-moments) ===== */
|
||||
.moment-editor,
|
||||
.editor-main,
|
||||
.moment-editor .card,
|
||||
.editor-main .card {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
border-color: var(--halo-border-base) !important;
|
||||
}
|
||||
.moments-content .card,
|
||||
.moments-content .preview,
|
||||
.moment-preview,
|
||||
.moment-card,
|
||||
[class*="moment-"] .card {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
.moment-author-name,
|
||||
.moment-content,
|
||||
.moment-time,
|
||||
[class*="moment"] p,
|
||||
[class*="moment"] span,
|
||||
[class*="moment"] div {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
.moment-editor input,
|
||||
.moment-editor textarea,
|
||||
.editor-main input,
|
||||
.editor-main textarea {
|
||||
background-color: var(--halo-bg-input) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
border-color: var(--halo-border-input) !important;
|
||||
}
|
||||
|
||||
/* ===== 抽奖插件 (plugin-lottery) ===== */
|
||||
[class*="lottery"] {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
|
||||
/* ===== 存储工具箱 (plugin-storage-toolkit) ===== */
|
||||
.toolkit-wrapper,
|
||||
.st-scope {
|
||||
background-color: var(--halo-bg-content) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
.st-pill-tabs,
|
||||
.stat-card.st-card,
|
||||
.panel-card.st-card,
|
||||
.st-card {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
border-color: var(--halo-border-base) !important;
|
||||
}
|
||||
.st-card h3,
|
||||
.st-card h4,
|
||||
.st-card p,
|
||||
.st-card span,
|
||||
.st-card li,
|
||||
[class*="st-"] .title,
|
||||
[class*="st-"] .name {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
[class*="st-"] {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
|
||||
/* ===== 应用市场已安装 (Halo 内置 app-store) ===== */
|
||||
.tabbar-wrapper,
|
||||
.tabbar-outline,
|
||||
[class*="tabbar"] {
|
||||
background-color: var(--halo-bg-hover) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
border-color: var(--halo-border-base) !important;
|
||||
}
|
||||
.tabbar-item,
|
||||
[class*="tabbar"] a,
|
||||
[class*="tabbar"] span {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
.tabbar-item:hover,
|
||||
[class*="tabbar"] a:hover {
|
||||
background-color: var(--halo-bg-active) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
/* 已安装应用页的筛选和卡片 */
|
||||
.app-card,
|
||||
.app-card-header,
|
||||
.app-card-body,
|
||||
[class*="app-card"] {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
border-color: var(--halo-border-base) !important;
|
||||
}
|
||||
.app-card .title,
|
||||
.app-card .name,
|
||||
.app-card .desc,
|
||||
[class*="app-card"] h3,
|
||||
[class*="app-card"] p {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
|
||||
/* ===== 文档/示例页面 (docs__box) ===== */
|
||||
.docs__box {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
border-color: var(--halo-border-base) !important;
|
||||
}
|
||||
.docs__box h3,
|
||||
.docs__box h4,
|
||||
.docs__box p,
|
||||
.docs__box li,
|
||||
.docs__box a {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
.docs__box a:hover {
|
||||
color: var(--halo-text-link) !important;
|
||||
}
|
||||
|
||||
/* ===== 通用:任何插件页面的亮色卡片兜底 ===== */
|
||||
main .bg-gray-50,
|
||||
main .bg-gray-100,
|
||||
main .bg-gray-200 {
|
||||
background-color: var(--halo-bg-hover) !important;
|
||||
}
|
||||
main .bg-white {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
}
|
||||
|
||||
/* ===== 应用卡片内筛选 tab(UnoCSS hash 类,用上下文定位) ===== */
|
||||
.card-wrapper .card-header [class*="i-"],
|
||||
.card-header [class*="i-"],
|
||||
.card-wrapper .card-body [class*="i-"],
|
||||
.card-body [class*="i-"],
|
||||
[class*="app-card"] [class*="i-"] {
|
||||
background-color: var(--halo-bg-hover) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
.card-wrapper .card-header [class*="i-"]:hover,
|
||||
.card-header [class*="i-"]:hover,
|
||||
.card-wrapper .card-body [class*="i-"]:hover,
|
||||
.card-body [class*="i-"]:hover {
|
||||
background-color: var(--halo-bg-active) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
|
||||
/* ===== 存储工具箱表格表头 (col-*) ===== */
|
||||
.col-name,
|
||||
.col-count,
|
||||
.col-size,
|
||||
.col-percent,
|
||||
[class*="col-"] {
|
||||
background-color: var(--halo-table-header-bg) !important;
|
||||
color: var(--halo-text-secondary) !important;
|
||||
}
|
||||
|
||||
/* ===== 侧边栏用户资料(Halo Pro 特有) ===== */
|
||||
.sidebar__profile .user-profile {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.user-profile__name {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
.user-profile__roles .tag-default,
|
||||
.user-profile__roles .tag-wrapper {
|
||||
background-color: var(--halo-bg-hover) !important;
|
||||
color: var(--halo-text-secondary) !important;
|
||||
}
|
||||
.user-profile__action-button {
|
||||
color: var(--halo-text-secondary) !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.user-profile__action-button:hover {
|
||||
background-color: var(--halo-bg-hover) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
}
|
||||
/* ===== AstraHub / 心愿便签(共用 ah-* UI) ===== */
|
||||
html[data-halo-theme="dark"] .ah-card,
|
||||
html[data-halo-theme="dark"] .ah-topbar,
|
||||
html[data-halo-theme="dark"] .ah-topbar-search,
|
||||
html[data-halo-theme="dark"] .ah-float-nav {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
border-color: var(--halo-border-base) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .ah-topbar-search {
|
||||
background-color: var(--halo-bg-input) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .ah-topbar-brand,
|
||||
html[data-halo-theme="dark"] .planet-hero-title {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .ah-topbar-search input {
|
||||
background-color: transparent !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .planet-links-more {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
color: var(--halo-text-secondary) !important;
|
||||
}
|
||||
|
||||
/* ===== RSS 订阅(links/rss) ===== */
|
||||
html[data-halo-theme="dark"] .subscription-panel,
|
||||
html[data-halo-theme="dark"] .feed-main,
|
||||
html[data-halo-theme="dark"] .feed-toolbar,
|
||||
html[data-halo-theme="dark"] .feed-stream,
|
||||
html[data-halo-theme="dark"] .feed-stream__footer {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
border-color: var(--halo-border-base) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .feed-status-tabs {
|
||||
background-color: var(--halo-bg-hover) !important;
|
||||
color: var(--halo-text-secondary) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .feed-brief__title,
|
||||
html[data-halo-theme="dark"] .feed-stream p,
|
||||
html[data-halo-theme="dark"] .feed-stream a,
|
||||
html[data-halo-theme="dark"] .subscription-panel a,
|
||||
html[data-halo-theme="dark"] .subscription-panel span {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
|
||||
/* ===== 私密文章插件 ===== */
|
||||
html[data-halo-theme="dark"] .focus-card,
|
||||
html[data-halo-theme="dark"] .overview-card,
|
||||
html[data-halo-theme="dark"] .list-card,
|
||||
html[data-halo-theme="dark"] .empty-state {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
border-color: var(--halo-border-base) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .overview-card h2,
|
||||
html[data-halo-theme="dark"] .page-shell h2,
|
||||
html[data-halo-theme="dark"] .overview-stats dd,
|
||||
html[data-halo-theme="dark"] .overview-stats dt {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
|
||||
/* ===== AI 评论自动处理插件 ===== */
|
||||
html[data-halo-theme="dark"] .settings-container,
|
||||
html[data-halo-theme="dark"] .setting-panel,
|
||||
html[data-halo-theme="dark"] .tabs-wrap,
|
||||
html[data-halo-theme="dark"] .sidebar-card,
|
||||
html[data-halo-theme="dark"] .list-empty,
|
||||
html[data-halo-theme="dark"] .reply-card {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
border-color: var(--halo-border-base) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .form-row,
|
||||
html[data-halo-theme="dark"] .list-col,
|
||||
html[data-halo-theme="dark"] .panel-header,
|
||||
html[data-halo-theme="dark"] .card-footer {
|
||||
background-color: var(--halo-bg-hover) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .section-header__text h3,
|
||||
html[data-halo-theme="dark"] .sidebar-header h4,
|
||||
html[data-halo-theme="dark"] .panel-header h3,
|
||||
html[data-halo-theme="dark"] .reply-card h3 {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .wake-word-tag {
|
||||
background-color: var(--halo-bg-hover) !important;
|
||||
color: var(--halo-text-secondary) !important;
|
||||
}
|
||||
|
||||
/* ===== 数据工坊 / 代码注入器等工具页语义容器 ===== */
|
||||
html[data-halo-theme="dark"] .data-studio-card-body,
|
||||
html[data-halo-theme="dark"] .injector-view-card-body,
|
||||
html[data-halo-theme="dark"] .injector-editor-container {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
border-color: var(--halo-border-base) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
|
||||
/* ===== 示例插件页 ===== */
|
||||
html[data-halo-theme="dark"] main section#plugin-starter {
|
||||
background-color: var(--halo-bg-body) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
|
||||
/* ===== 友链卡片徽章 ===== */
|
||||
html[data-halo-theme="dark"] .link-badge {
|
||||
background-color: var(--halo-bg-hover) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
border-color: var(--halo-border-base) !important;
|
||||
}
|
||||
/* ===== 日程日历插件(time-column 布局) ===== */
|
||||
html[data-halo-theme="dark"] .time-column__header,
|
||||
html[data-halo-theme="dark"] .time-column__body,
|
||||
html[data-halo-theme="dark"] [class*="time-column"] {
|
||||
background-color: var(--halo-bg-card) !important;
|
||||
color: var(--halo-text-primary) !important;
|
||||
border-color: var(--halo-border-base) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .entry-card-header__title {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
|
||||
/* ===== 存储工具箱补充类 ===== */
|
||||
html[data-halo-theme="dark"] .stat-value,
|
||||
html[data-halo-theme="dark"] .stat-card .card-title,
|
||||
html[data-halo-theme="dark"] .panel-card .card-title {
|
||||
color: var(--halo-text-primary) !important;
|
||||
}
|
||||
html[data-halo-theme="dark"] .bar-track {
|
||||
background-color: var(--halo-bg-disabled) !important;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/* ============================================================
|
||||
Halo Dark Mode — 滚动条覆盖
|
||||
OverlayScrollbars + 原生滚动条
|
||||
============================================================ */
|
||||
|
||||
[data-halo-theme="dark"] {
|
||||
/* ===== OverlayScrollbars ===== */
|
||||
.os-scrollbar .os-scrollbar-handle {
|
||||
background-color: var(--halo-scrollbar-thumb);
|
||||
}
|
||||
|
||||
.os-scrollbar .os-scrollbar-track {
|
||||
background-color: var(--halo-scrollbar-track);
|
||||
}
|
||||
|
||||
.os-scrollbar .os-scrollbar-handle:hover {
|
||||
background-color: oklch(45% 0.03 250);
|
||||
}
|
||||
|
||||
/* ===== 原生滚动条(Firefox) ===== */
|
||||
* {
|
||||
scrollbar-color: var(--halo-scrollbar-thumb) var(--halo-scrollbar-track);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
/* ===== 原生滚动条(Webkit:Chrome/Edge/Safari) ===== */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--halo-scrollbar-track);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--halo-scrollbar-thumb);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: oklch(45% 0.03 250);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: var(--halo-scrollbar-track);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/* ============================================================
|
||||
Halo Dark Mode — UnoCSS 哈希类兜底
|
||||
Halo 2.25 中 UnoCSS 同时生成 uno-* / i-* 哈希类,
|
||||
这些类随构建变化,只能按结构定位兜底,不写死哈希值。
|
||||
注意:本文件规则刻意不加 !important,便于后续语义规则覆盖。
|
||||
============================================================ */
|
||||
|
||||
/* 文字兜底:只修正文本元素,豁免表单控件 */
|
||||
html[data-halo-theme="dark"] main [class*="uno-"]:not(input):not(select):not(textarea) {
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
|
||||
/* 块级容器背景兜底:豁免交互元素与表单控件 */
|
||||
html[data-halo-theme="dark"] main [class*="uno-"]:not(button):not(a):not(input):not(select):not(textarea):not(label) {
|
||||
background-color: var(--halo-bg-card);
|
||||
border-color: var(--halo-border-base);
|
||||
}
|
||||
|
||||
/* 页面级容器使用内容区背景 */
|
||||
html[data-halo-theme="dark"] main > [class*="uno-"] {
|
||||
background-color: var(--halo-bg-content);
|
||||
}
|
||||
|
||||
/* 卡片头/卡片体内的 uno-* 区块使用 hover 级背景 */
|
||||
html[data-halo-theme="dark"] .card-header [class*="uno-"],
|
||||
html[data-halo-theme="dark"] .card-body [class*="uno-"] {
|
||||
background-color: var(--halo-bg-hover);
|
||||
border-color: var(--halo-border-light);
|
||||
}
|
||||
|
||||
/* 旧版 i-* 哈希:只做文字兜底,避免给图标类设置背景 */
|
||||
html[data-halo-theme="dark"] main [class*="i-"]:not(input):not(select):not(textarea) {
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/* ============================================================
|
||||
Halo Dark Mode — Tailwind 工具类覆盖
|
||||
覆盖 Tailwind 的颜色类和常见 UI 原子类
|
||||
============================================================ */
|
||||
|
||||
[data-halo-theme="dark"] {
|
||||
/* ===== 背景色 ===== */
|
||||
.bg-white {
|
||||
background-color: var(--halo-bg-card);
|
||||
}
|
||||
|
||||
.bg-gray-50,
|
||||
.bg-gray-100 {
|
||||
background-color: var(--halo-bg-content);
|
||||
}
|
||||
|
||||
.bg-gray-200 {
|
||||
background-color: var(--halo-bg-hover);
|
||||
}
|
||||
|
||||
/* ===== 文字色 ===== */
|
||||
.text-gray-900,
|
||||
.text-gray-800 {
|
||||
color: var(--halo-text-primary);
|
||||
}
|
||||
|
||||
.text-gray-700,
|
||||
.text-gray-600,
|
||||
.text-gray-500 {
|
||||
color: var(--halo-text-secondary);
|
||||
}
|
||||
|
||||
.text-gray-400,
|
||||
.text-gray-300 {
|
||||
color: var(--halo-text-tertiary);
|
||||
}
|
||||
|
||||
/* ===== 边框 ===== */
|
||||
.border,
|
||||
.border-gray-200,
|
||||
.border-gray-300 {
|
||||
border-color: var(--halo-border-base);
|
||||
}
|
||||
|
||||
.divide-y > :not([hidden]) ~ :not([hidden]),
|
||||
.divide-x > :not([hidden]) ~ :not([hidden]) {
|
||||
border-color: var(--halo-border-light);
|
||||
}
|
||||
|
||||
/* ===== 阴影 ===== */
|
||||
.shadow {
|
||||
box-shadow: var(--halo-shadow-base);
|
||||
}
|
||||
|
||||
.shadow-sm {
|
||||
box-shadow: var(--halo-shadow-sm);
|
||||
}
|
||||
|
||||
.shadow-lg,
|
||||
.shadow-xl {
|
||||
box-shadow: var(--halo-shadow-lg);
|
||||
}
|
||||
|
||||
/* ===== 链接 ===== */
|
||||
html[data-halo-theme="dark"] a:not([class*="text-"]) {
|
||||
color: var(--halo-text-link);
|
||||
}
|
||||
|
||||
/* ===== 通用悬停 ===== */
|
||||
.hover\:bg-gray-50:hover,
|
||||
.hover\:bg-gray-100:hover {
|
||||
background-color: var(--halo-bg-hover);
|
||||
}
|
||||
}
|
||||
+7
-104
@@ -3,114 +3,17 @@
|
||||
配色策略: OKLCH 颜色空间 | 中性色微蓝着色调 | 亮度层次替代阴影
|
||||
============================================================ */
|
||||
|
||||
/* 说明: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);
|
||||
}
|
||||
|
||||
/* ===== 深色模式 ===== */
|
||||
[data-halo-theme="dark"] {
|
||||
--halo-bg-body: oklch(14% 0.01 250);
|
||||
--halo-bg-sidebar: oklch(16% 0.015 250);
|
||||
--halo-bg-content: oklch(14% 0.01 250);
|
||||
--halo-bg-card: oklch(18% 0.015 250);
|
||||
--halo-bg-input: oklch(20% 0.015 250);
|
||||
--halo-bg-hover: oklch(24% 0.02 250);
|
||||
--halo-bg-active: oklch(28% 0.03 160);
|
||||
--halo-bg-disabled: oklch(16% 0.005 250);
|
||||
--halo-bg-tooltip: oklch(25% 0.01 250);
|
||||
--halo-bg-modal: oklch(0% 0 0 / 60%);
|
||||
--halo-bg-dropdown: oklch(20% 0.015 250);
|
||||
|
||||
--halo-text-primary: oklch(92% 0.005 250);
|
||||
--halo-text-secondary: oklch(70% 0.01 250);
|
||||
--halo-text-tertiary: oklch(50% 0.01 250);
|
||||
--halo-text-link: oklch(72% 0.14 160);
|
||||
--halo-text-inverse: oklch(14% 0.01 250);
|
||||
|
||||
--halo-border-base: oklch(28% 0.015 250);
|
||||
--halo-border-light: oklch(22% 0.01 250);
|
||||
--halo-border-input: oklch(30% 0.015 250);
|
||||
--halo-border-focus: oklch(65% 0.14 160);
|
||||
|
||||
--halo-accent-primary: oklch(60% 0.13 160);
|
||||
--halo-accent-primary-hover: oklch(66% 0.12 160);
|
||||
--halo-accent-primary-text: oklch(14% 0.02 160);
|
||||
--halo-accent-danger: oklch(50% 0.18 25);
|
||||
--halo-accent-danger-hover: oklch(56% 0.17 25);
|
||||
--halo-accent-success: oklch(58% 0.16 150);
|
||||
--halo-accent-warning: oklch(65% 0.16 85);
|
||||
|
||||
--halo-shadow-sm: 0 1px 2px oklch(0% 0 0 / 30%);
|
||||
--halo-shadow-base: 0 2px 8px oklch(0% 0 0 / 40%);
|
||||
--halo-shadow-lg: 0 4px 16px oklch(0% 0 0 / 50%);
|
||||
|
||||
--halo-scrollbar-thumb: oklch(35% 0.02 250);
|
||||
--halo-scrollbar-track: oklch(18% 0.01 250);
|
||||
|
||||
--halo-search-bg: oklch(20% 0.015 250);
|
||||
--halo-search-text: oklch(70% 0.01 250);
|
||||
--halo-search-placeholder: oklch(50% 0.01 250);
|
||||
|
||||
--halo-menu-item-hover: oklch(22% 0.02 160);
|
||||
--halo-menu-item-active: oklch(28% 0.04 160);
|
||||
--halo-menu-group-title: oklch(55% 0.01 250);
|
||||
|
||||
--halo-table-header-bg: oklch(18% 0.01 250);
|
||||
--halo-table-row-hover: oklch(22% 0.015 250);
|
||||
--halo-table-border: oklch(26% 0.015 250);
|
||||
|
||||
--halo-tag-bg: oklch(22% 0.03 160);
|
||||
--halo-tag-text: oklch(80% 0.08 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,86 +28,62 @@ 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">
|
||||
<div
|
||||
v-for="option in modeOptions"
|
||||
:key="option.value"
|
||||
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>
|
||||
<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>
|
||||
</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 {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid var(--halo-border-base);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
border-radius: 0.375rem;
|
||||
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;
|
||||
}
|
||||
|
||||
.dark-mode-settings__option:hover {
|
||||
|
||||
@@ -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