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:
@@ -2,12 +2,12 @@ import { DeleteOutlined, MoreOutlined } from "@ant-design/icons";
|
||||
import { Conversations } from "@ant-design/x";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { App, Spin } from "antd";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import type { Conversation } from "../../../shared/api";
|
||||
|
||||
import { useCurrentProject } from "../../layouts/workbench-layout/useCurrentProject";
|
||||
import { createConversation, deleteConversation, fetchConversations } from "../../shared/hooks/use-conversations";
|
||||
import { useCurrentProject } from "../../shared/hooks/use-current-project";
|
||||
import { useModelList } from "../../shared/hooks/use-models";
|
||||
import { ChatPanel } from "./ChatPanel";
|
||||
|
||||
@@ -25,8 +25,13 @@ export function ChatPage() {
|
||||
});
|
||||
|
||||
const { data: modelsData } = useModelList({ pageSize: 200 });
|
||||
const textModels = (modelsData?.items ?? []).filter((m) => m.capabilities.includes("text"));
|
||||
const defaultModelId = textModels[0]?.id;
|
||||
|
||||
const textModels = useMemo(
|
||||
() => (modelsData?.items ?? []).filter((m) => m.capabilities.includes("text")),
|
||||
[modelsData],
|
||||
);
|
||||
|
||||
const defaultModelId = textModels[0]?.id ?? null;
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => deleteConversation(project.id, id),
|
||||
@@ -39,10 +44,10 @@ export function ChatPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const conversations = (data?.items ?? []).map((c: Conversation) => ({
|
||||
key: c.id,
|
||||
label: c.title,
|
||||
}));
|
||||
const conversations = useMemo(
|
||||
() => (data?.items ?? []).map((c: Conversation) => ({ key: c.id, label: c.title })),
|
||||
[data],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="app-chat-page">
|
||||
@@ -54,7 +59,7 @@ export function ChatPage() {
|
||||
activeKey={activeConversationId ?? ""}
|
||||
creation={{
|
||||
onClick: () => {
|
||||
void createConversation(project.id, defaultModelId)
|
||||
void createConversation(project.id, defaultModelId ?? undefined)
|
||||
.then((conv) => {
|
||||
void queryClient.invalidateQueries({ queryKey: CONVERSATIONS_KEY });
|
||||
setActiveConversationId(conv.id);
|
||||
@@ -85,8 +90,10 @@ export function ChatPage() {
|
||||
</div>
|
||||
<ChatPanel
|
||||
conversationId={activeConversationId}
|
||||
defaultModelId={defaultModelId}
|
||||
onConversationCreated={setActiveConversationId}
|
||||
projectId={project.id}
|
||||
textModels={textModels}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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="输入消息..."
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import type { TableColumnsType } from "antd";
|
||||
|
||||
import { DeleteOutlined, EditOutlined } from "@ant-design/icons";
|
||||
import { App as AntApp, Button, Popconfirm, Space, Table, Tag } from "antd";
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
import type { Model, ModelListResponse, ProviderOption } from "../../../../shared/api";
|
||||
|
||||
@@ -31,7 +32,7 @@ function getProviderName(providerId: string, providers: ProviderOption[]): strin
|
||||
return providers.find((p) => p.id === providerId)?.name ?? providerId;
|
||||
}
|
||||
|
||||
const COLUMNS: ColumnsType<Model> = [
|
||||
const COLUMNS: TableColumnsType<Model> = [
|
||||
{ dataIndex: "name", ellipsis: true, title: "名称", width: 180 },
|
||||
{
|
||||
dataIndex: "providerId",
|
||||
@@ -65,49 +66,61 @@ export function ModelTable({
|
||||
}: ModelTableProps) {
|
||||
const { message } = AntApp.useApp();
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await onDelete(id);
|
||||
message.success("模型已删除");
|
||||
} catch (err: unknown) {
|
||||
message.error((err as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
const columnsWithProvider: ColumnsType<Model> = COLUMNS.map((col) =>
|
||||
"dataIndex" in col && col.dataIndex === "providerId"
|
||||
? {
|
||||
...col,
|
||||
render: (_value: unknown, record: Model) => getProviderName(record.providerId, providers),
|
||||
}
|
||||
: col,
|
||||
const handleDelete = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
await onDelete(id);
|
||||
message.success("模型已删除");
|
||||
} catch (err: unknown) {
|
||||
message.error((err as Error).message);
|
||||
}
|
||||
},
|
||||
[onDelete, message],
|
||||
);
|
||||
|
||||
const operationColumn: ColumnsType<Model>[number] = {
|
||||
dataIndex: "op",
|
||||
render: (_value: unknown, record: Model) => (
|
||||
<Space size="small">
|
||||
<Button icon={<EditOutlined />} onClick={() => onEdit(record)} size="small" type="link">
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
description="此操作不可恢复。"
|
||||
onConfirm={() => void handleDelete(record.id)}
|
||||
title="确认删除此模型?"
|
||||
>
|
||||
<Button danger icon={<DeleteOutlined />} size="small" type="link">
|
||||
删除
|
||||
const columnsWithProvider = useMemo<TableColumnsType<Model>>(
|
||||
() =>
|
||||
COLUMNS.map((col) =>
|
||||
"dataIndex" in col && col.dataIndex === "providerId"
|
||||
? {
|
||||
...col,
|
||||
render: (_value: unknown, record: Model) => getProviderName(record.providerId, providers),
|
||||
}
|
||||
: col,
|
||||
),
|
||||
[providers],
|
||||
);
|
||||
|
||||
const operationColumn = useMemo<TableColumnsType<Model>[number]>(
|
||||
() => ({
|
||||
dataIndex: "op",
|
||||
render: (_value: unknown, record: Model) => (
|
||||
<Space size="small">
|
||||
<Button icon={<EditOutlined />} onClick={() => onEdit(record)} size="small" type="link">
|
||||
编辑
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
title: "操作",
|
||||
width: 180,
|
||||
};
|
||||
<Popconfirm
|
||||
description="此操作不可恢复。"
|
||||
onConfirm={() => void handleDelete(record.id)}
|
||||
title="确认删除此模型?"
|
||||
>
|
||||
<Button danger icon={<DeleteOutlined />} size="small" type="link">
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
title: "操作",
|
||||
width: 180,
|
||||
}),
|
||||
[onEdit, handleDelete],
|
||||
);
|
||||
|
||||
const columns = useMemo(() => [...columnsWithProvider, operationColumn], [columnsWithProvider, operationColumn]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={[...columnsWithProvider, operationColumn]}
|
||||
columns={columns}
|
||||
dataSource={data?.items ?? []}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import type { TableColumnsType } from "antd";
|
||||
|
||||
import { DeleteOutlined, EditOutlined } from "@ant-design/icons";
|
||||
import { App as AntApp, Button, Popconfirm, Space, Table } from "antd";
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
import type { Provider, ProviderListResponse } from "../../../../shared/api";
|
||||
|
||||
@@ -21,7 +22,7 @@ const TYPE_LABELS: Record<Provider["type"], string> = {
|
||||
"openai-compatible": "OpenAI 兼容",
|
||||
};
|
||||
|
||||
const COLUMNS: ColumnsType<Provider> = [
|
||||
const COLUMNS: TableColumnsType<Provider> = [
|
||||
{ dataIndex: "name", ellipsis: true, title: "名称", width: 180 },
|
||||
{
|
||||
dataIndex: "type",
|
||||
@@ -35,40 +36,48 @@ const COLUMNS: ColumnsType<Provider> = [
|
||||
export function ProviderTable({ data, loading, onDelete, onEdit, onPageChange, page, pageSize }: ProviderTableProps) {
|
||||
const { message } = AntApp.useApp();
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await onDelete(id);
|
||||
message.success("供应商已删除");
|
||||
} catch (err: unknown) {
|
||||
message.error((err as Error).message);
|
||||
}
|
||||
};
|
||||
const handleDelete = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
await onDelete(id);
|
||||
message.success("供应商已删除");
|
||||
} catch (err: unknown) {
|
||||
message.error((err as Error).message);
|
||||
}
|
||||
},
|
||||
[onDelete, message],
|
||||
);
|
||||
|
||||
const operationColumn: ColumnsType<Provider>[number] = {
|
||||
dataIndex: "op",
|
||||
render: (_value: unknown, record: Provider) => (
|
||||
<Space size="small">
|
||||
<Button icon={<EditOutlined />} onClick={() => onEdit(record)} size="small" type="link">
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
description="该供应商下存在模型时无法删除,请先删除或迁移相关模型。"
|
||||
onConfirm={() => void handleDelete(record.id)}
|
||||
title="确认删除此供应商?"
|
||||
>
|
||||
<Button danger icon={<DeleteOutlined />} size="small" type="link">
|
||||
删除
|
||||
const operationColumn = useMemo<TableColumnsType<Provider>[number]>(
|
||||
() => ({
|
||||
dataIndex: "op",
|
||||
render: (_value: unknown, record: Provider) => (
|
||||
<Space size="small">
|
||||
<Button icon={<EditOutlined />} onClick={() => onEdit(record)} size="small" type="link">
|
||||
编辑
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
title: "操作",
|
||||
width: 180,
|
||||
};
|
||||
<Popconfirm
|
||||
description="该供应商下存在模型时无法删除,请先删除或迁移相关模型。"
|
||||
onConfirm={() => void handleDelete(record.id)}
|
||||
title="确认删除此供应商?"
|
||||
>
|
||||
<Button danger icon={<DeleteOutlined />} size="small" type="link">
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
title: "操作",
|
||||
width: 180,
|
||||
}),
|
||||
[onEdit, handleDelete],
|
||||
);
|
||||
|
||||
const columns = useMemo(() => [...COLUMNS, operationColumn], [operationColumn]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={[...COLUMNS, operationColumn]}
|
||||
columns={columns}
|
||||
dataSource={data?.items ?? []}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import type { TableColumnsType } from "antd";
|
||||
|
||||
import { DeleteOutlined, EditOutlined, InboxOutlined, LoginOutlined, RedoOutlined } from "@ant-design/icons";
|
||||
import { App as AntApp, Button, Popconfirm, Space, Table, Tag } from "antd";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
import type { Project, ProjectListResponse, ProjectStatus } from "../../../../shared/api";
|
||||
@@ -19,7 +20,7 @@ interface ProjectTableProps {
|
||||
status: ProjectStatus;
|
||||
}
|
||||
|
||||
const COLUMNS: ColumnsType<Project> = [
|
||||
const COLUMNS: TableColumnsType<Project> = [
|
||||
{ dataIndex: "name", ellipsis: true, title: "名称", width: 140 },
|
||||
{ dataIndex: "description", ellipsis: true, title: "描述" },
|
||||
{
|
||||
@@ -65,88 +66,102 @@ export function ProjectTable({
|
||||
const { message } = AntApp.useApp();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleArchive = async (id: string) => {
|
||||
try {
|
||||
await onArchive(id);
|
||||
message.success("项目已归档");
|
||||
} catch (err: unknown) {
|
||||
message.error((err as Error).message);
|
||||
}
|
||||
};
|
||||
const handleArchive = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
await onArchive(id);
|
||||
message.success("项目已归档");
|
||||
} catch (err: unknown) {
|
||||
message.error((err as Error).message);
|
||||
}
|
||||
},
|
||||
[onArchive, message],
|
||||
);
|
||||
|
||||
const handleRestore = async (id: string) => {
|
||||
try {
|
||||
await onRestore(id);
|
||||
message.success("项目已恢复");
|
||||
} catch (err: unknown) {
|
||||
message.error((err as Error).message);
|
||||
}
|
||||
};
|
||||
const handleRestore = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
await onRestore(id);
|
||||
message.success("项目已恢复");
|
||||
} catch (err: unknown) {
|
||||
message.error((err as Error).message);
|
||||
}
|
||||
},
|
||||
[onRestore, message],
|
||||
);
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await onDelete(id);
|
||||
message.success("项目已永久删除");
|
||||
} catch (err: unknown) {
|
||||
message.error((err as Error).message);
|
||||
}
|
||||
};
|
||||
const handleDelete = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
await onDelete(id);
|
||||
message.success("项目已永久删除");
|
||||
} catch (err: unknown) {
|
||||
message.error((err as Error).message);
|
||||
}
|
||||
},
|
||||
[onDelete, message],
|
||||
);
|
||||
|
||||
const operationColumn: ColumnsType<Project>[number] = {
|
||||
dataIndex: "op",
|
||||
render: (_value, record: Project) => {
|
||||
if (record.status === "active") {
|
||||
const operationColumn = useMemo<TableColumnsType<Project>[number]>(
|
||||
() => ({
|
||||
dataIndex: "op",
|
||||
render: (_value, record: Project) => {
|
||||
if (record.status === "active") {
|
||||
return (
|
||||
<Space size="small">
|
||||
<Button
|
||||
icon={<LoginOutlined />}
|
||||
onClick={() => void navigate(`/workbench/${record.id}`)}
|
||||
size="small"
|
||||
type="link"
|
||||
>
|
||||
工作台
|
||||
</Button>
|
||||
<Button icon={<EditOutlined />} onClick={() => onEdit(record)} size="small" type="link">
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
description="归档后项目将变为只读。"
|
||||
onConfirm={() => void handleArchive(record.id)}
|
||||
title="确认归档此项目?"
|
||||
>
|
||||
<Button color="orange" icon={<InboxOutlined />} size="small" variant="link">
|
||||
归档
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Space size="small">
|
||||
<Button
|
||||
icon={<LoginOutlined />}
|
||||
onClick={() => void navigate(`/workbench/${record.id}`)}
|
||||
size="small"
|
||||
type="link"
|
||||
>
|
||||
工作台
|
||||
</Button>
|
||||
<Button icon={<EditOutlined />} onClick={() => onEdit(record)} size="small" type="link">
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm onConfirm={() => void handleRestore(record.id)} title="确认恢复此项目?">
|
||||
<Button icon={<RedoOutlined />} size="small" type="link">
|
||||
恢复
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Popconfirm
|
||||
description="归档后项目将变为只读。"
|
||||
onConfirm={() => void handleArchive(record.id)}
|
||||
title="确认归档此项目?"
|
||||
description="此操作不可恢复。"
|
||||
onConfirm={() => void handleDelete(record.id)}
|
||||
title="确认永久删除此项目?"
|
||||
>
|
||||
<Button color="orange" icon={<InboxOutlined />} size="small" variant="link">
|
||||
归档
|
||||
<Button danger icon={<DeleteOutlined />} size="small" type="link">
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Space size="small">
|
||||
<Popconfirm onConfirm={() => void handleRestore(record.id)} title="确认恢复此项目?">
|
||||
<Button icon={<RedoOutlined />} size="small" type="link">
|
||||
恢复
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Popconfirm
|
||||
description="此操作不可恢复。"
|
||||
onConfirm={() => void handleDelete(record.id)}
|
||||
title="确认永久删除此项目?"
|
||||
>
|
||||
<Button danger icon={<DeleteOutlined />} size="small" type="link">
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
title: "操作",
|
||||
width: status === "active" ? 260 : 160,
|
||||
};
|
||||
},
|
||||
title: "操作",
|
||||
width: status === "active" ? 260 : 160,
|
||||
}),
|
||||
[navigate, onEdit, handleArchive, handleRestore, handleDelete, status],
|
||||
);
|
||||
|
||||
const columns = useMemo(() => [...COLUMNS, operationColumn], [operationColumn]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={[...COLUMNS, operationColumn]}
|
||||
columns={columns}
|
||||
dataSource={data?.items ?? []}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
|
||||
Reference in New Issue
Block a user