import { useEffect, useMemo, useState } from "react"; import { listTenants, createTenant, listMembers, addMember, setMemberRole, removeMember, setSharedBilling, setTenantPlan, setTenantStatus, type TenantRow, type Member } from "../api"; import { GrantCreditsModal } from "../components/GrantCreditsModal"; // 管理端「租户 & 用户」= 多成员租户管理(平台运维口径):租户目录 + 每租户成员增改删。 // 全真数据:GET/POST /admin/tenants、/admin/tenants/:id/members。 const ROLES = ["owner", "admin", "member", "viewer", "billing_admin"]; const ROLE_CN: Record = { owner: "所有者", admin: "管理员", member: "成员", viewer: "只读", billing_admin: "计费管理" }; const credits = (m: number) => (m / 1_000_000).toLocaleString("zh-CN", { maximumFractionDigits: 2 }); const PLANS = ["free", "pro", "enterprise"]; const PLAN_CN: Record = { free: "免费版", pro: "专业版", enterprise: "企业版" }; export function TenantsPage() { const [tenants, setTenants] = useState([]); const [selId, setSelId] = useState(""); const [members, setMembers] = useState([]); const [loading, setLoading] = useState(true); const [err, setErr] = useState(""); // 建租户表单 const [tName, setTName] = useState(""); const [tSlug, setTSlug] = useState(""); const [tOwner, setTOwner] = useState(""); const [tPlan, setTPlan] = useState("free"); const [creating, setCreating] = useState(false); // 加成员表单 const [mEmail, setMEmail] = useState(""); const [mRole, setMRole] = useState("member"); const [adding, setAdding] = useState(false); const [mErr, setMErr] = useState(""); // Modals state const [editPlanTenant, setEditPlanTenant] = useState(null); const [grantTenant, setGrantTenant] = useState(null); const [statusChanging, setStatusChanging] = useState(false); const selected = useMemo(() => tenants.find((t) => t.id === selId) ?? null, [tenants, selId]); const loadTenants = async (keepSel = true) => { try { const rows = await listTenants(); setTenants(rows); if (!keepSel || !rows.find((t) => t.id === selId)) setSelId(rows[0]?.id ?? ""); setErr(""); } catch (e) { setErr((e as Error).message); } finally { setLoading(false); } }; const loadMembers = async (id: string) => { if (!id) { setMembers([]); return; } try { setMembers(await listMembers(id)); } catch (e) { setMErr((e as Error).message); } }; useEffect(() => { void loadTenants(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { void loadMembers(selId); setMErr(""); // eslint-disable-next-line react-hooks/exhaustive-deps }, [selId]); const onCreate = async () => { if (!tName.trim() || !tSlug.trim()) return; setCreating(true); try { await createTenant(tName.trim(), tSlug.trim(), tOwner.trim() || undefined, tPlan); setTName(""); setTSlug(""); setTOwner(""); setTPlan("free"); await loadTenants(); } catch (e) { setErr((e as Error).message); } finally { setCreating(false); } }; const onAdd = async () => { if (!mEmail.trim() || !selId) return; setAdding(true); setMErr(""); try { await addMember(selId, mEmail.trim(), mRole); setMEmail(""); await Promise.all([loadMembers(selId), loadTenants()]); } catch (e) { setMErr((e as Error).message); } finally { setAdding(false); } }; const onRole = async (uid: string, role: string) => { try { await setMemberRole(selId, uid, role); await loadMembers(selId); } catch (e) { setMErr((e as Error).message); } }; const onRemove = async (uid: string, email: string) => { if (!window.confirm(`确定从「${selected?.name}」移除 ${email}?`)) return; try { await removeMember(selId, uid); await Promise.all([loadMembers(selId), loadTenants()]); } catch (e) { setMErr((e as Error).message); } }; const onToggleShared = async (on: boolean) => { try { await setSharedBilling(selId, on); await loadTenants(); } catch (e) { setMErr((e as Error).message); } }; const onPlanChange = async (plan: string) => { if (!editPlanTenant) return; try { await setTenantPlan(editPlanTenant.id, plan); setEditPlanTenant(null); await loadTenants(); } catch (e) { setErr(`修改 Plan 失败: ${(e as Error).message}`); } }; const onStatusToggle = async () => { if (!selected) return; const nextStatus = selected.status === "suspended" ? "active" : "suspended"; const actionText = nextStatus === "suspended" ? "暂停" : "恢复"; if (!window.confirm(`确定要 ${actionText} 租户「${selected.name}」吗?`)) return; setStatusChanging(true); try { await setTenantStatus(selected.id, nextStatus); await loadTenants(); } catch (e) { setMErr(`${actionText}租户失败: ${(e as Error).message}`); } finally { setStatusChanging(false); } }; if (loading) return
加载租户中…
; return (
{err &&
{err}
} {/* 充值 Modal */} setGrantTenant(null)} onDone={() => void loadTenants()} /> {/* 修改方案 Modal */} {editPlanTenant && (
e.target === e.currentTarget && setEditPlanTenant(null)} >

更改方案等级

调整租户 {editPlanTenant.name} 的等级

{PLANS.map((p) => ( ))}
)}
{/* 左:租户目录 + 建租户 */}

租户

{tenants.length} 个 · tenant_id 全链路隔离
{tenants.map((t) => ( ))}

新建租户

setTName(e.target.value)} /> setTSlug(e.target.value)} /> setTOwner(e.target.value)} />
{/* 右:选定租户的成员 */}
{!selected ? (
左侧选择一个租户查看成员
) : ( <>

{selected.name} · 成员

setEditPlanTenant(selected)} className={`rounded px-1.5 py-0.5 text-[9px] font-semibold cursor-pointer hover:opacity-85 ${selected.plan === "free" ? "bg-gray-100 text-gray-500" : "bg-violet-100 text-violet-700"}`} > {selected.plan} ✎

成员共享该租户的积分池与用量口径 · slug {selected.slug}

{credits(selected.credit_balance_micro)} 积分
{/* 状态控制 & 共享计费开关 */}
{/* 加成员 */}
setMEmail(e.target.value)} />
{mErr &&
{mErr}
} {members.length === 0 ? (
暂无成员
) : (
{members.map((m) => { const isOwner = m.role === "owner"; return ( ); })}
成员 角色 状态 操作
{m.name || m.email}
{m.email}
{isOwner ? ( 所有者 ) : ( )} {m.status} {isOwner ? ( ) : ( )}
)} )}
); }