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:
2026-06-03 11:32:28 +08:00
parent 297293cb61
commit 5b09a16bc3
18 changed files with 342 additions and 245 deletions

View File

@@ -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>
);
}

View File

@@ -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">

View 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>
);
}

View 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;
}

View File

@@ -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";
}

View File

@@ -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]);
}