import { useEffect, useState } from "react"; import { Plus, Search } from "lucide-react"; import { ApiRequestError, type Employee, type Workspace, type WorkspaceMember } from "../../services/api"; import { useApi, useAuth } from "../../context/AuthContext"; import { Button } from "~/components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table"; import { AdminColgroup, USER_PROJECT_COL_WIDTHS } from "./AdminTable"; import { ProjectEditDialog } from "./ProjectEditDialog"; import { ImportMemberDialog } from "./ImportMemberDialog"; import { ProjectMembersDrawer } from "./ProjectMembersDrawer"; import "../../styles/admin.css"; const EMPTY_PROJECT_FORM = { workspace_code: "", workspace_name: "", quota_bytes: 0, description: "", }; const PRIMARY_BUTTON_CLASS = "h-[37px] gap-[7px] rounded-md border border-[#126ac3] bg-gradient-to-br from-[#1881e7] to-[#1268c9] px-[15px] text-xs font-[650] text-white shadow-[0_6px_15px_rgb(20_111_202/18%)] enabled:hover:from-[#1175d8] enabled:hover:to-[#0e5bad]"; 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 [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 [projectSearchTerm, setProjectSearchTerm] = useState(""); const [saving, setSaving] = useState(false); const [membersDrawerOpen, setMembersDrawerOpen] = useState(false); const [currentProjectMembers, setCurrentProjectMembers] = useState([]); const [membersLoading, setMembersLoading] = 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); } }; 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]); void refreshWorkspaces(); 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 = async (project: Workspace): Promise => { setSelectedProject(project); setSelectedUserIds([]); setImportMemberDialogOpen(true); // 加载可用用户和项目成员 try { const [allUsers, currentMembers] = await Promise.all([ api.listPlatformEmployees(), api.listWorkspaceMembers(project.workspace_id), ]); setAvailableUsers(allUsers); setCurrentProjectMembers(currentMembers); } catch (error) { // 如果加载失败,仍显示所有用户 const allUsers = await api.listPlatformEmployees(); setAvailableUsers(allUsers); setCurrentProjectMembers([]); } }; 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 removeMember = async (userId: string): Promise => { if (!selectedProject) return; // 管理员不能被移除 const targetMember = currentProjectMembers.find((m) => m.user_id === userId); if (targetMember?.role_code === "admin") { onNotify({ tone: "error", message: "管理员不能被移除" }); return; } try { await api.deleteWorkspaceMember(selectedProject.workspace_id, userId); setCurrentProjectMembers((current) => current.filter((m) => m.user_id !== userId)); onNotify({ tone: "success", message: "成员已移除" }); } catch (error) { onNotify({ tone: "error", message: error instanceof ApiRequestError ? error.message : "移除成员失败", }); } }; 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, }) ) ); 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` : "无限制"} {/* */} )) )}
void submitProject(event)} onClose={() => setProjectDialogOpen(false)} /> m.user_id)} saving={saving} onNotify={onNotify} onChangeSelectedUserIds={setSelectedUserIds} onSubmit={() => void importMember()} onClose={() => setImportMemberDialogOpen(false)} /> setMembersDrawerOpen(false)} onAddMembers={() => { setMembersDrawerOpen(false); if (selectedProject) void openImportMemberDialog(selectedProject); }} onRemoveMember={(userId) => void removeMember(userId)} onNotify={onNotify} />
); }