From 2a81b9830970673f3d6904adf96f305df3a867d0 Mon Sep 17 00:00:00 2001 From: "tao.chen" <93983997+taochen-ct@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:00:01 +0800 Subject: [PATCH] update: replace table component --- frontend/app/features/admin/AdminTable.tsx | 18 + .../app/features/admin/ImportMemberDialog.tsx | 102 ++++++ .../app/features/admin/ProjectEditDialog.tsx | 134 +++++++ .../features/admin/ProjectManagementPage.tsx | 346 +++--------------- .../features/admin/ProjectMembersDrawer.tsx | 158 ++++++++ .../app/features/admin/RoleManagementPage.tsx | 9 +- .../app/features/admin/UserManagementPage.tsx | 9 +- frontend/app/styles/admin.css | 4 +- 8 files changed, 459 insertions(+), 321 deletions(-) create mode 100644 frontend/app/features/admin/AdminTable.tsx create mode 100644 frontend/app/features/admin/ImportMemberDialog.tsx create mode 100644 frontend/app/features/admin/ProjectEditDialog.tsx create mode 100644 frontend/app/features/admin/ProjectMembersDrawer.tsx diff --git a/frontend/app/features/admin/AdminTable.tsx b/frontend/app/features/admin/AdminTable.tsx new file mode 100644 index 0000000..94d8029 --- /dev/null +++ b/frontend/app/features/admin/AdminTable.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; + +// Column widths derived from the original CSS-grid fr ratios. +// 32+22+15+14+17 = 100; 20+23+14+16+27 = 100. +export const USER_PROJECT_COL_WIDTHS = [32, 22, 15, 14, 17] as const; +export const ROLE_COL_WIDTHS = [20, 23, 14, 16, 27] as const; + +export type AdminColumnWidths = readonly number[]; + +export function AdminColgroup({ widths }: { widths: AdminColumnWidths }): ReactNode { + return ( + + {widths.map((width, index) => ( + + ))} + + ); +} diff --git a/frontend/app/features/admin/ImportMemberDialog.tsx b/frontend/app/features/admin/ImportMemberDialog.tsx new file mode 100644 index 0000000..d49e15b --- /dev/null +++ b/frontend/app/features/admin/ImportMemberDialog.tsx @@ -0,0 +1,102 @@ +import { X } from "lucide-react"; +import { type Employee, type Workspace } from "../../services/api"; +import { Button } from "~/components/ui/button"; +import { + formFieldClass, + modalBackdropClass, + modalCompactClass, + modalEyebrowClass, + modalFooterClass, + modalFormClass, + modalHeaderClass, + modalTitleClass, +} from "../platform/modalUi"; +import { UserMultiSelect } from "./UserMultiSelect"; + +const CLOSE_BUTTON_CLASS = + "size-[34px] rounded-[7px] border border-[#dfe6ed] bg-white hover:bg-[#f7faff] hover:border-[#b9c9da]"; +const SECONDARY_BUTTON_CLASS = + "h-[37px] gap-[7px] rounded-md border-[#d8e1e9] bg-white px-[15px] text-xs font-[650] text-[#56687c]"; +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 ImportMemberDialog({ + open, + selectedProject, + availableUsers, + selectedUserIds, + existingMemberIds, + saving, + onNotify, + onChangeSelectedUserIds, + onSubmit, + onClose, +}: { + open: boolean; + selectedProject: Workspace | null; + availableUsers: Employee[]; + selectedUserIds: string[]; + existingMemberIds: string[]; + saving: boolean; + onNotify: (notice: { tone: "success" | "error" | "info"; message: string }) => void; + onChangeSelectedUserIds: (ids: string[]) => void; + onSubmit: () => Promise | void; + onClose: () => void; +}) { + if (!open || !selectedProject) return null; + + return ( +
+
+
+
+ IMPORT MEMBER +

导入成员到 {selectedProject?.workspace_name ?? "项目"}

+
+ +
+
+ +
+ + +
+
+
+
+ ); +} diff --git a/frontend/app/features/admin/ProjectEditDialog.tsx b/frontend/app/features/admin/ProjectEditDialog.tsx new file mode 100644 index 0000000..f03a6ec --- /dev/null +++ b/frontend/app/features/admin/ProjectEditDialog.tsx @@ -0,0 +1,134 @@ +import { X } from "lucide-react"; +import { type Workspace } from "../../services/api"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Textarea } from "~/components/ui/textarea"; +import { + formFieldClass, + modalBackdropClass, + modalCompactClass, + modalEyebrowClass, + modalFooterClass, + modalFormClass, + modalHeaderClass, + modalTitleClass, +} from "../platform/modalUi"; + +const CLOSE_BUTTON_CLASS = + "size-[34px] rounded-[7px] border border-[#dfe6ed] bg-white hover:bg-[#f7faff] hover:border-[#b9c9da]"; +const SECONDARY_BUTTON_CLASS = + "h-[37px] gap-[7px] rounded-md border-[#d8e1e9] bg-white px-[15px] text-xs font-[650] text-[#56687c]"; +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]"; +const FORM_INPUT_CLASS = + "h-[39px] w-full rounded-md border border-[#d6e0e9] bg-white px-[11px] text-xs text-[#283a4e] outline-none focus:border-[#5b9ddb] focus:shadow-[0_0_0_3px_rgb(38_125_207/8%)] disabled:cursor-not-allowed disabled:opacity-50"; +const FORM_TEXTAREA_CLASS = + "min-h-[92px] w-full resize-y rounded-md border border-[#d6e0e9] bg-white px-[11px] py-2.5 text-xs leading-[1.55] text-[#283a4e] outline-none focus:border-[#5b9ddb] focus:shadow-[0_0_0_3px_rgb(38_125_207/8%)]"; + +export function ProjectEditDialog({ + open, + editingProject, + projectForm, + saving, + onNotify, + onChangeForm, + onSubmit, + onClose, +}: { + open: boolean; + editingProject: Workspace | null; + projectForm: { workspace_code: string; workspace_name: string; quota_bytes: number; description: string }; + saving: boolean; + onNotify: (notice: { tone: "success" | "error" | "info"; message: string }) => void; + onChangeForm: (next: { workspace_code: string; workspace_name: string; quota_bytes: number; description: string }) => void; + onSubmit: (event: React.FormEvent) => Promise | void; + onClose: () => void; +}) { + if (!open) return null; + + return ( +
+
+
+
+ PROJECT +

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

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