- consoles/admin/ → layouts/admin-layout/ - consoles/workbench/ → layouts/workbench-layout/ + features/chat/ - pages/ → features/ (dashboard, models, projects, not-found) - components/ → shared/components/ - hooks/ → shared/hooks/ - utils/ → shared/utils/ - 更新所有 import 路径 (src/web/ + tests/web/) - 更新开发文档 (README.md, frontend.md, architecture.md)
170 lines
4.7 KiB
TypeScript
170 lines
4.7 KiB
TypeScript
import type { ColumnsType } from "antd/es/table";
|
|
|
|
import { DeleteOutlined, EditOutlined, InboxOutlined, LoginOutlined, RedoOutlined } from "@ant-design/icons";
|
|
import { App as AntApp, Button, Popconfirm, Space, Table, Tag } from "antd";
|
|
import { useNavigate } from "react-router";
|
|
|
|
import type { Project, ProjectListResponse, ProjectStatus } from "../../../../shared/api";
|
|
|
|
interface ProjectTableProps {
|
|
data: ProjectListResponse | undefined;
|
|
loading: boolean;
|
|
onArchive: (id: string) => Promise<unknown>;
|
|
onDelete: (id: string) => Promise<unknown>;
|
|
onEdit: (project: Project) => void;
|
|
onPageChange: (page: number, pageSize: number) => void;
|
|
onRestore: (id: string) => Promise<unknown>;
|
|
page: number;
|
|
pageSize: number;
|
|
status: ProjectStatus;
|
|
}
|
|
|
|
const COLUMNS: ColumnsType<Project> = [
|
|
{ dataIndex: "name", ellipsis: true, title: "名称", width: 140 },
|
|
{ dataIndex: "description", ellipsis: true, title: "描述" },
|
|
{
|
|
align: "center",
|
|
dataIndex: "status",
|
|
render: (_value, record: Project) => {
|
|
if (record.status === "archived") {
|
|
return <Tag>已归档</Tag>;
|
|
}
|
|
return <Tag color="blue">进行中</Tag>;
|
|
},
|
|
title: "状态",
|
|
width: 90,
|
|
},
|
|
{
|
|
align: "center",
|
|
dataIndex: "createdAt",
|
|
render: (_value, record: Project) => formatDatetime(record.createdAt),
|
|
title: "创建时间",
|
|
width: 180,
|
|
},
|
|
{
|
|
align: "center",
|
|
dataIndex: "updatedAt",
|
|
render: (_value, record: Project) => formatDatetime(record.updatedAt),
|
|
title: "更新时间",
|
|
width: 180,
|
|
},
|
|
];
|
|
|
|
export function ProjectTable({
|
|
data,
|
|
loading,
|
|
onArchive,
|
|
onDelete,
|
|
onEdit,
|
|
onPageChange,
|
|
onRestore,
|
|
page,
|
|
pageSize,
|
|
status,
|
|
}: ProjectTableProps) {
|
|
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 handleRestore = async (id: string) => {
|
|
try {
|
|
await onRestore(id);
|
|
message.success("项目已恢复");
|
|
} catch (err: unknown) {
|
|
message.error((err as Error).message);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (id: string) => {
|
|
try {
|
|
await onDelete(id);
|
|
message.success("项目已永久删除");
|
|
} catch (err: unknown) {
|
|
message.error((err as Error).message);
|
|
}
|
|
};
|
|
|
|
const operationColumn: ColumnsType<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">
|
|
<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,
|
|
};
|
|
|
|
return (
|
|
<Table
|
|
columns={[...COLUMNS, operationColumn]}
|
|
dataSource={data?.items ?? []}
|
|
loading={loading}
|
|
pagination={{
|
|
current: page,
|
|
hideOnSinglePage: false,
|
|
onChange: onPageChange,
|
|
pageSize,
|
|
showSizeChanger: true,
|
|
total: data?.total ?? 0,
|
|
}}
|
|
rowKey="id"
|
|
/>
|
|
);
|
|
}
|
|
|
|
function formatDatetime(dateStr: string): string {
|
|
const d = new Date(dateStr);
|
|
const pad = (n: number) => String(n).padStart(2, "0");
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
}
|