update: replace table component

This commit is contained in:
tao.chen
2026-08-26 11:00:01 +08:00
parent 32d3309ecf
commit b0281a73a0
8 changed files with 459 additions and 321 deletions
@@ -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 (
<colgroup>
{widths.map((width, index) => (
<col key={index} style={{ width: `${width}%` }} />
))}
</colgroup>
);
}
@@ -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> | void;
onClose: () => void;
}) {
if (!open || !selectedProject) return null;
return (
<div className={modalBackdropClass} role="presentation">
<section className={modalCompactClass} role="dialog" aria-modal="true">
<div className={modalHeaderClass}>
<div>
<span className={modalEyebrowClass}>IMPORT MEMBER</span>
<h2 className={modalTitleClass}> {selectedProject?.workspace_name ?? "项目"}</h2>
</div>
<Button
variant="ghost"
size="icon-sm"
type="button"
aria-label="关闭"
onClick={() => onClose()}
className={CLOSE_BUTTON_CLASS}
>
<X size={16} />
</Button>
</div>
<form className={modalFormClass}>
<label className={`${formFieldClass} pb-[120px]`}>
<span><span className="text-[#e74c3c]">*</span></span>
<UserMultiSelect
users={availableUsers}
selectedUserIds={selectedUserIds}
onChange={onChangeSelectedUserIds}
existingMemberIds={existingMemberIds}
/>
</label>
<div className={modalFooterClass}>
<Button
variant="outline"
size="sm"
type="button"
onClick={() => onClose()}
className={SECONDARY_BUTTON_CLASS}
>
</Button>
<Button
variant="default"
size="sm"
type="button"
disabled={saving || selectedUserIds.length === 0}
onClick={() => void onSubmit()}
className={PRIMARY_BUTTON_CLASS}
>
{saving ? "添加中…" : `添加 (${selectedUserIds.length})`}
</Button>
</div>
</form>
</section>
</div>
);
}
@@ -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> | void;
onClose: () => void;
}) {
if (!open) return null;
return (
<div className={modalBackdropClass} role="presentation">
<section className={modalCompactClass} role="dialog" aria-modal="true">
<div className={modalHeaderClass}>
<div>
<span className={modalEyebrowClass}>PROJECT</span>
<h2 className={modalTitleClass}>{editingProject ? "编辑项目" : "新建项目"}</h2>
</div>
<Button
variant="ghost"
size="icon-sm"
type="button"
aria-label="关闭"
onClick={() => onClose()}
className={CLOSE_BUTTON_CLASS}
>
<X size={16} />
</Button>
</div>
<form className={modalFormClass} onSubmit={(event) => void onSubmit(event)}>
{!editingProject && (
<label className={formFieldClass}>
<span><span className="text-[#e74c3c]">*</span></span>
<Input
className={FORM_INPUT_CLASS}
autoFocus
value={projectForm.workspace_code}
onChange={(event) => onChangeForm({ ...projectForm, workspace_code: event.target.value })}
placeholder="例如:model-development(小写字母、数字、连字符,3-32 字符)"
/>
</label>
)}
<label className={formFieldClass}>
<span><span className="text-[#e74c3c]">*</span></span>
<Input
className={FORM_INPUT_CLASS}
value={projectForm.workspace_name}
onChange={(event) => onChangeForm({ ...projectForm, workspace_name: event.target.value })}
placeholder="请输入项目名称"
/>
</label>
<label className={formFieldClass}>
<span></span>
<Input
className={FORM_INPUT_CLASS}
type="number"
value={projectForm.quota_bytes}
onChange={(event) => onChangeForm({ ...projectForm, quota_bytes: parseInt(event.target.value) || 0 })}
placeholder="0 表示无配额限制"
/>
</label>
<label className={formFieldClass}>
<span></span>
<Textarea
className={FORM_TEXTAREA_CLASS}
value={projectForm.description}
onChange={(event) => onChangeForm({ ...projectForm, description: event.target.value })}
placeholder="请输入项目描述(可选)"
rows={4}
/>
</label>
<div className={modalFooterClass}>
<Button
variant="outline"
size="sm"
type="button"
onClick={() => onClose()}
className={SECONDARY_BUTTON_CLASS}
>
</Button>
<Button
variant="default"
size="sm"
type="submit"
disabled={saving}
className={PRIMARY_BUTTON_CLASS}
>
{saving ? "保存中…" : "保存"}
</Button>
</div>
</form>
</section>
</div>
);
}
@@ -1,13 +1,9 @@
import { useEffect, useState } from "react";
// TODO: split file (>500 lines per CLAUDE.md 3.2.1 budget)
import { X } from "lucide-react";
import { ApiRequestError, type Employee, type Workspace, type WorkspaceMember } from "../../services/api";
import { useApi, useAuth } from "../../context/AuthContext";
import Icon from "../../components/common/Icon";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Textarea } from "~/components/ui/textarea";
import {
Table,
TableBody,
@@ -16,17 +12,10 @@ import {
TableHeader,
TableRow,
} from "~/components/ui/table";
import { UserMultiSelect } from "./UserMultiSelect";
import {
formFieldClass,
modalBackdropClass,
modalCompactClass,
modalEyebrowClass,
modalFooterClass,
modalFormClass,
modalHeaderClass,
modalTitleClass,
} from "../platform/modalUi";
import { AdminColgroup, USER_PROJECT_COL_WIDTHS } from "./AdminTable";
import { ProjectEditDialog } from "./ProjectEditDialog";
import { ImportMemberDialog } from "./ImportMemberDialog";
import { ProjectMembersDrawer } from "./ProjectMembersDrawer";
import "../../styles/admin.css";
@@ -37,16 +26,8 @@ const EMPTY_PROJECT_FORM = {
description: "",
};
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 ProjectManagementPage({
onNotify,
@@ -287,13 +268,7 @@ export function ProjectManagementPage({
{!canManage && <div className="admin-readonly"></div>}
<Table className="rounded-[9px] border border-[#dfe7ef] overflow-hidden text-[11px] text-[#44576a] [--border-color:#dfe7ef] table-fixed bg-white">
<colgroup>
<col style={{ width: "32%" }} />
<col style={{ width: "22%" }} />
<col style={{ width: "15%" }} />
<col style={{ width: "14%" }} />
<col style={{ width: "17%" }} />
</colgroup>
<AdminColgroup widths={USER_PROJECT_COL_WIDTHS} />
<TableHeader className="bg-[#f5f8fb] text-[#748598]">
<TableRow className="hover:bg-transparent border-b border-[#edf1f5]">
<TableHead className="h-10 px-4 text-[10px] font-bold text-[#748598]"></TableHead>
@@ -369,283 +344,44 @@ export function ProjectManagementPage({
</TableBody>
</Table>
{projectDialogOpen && (
<div className={modalBackdropClass} role="presentation">
<section className={modalCompactClass} role="dialog" aria-modal="true">
<div className={modalHeaderClass}>
<div>
<span className={modalEyebrowClass}>PROJECT</span>
<h2 className={modalTitleClass}>{editingProject ? "编辑项目" : "新建项目"}</h2>
</div>
<Button
variant="ghost"
size="icon-sm"
type="button"
aria-label="关闭"
onClick={() => setProjectDialogOpen(false)}
className={CLOSE_BUTTON_CLASS}
>
<X size={16} />
</Button>
</div>
<form className={modalFormClass} onSubmit={(event) => void submitProject(event)}>
{!editingProject && (
<label className={formFieldClass}>
<span><span className="text-[#e74c3c]">*</span></span>
<Input
className={FORM_INPUT_CLASS}
autoFocus
value={projectForm.workspace_code}
onChange={(event) => setProjectForm({ ...projectForm, workspace_code: event.target.value })}
placeholder="例如:model-development(小写字母、数字、连字符,3-32 字符)"
/>
</label>
)}
<label className={formFieldClass}>
<span><span className="text-[#e74c3c]">*</span></span>
<Input
className={FORM_INPUT_CLASS}
value={projectForm.workspace_name}
onChange={(event) => setProjectForm({ ...projectForm, workspace_name: event.target.value })}
placeholder="请输入项目名称"
/>
</label>
<label className={formFieldClass}>
<span></span>
<Input
className={FORM_INPUT_CLASS}
type="number"
value={projectForm.quota_bytes}
onChange={(event) => setProjectForm({ ...projectForm, quota_bytes: parseInt(event.target.value) || 0 })}
placeholder="0 表示无配额限制"
/>
</label>
<label className={formFieldClass}>
<span></span>
<Textarea
className={FORM_TEXTAREA_CLASS}
value={projectForm.description}
onChange={(event) => setProjectForm({ ...projectForm, description: event.target.value })}
placeholder="请输入项目描述(可选)"
rows={4}
/>
</label>
<div className={modalFooterClass}>
<Button
variant="outline"
size="sm"
type="button"
onClick={() => setProjectDialogOpen(false)}
className={SECONDARY_BUTTON_CLASS}
>
</Button>
<Button
variant="default"
size="sm"
type="submit"
disabled={saving}
className={PRIMARY_BUTTON_CLASS}
>
{saving ? "保存中…" : "保存"}
</Button>
</div>
</form>
</section>
</div>
)}
<ProjectEditDialog
open={projectDialogOpen}
editingProject={editingProject}
projectForm={projectForm}
saving={saving}
onNotify={onNotify}
onChangeForm={setProjectForm}
onSubmit={(event) => void submitProject(event)}
onClose={() => setProjectDialogOpen(false)}
/>
{importMemberDialogOpen && (
<div className={modalBackdropClass} role="presentation">
<section className={modalCompactClass} role="dialog" aria-modal="true">
<div className={modalHeaderClass}>
<div>
<span className={modalEyebrowClass}>IMPORT MEMBER</span>
<h2 className={modalTitleClass}> {selectedProject?.workspace_name ?? "项目"}</h2>
</div>
<Button
variant="ghost"
size="icon-sm"
type="button"
aria-label="关闭"
onClick={() => setImportMemberDialogOpen(false)}
className={CLOSE_BUTTON_CLASS}
>
<X size={16} />
</Button>
</div>
<form className={modalFormClass}>
<label className={`${formFieldClass} pb-[120px]`}>
<span><span className="text-[#e74c3c]">*</span></span>
<UserMultiSelect
users={availableUsers}
selectedUserIds={selectedUserIds}
onChange={setSelectedUserIds}
existingMemberIds={currentProjectMembers.map((m) => m.user_id)}
/>
</label>
<div className={modalFooterClass}>
<Button
variant="outline"
size="sm"
type="button"
onClick={() => setImportMemberDialogOpen(false)}
className={SECONDARY_BUTTON_CLASS}
>
</Button>
<Button
variant="default"
size="sm"
type="button"
disabled={saving || selectedUserIds.length === 0}
onClick={() => void importMember()}
className={PRIMARY_BUTTON_CLASS}
>
{saving ? "添加中…" : `添加 (${selectedUserIds.length})`}
</Button>
</div>
</form>
</section>
</div>
)}
<ImportMemberDialog
open={importMemberDialogOpen}
selectedProject={selectedProject}
availableUsers={availableUsers}
selectedUserIds={selectedUserIds}
existingMemberIds={currentProjectMembers.map((m) => m.user_id)}
saving={saving}
onNotify={onNotify}
onChangeSelectedUserIds={setSelectedUserIds}
onSubmit={() => void importMember()}
onClose={() => setImportMemberDialogOpen(false)}
/>
{membersDrawerOpen && selectedProject && (
<>
<div className="drawer-overlay" onClick={() => setMembersDrawerOpen(false)} style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0,0,0,0.3)",
zIndex: 99,
}} />
<aside className="drawer" style={{
position: "fixed",
right: 0,
top: 0,
bottom: 0,
width: 480,
backgroundColor: "#fff",
boxShadow: "-2px 0 8px rgba(0,0,0,0.1)",
display: "flex",
flexDirection: "column",
zIndex: 100,
}}>
<div className="drawer__header" style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "16px 20px",
borderBottom: "1px solid #e8e8e8",
}}>
<div>
<span className="modal__eyebrow" style={{ fontSize: 12, color: "#666" }}>MEMBERS</span>
<h3 style={{ margin: 0, fontSize: 18 }}></h3>
<p style={{ margin: "4px 0 0", fontSize: 13, color: "#888" }}>{selectedProject.workspace_name}</p>
</div>
<button className="icon-button" type="button" onClick={() => setMembersDrawerOpen(false)}>
<Icon name="close" />
</button>
</div>
<div style={{
padding: "12px 20px",
borderBottom: "1px solid #f0f0f0",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}>
<span style={{ fontSize: 14, color: "#666" }}>
{currentProjectMembers.length}
</span>
<button
className="primary-button"
type="button"
onClick={() => {
setMembersDrawerOpen(false);
openImportMemberDialog(selectedProject);
}}
style={{ fontSize: 13, padding: "6px 12px" }}
>
<Icon name="plus" size={14} />
</button>
</div>
<div style={{ flex: 1, overflow: "auto", padding: "12px 20px" }}>
{membersLoading ? (
<p style={{ textAlign: "center", color: "#999", padding: "40px 0" }}></p>
) : currentProjectMembers.length === 0 ? (
<p style={{ textAlign: "center", color: "#999", padding: "40px 0" }}></p>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{currentProjectMembers.map((member) => (
<div
key={member.user_id}
style={{
display: "flex",
alignItems: "center",
padding: "6px",
border: "1px solid #e8e8e8",
borderRadius: 6,
backgroundColor: "#fafafa",
}}
>
<div
className="avatar"
style={{
width: 32,
height: 32,
borderRadius: "50%",
backgroundColor: "#1890ff",
color: "#fff",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 14,
fontWeight: 600,
marginRight: 10,
}}
>
{member.display_name.slice(0, 1)}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 500, marginBottom: 2, fontSize: 13 }}>{member.display_name}</div>
<div style={{ fontSize: 11, color: "#888" }}>{member.username}</div>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
{(() => {
const isAdmin = member.role_code === "admin";
return (
<button
type="button"
onClick={() => void removeMember(member.user_id)}
style={{
padding: "3px 8px",
fontSize: 11,
color: isAdmin ? "#999" : "#ff4d4f",
border: "1px solid " + (isAdmin ? "#ddd" : "#ff4d4f"),
borderRadius: 4,
backgroundColor: "#fff",
cursor: isAdmin ? "not-allowed" : canManage ? "pointer" : "not-allowed",
}}
disabled={!canManage || isAdmin}
title={isAdmin ? "管理员不能被移除" : canManage ? "移除成员" : "无权限"}
>
</button>
);
})()}
</div>
</div>
))}
</div>
)}
</div>
</aside>
</>
)}
<ProjectMembersDrawer
open={membersDrawerOpen}
selectedProject={selectedProject}
members={currentProjectMembers}
membersLoading={membersLoading}
canManage={canManage}
onClose={() => setMembersDrawerOpen(false)}
onAddMembers={() => {
setMembersDrawerOpen(false);
if (selectedProject) void openImportMemberDialog(selectedProject);
}}
onRemoveMember={(userId) => void removeMember(userId)}
onNotify={onNotify}
/>
</section>
);
}
@@ -0,0 +1,158 @@
import { type Workspace, type WorkspaceMember } from "../../services/api";
import Icon from "../../components/common/Icon";
export function ProjectMembersDrawer({
open,
selectedProject,
members,
membersLoading,
canManage,
onClose,
onAddMembers,
onRemoveMember,
onNotify,
}: {
open: boolean;
selectedProject: Workspace | null;
members: WorkspaceMember[];
membersLoading: boolean;
canManage: boolean;
onClose: () => void;
onAddMembers: () => void;
onRemoveMember: (userId: string) => void;
onNotify: (notice: { tone: "success" | "error" | "info"; message: string }) => void;
}) {
if (!open || !selectedProject) return null;
return (
<>
<div className="drawer-overlay" onClick={() => onClose()} style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0,0,0,0.3)",
zIndex: 99,
}} />
<aside className="drawer" style={{
position: "fixed",
right: 0,
top: 0,
bottom: 0,
width: 480,
backgroundColor: "#fff",
boxShadow: "-2px 0 8px rgba(0,0,0,0.1)",
display: "flex",
flexDirection: "column",
zIndex: 100,
}}>
<div className="drawer__header" style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "16px 20px",
borderBottom: "1px solid #e8e8e8",
}}>
<div>
<span className="modal__eyebrow" style={{ fontSize: 12, color: "#666" }}>MEMBERS</span>
<h3 style={{ margin: 0, fontSize: 18 }}></h3>
<p style={{ margin: "4px 0 0", fontSize: 13, color: "#888" }}>{selectedProject.workspace_name}</p>
</div>
<button className="icon-button" type="button" onClick={() => onClose()}>
<Icon name="close" />
</button>
</div>
<div style={{
padding: "12px 20px",
borderBottom: "1px solid #f0f0f0",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}>
<span style={{ fontSize: 14, color: "#666" }}>
{members.length}
</span>
<button
className="primary-button"
type="button"
onClick={() => onAddMembers()}
style={{ fontSize: 13, padding: "6px 12px" }}
>
<Icon name="plus" size={14} />
</button>
</div>
<div style={{ flex: 1, overflow: "auto", padding: "12px 20px" }}>
{membersLoading ? (
<p style={{ textAlign: "center", color: "#999", padding: "40px 0" }}></p>
) : members.length === 0 ? (
<p style={{ textAlign: "center", color: "#999", padding: "40px 0" }}></p>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{members.map((member) => {
const isAdmin = member.role_code === "admin";
return (
<div
key={member.user_id}
style={{
display: "flex",
alignItems: "center",
padding: "6px",
border: "1px solid #e8e8e8",
borderRadius: 6,
backgroundColor: "#fafafa",
}}
>
<div
className="avatar"
style={{
width: 32,
height: 32,
borderRadius: "50%",
backgroundColor: "#1890ff",
color: "#fff",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 14,
fontWeight: 600,
marginRight: 10,
}}
>
{member.display_name.slice(0, 1)}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 500, marginBottom: 2, fontSize: 13 }}>{member.display_name}</div>
<div style={{ fontSize: 11, color: "#888" }}>{member.username}</div>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<button
type="button"
onClick={() => onRemoveMember(member.user_id)}
style={{
padding: "3px 8px",
fontSize: 11,
color: isAdmin ? "#999" : "#ff4d4f",
border: "1px solid " + (isAdmin ? "#ddd" : "#ff4d4f"),
borderRadius: 4,
backgroundColor: "#fff",
cursor: isAdmin ? "not-allowed" : canManage ? "pointer" : "not-allowed",
}}
disabled={!canManage || isAdmin}
title={isAdmin ? "管理员不能被移除" : canManage ? "移除成员" : "无权限"}
>
</button>
</div>
</div>
);
})}
</div>
)}
</div>
</aside>
</>
);
}
@@ -20,6 +20,7 @@ import {
TableHeader,
TableRow,
} from "~/components/ui/table";
import { AdminColgroup, ROLE_COL_WIDTHS } from "./AdminTable";
import "../../styles/admin.css";
@@ -204,13 +205,7 @@ export function RoleManagementPage({
{loadError && <div className="admin-readonly">{loadError}</div>}
<Table className="rounded-[9px] border border-[#dfe7ef] overflow-hidden text-[11px] text-[#44576a] [--border-color:#dfe7ef] table-fixed bg-white">
<colgroup>
<col style={{ width: "20%" }} />
<col style={{ width: "23%" }} />
<col style={{ width: "14%" }} />
<col style={{ width: "16%" }} />
<col style={{ width: "27%" }} />
</colgroup>
<AdminColgroup widths={ROLE_COL_WIDTHS} />
<TableHeader className="bg-[#f5f8fb] text-[#748598]">
<TableRow className="hover:bg-transparent border-b border-[#edf1f5]">
<TableHead className="h-10 px-4 text-[10px] font-bold text-[#748598]"></TableHead>
@@ -14,6 +14,7 @@ import {
TableHeader,
TableRow,
} from "~/components/ui/table";
import { AdminColgroup, USER_PROJECT_COL_WIDTHS } from "./AdminTable";
import {
formFieldClass,
formInputClass,
@@ -221,13 +222,7 @@ export function UserManagementPage({
{!canManage && <div className="admin-readonly"></div>}
<Table className="rounded-[9px] border border-[#dfe7ef] overflow-hidden text-[11px] text-[#44576a] [--border-color:#dfe7ef] table-fixed bg-white">
<colgroup>
<col style={{ width: "32%" }} />
<col style={{ width: "22%" }} />
<col style={{ width: "15%" }} />
<col style={{ width: "14%" }} />
<col style={{ width: "17%" }} />
</colgroup>
<AdminColgroup widths={USER_PROJECT_COL_WIDTHS} />
<TableHeader className="bg-[#f5f8fb] text-[#748598]">
<TableRow className="hover:bg-transparent border-b border-[#edf1f5]">
<TableHead className="h-10 px-4 text-[10px] font-bold text-[#748598]"></TableHead>