import { useState, useEffect, useCallback } from 'react'; import { adminAPI } from '@/api'; interface Tenant { id: string; name: string; slug: string; logo: string; description: string; status: string; max_members: number; max_storage_mb: number; max_api_calls_per_day: number; member_count?: number; created_at: string; expire_at?: string; } interface Member { id: string; user_id: string; username: string; email: string; role: string; status: string; joined_at: string; avatar?: string; } interface Stats { total_tenants: number; active_tenants: number; suspended_tenants: number; total_members: number; pending_invitations: number; } const Tenant: React.FC = () => { const [tab, setTab] = useState<'list' | 'members' | 'stats'>('list'); const [tenants, setTenants] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); const [stats, setStats] = useState(null); const [members, setMembers] = useState([]); const [selectedTenant, setSelectedTenant] = useState(''); const [showCreate, setShowCreate] = useState(false); const [form, setForm] = useState({ name: '', slug: '', description: '', max_members: 10, max_storage_mb: 10240, max_api_calls_per_day: 10000 }); const fetchTenants = useCallback(async () => { setLoading(true); try { const res = await adminAPI.getTenants(page); const d = (res as any)?.data || res; setTenants(d?.items || []); setTotal(d?.total || 0); } catch (e) { console.error('fetchTenants:', e); } setLoading(false); }, [page]); const fetchStats = useCallback(async () => { try { const res = await adminAPI.getTenantStats(); setStats((res as any)?.data || res); } catch (e) { console.error('fetchStats:', e); } }, []); const fetchMembers = useCallback(async () => { if (!selectedTenant) return; try { const res = await adminAPI.getTenantMembers(selectedTenant); const d = (res as any)?.data || res; setMembers(d?.items || []); } catch (e) { console.error('fetchMembers:', e); } }, [selectedTenant]); useEffect(() => { fetchTenants(); fetchStats(); }, [fetchTenants, fetchStats]); useEffect(() => { if (tab === 'members' && selectedTenant) fetchMembers(); }, [tab, selectedTenant, fetchMembers]); const handleCreate = async () => { try { const params = `?name=${encodeURIComponent(form.name)}&slug=${encodeURIComponent(form.slug)}&description=${encodeURIComponent(form.description)}&max_members=${form.max_members}&max_storage_mb=${form.max_storage_mb}&max_api_calls_per_day=${form.max_api_calls_per_day}`; await adminAPI.createTenant(params); setShowCreate(false); setForm({ name: '', slug: '', description: '', max_members: 10, max_storage_mb: 10240, max_api_calls_per_day: 10000 }); fetchTenants(); } catch (e) { console.error('create:', e); } }; const statusColors: Record = { active: '#10b981', suspended: '#f59e0b', terminated: '#ef4444' }; const roleColors: Record = { owner: '#a78bfa', admin: '#60a5fa', member: '#94a3b8', viewer: '#64748b' }; return (

多租户管理

{/* Stats cards */} {stats && (
{[ { label: '总租户数', value: stats.total_tenants, color: '#6366f1' }, { label: '活跃租户', value: stats.active_tenants, color: '#10b981' }, { label: '已暂停', value: stats.suspended_tenants, color: '#f59e0b' }, { label: '总成员数', value: stats.total_members, color: '#3b82f6' }, { label: '待处理邀请', value: stats.pending_invitations, color: '#ec4899' }, ].map((s, i) => (
{s.value}
{s.label}
))}
)} {/* Tabs */}
{([['list', '租户列表'], ['members', '成员管理'], ['stats', '统计详情']] as const).map(([k, label]) => ( ))} {tab === 'list' && ( )}
{/* Tab content */} {tab === 'list' && (
{loading ?
加载中...
: ( {['租户名称', '标识', '状态', '成员数', '存储上限', 'API/天', '创建时间', '操作'].map(h => ( ))} {tenants.map(t => ( ))}
{h}
{t.name} {t.slug} {t.status} {t.member_count ?? '-'} {(t.max_storage_mb / 1024).toFixed(1)} GB {t.max_api_calls_per_day.toLocaleString()} {t.created_at?.substring(0, 10)}
)} {total > 20 && (
第 {page} 页
)}
)} {tab === 'members' && (
{selectedTenant && ( {['用户名', '邮箱', '角色', '状态', '加入时间'].map(h => ( ))} {members.map(m => ( ))}
{h}
{m.username || m.user_id.substring(0, 8)} {m.email || '-'} {m.role} {m.status} {m.joined_at?.substring(0, 16)}
)}
)} {tab === 'stats' && stats && (
{Object.entries(stats).map(([k, v]) => (
{k.replace(/_/g, ' ').toUpperCase()} {String(v)}
))}
)} {/* Create modal */} {showCreate && (
setShowCreate(false)}>
e.stopPropagation()} style={{ background: '#1e293b', borderRadius: '16px', padding: '32px', width: '480px', maxWidth: '90vw' }}>

创建租户

{[ { k: 'name', label: '租户名称', type: 'text' }, { k: 'slug', label: '标识(英文)', type: 'text' }, { k: 'description', label: '描述', type: 'text' }, { k: 'max_members', label: '最大成员数', type: 'number' }, { k: 'max_storage_mb', label: '存储上限(MB)', type: 'number' }, { k: 'max_api_calls_per_day', label: 'API调用/天', type: 'number' }, ].map(f => (
setForm({ ...form, [f.k]: f.type === 'number' ? Number(e.target.value) : e.target.value })} style={{ width: '100%', padding: '10px 12px', borderRadius: '8px', background: '#0f172a', color: '#e2e8f0', border: '1px solid #475569', fontSize: '14px', boxSizing: 'border-box' }} />
))}
)}
); }; export default Tenant;