mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-04 16:14:22 +08:00
refactor(layout): 重构应用布局结构和全局副作用处理
- 移除 AppShell 组件,将布局逻辑直接集成到各层级 layout - 提取全局 Provider 到 AppProviders 组件统一管理 - 移除独立的 ThemeSync 和 QueryProvider 组件 - 将 AntThemeProvider 功能整合到 AppProviders - 更新用户状态和主题相关的 prop 传递方式 - 优化管理后台菜单结构为全局常量定义 - 迁移页面私有 hooks 到对应页面目录下 - 提取通用 UI 副作用动作为全局 hooks 以减少重复代码
This commit is contained in:
@@ -1,25 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { ProConfigProvider } from "@ant-design/pro-components";
|
||||
import { App, ConfigProvider } from "antd";
|
||||
import zhCN from "antd/locale/zh_CN";
|
||||
|
||||
import { getAntThemeConfig } from "@/lib/app-theme";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
|
||||
export function AntThemeProvider({ children }: { children: ReactNode }) {
|
||||
const theme = useThemeStore((state) => state.theme);
|
||||
const dark = theme === "dark";
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
locale={zhCN}
|
||||
theme={getAntThemeConfig(dark)}
|
||||
>
|
||||
<ProConfigProvider dark={dark}>
|
||||
<App>{children}</App>
|
||||
</ProConfigProvider>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { ProConfigProvider } from "@ant-design/pro-components";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { App, ConfigProvider } from "antd";
|
||||
import zhCN from "antd/locale/zh_CN";
|
||||
|
||||
import { getAntThemeConfig } from "@/lib/app-theme";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 30_000,
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function AppProviders({ children }: { children: ReactNode }) {
|
||||
const theme = useThemeStore((state) => state.theme);
|
||||
const hydrateUser = useUserStore((state) => state.hydrateUser);
|
||||
const dark = theme === "dark";
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.classList.toggle("dark", dark);
|
||||
document.documentElement.style.colorScheme = theme;
|
||||
}, [dark, theme]);
|
||||
|
||||
useEffect(() => {
|
||||
void hydrateUser();
|
||||
}, [hydrateUser]);
|
||||
|
||||
return (
|
||||
<ConfigProvider locale={zhCN} theme={getAntThemeConfig(dark)}>
|
||||
<ProConfigProvider dark={dark}>
|
||||
<App>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</App>
|
||||
</ProConfigProvider>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { AppTopNav } from "@/components/app-top-nav";
|
||||
import { useAiConfigStore } from "@/stores/use-ai-config-store";
|
||||
import { navigationTools, type NavigationToolSlug } from "@/lib/navigation-tools";
|
||||
|
||||
export function AppShell({ children }: { children: ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return <MainAppShell pathname={pathname}>{children}</MainAppShell>;
|
||||
}
|
||||
|
||||
function MainAppShell({ pathname, children }: { pathname: string; children: ReactNode }) {
|
||||
const config = useAiConfigStore((state) => state.config);
|
||||
const updateConfig = useAiConfigStore((state) => state.updateConfig);
|
||||
const slug = pathname.split("/").filter(Boolean)[0];
|
||||
const activeToolSlug = navigationTools.some((tool) => tool.slug === slug) ? (slug as NavigationToolSlug) : undefined;
|
||||
const isCanvasDetail = /^\/canvas\/[^/]+/.test(pathname);
|
||||
|
||||
return (
|
||||
<ShellFrame>
|
||||
<AppTopNav activeToolSlug={activeToolSlug} config={config} onConfigChange={updateConfig} hideHeader={isCanvasDetail} />
|
||||
<div className="min-h-0 flex-1 overflow-hidden">{children}</div>
|
||||
</ShellFrame>
|
||||
);
|
||||
}
|
||||
|
||||
function ShellFrame({ children }: { children: ReactNode }) {
|
||||
return <div className="flex h-dvh flex-col overflow-hidden bg-background text-foreground">{children}</div>;
|
||||
}
|
||||
@@ -126,11 +126,7 @@ export function AppTopNav({ activeToolSlug, config, onConfigChange, hideHeader =
|
||||
<div className="my-auto flex h-9 min-w-0 items-center justify-end gap-2 justify-self-end whitespace-nowrap">
|
||||
{isReady && user ? (
|
||||
<UserStatusActions
|
||||
version={appVersion}
|
||||
theme={theme}
|
||||
onThemeChange={setTheme}
|
||||
onOpenConfig={() => openConfigDialog(false)}
|
||||
userName={user.username}
|
||||
menuItems={[
|
||||
...(user.role === "admin" ? [{ key: "admin", icon: <Shield className="size-4" />, label: <Link href="/admin">管理后台</Link> }] : []),
|
||||
{ key: "logout", icon: <LogOut className="size-4" />, label: "退出登录", onClick: logout },
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 30_000,
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function QueryProvider({ children }: { children: ReactNode }) {
|
||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useSyncThemeClass } from "@/stores/use-theme-store";
|
||||
|
||||
export function ThemeSync() {
|
||||
useSyncThemeClass();
|
||||
return null;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
|
||||
export function UserSessionSync() {
|
||||
const hydrateUser = useUserStore((state) => state.hydrateUser);
|
||||
|
||||
useEffect(() => {
|
||||
void hydrateUser();
|
||||
}, [hydrateUser]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -9,12 +9,10 @@ import { GitHubLink } from "@/components/github-link";
|
||||
import { AnimatedThemeToggler } from "@/components/ui/animated-theme-toggler";
|
||||
import { VersionReleaseModal } from "@/components/version-release-modal";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ThemeName } from "@/stores/use-theme-store";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
|
||||
type UserStatusActionsProps = {
|
||||
version: string;
|
||||
theme: ThemeName;
|
||||
onThemeChange: (theme: ThemeName) => void;
|
||||
onOpenConfig?: () => void;
|
||||
showConfig?: boolean;
|
||||
userName?: string;
|
||||
@@ -34,9 +32,6 @@ type UserStatusActionsProps = {
|
||||
};
|
||||
|
||||
export function UserStatusActions({
|
||||
version,
|
||||
theme,
|
||||
onThemeChange,
|
||||
onOpenConfig,
|
||||
showConfig = true,
|
||||
userName,
|
||||
@@ -54,7 +49,12 @@ export function UserStatusActions({
|
||||
userLabel,
|
||||
iconStyle,
|
||||
}: UserStatusActionsProps) {
|
||||
const avatarText = initial || (userName?.trim()[0] || "U").toUpperCase();
|
||||
const theme = useThemeStore((state) => state.theme);
|
||||
const setTheme = useThemeStore((state) => state.setTheme);
|
||||
const storedUserName = useUserStore((state) => state.user?.username);
|
||||
const appVersion = process.env.NEXT_PUBLIC_APP_VERSION || "dev";
|
||||
const resolvedUserName = userName || storedUserName;
|
||||
const avatarText = initial || (resolvedUserName?.trim()[0] || "U").toUpperCase();
|
||||
const naturalIconClass = "inline-flex size-8 shrink-0 items-center justify-center text-stone-600 transition hover:text-stone-950 dark:text-stone-300 dark:hover:text-white [&_svg]:size-4";
|
||||
|
||||
return (
|
||||
@@ -73,13 +73,13 @@ export function UserStatusActions({
|
||||
) : null}
|
||||
<AnimatedThemeToggler
|
||||
theme={theme}
|
||||
onThemeChange={onThemeChange}
|
||||
onThemeChange={setTheme}
|
||||
className={naturalIconClass}
|
||||
style={iconStyle}
|
||||
aria-label={theme === "dark" ? "切换到浅色主题" : "切换到深色主题"}
|
||||
title={theme === "dark" ? "切换到浅色主题" : "切换到深色主题"}
|
||||
/>
|
||||
<VersionReleaseModal currentVersion={version} style={versionStyle} />
|
||||
<VersionReleaseModal currentVersion={appVersion} style={versionStyle} />
|
||||
<GitHubLink className={cn("bg-transparent hover:bg-transparent dark:hover:bg-transparent", gitHubClassName)} style={gitHubStyle} />
|
||||
<div ref={accountRef}>
|
||||
<Dropdown
|
||||
|
||||
Reference in New Issue
Block a user