refactor(layout): 重构应用布局结构和全局副作用处理

- 移除 AppShell 组件,将布局逻辑直接集成到各层级 layout
- 提取全局 Provider 到 AppProviders 组件统一管理
- 移除独立的 ThemeSync 和 QueryProvider 组件
- 将 AntThemeProvider 功能整合到 AppProviders
- 更新用户状态和主题相关的 prop 传递方式
- 优化管理后台菜单结构为全局常量定义
- 迁移页面私有 hooks 到对应页面目录下
- 提取通用 UI 副作用动作为全局 hooks 以减少重复代码
This commit is contained in:
HouYunFei
2026-05-21 11:42:22 +08:00
parent dce6ab2282
commit 603deee962
25 changed files with 151 additions and 257 deletions
-25
View File
@@ -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>
);
}
+47
View File
@@ -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>
);
}
-33
View File
@@ -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>;
}
-4
View File
@@ -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 },
-18
View File
@@ -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>;
}
-8
View File
@@ -1,8 +0,0 @@
"use client";
import { useSyncThemeClass } from "@/stores/use-theme-store";
export function ThemeSync() {
useSyncThemeClass();
return null;
}
-15
View File
@@ -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;
}
+10 -10
View File
@@ -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