import { useCallback, useMemo, useState } from "react"; import { Plus, Search } from "lucide-react"; import { ApiRequestError, type Workspace, type WorkspaceMember } from "../../services/api"; import { useApi, useAuth } from "../../context/AuthContext"; import { Button } from "~/components/ui/button"; import { ConfirmDialog } from "~/components/common/ConfirmDialog"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table"; import { AdminColgroup, USER_PROJECT_COL_WIDTHS } from "./AdminTable"; import { AdminPagination } from "./AdminPagination"; import { ProjectEditDialog } from "./ProjectEditDialog"; import { ProjectMembersDrawer } from "./ProjectMembersDrawer"; import { useCursorPage } from "./useCursorPage"; import { useDebouncedValue } from "./useDebouncedValue"; import { adminEmptyClass, adminPageClass, adminReadonlyClass, adminToolbarClass, adminToolbarInputClass, adminToolbarSearchClass, employeeActionsClass, employeeAvatarClass, employeeNameClass, rowButtonClass, rowDangerButtonClass, statusPillClass, } from "./adminUi"; import { primaryGradientButtonClass } from "~/components/common/buttonClasses"; 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, refreshWorkspaces } = useAuth(); const [projectDialogOpen, setProjectDialogOpen] = useState(false); const [projectForm, setProjectForm] = useState(EMPTY_PROJECT_FORM); const [editingProject, setEditingProject] = useState(null); const [selectedProject, setSelectedProject] = useState(null); const [projectSearchTerm, setProjectSearchTerm] = useState(""); const [saving, setSaving] = useState(false); const [membersDrawerOpen, setMembersDrawerOpen] = useState(false); const [currentProjectMembers, setCurrentProjectMembers] = useState([]); const [membersLoading, setMembersLoading] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const [removeMemberTarget, setRemoveMemberTarget] = useState(null); const debouncedSearch = useDebouncedValue(projectSearchTerm, 300); const canManage = user?.role_code === "admin"; const fetchProjects = useCallback( async (input: { limit: number; cursor: string | null; q: string }) => { try { const page = await api.listWorkspaces(input); onConnectionChange(true); return page; } catch (error) { onConnectionChange(false); onNotify({ tone: "error", message: error instanceof Error ? error.message : "项目列表加载失败", }); throw error; } }, [api, onConnectionChange, onNotify], ); const { items: projects, setItems: setProjects, page, loading: projectLoading, meta, totalPages, goNext, goPrev, goToPage, canGoToPage, reload, refreshFromStart, } = useCursorPage(fetchProjects, debouncedSearch); 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); await api.createWorkspace({ workspace_code: generatedCode, workspace_name: projectForm.workspace_name.trim(), quota_bytes: projectForm.quota_bytes, description: projectForm.description.trim() || undefined, }); void refreshWorkspaces(); onNotify({ tone: "success", message: "项目已创建" }); await refreshFromStart(); } setProjectDialogOpen(false); } catch (error) { onNotify({ tone: "error", message: error instanceof ApiRequestError ? error.message : editingProject ? "更新项目失败" : "创建项目失败", }); } finally { setSaving(false); } }; const executeDeleteProject = async (project: Workspace): Promise => { try { await api.deleteWorkspace(project.workspace_id); onNotify({ tone: "success", message: "项目已删除" }); if (projects.length <= 1 && page > 1) { goPrev(); } else { reload(); } } catch (error) { onNotify({ tone: "error", message: error instanceof Error ? error.message : "删除项目失败", }); } }; const loadImportUsers = useCallback( async (input: { q: string; cursor: string | null; limit: number }) => { const page = await api.listPlatformEmployees({ limit: input.limit, cursor: input.cursor, q: input.q, }); return { items: page.items, hasMore: page.meta.has_more, nextCursor: page.meta.next_cursor, }; }, [api], ); const openMembersDrawer = (project: Workspace): void => { setSelectedProject(project); setMembersDrawerOpen(true); void loadProjectMembers(project.workspace_id); }; const loadProjectMembers = async (workspaceId: string): Promise => { setMembersLoading(true); try { const members = await api.listWorkspaceMembers(workspaceId); setCurrentProjectMembers(members); } catch (error) { onNotify({ tone: "error", message: error instanceof Error ? error.message : "成员列表加载失败", }); } finally { setMembersLoading(false); } }; const requestRemoveMember = (userId: string): void => { const targetMember = currentProjectMembers.find((m) => m.user_id === userId); if (!targetMember) return; if (targetMember.role_code === "admin") { onNotify({ tone: "error", message: "管理员不能被移除" }); return; } setRemoveMemberTarget(targetMember); }; const executeRemoveMember = async (member: WorkspaceMember): Promise => { if (!selectedProject) return; try { await api.deleteWorkspaceMember(selectedProject.workspace_id, member.user_id); setCurrentProjectMembers((current) => current.filter((m) => m.user_id !== member.user_id), ); onNotify({ tone: "success", message: "成员已移除" }); } catch (error) { onNotify({ tone: "error", message: error instanceof ApiRequestError ? error.message : "移除成员失败", }); } }; const importMembers = async (userIds: string[]): Promise => { if (!selectedProject || userIds.length === 0) { onNotify({ tone: "error", message: "请选择要添加的用户" }); return; } setSaving(true); try { await Promise.all( userIds.map((userId) => api.addWorkspaceMember(selectedProject.workspace_id, { user_id: userId, }), ), ); onNotify({ tone: "success", message: `已添加 ${userIds.length} 名成员` }); await loadProjectMembers(selectedProject.workspace_id); } catch (error) { onNotify({ tone: "error", message: error instanceof ApiRequestError ? error.message : "添加成员失败", }); throw error; } finally { setSaving(false); } }; const emptyMessage = useMemo(() => { if (projectLoading) return "正在加载项目…"; if (debouncedSearch.trim()) return "未找到匹配的项目"; return "暂无项目"; }, [projectLoading, debouncedSearch]); return (
setProjectSearchTerm(event.target.value)} />
{!canManage &&
当前为开发人员,只能查看项目列表。
} 项目名称 项目编码 状态 配额 操作 {projectLoading || projects.length === 0 ? (

{emptyMessage}

) : ( projects.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` : "无限制"} )) )}
void submitProject(event)} onClose={() => setProjectDialogOpen(false)} /> { setMembersDrawerOpen(false); setRemoveMemberTarget(null); }} onRemoveMember={requestRemoveMember} onAddMembers={importMembers} loadUsers={loadImportUsers} /> { if (!nextOpen) setDeleteTarget(null); }} title="确定删除项目?" description={ deleteTarget ? `确定要删除项目"${deleteTarget.workspace_name}"吗?此操作将级联软删所有成员。` : "" } confirmLabel="删除" destructive onConfirm={async () => { if (!deleteTarget) return; await executeDeleteProject(deleteTarget); setDeleteTarget(null); }} /> { if (!nextOpen) setRemoveMemberTarget(null); }} title="确定移除成员?" description={ removeMemberTarget ? `确定将"${removeMemberTarget.display_name}"从项目中移除吗?` : "" } confirmLabel="移除" destructive onConfirm={async () => { if (!removeMemberTarget) return; await executeRemoveMember(removeMemberTarget); setRemoveMemberTarget(null); }} />
); }