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

@@ -4,7 +4,7 @@ import { Sender } from "@ant-design/x";
import { useQueryClient } from "@tanstack/react-query";
import { DefaultChatTransport, type UIMessage } from "ai";
import { App, Button, Card, Divider, Flex, Input, Select, Spin, Typography } from "antd";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
createConversation,
@@ -13,7 +13,6 @@ import {
updateConversation,
} from "../../shared/hooks/use-conversations";
import { useLogger } from "../../shared/hooks/use-logger";
import { useModelList } from "../../shared/hooks/use-models";
import { ChatScrollArea } from "./ChatScrollArea";
import { ReasoningPart } from "./parts/ReasoningPart";
import { TextPart } from "./parts/TextPart";
@@ -21,11 +20,19 @@ import { ToolPart } from "./parts/ToolPart";
interface ChatPanelProps {
conversationId: null | string;
defaultModelId: null | string;
onConversationCreated: (id: string) => void;
projectId: string;
textModels: Array<{ id: string; name: string }>;
}
export function ChatPanel({ conversationId, onConversationCreated, projectId }: ChatPanelProps) {
export function ChatPanel({
conversationId,
defaultModelId: _defaultModelId,
onConversationCreated,
projectId,
textModels,
}: ChatPanelProps) {
const { message } = App.useApp();
const logger = useLogger({ component: "ChatPanel", page: "workbench" });
const queryClient = useQueryClient();
@@ -37,12 +44,6 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
const fetchRef = useRef(fetchMessages);
const skipHistoryLoadRef = useRef<null | string>(null);
const { data: modelsData } = useModelList({ pageSize: 200 });
const textModels = useMemo(
() => (modelsData?.items ?? []).filter((m) => m.capabilities.includes("text")),
[modelsData],
);
const modelOptions = useMemo(() => textModels.map((m) => ({ label: m.name, value: m.id })), [textModels]);
const { messages, regenerate, sendMessage, setMessages, status, stop } = useChat({
@@ -178,6 +179,33 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
[sendMessage, conversationId, projectId, onConversationCreated, message, queryClient, displayModelId, logger],
);
const renderSenderFooter = useCallback(
(actionNode: ReactNode) => (
<Flex align="center" justify="space-between">
<Select
className="chat-model-select"
disabled={isLoading}
onChange={handleModelChange}
options={modelOptions}
placeholder="选择模型"
value={displayModelId}
/>
<Flex align="center">
<Divider orientation="vertical" />
{actionNode}
</Flex>
</Flex>
),
[isLoading, handleModelChange, modelOptions, displayModelId],
);
const handleStop = useCallback(() => {
void stop().catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
logger.warn("停止聊天失败", { error: msg });
});
}, [stop, logger]);
const extractText = useCallback((msg: UIMessage) => {
return msg.parts
.filter((p) => p.type === "text")
@@ -304,29 +332,9 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
<Sender
autoSize={{ maxRows: 3, minRows: 1 }}
classNames={{ root: "chat-sender-box" }}
footer={(actionNode) => (
<Flex align="center" justify="space-between">
<Select
className="chat-model-select"
disabled={isLoading}
onChange={handleModelChange}
options={modelOptions}
placeholder="选择模型"
value={displayModelId}
/>
<Flex align="center">
<Divider orientation="vertical" />
{actionNode}
</Flex>
</Flex>
)}
footer={renderSenderFooter}
loading={isLoading}
onCancel={() => {
void stop().catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
logger.warn("停止聊天失败", { error: msg });
});
}}
onCancel={handleStop}
onChange={setInput}
onSubmit={handleSenderSubmit}
placeholder="输入消息..."
@@ -397,29 +405,9 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
<Sender
autoSize={{ maxRows: 3, minRows: 1 }}
classNames={{ root: "chat-sender-box" }}
footer={(actionNode) => (
<Flex align="center" justify="space-between">
<Select
className="chat-model-select"
disabled={isLoading}
onChange={handleModelChange}
options={modelOptions}
placeholder="选择模型"
value={displayModelId}
/>
<Flex align="center">
<Divider orientation="vertical" />
{actionNode}
</Flex>
</Flex>
)}
footer={renderSenderFooter}
loading={isLoading}
onCancel={() => {
void stop().catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
logger.warn("停止聊天失败", { error: msg });
});
}}
onCancel={handleStop}
onChange={setInput}
onSubmit={handleSenderSubmit}
placeholder="输入消息..."