From 923903b4b27ca2a1fcc18786afa8cec71e465441 Mon Sep 17 00:00:00 2001 From: CNWei Date: Sun, 1 Nov 2026 19:00:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=89=8D=E5=8F=B0=E6=98=8E=E6=9A=97?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E6=94=B9=E4=B8=BA=E6=9C=AC=E6=9C=BA=E5=81=8F?= =?UTF-8?q?=E5=A5=BD=EF=BC=8C=E4=B8=8D=E5=86=8D=E8=A6=81=E6=B1=82=E7=99=BB?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 src/theme.ts:localStorage 的 stash-theme 作为本机偏好,优先于站点默认 - 前台页头的月亮/太阳只写本机偏好,未登录也能用,刷新后保持 - 后台「页头 → 配色」与后台页头按钮改的是站点默认值;后台按钮同时清掉本机偏好, 保证点完看到的就是新默认 - 两个壳都按「本机偏好 ?? 站点默认」决定 data-scheme --- stash-web/src/AppNext.tsx | 43 ++++++++++---------- stash-web/src/components/admin/AdminNext.tsx | 10 +++-- stash-web/src/theme.ts | 35 ++++++++++++++++ 3 files changed, 62 insertions(+), 26 deletions(-) create mode 100644 stash-web/src/theme.ts diff --git a/stash-web/src/AppNext.tsx b/stash-web/src/AppNext.tsx index 2d5e95e..136c85d 100644 --- a/stash-web/src/AppNext.tsx +++ b/stash-web/src/AppNext.tsx @@ -6,7 +6,6 @@ import { SearchField, Space, Toaster, - toast, type MenuItem, } from '@stash/ui' import { useEffect, useMemo, useRef, useState } from 'react' @@ -27,10 +26,15 @@ import { useProjects } from './useProjects' import { fetchSession, logout, type SessionState } from './auth' import { fetchSettings, - saveSettings, type SettingsDocument, type SiteSettings, } from './settings' +import { + isDarkScheme, + readThemeOverride, + writeThemeOverride, + type ThemeChoice, +} from './theme' import { appDetailModel, posterDetailModel, @@ -295,30 +299,25 @@ export default function AppNext() { const keywordLower = keyword.trim().toLowerCase() - // 配色:写在 上,深色由组件库的令牌块接管 + // 配色:本机偏好优先于站点默认,写在 上, + // 深色由组件库的令牌块接管,页面与组件都不用改 + const [themeOverride, setThemeOverride] = useState(() => + readThemeOverride(), + ) + const effectiveTheme = themeOverride ?? site.site.theme + useEffect(() => { - document.documentElement.dataset.scheme = site.site.theme - }, [site.site.theme]) + document.documentElement.dataset.scheme = effectiveTheme + }, [effectiveTheme]) /** 当前实际是深色吗(auto 时按系统偏好算) */ - const isDark = - site.site.theme === 'dark' || - (site.site.theme === 'auto' && - window.matchMedia('(prefers-color-scheme: dark)').matches) + const isDark = isDarkScheme(effectiveTheme) - /** 页头的明暗切换:写回站点设置,前后台一起变;只有登录后才会显示 */ - const toggleTheme = async () => { - if (settings === null) return - const next = isDark ? 'light' : 'dark' - const result = await saveSettings(settings.version, { - ...settings.settings, - site: { ...settings.settings.site, theme: next }, - }) - if (result.ok) { - setSettings(result.document) - return - } - toast.error(result.error) + /** 页头的明暗切换:只写本机偏好,不需要登录;站点默认值在后台改 */ + const toggleTheme = () => { + const next: ThemeChoice = isDark ? 'light' : 'dark' + setThemeOverride(next) + writeThemeOverride(next) } // 站点级设置:浏览器标签标题与 favicon(空值回落到内置) diff --git a/stash-web/src/components/admin/AdminNext.tsx b/stash-web/src/components/admin/AdminNext.tsx index 8699f15..54c5ab7 100644 --- a/stash-web/src/components/admin/AdminNext.tsx +++ b/stash-web/src/components/admin/AdminNext.tsx @@ -26,6 +26,7 @@ import { useEffect, useMemo, useState } from 'react' import { changePassword, fetchSession, logout, type SessionState } from '../../auth' import { LoginDialog } from '../auth/LoginDialog' import { GroupMark as GroupGlyph } from '../common/GroupMark' +import { isDarkScheme, readThemeOverride, writeThemeOverride } from '../../theme' import { fetchSettings, saveSettings, @@ -135,7 +136,8 @@ export function AdminNext() { if (meta === undefined) return const title = meta.title.trim() if (title !== '') window.document.title = title - window.document.documentElement.dataset.scheme = meta.theme + // 本机偏好优先于站点默认,跟前台一致 + window.document.documentElement.dataset.scheme = readThemeOverride() ?? meta.theme const href = meta.favicon.trim() === '' ? '/favicon.svg' : meta.favicon.trim() let link = window.document.head.querySelector('link[rel="icon"]') if (link === null) { @@ -178,15 +180,15 @@ export function AdminNext() { toast.success('侧边栏已保存') } - /** 页头的明暗切换:写回 site.theme,前后台一起变 */ + /** 页头的明暗切换:改的是站点默认值,并清掉本机偏好,保证点完看到的就是新默认 */ const themeSetting = draft?.site.theme ?? document?.settings.site.theme ?? 'auto' - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches - const isDark = themeSetting === 'dark' || (themeSetting === 'auto' && prefersDark) + const isDark = isDarkScheme(readThemeOverride() ?? themeSetting) const toggleTheme = async () => { if (document === null || draft === null) return const next = isDark ? 'light' : 'dark' setSaving(true) + writeThemeOverride(null) const result = await saveSettings(document.version, { ...draft, site: { ...draft.site, theme: next }, diff --git a/stash-web/src/theme.ts b/stash-web/src/theme.ts new file mode 100644 index 0000000..ccc67ee --- /dev/null +++ b/stash-web/src/theme.ts @@ -0,0 +1,35 @@ +/** + * 明暗主题的「本机偏好」。 + * + * 站点设置里的 `site.theme` 是**所有人**的默认值;这里的 localStorage 是**这台浏览器** + * 的选择,读到时优先于默认值。前台页头的切换按钮只改本机偏好,不需要登录; + * 后台「页头 → 配色」改的是站点默认值。 + */ +export type ThemeChoice = 'light' | 'dark' + +const STORAGE_KEY = 'stash-theme' + +export function readThemeOverride(): ThemeChoice | null { + try { + const value = window.localStorage.getItem(STORAGE_KEY) + return value === 'light' || value === 'dark' ? value : null + } catch { + return null + } +} + +export function writeThemeOverride(value: ThemeChoice | null): void { + try { + if (value === null) window.localStorage.removeItem(STORAGE_KEY) + else window.localStorage.setItem(STORAGE_KEY, value) + } catch { + // 隐私模式下 localStorage 可能不可写,忽略即可 + } +} + +/** `light` / `dark` / `auto` 里,当前实际是不是深色 */ +export function isDarkScheme(scheme: string): boolean { + if (scheme === 'dark') return true + if (scheme === 'light') return false + return window.matchMedia('(prefers-color-scheme: dark)').matches +}