- 抽取 ConsoleShell 共享外壳(Layout/Header/Sider/主题切换/侧边栏折叠) - Sidebar 纯化为接受 menuItems prop 的展示组件 - Admin 管理台:/ 总览 + /projects 项目管理 - Workbench 工作台:/workbench/:projectId 项目作用域 - WorkbenchProjectGate 入口守卫(loading/error/archived/不存在拦截) - ProjectContext 提供当前项目上下文 - 项目管理表格 active 行增加'进入工作台'按钮 - 项目名称 trim 后最多 10 字符(前后端一致) - Workbench 总览页展示项目 Descriptions - Header 区分:管理台显示副标题,工作台显示项目名 + 返回管理台按钮 - 28/28 前端测试通过 - 文档更新:frontend.md ConsoleShell 规范、usage.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 } 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;
|
|
}
|
|
|
|
const COLUMNS: ColumnsType<Project> = [
|
|
{ dataIndex: "name", ellipsis: true, title: "项目名称", width: 160 },
|
|
{ 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: 100,
|
|
},
|
|
{
|
|
align: "center",
|
|
dataIndex: "createdAt",
|
|
render: (_value, record: Project) => formatDatetime(record.createdAt),
|
|
title: "创建时间",
|
|
width: 185,
|
|
},
|
|
{
|
|
align: "center",
|
|
dataIndex: "updatedAt",
|
|
render: (_value, record: Project) => formatDatetime(record.updatedAt),
|
|
title: "更新时间",
|
|
width: 185,
|
|
},
|
|
];
|
|
|
|
export function ProjectTable({
|
|
data,
|
|
loading,
|
|
onArchive,
|
|
onDelete,
|
|
onEdit,
|
|
onPageChange,
|
|
onRestore,
|
|
page,
|
|
pageSize,
|
|
}: ProjectTableProps) {
|
|
const { message } = AntApp.useApp();
|
|
const navigate = useNavigate();
|
|
|
|
const handleArchive = async (id: string) => {
|
|
try {
|
|
await onArchive(id);
|
|
message.success("项目已归档");
|
|
} catch (err) {
|
|
message.error((err as Error).message);
|
|
}
|
|
};
|
|
|
|
const handleRestore = async (id: string) => {
|
|
try {
|
|
await onRestore(id);
|
|
message.success("项目已恢复");
|
|
} catch (err) {
|
|
message.error((err as Error).message);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (id: string) => {
|
|
try {
|
|
await onDelete(id);
|
|
message.success("项目已永久删除");
|
|
} catch (err) {
|
|
message.error((err as Error).message);
|
|
}
|
|
};
|
|
|
|
const operationColumn: ColumnsType<Project>[number] = {
|
|
dataIndex: "op",
|
|
fixed: "right",
|
|
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: 280,
|
|
};
|
|
|
|
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"
|
|
scroll={{ x: 900 }}
|
|
/>
|
|
);
|
|
}
|
|
|
|
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())}`;
|
|
}
|