chore:platform文件拆分

This commit is contained in:
xiaozhu
2026-08-04 18:46:18 +08:00
parent 87382d7bda
commit 4e95399d4f
17 changed files with 2701 additions and 793 deletions
+183
View File
@@ -0,0 +1,183 @@
type IconName =
| "brand"
| "home"
| "script"
| "schedule"
| "experiment"
| "database"
| "settings"
| "chevron"
| "search"
| "plus"
| "upload"
| "folder"
| "notebook"
| "python"
| "refresh"
| "close"
| "check"
| "info"
| "workspace"
| "menu"
| "external"
| "release"
| "play";
type IconProps = {
name: IconName;
size?: number;
strokeWidth?: number;
};
export default function Icon({
name,
size = 18,
strokeWidth = 1.8,
}: IconProps) {
const common = {
width: size,
height: size,
viewBox: "0 0 24 24",
fill: "none",
xmlns: "http://www.w3.org/2000/svg",
"aria-hidden": true,
};
if (name === "brand") {
return (
<svg {...common} viewBox="0 0 32 32">
<path
d="M16 3.5 27 9.8v12.4L16 28.5 5 22.2V9.8L16 3.5Z"
fill="currentColor"
opacity=".2"
/>
<path
d="M16 7.7 23.4 12v8L16 24.3 8.6 20v-8L16 7.7Z"
fill="currentColor"
/>
<circle cx="16" cy="16" r="3.3" fill="white" />
</svg>
);
}
const paths: Record<Exclude<IconName, "brand">, React.ReactNode> = {
home: (
<>
<path d="m3.5 11 8.5-7 8.5 7" />
<path d="M5.5 10v10h13V10M9.5 20v-6h5v6" />
</>
),
script: (
<>
<rect x="5" y="3.5" width="14" height="17" rx="2" />
<path d="M8.5 8h7M8.5 12h7M8.5 16h4.5" />
</>
),
schedule: (
<>
<circle cx="12" cy="12" r="8.5" />
<path d="M12 7.5V12l3.2 2" />
</>
),
experiment: (
<>
<path d="M9 3.5h6M10 3.5v5l-5.2 9a2 2 0 0 0 1.7 3h11a2 2 0 0 0 1.7-3L14 8.5v-5" />
<path d="M7.3 15h9.4" />
</>
),
database: (
<>
<ellipse cx="12" cy="5.5" rx="7.5" ry="3" />
<path d="M4.5 5.5v6c0 1.7 3.4 3 7.5 3s7.5-1.3 7.5-3v-6" />
<path d="M4.5 11.5v6c0 1.7 3.4 3 7.5 3s7.5-1.3 7.5-3v-6" />
</>
),
settings: (
<>
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1-2.8 2.8-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.6v.2h-4V21a1.7 1.7 0 0 0-1-1.6 1.7 1.7 0 0 0-1.9.3l-.1.1L4.2 17l.1-.1a1.7 1.7 0 0 0 .3-1.9A1.7 1.7 0 0 0 3 14H2.8v-4H3a1.7 1.7 0 0 0 1.6-1 1.7 1.7 0 0 0-.3-1.9L4.2 7 7 4.2l.1.1a1.7 1.7 0 0 0 1.9.3A1.7 1.7 0 0 0 10 3v-.2h4V3a1.7 1.7 0 0 0 1 1.6 1.7 1.7 0 0 0 1.9-.3l.1-.1L19.8 7l-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.6 1h.2v4H21a1.7 1.7 0 0 0-1.6 1Z" />
</>
),
chevron: <path d="m9 7 5 5-5 5" />,
search: (
<>
<circle cx="10.5" cy="10.5" r="6.5" />
<path d="m15.5 15.5 4 4" />
</>
),
plus: <path d="M12 5v14M5 12h14" />,
upload: (
<>
<path d="M12 16V4m0 0L7.5 8.5M12 4l4.5 4.5" />
<path d="M4 15.5V20h16v-4.5" />
</>
),
folder: (
<path d="M3.5 6.5h6l2 2H20.5v10.5a1.5 1.5 0 0 1-1.5 1.5H5a1.5 1.5 0 0 1-1.5-1.5V6.5Z" />
),
notebook: (
<>
<rect x="5" y="3.5" width="14" height="17" rx="2" />
<path d="M8.5 3.5v17M11.5 8h4M11.5 12h4M11.5 16h2.5" />
</>
),
python: (
<>
<path d="M8 4.5h5.5a2.5 2.5 0 0 1 2.5 2.5v3H8a3 3 0 0 0-3 3v2" />
<path d="M16 19.5h-5.5A2.5 2.5 0 0 1 8 17v-3h8a3 3 0 0 0 3-3V9" />
<circle cx="11" cy="7" r=".8" fill="currentColor" stroke="none" />
<circle cx="13" cy="17" r=".8" fill="currentColor" stroke="none" />
</>
),
refresh: (
<>
<path d="M19 8V4l-1.7 1.7A8 8 0 1 0 20 12" />
<path d="M19 4h-4" />
</>
),
close: <path d="m6 6 12 12M18 6 6 18" />,
check: <path d="m5 12.5 4 4L19 7" />,
info: (
<>
<circle cx="12" cy="12" r="9" />
<path d="M12 11v5M12 8h.01" />
</>
),
workspace: (
<>
<rect x="3.5" y="5" width="17" height="14" rx="2" />
<path d="M8 5V3.5h8V5M8 11h8M12 8v6" />
</>
),
menu: (
<>
<path d="M4 6h16M4 12h16M4 18h16" />
</>
),
external: (
<>
<path d="M14 5h5v5M19 5l-8 8" />
<path d="M18 13v6H5V6h6" />
</>
),
release: (
<>
<path d="M12 3.5v11M7.5 8 12 3.5 16.5 8" />
<path d="M5 12.5v7h14v-7" />
</>
),
play: <path d="m8 5 11 7-11 7V5Z" />,
};
return (
<svg
{...common}
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
>
{paths[name]}
</svg>
);
}
@@ -0,0 +1,73 @@
import Icon from "./Icon";
const navigation = [
{ label: "工作台", icon: "home" as const, page: "home" as const },
{ label: "构建脚本", icon: "script" as const, page: "scripts" as const },
{ label: "调度配置", icon: "schedule" as const, page: "schedules" as const },
{ label: "系统管理", icon: "settings" as const, page: "system" as const },
];
type ActivePage = "home" | "scripts" | "schedules" | "system";
type SidebarProps = {
activePage: ActivePage;
collapsed: boolean;
onNavigate: (page: ActivePage) => void;
onToggleCollapse: () => void;
onEndEditing?: () => void;
onSelectScript?: (scriptId: null) => void;
editSessionRef?: React.RefObject<unknown | null>;
};
export function Sidebar({
activePage,
collapsed,
onNavigate,
onToggleCollapse,
onEndEditing,
onSelectScript,
editSessionRef,
}: SidebarProps) {
const handleNavigationClick = (page: ActivePage) => {
if (page !== "scripts") {
if (editSessionRef?.current) {
onEndEditing?.();
} else {
onSelectScript?.(null);
}
}
onNavigate(page);
};
return (
<aside className={`sidebar${collapsed ? " is-collapsed" : ""}`}>
<div className="brand">
<span className="brand__mark"><Icon name="brand" size={31} /></span>
{!collapsed && <span className="brand__name"></span>}
</div>
{!collapsed && (
<nav className="navigation" aria-label="主导航">
{navigation.map((item) => (
<button
className={`nav-item${
item.page === activePage ? " nav-item--active" : ""
}`}
key={item.label}
type="button"
onClick={() => handleNavigationClick(item.page)}
>
<Icon name={item.icon} size={19} />
<span>{item.label}</span>
</button>
))}
</nav>
)}
<button className="sidebar-footer" type="button" onClick={onToggleCollapse}>
<Icon name="menu" size={19} />
<span>{collapsed ? "展开菜单" : "收起菜单"}</span>
</button>
</aside>
);
}
+23
View File
@@ -0,0 +1,23 @@
import Icon from "./Icon";
type ToastState = {
tone: "success" | "error" | "info";
message: string;
};
type ToastProps = {
toast: ToastState | null;
};
export function Toast({ toast }: ToastProps) {
if (!toast) return null;
return (
<div className={`toast toast--${toast.tone}`} role="status">
<span>
<Icon name={toast.tone === "success" ? "check" : "info"} size={17} />
</span>
{toast.message}
</div>
);
}
+121
View File
@@ -0,0 +1,121 @@
import Icon from "./Icon";
import type { AuthUser, AuthWorkspace } from "../../context/AuthContext";
type TopbarProps = {
activePage: string;
apiOnline: boolean;
user: AuthUser | null;
currentWorkspace: AuthWorkspace;
workspaces: AuthWorkspace[];
workspaceMenuOpen: boolean;
onSetWorkspaceMenuOpen: (open: boolean) => void;
onSetCurrentWorkspace: (workspaceId: string) => void;
onLogout: () => void;
};
export function Topbar({
activePage,
apiOnline,
user,
currentWorkspace,
workspaces,
workspaceMenuOpen,
onSetWorkspaceMenuOpen,
onSetCurrentWorkspace,
onLogout,
}: TopbarProps) {
const pageTitles: Record<string, string> = {
home: "工作台",
scripts: "构建脚本",
schedules: "调度配置",
system: "系统管理",
};
return (
<header className="topbar">
<div className="page-title">
<button className="icon-button icon-button--back" type="button">
<Icon name="chevron" size={19} />
</button>
<div>
<span className="page-title__eyebrow"></span>
<h1>{pageTitles[activePage] || "工作台"}</h1>
</div>
</div>
<div className="topbar__actions">
<div className="api-state">
<span className={`api-state__dot${apiOnline ? " is-online" : ""}`} />
{apiOnline ? "服务已连接" : "服务未连接"}
</div>
<div className="topbar-menu-wrap">
<button
className="workspace-switcher"
type="button"
onClick={() => onSetWorkspaceMenuOpen(!workspaceMenuOpen)}
>
<span className="workspace-switcher__icon">
<Icon name="workspace" size={18} />
</span>
<span>
<small> Workspace</small>
<strong>{currentWorkspace.workspace_name}</strong>
</span>
<Icon name="chevron" size={15} />
</button>
{workspaceMenuOpen && (
<div className="topbar-dropdown">
{workspaces.map((workspace) => (
<button
className={
workspace.workspace_id === currentWorkspace.workspace_id
? "is-selected"
: ""
}
type="button"
key={workspace.workspace_id}
onClick={() => {
onSetCurrentWorkspace(workspace.workspace_id);
onSetWorkspaceMenuOpen(false);
}}
>
<Icon name="workspace" size={15} />
<span>
<strong>{workspace.workspace_name}</strong>
<small>
{workspace.workspace_id === currentWorkspace.workspace_id
? "当前使用"
: "点击切换"}
</small>
</span>
</button>
))}
</div>
)}
</div>
<div className="topbar-menu-wrap">
<button className="user-menu" type="button">
<span className="avatar">
{user?.display_name?.slice(0, 1) ?? "?"}
</span>
<span className="user-menu__copy">
<strong>{user?.display_name ?? "未知用户"}</strong>
<small>
{user?.role_code === "admin" ? "管理员" : "开发人员"}
</small>
</span>
</button>
<button
className="text-button"
type="button"
onClick={() => {
onLogout();
window.location.assign("/login");
}}
>
</button>
</div>
</div>
</header>
);
}
@@ -0,0 +1,24 @@
import Icon from "./Icon";
type WelcomePanelProps = {
onCreateScript: () => void;
};
export function WelcomePanel({ onCreateScript }: WelcomePanelProps) {
return (
<div className="welcome-panel">
<div className="welcome-panel__visual">
<Icon name="script" size={42} />
</div>
<span className="welcome-panel__label"></span>
<h2></h2>
<p>
Notebook 使 Python
</p>
<button className="primary-button" onClick={onCreateScript}>
<Icon name="plus" size={17} />
</button>
</div>
);
}