默认主题改为浅色 + 管理员可配置全局默认主题
- system_settings 新增 AppearanceSettings(default_theme),默认为 light, 对应 GET /api/system-settings/appearance(任何人可读)+ PUT(admin 可写) - App.tsx 把 NextThemesProvider 的 defaultTheme 从 dark 改为 light,并把 QueryClientProvider 提到 ThemeProvider 外层以便后者使用 useQuery - ThemeContext 新增 localStorage 标志 strategy-theme-user-set:用户调用 toggleTheme / setTheme 时打标;每次启动若标志未设,则跟随 admin 配置的 default_theme 同步主题,admin 改动用户刷新即生效;标志一旦存在永远尊重 用户个人选择 - legacy 'theme' 键迁移视为用户显式选择,避免老用户被新默认覆盖 - frontend system-settings api/hook 增 appearance 拉取与更新 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8b2f398015
commit
c9c59b761f
6 files changed
+193
-21
No files matched your search
@@ -7,6 +7,8 @@ import React, {
|
||||
} from 'react';
|
||||
import { useTheme as useNextTheme } from 'next-themes';
|
||||
|
||||
import { useAppearanceSettings } from '@/core/system-settings/hooks';
|
||||
|
||||
// 主题类型
|
||||
export type ThemeType = 'dark' | 'light';
|
||||
|
||||
@@ -83,13 +85,29 @@ const themes: Record<ThemeType, ThemeConfig> = {
|
||||
};
|
||||
|
||||
const STORAGE_KEY = 'strategy-theme';
|
||||
// Marker that the user has explicitly toggled the theme (via the appearance
|
||||
// settings page or any future toggle UI). When this flag is absent we treat
|
||||
// any value stored in ``strategy-theme`` as one we put there ourselves while
|
||||
// syncing the admin's default — so we'll re-sync on every boot, picking up
|
||||
// admin changes. When the flag is present we never override the user.
|
||||
const EXPLICIT_FLAG_KEY = 'strategy-theme-user-set';
|
||||
|
||||
function readPersistedTheme(): ThemeType {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'dark';
|
||||
return 'light';
|
||||
}
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
return saved === 'light' ? 'light' : 'dark';
|
||||
return saved === 'dark' ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
function hasExplicitChoice(): boolean {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return localStorage.getItem(EXPLICIT_FLAG_KEY) === '1';
|
||||
}
|
||||
|
||||
function markExplicitChoice(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.setItem(EXPLICIT_FLAG_KEY, '1');
|
||||
}
|
||||
|
||||
// 主题上下文
|
||||
@@ -112,42 +130,68 @@ interface ThemeProviderProps {
|
||||
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
||||
const { resolvedTheme, setTheme: setNextTheme } = useNextTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
// Admin-configured global default. If the user hasn't explicitly chosen
|
||||
// a theme yet, this value drives the rendered theme — and re-syncs on
|
||||
// every mount so an admin change propagates without users clearing
|
||||
// localStorage by hand.
|
||||
const { data: appearance } = useAppearanceSettings();
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// 迁移 / 规范化 localStorage,避免与 next-themes 冲突导致闪回浅色
|
||||
// 迁移 / 规范化 localStorage:去掉旧的 'theme' 键,并清掉一切非法值。
|
||||
// 注意:不要在这里强制写入一个 fallback —— 那会让 next-themes 视作
|
||||
// 「用户已选过」,admin 默认就再也注入不进来了。
|
||||
useEffect(() => {
|
||||
if (!mounted) return;
|
||||
const current = localStorage.getItem(STORAGE_KEY);
|
||||
const legacy = localStorage.getItem('theme');
|
||||
|
||||
if (current !== 'light' && current !== 'dark') {
|
||||
const fallback =
|
||||
legacy === 'light' || legacy === 'dark' ? legacy : 'dark';
|
||||
setNextTheme(fallback);
|
||||
} else if (!current && legacy && (legacy === 'light' || legacy === 'dark')) {
|
||||
setNextTheme(legacy);
|
||||
if (current && current !== 'light' && current !== 'dark') {
|
||||
// Garbage value — wipe it so the boot-sync below can take over.
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
if (!current && legacy && (legacy === 'light' || legacy === 'dark')) {
|
||||
// Migrate the legacy key as the user's explicit choice (it was set
|
||||
// long enough ago that we should preserve their pick rather than
|
||||
// override with admin default).
|
||||
setNextTheme(legacy);
|
||||
markExplicitChoice();
|
||||
}
|
||||
|
||||
if (legacy) {
|
||||
localStorage.removeItem('theme');
|
||||
}
|
||||
}, [mounted, setNextTheme]);
|
||||
|
||||
// Boot-time sync: when the user has never picked a theme themselves,
|
||||
// apply whatever the admin configured. Re-runs whenever the admin
|
||||
// setting changes (so a logged-in user sees the new default after
|
||||
// refreshing without having to clear cookies).
|
||||
useEffect(() => {
|
||||
if (!mounted) return;
|
||||
if (hasExplicitChoice()) return;
|
||||
const adminDefault = appearance?.default_theme;
|
||||
if (!adminDefault) return;
|
||||
if (resolvedTheme !== adminDefault) {
|
||||
setNextTheme(adminDefault);
|
||||
}
|
||||
}, [mounted, appearance?.default_theme, resolvedTheme, setNextTheme]);
|
||||
|
||||
const theme: ThemeType = mounted
|
||||
? resolvedTheme === 'light'
|
||||
? 'light'
|
||||
: 'dark'
|
||||
? resolvedTheme === 'dark'
|
||||
? 'dark'
|
||||
: 'light'
|
||||
: readPersistedTheme();
|
||||
|
||||
const toggleTheme = () => {
|
||||
setNextTheme(theme === 'dark' ? 'light' : 'dark');
|
||||
markExplicitChoice();
|
||||
};
|
||||
|
||||
const setTheme = (newTheme: ThemeType) => {
|
||||
setNextTheme(newTheme);
|
||||
markExplicitChoice();
|
||||
};
|
||||
|
||||
const themeConfig = themes[theme];
|
||||
|
||||
Reference in new issue
Block a user