fix: 按审查报告修复 P2/P3 问题并升级 1.0.5

This commit is contained in:
2026-08-08 18:15:11 +08:00
parent b6403b7536
commit d95d17234e
20 changed files with 457 additions and 439 deletions
+19 -4
View File
@@ -7,13 +7,19 @@ const { isDark, toggle } = useDarkMode()
</script>
<template>
<div class="theme-toggle" @click="toggle">
<button
type="button"
class="theme-toggle"
:aria-pressed="isDark"
:aria-label="isDark ? '切换到浅色模式' : '切换到深色模式'"
@click="toggle"
>
<IconSunLine v-if="isDark" class="theme-toggle__icon" />
<IconMoonLine v-else class="theme-toggle__icon" />
<span class="theme-toggle__label">
{{ isDark ? '浅色模式' : '深色模式' }}
</span>
</div>
</button>
</template>
<style scoped>
@@ -21,9 +27,13 @@ const { isDark, toggle } = useDarkMode()
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.5rem 0.75rem;
cursor: pointer;
border: 0;
background: transparent;
color: var(--halo-text-secondary);
font: inherit;
cursor: pointer;
border-radius: 0.25rem;
transition: background-color 0.15s, color 0.15s;
user-select: none;
@@ -34,6 +44,11 @@ const { isDark, toggle } = useDarkMode()
color: var(--halo-text-primary);
}
.theme-toggle:focus-visible {
outline: 2px solid var(--halo-accent-primary);
outline-offset: 2px;
}
.theme-toggle__icon {
width: 1.125rem;
height: 1.125rem;
@@ -44,4 +59,4 @@ const { isDark, toggle } = useDarkMode()
font-size: 0.8125rem;
white-space: nowrap;
}
</style>
</style>
@@ -0,0 +1,106 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
const STORAGE_KEY = 'halo-dark-mode-theme'
function mockMatchMedia(matches: boolean): void {
window.matchMedia = vi.fn().mockImplementation((query: string) => ({
matches,
media: query,
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
addListener: vi.fn(),
removeListener: vi.fn(),
dispatchEvent: vi.fn(),
}))
}
describe('useDarkMode', () => {
let useDarkMode: typeof import('../useDarkMode').useDarkMode
async function load(systemDark = false): Promise<void> {
vi.resetModules()
localStorage.clear()
mockMatchMedia(systemDark)
const mod = await import('../useDarkMode')
useDarkMode = mod.useDarkMode
}
beforeEach(async () => {
await load(false)
})
it('默认 auto 且系统为深色时 isDark 为 true', async () => {
await load(true)
const { theme, isDark } = useDarkMode()
expect(theme.value).toBe('auto')
expect(isDark.value).toBe(true)
})
it('auto 模式下点击切换变为显式深色并持久化', async () => {
const { theme, toggle } = useDarkMode()
toggle()
expect(theme.value).toBe('dark')
await nextTick()
expect(localStorage.getItem(STORAGE_KEY)).toBe('dark')
})
it('auto 且系统为深色时点击切换变为显式浅色', async () => {
await load(true)
const { theme, toggle } = useDarkMode()
toggle()
expect(theme.value).toBe('light')
await nextTick()
expect(localStorage.getItem(STORAGE_KEY)).toBe('light')
})
it('dark 与 light 之间往返切换', async () => {
const { theme, toggle } = useDarkMode()
theme.value = 'dark'
toggle()
expect(theme.value).toBe('light')
toggle()
expect(theme.value).toBe('dark')
await nextTick()
expect(localStorage.getItem(STORAGE_KEY)).toBe('dark')
})
it('setTheme 会持久化到 localStorage', async () => {
const { setTheme } = useDarkMode()
setTheme('dark')
await nextTick()
expect(localStorage.getItem(STORAGE_KEY)).toBe('dark')
})
it('localStorage 非法值回退到 auto', async () => {
localStorage.setItem(STORAGE_KEY, 'not-a-theme')
await load(false)
const { theme } = useDarkMode()
expect(theme.value).toBe('auto')
})
it('storage 事件会同步其他标签页的主题', () => {
const { theme, setTheme } = useDarkMode()
setTheme('light')
window.dispatchEvent(
new StorageEvent('storage', {
key: STORAGE_KEY,
newValue: 'dark',
}),
)
expect(theme.value).toBe('dark')
})
it('storage 事件忽略非法值', () => {
const { theme, setTheme } = useDarkMode()
setTheme('light')
window.dispatchEvent(
new StorageEvent('storage', {
key: STORAGE_KEY,
newValue: 'invalid',
}),
)
expect(theme.value).toBe('light')
})
})
+19 -4
View File
@@ -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') {
@@ -83,4 +98,4 @@ export function useDarkMode() {
setTheme,
toggle,
}
}
}
+1
View File
@@ -6,6 +6,7 @@ import { useDarkMode } from './composables/useDarkMode'
* Dark Reader 引擎参数。
* 纯 Dark Reader 策略下不再维护手工 CSS 覆盖;
* 这里只调整通用观感参数,让核心与第三方插件页面转换后风格统一。
* 注意:DR 注入为异步过程,刷新闪烁只能由 useDarkMode 的 color-scheme 同步缓解。
*/
const DARK_READER_THEME = {
brightness: 100,
+1
View File
@@ -3,6 +3,7 @@
配色策略: OKLCH 颜色空间 | 中性色微蓝着色调 | 亮度层次替代阴影
============================================================ */
/* 说明:data-halo-theme 为兼容性遗留标记,当前已无 CSS 消费方,保留给外部脚本与验证工具。 */
/* ===== 浅色模式(Halo 默认,此处定义为显式回退) ===== */
:root {
--halo-bg-body: oklch(97% 0.005 250);
+22 -5
View File
@@ -31,17 +31,24 @@ const modeOptions: { value: ThemeMode; label: string; description: string }[] =
当前生效<strong>{{ currentEffectiveMode }}</strong>
</div>
<div class="dark-mode-settings__options">
<div
<div
class="dark-mode-settings__options"
role="radiogroup"
aria-label="主题模式"
>
<button
v-for="option in modeOptions"
:key="option.value"
type="button"
role="radio"
class="dark-mode-settings__option"
:class="{ 'is-active': theme === option.value }"
:aria-checked="theme === option.value"
@click="setTheme(option.value)"
>
<div class="dark-mode-settings__option-label">{{ option.label }}</div>
<div class="dark-mode-settings__option-desc">{{ option.description }}</div>
</div>
</button>
</div>
</div>
</div>
@@ -95,10 +102,15 @@ const modeOptions: { value: ThemeMode; label: string; description: string }[] =
}
.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;
}
@@ -106,6 +118,11 @@ const modeOptions: { value: ThemeMode; label: string; description: string }[] =
background-color: var(--halo-bg-hover);
}
.dark-mode-settings__option:focus-visible {
outline: 2px solid var(--halo-accent-primary);
outline-offset: 2px;
}
.dark-mode-settings__option.is-active {
background-color: var(--halo-menu-item-active);
border-color: var(--halo-accent-primary);
@@ -122,4 +139,4 @@ const modeOptions: { value: ThemeMode; label: string; description: string }[] =
font-size: 0.8125rem;
color: var(--halo-text-tertiary);
}
</style>
</style>