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:
6
src/web/consoles/admin/AdminConsoleLayout.tsx
Normal file
6
src/web/consoles/admin/AdminConsoleLayout.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ConsoleShell } from "../../components/ConsoleShell/ConsoleShell";
|
||||
import { ADMIN_MENU_ITEMS } from "./menu";
|
||||
|
||||
export function AdminConsoleLayout() {
|
||||
return <ConsoleShell menuItems={ADMIN_MENU_ITEMS} title="管理台" />;
|
||||
}
|
||||
9
src/web/consoles/admin/menu.tsx
Normal file
9
src/web/consoles/admin/menu.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { DashboardOutlined, FolderOutlined } from "@ant-design/icons";
|
||||
import { createElement } from "react";
|
||||
|
||||
import type { MenuItemConfig } from "../../menu";
|
||||
|
||||
export const ADMIN_MENU_ITEMS: readonly MenuItemConfig[] = [
|
||||
{ icon: createElement(DashboardOutlined), label: "总览", path: "/", value: "dashboard" },
|
||||
{ icon: createElement(FolderOutlined), label: "项目管理", path: "/projects", value: "projects" },
|
||||
] as const;
|
||||
17
src/web/consoles/workbench/ProjectContext.tsx
Normal file
17
src/web/consoles/workbench/ProjectContext.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { createContext, type ReactNode, useContext } from "react";
|
||||
|
||||
import type { Project } from "../../../shared/api";
|
||||
|
||||
const ProjectContext = createContext<null | Project>(null);
|
||||
|
||||
export function ProjectProvider({ children, project }: { children: ReactNode; project: Project }) {
|
||||
return <ProjectContext.Provider value={project}>{children}</ProjectContext.Provider>;
|
||||
}
|
||||
|
||||
export function useCurrentProject(): Project {
|
||||
const project = useContext(ProjectContext);
|
||||
if (!project) {
|
||||
throw new Error("useCurrentProject 必须在 Workbench 项目上下文内使用");
|
||||
}
|
||||
return project;
|
||||
}
|
||||
37
src/web/consoles/workbench/WorkbenchConsoleLayout.tsx
Normal file
37
src/web/consoles/workbench/WorkbenchConsoleLayout.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { HomeOutlined } from "@ant-design/icons";
|
||||
import { Button } from "antd";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
import type { Project } from "../../../shared/api";
|
||||
|
||||
import { ConsoleShell } from "../../components/ConsoleShell/ConsoleShell";
|
||||
import { ProjectProvider, useCurrentProject } from "./ProjectContext";
|
||||
import { getWorkbenchMenuItems } from "./routes";
|
||||
|
||||
interface WorkbenchConsoleLayoutProps {
|
||||
project: Project;
|
||||
}
|
||||
|
||||
export function WorkbenchConsoleLayout({ project }: WorkbenchConsoleLayoutProps) {
|
||||
const navigate = useNavigate();
|
||||
const menuItems = getWorkbenchMenuItems(project.id);
|
||||
|
||||
return (
|
||||
<ProjectProvider project={project}>
|
||||
<ConsoleShell
|
||||
headerExtra={
|
||||
<Button icon={<HomeOutlined />} onClick={() => void navigate("/")} size="small" type="link">
|
||||
返回管理台
|
||||
</Button>
|
||||
}
|
||||
menuItems={menuItems}
|
||||
title={<WorkbenchTitle />}
|
||||
/>
|
||||
</ProjectProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkbenchTitle() {
|
||||
const project = useCurrentProject();
|
||||
return <>工作台 · {project.name}</>;
|
||||
}
|
||||
47
src/web/consoles/workbench/WorkbenchProjectGate.tsx
Normal file
47
src/web/consoles/workbench/WorkbenchProjectGate.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Alert, Button, Spin } from "antd";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
|
||||
import { useProject } from "../../hooks/use-projects";
|
||||
import { WorkbenchConsoleLayout } from "./WorkbenchConsoleLayout";
|
||||
|
||||
export function WorkbenchProjectGate() {
|
||||
const { projectId } = useParams<{ projectId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { data: project, error, isLoading } = useProject(projectId ?? "");
|
||||
|
||||
if (!projectId) {
|
||||
return <WorkbenchUnavailable onBack={() => void navigate("/")} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="app-loading">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !project || project.status === "archived") {
|
||||
return <WorkbenchUnavailable onBack={() => void navigate("/")} />;
|
||||
}
|
||||
|
||||
return <WorkbenchConsoleLayout project={project} />;
|
||||
}
|
||||
|
||||
function WorkbenchUnavailable({ onBack }: { onBack: () => void }) {
|
||||
return (
|
||||
<div className="app-unavailable">
|
||||
<Alert
|
||||
action={
|
||||
<Button onClick={onBack} size="small" type="primary">
|
||||
返回管理台
|
||||
</Button>
|
||||
}
|
||||
description="请确认项目是否存在且未归档。"
|
||||
showIcon
|
||||
title="项目不存在或不可访问"
|
||||
type="error"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
22
src/web/consoles/workbench/routes.ts
Normal file
22
src/web/consoles/workbench/routes.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { DashboardOutlined } from "@ant-design/icons";
|
||||
import { createElement } from "react";
|
||||
|
||||
import type { MenuItemConfig } from "../../menu";
|
||||
|
||||
export const WORKBENCH_MENU_ITEMS: readonly MenuItemConfig[] = [
|
||||
{ icon: createElement(DashboardOutlined), label: "总览", path: "", value: "overview" },
|
||||
] as const;
|
||||
|
||||
export function buildWorkbenchPath(projectId: string, relativePath = ""): string {
|
||||
const base = `/workbench/${projectId}`;
|
||||
if (!relativePath || relativePath === "/") return base;
|
||||
const normalized = relativePath.startsWith("/") ? relativePath : `/${relativePath}`;
|
||||
return `${base}${normalized}`;
|
||||
}
|
||||
|
||||
export function getWorkbenchMenuItems(projectId: string): readonly MenuItemConfig[] {
|
||||
return WORKBENCH_MENU_ITEMS.map((item) => ({
|
||||
...item,
|
||||
path: buildWorkbenchPath(projectId, item.path || "/"),
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user