import { useCallback, useEffect, useMemo, useState } from "react"; import { adminSpaces, type AdminSpace } from "../api"; // 全平台空间(Space)观测:跨租户看所有空间(类型/租户/建者/成员数/归档态)。 // Space 是租户与成员之间的中间容器(Tenant > Space > 成员),数据来自 sundynix_space。 const KIND: Record = { personal: { label: "个人", badge: "bg-gray-100 text-gray-500" }, project: { label: "项目", badge: "bg-cyan-50 text-cyan-600" }, tenant: { label: "全员", badge: "bg-violet-50 text-violet-600" }, }; const kind = (k: string) => KIND[k] ?? { label: k, badge: "bg-gray-100 text-gray-500" }; export function SpacesPage() { const [spaces, setSpaces] = useState([]); const [err, setErr] = useState(""); const [loading, setLoading] = useState(false); const [hideArchived, setHideArchived] = useState(true); const load = useCallback(() => { setLoading(true); adminSpaces(300) .then((s) => { setSpaces(s); setErr(""); }) .catch((e) => setErr((e as Error).message)) .finally(() => setLoading(false)); }, []); useEffect(load, [load]); const shown = useMemo(() => (hideArchived ? spaces.filter((s) => !s.archived) : spaces), [spaces, hideArchived]); const stats = useMemo(() => { const byKind: Record = {}; let archived = 0; for (const s of spaces) { byKind[s.kind] = (byKind[s.kind] ?? 0) + 1; if (s.archived) archived++; } return { total: spaces.length, byKind, archived }; }, [spaces]); return (

全平台空间(Space)

租户与成员之间的中间容器:Agent / 知识库按空间共享
{/* 计数卡片 */}
{loading ? "加载中…" : `共 ${shown.length} 个空间`}
{err &&

{err}

}
{shown.map((s) => ( ))} {shown.length === 0 && !loading && ( )}
空间 类型 租户 建者 成员 创建时间 状态
{s.name} {kind(s.kind).label} {s.tenant_name || {s.tenant_id || "—"}} {s.creator_email || s.creator || "—"} {s.members} {new Date(s.created_at).toLocaleDateString("zh-CN")} {s.archived ? 已归档 : 活跃}
暂无空间
); } function Stat({ label, value, tone }: { label: string; value: number; tone: string }) { return (
{label}
{value}
); }