默认主题改为浅色 + 管理员可配置全局默认主题
- 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
650102cedb
commit
ae3e2fb4e8
5 files changed
+135
-7
No files matched your search
@@ -31,13 +31,15 @@ function AppProviders({ children }: { children: React.ReactNode }) {
|
||||
disableTransitionOnChange
|
||||
forcedTheme={forcedTheme}
|
||||
>
|
||||
<I18nProvider initialLocale={locale}>
|
||||
<ThemeProvider>
|
||||
<ChatProvider>
|
||||
<QueryClientProvider>{children}</QueryClientProvider>
|
||||
</ChatProvider>
|
||||
</ThemeProvider>
|
||||
</I18nProvider>
|
||||
{/* QueryClientProvider must wrap ThemeProvider because the custom
|
||||
ThemeProvider reads the admin-configured default via useQuery. */}
|
||||
<QueryClientProvider>
|
||||
<I18nProvider initialLocale={locale}>
|
||||
<ThemeProvider>
|
||||
<ChatProvider>{children}</ChatProvider>
|
||||
</ThemeProvider>
|
||||
</I18nProvider>
|
||||
</QueryClientProvider>
|
||||
</NextThemesProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,6 +65,48 @@ export async function loadPrivateSquareStatus(): Promise<PrivateSquareStatus> {
|
||||
return (await response.json()) as PrivateSquareStatus;
|
||||
}
|
||||
|
||||
export type ThemeMode = "light" | "dark";
|
||||
|
||||
export interface AppearanceSettings {
|
||||
default_theme: ThemeMode;
|
||||
}
|
||||
|
||||
export interface AppearanceUpdate {
|
||||
default_theme?: ThemeMode;
|
||||
}
|
||||
|
||||
export async function loadAppearanceSettings(): Promise<AppearanceSettings> {
|
||||
const response = await apiFetch(
|
||||
`${getBackendBaseURL()}/api/system-settings/appearance`,
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to load appearance settings: ${response.status}`,
|
||||
);
|
||||
}
|
||||
return (await response.json()) as AppearanceSettings;
|
||||
}
|
||||
|
||||
export async function updateAppearanceSettings(
|
||||
body: AppearanceUpdate,
|
||||
): Promise<AppearanceSettings> {
|
||||
const response = await apiFetch(
|
||||
`${getBackendBaseURL()}/api/system-settings/appearance`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
const detail = await response
|
||||
.json()
|
||||
.catch(() => ({ detail: "Failed to update appearance" }));
|
||||
throw new Error(detail.detail ?? "Failed to update appearance");
|
||||
}
|
||||
return (await response.json()) as AppearanceSettings;
|
||||
}
|
||||
|
||||
export async function updatePrivateSquareSettings(
|
||||
body: PrivateSquareUpdate,
|
||||
): Promise<PrivateSquareStatus> {
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import {
|
||||
loadAppearanceSettings,
|
||||
loadPrivateSquareStatus,
|
||||
loadPromptPrefixSettings,
|
||||
updateAppearanceSettings,
|
||||
updatePrivateSquareSettings,
|
||||
updatePromptPrefixSettings,
|
||||
type AppearanceUpdate,
|
||||
type PrivateSquareUpdate,
|
||||
type PromptPrefixUpdate,
|
||||
} from "./api";
|
||||
|
||||
const PROMPT_PREFIX_KEY = ["systemSettings", "promptPrefix"] as const;
|
||||
const PRIVATE_SQUARE_KEY = ["systemSettings", "privateSquare"] as const;
|
||||
const APPEARANCE_KEY = ["systemSettings", "appearance"] as const;
|
||||
|
||||
export function usePromptPrefixSettings() {
|
||||
return useQuery({
|
||||
@@ -51,3 +55,25 @@ export function useUpdatePrivateSquareSettings() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useAppearanceSettings() {
|
||||
return useQuery({
|
||||
queryKey: APPEARANCE_KEY,
|
||||
queryFn: () => loadAppearanceSettings(),
|
||||
staleTime: 60_000,
|
||||
refetchOnWindowFocus: false,
|
||||
// The provider runs even on the login page (where the user has no auth)
|
||||
// — gracefully tolerate the fetch failing rather than retrying.
|
||||
retry: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateAppearanceSettings() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: AppearanceUpdate) => updateAppearanceSettings(body),
|
||||
onSuccess: (data) => {
|
||||
queryClient.setQueryData(APPEARANCE_KEY, data);
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user