feat: Admin/Workbench 双入口架构

- 抽取 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 双入口说明
This commit is contained in:
2026-05-28 22:33:03 +08:00
parent d33eb00377
commit 6cb378d7cb
26 changed files with 618 additions and 120 deletions

View File

@@ -0,0 +1,113 @@
import { screen, waitFor } from "@testing-library/react";
import { describe, expect, test } from "bun:test";
import { createElement } from "react";
import { App } from "../../../src/web/app";
import { ProjectProvider } from "../../../src/web/consoles/workbench/ProjectContext";
import { WorkbenchOverviewPage } from "../../../src/web/pages/workbench";
import { renderWithProviders } from "../test-utils";
const MOCK_PROJECT = {
archivedAt: null,
createdAt: "2024-01-01T00:00:00.000Z",
description: "测试项目",
id: "test-project-id",
name: "测试项目",
status: "active" as const,
updatedAt: "2024-01-01T00:00:00.000Z",
};
function createMockHandler(overrides?: { archivedAt?: string; status?: "active" | "archived" }) {
const project = { ...MOCK_PROJECT, ...overrides };
const handler = (input: RequestInfo | URL) => {
const url = input instanceof Request ? input.url : typeof input === "string" ? input : input.toString();
if (url.includes("/api/meta")) {
return new Response(
JSON.stringify({ ok: true, service: "test-app", timestamp: new Date().toISOString(), version: "0.1.0" }),
{ headers: { "Content-Type": "application/json" }, status: 200 },
);
}
if (url.includes(`/api/projects/${project.id}`)) {
return new Response(JSON.stringify({ project }), {
headers: { "Content-Type": "application/json" },
status: 200,
});
}
return new Response(JSON.stringify({ error: "Not Found" }), { status: 404 });
};
const mocked = handler as unknown as typeof fetch;
globalThis.fetch = mocked;
window.fetch = mocked;
}
describe("Workbench 路由", () => {
test("active 项目可进入 Workbench 并展示总览", async () => {
createMockHandler();
renderWithProviders(createElement(App), {
initialRoute: `/workbench/${MOCK_PROJECT.id}`,
});
await waitFor(
() => {
const body = document.body.textContent ?? "";
expect(body).toContain("工作台");
},
{ timeout: 10000 },
);
});
test("Workbench 显示返回管理台按钮", async () => {
createMockHandler();
renderWithProviders(createElement(App), {
initialRoute: `/workbench/${MOCK_PROJECT.id}`,
});
await waitFor(
() => {
expect(screen.getByText("返回管理台")).not.toBeNull();
},
{ timeout: 10000 },
);
});
test("不存在项目显示不可访问", async () => {
createMockHandler();
renderWithProviders(createElement(App), {
initialRoute: "/workbench/nonexistent-id",
});
await waitFor(
() => {
expect(screen.getByText("项目不存在或不可访问")).not.toBeNull();
},
{ timeout: 10000 },
);
});
test("archived 项目显示不可访问", async () => {
createMockHandler({ archivedAt: "2024-06-01T00:00:00.000Z", status: "archived" });
renderWithProviders(createElement(App), {
initialRoute: `/workbench/${MOCK_PROJECT.id}`,
});
await waitFor(
() => {
expect(screen.getByText("项目不存在或不可访问")).not.toBeNull();
},
{ timeout: 10000 },
);
});
test("Workbench 总览页标题显示'总览'", () => {
renderWithProviders(
createElement(ProjectProvider, { children: createElement(WorkbenchOverviewPage), project: MOCK_PROJECT }),
);
expect(screen.getByText("总览")).not.toBeNull();
expect(screen.getAllByText(MOCK_PROJECT.name).length).toBeGreaterThan(0);
});
});