diff --git a/frontend/app/components/admin/DashboardPage.tsx b/frontend/app/components/admin/DashboardPage.tsx new file mode 100644 index 0000000..bcbd9c0 --- /dev/null +++ b/frontend/app/components/admin/DashboardPage.tsx @@ -0,0 +1,83 @@ +import Icon from "../../components/common/Icon"; +import { useAuth } from "../../context/AuthContext"; + +export function DashboardPage({ + scriptCount, + online, + onNavigate, +}: { + scriptCount: number; + online: boolean; + onNavigate: (page: "scripts" | "schedules" | "system") => void; +}) { + const { user, currentWorkspace } = useAuth(); + return ( +
+
+
+ MODEL DEVELOPMENT PLATFORM +

下午好,{user?.display_name ?? "用户"}

+

+ 当前位于 {currentWorkspace?.workspace_name ?? "(未选择 Workspace)"} + ,可以继续构建脚本或配置调度。 +

+
+ {online ? "服务正常" : "服务连接中"} +
+
+
{scriptCount}工作副本
+
2Workspace
+
4平台用户
+
{online ? "正常" : "检查中"}平台状态
+
+
+ + + +
+
+
+
运行趋势

近 7 天调度执行

成功率 92.6%
+
+ {[38, 55, 44, 73, 61, 86, 78].map((value, index) => ( +
+ {Math.round(value / 7)} + + {["周一", "周二", "周三", "周四", "周五", "周六", "今天"][index]} +
+ ))} +
+
成功 75失败 6
+
+
+
脚本资产

类型分布

+
+
{scriptCount}全部脚本
+
+ Notebook{Math.max(1, Math.round(scriptCount * .67))} 个 · 67% + Python{Math.max(0, scriptCount - Math.round(scriptCount * .67))} 个 · 33% + 稳定版本3 个已发布 +
+
+
+
+
ACTIVITY

最近平台活动

+
+
操作内容执行人状态时间
+ {[ + ["数据探索.ipynb 发布稳定版本 v3.0", "张三", "成功", "16:42"], + ["每日模型训练流程完成调度运行", "Scheduler", "成功", "15:25"], + ["批量预测.py 更新工作副本", "王五", "已同步", "14:18"], + ["风险验证流程完成 DAG 校验", "李四", "成功", "11:06"], + ].map((row) => ( +
+ {row[0]} + {row[1]}{row[2]}{row[3]} +
+ ))} +
+
+
+
+ ); +} diff --git a/frontend/app/components/admin/ProjectManagementPage.tsx b/frontend/app/components/admin/ProjectManagementPage.tsx new file mode 100644 index 0000000..1b3ec65 --- /dev/null +++ b/frontend/app/components/admin/ProjectManagementPage.tsx @@ -0,0 +1,364 @@ +import { useEffect, useState } from "react"; + +import { ApiRequestError, type Employee, type Workspace } from "../../services/api"; +import { useApi, useAuth } from "../../context/AuthContext"; +import Icon from "../common/Icon"; +import { UserMultiSelect } from "./UserMultiSelect"; + +const EMPTY_PROJECT_FORM = { + workspace_code: "", + workspace_name: "", + quota_bytes: 0, + description: "", +}; + +export function ProjectManagementPage({ + onNotify, + onConnectionChange, +}: { + onNotify: (notice: { tone: "success" | "error" | "info"; message: string }) => void; + onConnectionChange: (online: boolean) => void; +}) { + const api = useApi(); + const { user } = useAuth(); + const [projects, setProjects] = useState([]); + const [projectLoading, setProjectLoading] = useState(true); + const [projectDialogOpen, setProjectDialogOpen] = useState(false); + const [projectForm, setProjectForm] = useState(EMPTY_PROJECT_FORM); + const [editingProject, setEditingProject] = useState(null); + const [importMemberDialogOpen, setImportMemberDialogOpen] = useState(false); + const [selectedProject, setSelectedProject] = useState(null); + const [availableUsers, setAvailableUsers] = useState([]); + const [selectedUserIds, setSelectedUserIds] = useState([]); + const [selectedRoleCode, setSelectedRoleCode] = useState<"admin" | "developer">("developer"); + const [projectSearchTerm, setProjectSearchTerm] = useState(""); + const [saving, setSaving] = useState(false); + + const canManage = user?.role_code === "admin"; + + const loadProjects = async (): Promise => { + setProjectLoading(true); + try { + const workspaceList = await api.listWorkspaces(); + setProjects(workspaceList); + onConnectionChange(true); + } catch (error) { + onConnectionChange(false); + onNotify({ + tone: "error", + message: error instanceof Error ? error.message : "项目列表加载失败", + }); + } finally { + setProjectLoading(false); + } + }; + + const loadAvailableUsers = async (): Promise => { + try { + const list = await api.listEmployees(); + setAvailableUsers(list); + } catch (error) { + onNotify({ + tone: "error", + message: error instanceof Error ? error.message : "用户列表加载失败", + }); + } + }; + + useEffect(() => { + void loadProjects(); + }, []); + + const openCreateProject = (): void => { + setEditingProject(null); + setProjectForm(EMPTY_PROJECT_FORM); + setProjectDialogOpen(true); + }; + + const openEditProject = (project: Workspace): void => { + setEditingProject(project); + setProjectForm({ + workspace_code: project.workspace_code, + workspace_name: project.workspace_name, + quota_bytes: project.quota_bytes, + description: project.description ?? "", + }); + setProjectDialogOpen(true); + }; + + const submitProject = async (event: React.FormEvent): Promise => { + event.preventDefault(); + if (!projectForm.workspace_name.trim()) { + onNotify({ tone: "error", message: "请输入项目名称" }); + return; + } + if (!projectForm.workspace_code.trim() && !editingProject) { + onNotify({ tone: "error", message: "请输入项目编码" }); + return; + } + setSaving(true); + try { + if (editingProject) { + const updated = await api.updateWorkspace(editingProject.workspace_id, { + workspace_name: projectForm.workspace_name.trim(), + quota_bytes: projectForm.quota_bytes, + description: projectForm.description.trim() || undefined, + }); + setProjects((current) => + current.map((p) => (p.workspace_id === updated.workspace_id ? updated : p)) + ); + onNotify({ tone: "success", message: "项目信息已更新" }); + } else { + const generatedCode = projectForm.workspace_code.trim() || projectForm.workspace_name.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 32); + const created = await api.createWorkspace({ + workspace_code: generatedCode, + workspace_name: projectForm.workspace_name.trim(), + quota_bytes: projectForm.quota_bytes, + description: projectForm.description.trim() || undefined, + }); + setProjects((current) => [...current, created]); + onNotify({ tone: "success", message: "项目已创建" }); + } + setProjectDialogOpen(false); + } catch (error) { + onNotify({ + tone: "error", + message: error instanceof ApiRequestError ? error.message : (editingProject ? "更新项目失败" : "创建项目失败"), + }); + } finally { + setSaving(false); + } + }; + + const deleteProject = async (project: Workspace): Promise => { + if (!window.confirm(`确定要删除项目"${project.workspace_name}"吗?此操作将级联软删所有成员。`)) return; + try { + await api.deleteWorkspace(project.workspace_id); + setProjects((current) => current.filter((p) => p.workspace_id !== project.workspace_id)); + onNotify({ tone: "success", message: "项目已删除" }); + } catch (error) { + onNotify({ + tone: "error", + message: error instanceof Error ? error.message : "删除项目失败", + }); + } + }; + + const openImportMemberDialog = (project: Workspace): void => { + setSelectedProject(project); + setSelectedUserIds([]); + setSelectedRoleCode("developer"); + void loadAvailableUsers(); + setImportMemberDialogOpen(true); + }; + + const importMember = async (): Promise => { + if (!selectedProject || selectedUserIds.length === 0) { + onNotify({ tone: "error", message: "请选择要添加的用户" }); + return; + } + setSaving(true); + try { + await Promise.all( + selectedUserIds.map((userId) => + api.addWorkspaceMember(selectedProject.workspace_id, { + user_id: userId, + role_code: selectedRoleCode, + }) + ) + ); + setImportMemberDialogOpen(false); + onNotify({ tone: "success", message: `已添加 ${selectedUserIds.length} 名成员` }); + } catch (error) { + onNotify({ + tone: "error", + message: error instanceof ApiRequestError ? error.message : "添加成员失败", + }); + } finally { + setSaving(false); + } + }; + + return ( +
+
+
+ + setProjectSearchTerm(event.target.value)} + /> +
+ +
+ + {!canManage &&
当前为开发人员,只能查看项目列表。
} +
+
+ 项目名称 + 项目编码 + 状态 + 配额 + 操作 +
+ {projectLoading ? ( +

正在加载项目…

+ ) : projects.length === 0 ? ( +

暂无项目

+ ) : ( + projects + .filter((project) => { + const term = projectSearchTerm.toLowerCase().trim(); + if (!term) return true; + return ( + project.workspace_name.toLowerCase().includes(term) || + project.workspace_code.toLowerCase().includes(term) || + (project.description && project.description.toLowerCase().includes(term)) + ); + }) + .map((project) => ( +
+ + {project.workspace_name.slice(0, 1)} + + {project.workspace_name} + {project.description ?? "无描述"} + + + {project.workspace_code} + + + {project.status === "active" ? "正常" : project.status === "archived" ? "已归档" : "已删除"} + + + {project.quota_bytes > 0 ? `${(project.quota_bytes / 1024 / 1024 / 1024).toFixed(1)} GB` : "无限制"} + + + + + +
+ )) + )} +
+ + {projectDialogOpen && ( +
+
+
+
+ PROJECT +

{editingProject ? "编辑项目" : "新建项目"}

+
+ +
+
void submitProject(event)}> + {!editingProject && ( + + )} + + +