201 lines
6.7 KiB
TypeScript
201 lines
6.7 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
|
|
import { Plus, X } from "lucide-react";
|
|
import { type Workspace, type WorkspaceMember } from "../../services/api";
|
|
import { Button } from "~/components/ui/button";
|
|
import { primaryGradientButtonClass } from "~/components/common/buttonClasses";
|
|
import { MemberAddPanel, type LoadUsersFn } from "./MemberAddPanel";
|
|
|
|
type DrawerPanel = "members" | "add";
|
|
|
|
export function ProjectMembersDrawer({
|
|
open,
|
|
selectedProject,
|
|
members,
|
|
membersLoading,
|
|
canManage,
|
|
saving,
|
|
onClose,
|
|
onRemoveMember,
|
|
onAddMembers,
|
|
loadUsers,
|
|
}: {
|
|
open: boolean;
|
|
selectedProject: Workspace | null;
|
|
members: WorkspaceMember[];
|
|
membersLoading: boolean;
|
|
canManage: boolean;
|
|
saving: boolean;
|
|
onClose: () => void;
|
|
onRemoveMember: (userId: string) => void;
|
|
onAddMembers: (userIds: string[]) => Promise<void> | void;
|
|
loadUsers: LoadUsersFn;
|
|
}) {
|
|
const [panel, setPanel] = useState<DrawerPanel>("members");
|
|
|
|
useEffect(() => {
|
|
if (!open) setPanel("members");
|
|
}, [open]);
|
|
|
|
if (!open || !selectedProject) return null;
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
className="drawer-overlay"
|
|
onClick={() => onClose()}
|
|
style={{
|
|
position: "fixed",
|
|
inset: 0,
|
|
backgroundColor: "rgba(0,0,0,0.3)",
|
|
zIndex: 40,
|
|
}}
|
|
/>
|
|
<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: 41,
|
|
}}
|
|
>
|
|
<div
|
|
className="drawer__header"
|
|
style={{
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
padding: "16px 20px",
|
|
borderBottom: "1px solid #e8e8e8",
|
|
}}
|
|
>
|
|
<div>
|
|
{/* <span style={{ fontSize: 12, color: "#666" }}>
|
|
{panel === "members" ? "MEMBERS" : "ADD MEMBER"}
|
|
</span> */}
|
|
<h3 style={{ margin: 0, fontSize: 16 }}>
|
|
{panel === "members" ? "项目成员" : "添加成员"}
|
|
</h3>
|
|
<p style={{ margin: "4px 0 0", fontSize: 12, color: "#888" }}>
|
|
{selectedProject.workspace_name}
|
|
</p>
|
|
</div>
|
|
<button className="icon-button" type="button" onClick={() => onClose()}>
|
|
<X />
|
|
</button>
|
|
</div>
|
|
|
|
{panel === "members" ? (
|
|
<>
|
|
<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>
|
|
{canManage && (
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
className={primaryGradientButtonClass}
|
|
onClick={() => setPanel("add")}
|
|
style={{ fontSize: 13, padding: "6px 12px" }}
|
|
>
|
|
<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: 6,
|
|
border: "1px solid #e8e8e8",
|
|
borderRadius: 6,
|
|
backgroundColor: "#fafafa",
|
|
}}
|
|
>
|
|
<div
|
|
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>
|
|
<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 || !canManage ? "not-allowed" : "pointer",
|
|
}}
|
|
disabled={!canManage || isAdmin}
|
|
title={isAdmin ? "管理员不能被移除" : canManage ? "移除成员" : "无权限"}
|
|
>
|
|
移除
|
|
</button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
) : (
|
|
<MemberAddPanel
|
|
existingMemberIds={members.map((m) => m.user_id)}
|
|
saving={saving}
|
|
loadUsers={loadUsers}
|
|
onBack={() => setPanel("members")}
|
|
onAddMembers={onAddMembers}
|
|
/>
|
|
)}
|
|
</aside>
|
|
</>
|
|
);
|
|
}
|