feat(ui): antd theme

This commit is contained in:
Fu Diwei 2024-12-09 17:04:02 +08:00
parent fdfe54b6da
commit 789c120fc9
6 changed files with 63 additions and 108 deletions

View File

@ -1,7 +1,7 @@
import { useLayoutEffect, useState } from "react"; import { useEffect, useLayoutEffect, useState } from "react";
import { RouterProvider } from "react-router-dom"; import { RouterProvider } from "react-router-dom";
import { useTranslation } from "react-i18next"; 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 { type Locale } from "antd/es/locale";
import AntdLocaleEnUs from "antd/locale/en_US"; import AntdLocaleEnUs from "antd/locale/en_US";
import AntdLocaleZhCN from "antd/locale/zh_CN"; import AntdLocaleZhCN from "antd/locale/zh_CN";
@ -9,18 +9,19 @@ import dayjs from "dayjs";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
import { localeNames } from "./i18n"; import { localeNames } from "./i18n";
import { useTheme } from "./hooks";
import { router } from "./router.tsx"; import { router } from "./router.tsx";
import { ThemeProvider } from "./components/ThemeProvider.tsx";
const App = () => { const RootApp = () => {
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const { theme: browserTheme } = useTheme();
const antdLocalesMap: Record<string, Locale> = { const antdLocalesMap: Record<string, Locale> = {
[localeNames.ZH]: AntdLocaleZhCN, [localeNames.ZH]: AntdLocaleZhCN,
[localeNames.EN]: AntdLocaleEnUs, [localeNames.EN]: AntdLocaleEnUs,
}; };
const [antdLocale, setAntdLocale] = useState(antdLocalesMap[i18n.language]); const [antdLocale, setAntdLocale] = useState(antdLocalesMap[i18n.language]);
const handleLanguageChanged = () => { const handleLanguageChanged = () => {
setAntdLocale(antdLocalesMap[i18n.language]); setAntdLocale(antdLocalesMap[i18n.language]);
dayjs.locale(i18n.language); dayjs.locale(i18n.language);
@ -28,22 +29,34 @@ const App = () => {
i18n.on("languageChanged", handleLanguageChanged); i18n.on("languageChanged", handleLanguageChanged);
useLayoutEffect(handleLanguageChanged, [i18n]); useLayoutEffect(handleLanguageChanged, [i18n]);
const antdThemesMap: Record<string, ThemeConfig> = {
["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 ( return (
<AntdConfigProvider <ConfigProvider
locale={antdLocale} locale={antdLocale}
theme={{ theme={{
...antdTheme,
token: { token: {
colorPrimary: "hsl(24.6 95% 53.1%)", colorPrimary: "hsl(24.6 95% 53.1%)",
}, },
}} }}
> >
<AntdApp> <App>
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme"> <RouterProvider router={router} />
<RouterProvider router={router} /> </App>
</ThemeProvider> </ConfigProvider>
</AntdApp>
</AntdConfigProvider>
); );
}; };
export default App; export default RootApp;

View File

@ -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<ThemeProviderState>(initialState);
export function ThemeProvider({ children, defaultTheme = "system", storageKey = "vite-ui-theme", ...props }: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(() => (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 (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined) throw new Error("useTheme must be used within a ThemeProvider");
return context;
};

View File

@ -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 (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100 dark:text-white" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>{t("common.theme.light")}</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>{t("common.theme.dark")}</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>{t("common.theme.system")}</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

3
ui/src/hooks/index.ts Normal file
View File

@ -0,0 +1,3 @@
import useTheme from "./use-theme";
export { useTheme };

View File

@ -0,0 +1,6 @@
import { useTheme } from "ahooks";
export default function () {
const { theme, themeMode, setThemeMode } = useTheme({ localStorageKey: "certimate-ui-theme" });
return { theme, themeMode, setThemeMode };
}

View File

@ -7,6 +7,7 @@ import {
LogOut as LogOutIcon, LogOut as LogOutIcon,
Home as HomeIcon, Home as HomeIcon,
Menu as MenuIcon, Menu as MenuIcon,
Moon as MoonIcon,
Server as ServerIcon, Server as ServerIcon,
Settings as SettingsIcon, Settings as SettingsIcon,
ShieldCheck as ShieldCheckIcon, ShieldCheck as ShieldCheckIcon,
@ -15,6 +16,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import Version from "@/components/certimate/Version"; import Version from "@/components/certimate/Version";
import { useTheme } from "@/hooks";
import { getPocketBase } from "@/repository/pocketbase"; import { getPocketBase } from "@/repository/pocketbase";
import { ConfigProvider } from "@/providers/config"; import { ConfigProvider } from "@/providers/config";
@ -25,6 +27,7 @@ const ConsoleLayout = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { token: themeToken } = theme.useToken(); const { token: themeToken } = theme.useToken();
const { theme: browserTheme } = useTheme();
const menuItems: Required<MenuProps>["items"] = [ const menuItems: Required<MenuProps>["items"] = [
{ {
@ -96,7 +99,7 @@ const ConsoleLayout = () => {
<> <>
<ConfigProvider> <ConfigProvider>
<Layout className="w-full min-h-screen"> <Layout className="w-full min-h-screen">
<Layout.Sider theme="light" width={256}> <Layout.Sider theme={browserTheme} width={256}>
<div className="flex flex-col items-center justify-between w-full h-full overflow-hidden"> <div className="flex flex-col items-center justify-between w-full h-full overflow-hidden">
<Link to="/" className="flex items-center gap-2 w-full px-4 font-semibold overflow-hidden"> <Link to="/" className="flex items-center gap-2 w-full px-4 font-semibold overflow-hidden">
<img src="/logo.svg" className="w-[36px] h-[36px]" /> <img src="/logo.svg" className="w-[36px] h-[36px]" />
@ -104,9 +107,10 @@ const ConsoleLayout = () => {
</Link> </Link>
<div className="flex-grow w-full overflow-x-hidden overflow-y-auto"> <div className="flex-grow w-full overflow-x-hidden overflow-y-auto">
<Menu <Menu
mode="vertical"
items={menuItems} items={menuItems}
mode="vertical"
selectedKeys={menuSelectedKey ? [menuSelectedKey] : []} selectedKeys={menuSelectedKey ? [menuSelectedKey] : []}
theme={browserTheme}
onSelect={({ key }) => { onSelect={({ key }) => {
setMenuSelectedKey(key); setMenuSelectedKey(key);
}} }}
@ -152,12 +156,31 @@ const ConsoleLayout = () => {
}; };
const ThemeToggleButton = ({ size }: { size?: ButtonProps["size"] }) => { const ThemeToggleButton = ({ size }: { size?: ButtonProps["size"] }) => {
// TODO: 主题切换 const { t } = useTranslation();
const items: Required<MenuProps>["items"] = [];
const { theme, setThemeMode } = useTheme();
const items: Required<MenuProps>["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 ( return (
<Dropdown menu={{ items }} trigger={["click"]}> <Dropdown menu={{ items }} trigger={["click"]}>
<Button icon={<SunIcon size={18} />} size={size} onClick={() => alert("TODO")} /> <Button icon={theme === "dark" ? <MoonIcon size={18} /> : <SunIcon size={18} />} size={size} />
</Dropdown> </Dropdown>
); );
}; };