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