refactor(web): React 最佳实践优化 — memo/callback + 目录边界 + 路由增强
- useLogger: useMemo + JSON.stringify 替代 useState 派生 - useIsDark: effectiveTheme 替代 token 色值比较 - useCurrentProject: layouts/ 提升到 shared/hooks/ - ConsoleShell: locale useMemo 缓存 - ConsoleOutlet: 添加 Suspense 边界 - routes: 添加 layout 级 errorElement - Table 组件: operationColumn useMemo + useCallback - ChatPanel: footer 合并为 useCallback, props 传入模型数据 - ChatPage: textModels/conversations useMemo 缓存
This commit is contained in:
@@ -1,5 +1,17 @@
|
||||
import { Spin } from "antd";
|
||||
import { Suspense } from "react";
|
||||
import { Outlet } from "react-router";
|
||||
|
||||
export function ConsoleOutlet() {
|
||||
return <Outlet />;
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="app-loading">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { XProvider } from "@ant-design/x";
|
||||
import zhCN_X from "@ant-design/x/locale/zh_CN";
|
||||
import { App as AntApp, Layout, Segmented, theme } from "antd";
|
||||
import zhCN from "antd/locale/zh_CN";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import type { ConsoleShellProps } from "./types";
|
||||
|
||||
@@ -28,9 +29,10 @@ export function ConsoleShell({ headerExtra, menuItems, title }: ConsoleShellProp
|
||||
|
||||
const versionDisplay = meta?.version ? `v${meta.version}` : null;
|
||||
const themeAlgorithm = effectiveTheme === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm;
|
||||
const locale = useMemo(() => ({ ...zhCN, ...zhCN_X }), []);
|
||||
|
||||
return (
|
||||
<XProvider locale={{ ...zhCN, ...zhCN_X }} theme={{ algorithm: themeAlgorithm }}>
|
||||
<XProvider locale={locale} theme={{ algorithm: themeAlgorithm }}>
|
||||
<AntApp>
|
||||
<Layout className="app-layout">
|
||||
<Header className="app-header">
|
||||
|
||||
29
src/web/shared/components/RouteError.tsx
Normal file
29
src/web/shared/components/RouteError.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Alert, Button } from "antd";
|
||||
import { isRouteErrorResponse, useNavigate, useRouteError } from "react-router";
|
||||
|
||||
export function RouteError() {
|
||||
const error = useRouteError();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const message = isRouteErrorResponse(error)
|
||||
? `${error.status} ${error.statusText}`
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: "未知错误";
|
||||
|
||||
return (
|
||||
<div className="app-loading">
|
||||
<Alert
|
||||
action={
|
||||
<Button onClick={() => void navigate("/")} size="small" type="primary">
|
||||
返回首页
|
||||
</Button>
|
||||
}
|
||||
description={message}
|
||||
showIcon
|
||||
title="页面加载出错"
|
||||
type="error"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
src/web/shared/hooks/use-current-project.ts
Normal file
13
src/web/shared/hooks/use-current-project.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
import type { Project } from "../../../shared/api";
|
||||
|
||||
export const ProjectContext = createContext<null | Project>(null);
|
||||
|
||||
export function useCurrentProject(): Project {
|
||||
const project = useContext(ProjectContext);
|
||||
if (!project) {
|
||||
throw new Error("useCurrentProject 必须在 Workbench 项目上下文内使用");
|
||||
}
|
||||
return project;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { theme } from "antd";
|
||||
import { useThemePreference } from "./use-theme-preference";
|
||||
|
||||
export function useIsDark(): boolean {
|
||||
const { token } = theme.useToken();
|
||||
return token.colorBgBase === "#000";
|
||||
const { effectiveTheme } = useThemePreference();
|
||||
return effectiveTheme === "dark";
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { App } from "antd";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import type { Logger } from "../utils/logger";
|
||||
|
||||
@@ -7,19 +7,12 @@ import { AntdMessageSink, ConsoleSink, createDefaultLogger } from "../utils/logg
|
||||
|
||||
export function useLogger(bindings?: Record<string, unknown>): Logger {
|
||||
const { message } = App.useApp();
|
||||
const [stableJson, setStableJson] = useState(() => JSON.stringify(bindings ?? {}));
|
||||
const [stableBindings, setStableBindings] = useState(() => bindings);
|
||||
const currentJson = JSON.stringify(bindings ?? {});
|
||||
|
||||
if (currentJson !== stableJson) {
|
||||
setStableJson(currentJson);
|
||||
setStableBindings(bindings);
|
||||
}
|
||||
const bindingsKey = JSON.stringify(bindings ?? {});
|
||||
|
||||
return useMemo(() => {
|
||||
const isProduction = !!import.meta.env["PROD"];
|
||||
const base = createDefaultLogger([new ConsoleSink(isProduction), new AntdMessageSink(message)], isProduction);
|
||||
if (!stableBindings || Object.keys(stableBindings).length === 0) return base;
|
||||
return base.child(stableBindings);
|
||||
}, [message, stableBindings]);
|
||||
if (bindingsKey === "{}") return base;
|
||||
return base.child(JSON.parse(bindingsKey) as Record<string, unknown>);
|
||||
}, [message, bindingsKey]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user