From 789c120fc9968b919378dfcfceed9861d3408473 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 9 Dec 2024 17:04:02 +0800 Subject: [PATCH] feat(ui): antd theme --- ui/src/App.tsx | 39 ++++++++++++------ ui/src/components/ThemeProvider.tsx | 62 ----------------------------- ui/src/components/ThemeToggle.tsx | 28 ------------- ui/src/hooks/index.ts | 3 ++ ui/src/hooks/use-theme.ts | 6 +++ ui/src/pages/ConsoleLayout.tsx | 33 ++++++++++++--- 6 files changed, 63 insertions(+), 108 deletions(-) delete mode 100644 ui/src/components/ThemeProvider.tsx delete mode 100644 ui/src/components/ThemeToggle.tsx create mode 100644 ui/src/hooks/index.ts create mode 100644 ui/src/hooks/use-theme.ts diff --git a/ui/src/App.tsx b/ui/src/App.tsx index ab42081d..d1c9cf2d 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,7 +1,7 @@ -import { useLayoutEffect, useState } from "react"; +import { useEffect, useLayoutEffect, useState } from "react"; import { RouterProvider } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { App as AntdApp, ConfigProvider as AntdConfigProvider } from "antd"; +import { App, ConfigProvider, theme, type ThemeConfig } from "antd"; import { type Locale } from "antd/es/locale"; import AntdLocaleEnUs from "antd/locale/en_US"; import AntdLocaleZhCN from "antd/locale/zh_CN"; @@ -9,18 +9,19 @@ import dayjs from "dayjs"; import "dayjs/locale/zh-cn"; import { localeNames } from "./i18n"; +import { useTheme } from "./hooks"; import { router } from "./router.tsx"; -import { ThemeProvider } from "./components/ThemeProvider.tsx"; -const App = () => { +const RootApp = () => { const { i18n } = useTranslation(); + const { theme: browserTheme } = useTheme(); + const antdLocalesMap: Record = { [localeNames.ZH]: AntdLocaleZhCN, [localeNames.EN]: AntdLocaleEnUs, }; const [antdLocale, setAntdLocale] = useState(antdLocalesMap[i18n.language]); - const handleLanguageChanged = () => { setAntdLocale(antdLocalesMap[i18n.language]); dayjs.locale(i18n.language); @@ -28,22 +29,34 @@ const App = () => { i18n.on("languageChanged", handleLanguageChanged); useLayoutEffect(handleLanguageChanged, [i18n]); + const antdThemesMap: Record = { + ["light"]: { algorithm: theme.defaultAlgorithm }, + ["dark"]: { algorithm: theme.darkAlgorithm }, + }; + const [antdTheme, setAntdTheme] = useState(antdThemesMap[browserTheme]); + useEffect(() => { + setAntdTheme(antdThemesMap[browserTheme]); + + const root = window.document.documentElement; + root.classList.remove("light", "dark"); + root.classList.add(browserTheme); + }, [browserTheme]); + return ( - - - - - - - + + + + ); }; -export default App; +export default RootApp; diff --git a/ui/src/components/ThemeProvider.tsx b/ui/src/components/ThemeProvider.tsx deleted file mode 100644 index b117f623..00000000 --- a/ui/src/components/ThemeProvider.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { createContext, useContext, useEffect, useState } from "react"; - -type Theme = "dark" | "light" | "system"; - -type ThemeProviderProps = { - children: React.ReactNode; - defaultTheme?: Theme; - storageKey?: string; -}; - -type ThemeProviderState = { - theme: Theme; - setTheme: (theme: Theme) => void; -}; - -const initialState: ThemeProviderState = { - theme: "system", - setTheme: () => null, -}; - -const ThemeProviderContext = createContext(initialState); - -export function ThemeProvider({ children, defaultTheme = "system", storageKey = "vite-ui-theme", ...props }: ThemeProviderProps) { - const [theme, setTheme] = useState(() => (localStorage.getItem(storageKey) as Theme) || defaultTheme); - - useEffect(() => { - const root = window.document.documentElement; - - root.classList.remove("light", "dark"); - - if (theme === "system") { - const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; - - root.classList.add(systemTheme); - return; - } - - root.classList.add(theme); - }, [theme]); - - const value = { - theme, - setTheme: (theme: Theme) => { - localStorage.setItem(storageKey, theme); - setTheme(theme); - }, - }; - - return ( - - {children} - - ); -} - -export const useTheme = () => { - const context = useContext(ThemeProviderContext); - - if (context === undefined) throw new Error("useTheme must be used within a ThemeProvider"); - - return context; -}; diff --git a/ui/src/components/ThemeToggle.tsx b/ui/src/components/ThemeToggle.tsx deleted file mode 100644 index ef69888f..00000000 --- a/ui/src/components/ThemeToggle.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Moon, Sun } from "lucide-react"; - -import { Button } from "@/components/ui/button"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; -import { useTheme } from "./ThemeProvider"; - -export function ThemeToggle() { - const { setTheme } = useTheme(); - const { t } = useTranslation(); - - return ( - - - - - - setTheme("light")}>{t("common.theme.light")} - setTheme("dark")}>{t("common.theme.dark")} - setTheme("system")}>{t("common.theme.system")} - - - ); -} diff --git a/ui/src/hooks/index.ts b/ui/src/hooks/index.ts new file mode 100644 index 00000000..427e92cd --- /dev/null +++ b/ui/src/hooks/index.ts @@ -0,0 +1,3 @@ +import useTheme from "./use-theme"; + +export { useTheme }; diff --git a/ui/src/hooks/use-theme.ts b/ui/src/hooks/use-theme.ts new file mode 100644 index 00000000..92c2e90f --- /dev/null +++ b/ui/src/hooks/use-theme.ts @@ -0,0 +1,6 @@ +import { useTheme } from "ahooks"; + +export default function () { + const { theme, themeMode, setThemeMode } = useTheme({ localStorageKey: "certimate-ui-theme" }); + return { theme, themeMode, setThemeMode }; +} diff --git a/ui/src/pages/ConsoleLayout.tsx b/ui/src/pages/ConsoleLayout.tsx index 197cc9d9..652a7eba 100644 --- a/ui/src/pages/ConsoleLayout.tsx +++ b/ui/src/pages/ConsoleLayout.tsx @@ -7,6 +7,7 @@ import { LogOut as LogOutIcon, Home as HomeIcon, Menu as MenuIcon, + Moon as MoonIcon, Server as ServerIcon, Settings as SettingsIcon, ShieldCheck as ShieldCheckIcon, @@ -15,6 +16,7 @@ import { } from "lucide-react"; import Version from "@/components/certimate/Version"; +import { useTheme } from "@/hooks"; import { getPocketBase } from "@/repository/pocketbase"; import { ConfigProvider } from "@/providers/config"; @@ -25,6 +27,7 @@ const ConsoleLayout = () => { const { t } = useTranslation(); const { token: themeToken } = theme.useToken(); + const { theme: browserTheme } = useTheme(); const menuItems: Required["items"] = [ { @@ -96,7 +99,7 @@ const ConsoleLayout = () => { <> - +
@@ -104,9 +107,10 @@ const ConsoleLayout = () => {
{ setMenuSelectedKey(key); }} @@ -152,12 +156,31 @@ const ConsoleLayout = () => { }; const ThemeToggleButton = ({ size }: { size?: ButtonProps["size"] }) => { - // TODO: 主题切换 - const items: Required["items"] = []; + const { t } = useTranslation(); + + const { theme, setThemeMode } = useTheme(); + + const items: Required["items"] = [ + { + key: "light", + label: <>{t("common.theme.light")}, + onClick: () => setThemeMode("light"), + }, + { + key: "dark", + label: <>{t("common.theme.dark")}, + onClick: () => setThemeMode("dark"), + }, + { + key: "system", + label: <>{t("common.theme.system")}, + onClick: () => setThemeMode("system"), + }, + ]; return ( -