样式修改
This commit is contained in:
1 parent
284445e0c9
commit
ab6b6876aa
4 files changed
+84
-47
No files matched your search
+12
-1
@@ -1,7 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<html lang="en-US" suppressHydrationWarning>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var t = localStorage.getItem("strategy-theme");
|
||||
var root = document.documentElement;
|
||||
root.classList.remove("dark", "light");
|
||||
if (t === "light") return;
|
||||
root.classList.add("dark");
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CM 助手</title>
|
||||
</head>
|
||||
|
||||
@@ -25,7 +25,9 @@ function AppProviders({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<NextThemesProvider
|
||||
attribute="class"
|
||||
enableSystem
|
||||
storageKey="strategy-theme"
|
||||
defaultTheme="dark"
|
||||
enableSystem={false}
|
||||
disableTransitionOnChange
|
||||
forcedTheme={forcedTheme}
|
||||
>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { MonitorSmartphoneIcon, MoonIcon, SunIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import { useMemo, type ComponentType, type SVGProps } from "react";
|
||||
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -27,18 +28,12 @@ const languageOptions: { value: Locale; label: string }[] = [
|
||||
|
||||
export function AppearanceSettingsPage() {
|
||||
const { t, locale, changeLocale } = useI18n();
|
||||
const { theme, setTheme, systemTheme } = useTheme();
|
||||
const currentTheme = (theme ?? "system") as "system" | "light" | "dark";
|
||||
const { theme, setTheme } = useTheme();
|
||||
const currentTheme = theme;
|
||||
const [settings, setSettings] = useLocalSettings();
|
||||
|
||||
const themeOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: "system",
|
||||
label: t.settings.appearance.system,
|
||||
description: t.settings.appearance.systemDescription,
|
||||
icon: MonitorSmartphoneIcon,
|
||||
},
|
||||
{
|
||||
id: "light",
|
||||
label: t.settings.appearance.light,
|
||||
@@ -57,8 +52,6 @@ export function AppearanceSettingsPage() {
|
||||
t.settings.appearance.darkDescription,
|
||||
t.settings.appearance.light,
|
||||
t.settings.appearance.lightDescription,
|
||||
t.settings.appearance.system,
|
||||
t.settings.appearance.systemDescription,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -68,7 +61,7 @@ export function AppearanceSettingsPage() {
|
||||
title={t.settings.appearance.themeTitle}
|
||||
description={t.settings.appearance.themeDescription}
|
||||
>
|
||||
<div className="grid gap-3 lg:grid-cols-3">
|
||||
<div className="grid gap-3 lg:grid-cols-2">
|
||||
{themeOptions.map((option) => (
|
||||
<ThemePreviewCard
|
||||
key={option.id}
|
||||
@@ -76,8 +69,7 @@ export function AppearanceSettingsPage() {
|
||||
label={option.label}
|
||||
description={option.description}
|
||||
active={currentTheme === option.id}
|
||||
mode={option.id as "system" | "light" | "dark"}
|
||||
systemTheme={systemTheme}
|
||||
mode={option.id as "light" | "dark"}
|
||||
onSelect={(value) => setTheme(value)}
|
||||
/>
|
||||
))}
|
||||
@@ -144,19 +136,16 @@ function ThemePreviewCard({
|
||||
description,
|
||||
active,
|
||||
mode,
|
||||
systemTheme,
|
||||
onSelect,
|
||||
}: {
|
||||
icon: ComponentType<SVGProps<SVGSVGElement>>;
|
||||
label: string;
|
||||
description: string;
|
||||
active: boolean;
|
||||
mode: "system" | "light" | "dark";
|
||||
systemTheme?: string;
|
||||
onSelect: (mode: "system" | "light" | "dark") => void;
|
||||
mode: "light" | "dark";
|
||||
onSelect: (mode: "light" | "dark") => void;
|
||||
}) {
|
||||
const previewMode =
|
||||
mode === "system" ? (systemTheme === "dark" ? "dark" : "light") : mode;
|
||||
const previewMode = mode;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { useTheme as useNextTheme } from 'next-themes';
|
||||
|
||||
// 主题类型
|
||||
@@ -76,12 +82,24 @@ const themes: Record<ThemeType, ThemeConfig> = {
|
||||
},
|
||||
};
|
||||
|
||||
const STORAGE_KEY = 'strategy-theme';
|
||||
|
||||
function readPersistedTheme(): ThemeType {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'dark';
|
||||
}
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
return saved === 'light' ? 'light' : 'dark';
|
||||
}
|
||||
|
||||
// 主题上下文
|
||||
interface ThemeContextType {
|
||||
theme: ThemeType;
|
||||
themeConfig: ThemeConfig;
|
||||
toggleTheme: () => void;
|
||||
setTheme: (theme: ThemeType) => void;
|
||||
/** next-themes 完成挂载前为 false,避免 hydration 与 DOM 不同步 */
|
||||
mounted: boolean;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
@@ -92,35 +110,52 @@ interface ThemeProviderProps {
|
||||
}
|
||||
|
||||
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
||||
const { setTheme: setNextTheme } = useNextTheme();
|
||||
const [theme, setThemeState] = useState<ThemeType>(() => {
|
||||
// 从 localStorage 读取保存的主题
|
||||
const saved = localStorage.getItem('strategy-theme');
|
||||
return (saved as ThemeType) || 'dark';
|
||||
});
|
||||
const { resolvedTheme, setTheme: setNextTheme } = useNextTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
// 切换主题
|
||||
const toggleTheme = () => {
|
||||
setThemeState((prev) => (prev === 'dark' ? 'light' : 'dark'));
|
||||
};
|
||||
|
||||
// 设置特定主题
|
||||
const setTheme = (newTheme: ThemeType) => {
|
||||
setThemeState(newTheme);
|
||||
};
|
||||
|
||||
// 保存到 localStorage,并同步 html class 与 next-themes
|
||||
useEffect(() => {
|
||||
localStorage.setItem('strategy-theme', theme);
|
||||
document.documentElement.classList.remove('dark', 'light');
|
||||
document.documentElement.classList.add(theme);
|
||||
setNextTheme(theme);
|
||||
}, [theme, setNextTheme]);
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// 迁移 / 规范化 localStorage,避免与 next-themes 冲突导致闪回浅色
|
||||
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 (legacy) {
|
||||
localStorage.removeItem('theme');
|
||||
}
|
||||
}, [mounted, setNextTheme]);
|
||||
|
||||
const theme: ThemeType = mounted
|
||||
? resolvedTheme === 'light'
|
||||
? 'light'
|
||||
: 'dark'
|
||||
: readPersistedTheme();
|
||||
|
||||
const toggleTheme = () => {
|
||||
setNextTheme(theme === 'dark' ? 'light' : 'dark');
|
||||
};
|
||||
|
||||
const setTheme = (newTheme: ThemeType) => {
|
||||
setNextTheme(newTheme);
|
||||
};
|
||||
|
||||
const themeConfig = themes[theme];
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, themeConfig, toggleTheme, setTheme }}>
|
||||
<ThemeContext.Provider
|
||||
value={{ theme, themeConfig, toggleTheme, setTheme, mounted }}
|
||||
>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user