refactor: sync running-container source back to repo for reproducible docker compose build; remove dead code (orphan pipeline.py + 104 stale files from iterative dev) and drop unused app/ mirror dir

This commit is contained in:
qyyfw committed 2026-07-11 06:29:32 +08:00
1 parent 1346fbbd4b
commit 5d7f32db0a
138 files changed
+12846 -84248

No files matched your search

-235
View File
@@ -1,235 +0,0 @@
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<Tenant[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [stats, setStats] = useState<Stats | null>(null);
const [members, setMembers] = useState<Member[]>([]);
const [selectedTenant, setSelectedTenant] = useState<string>('');
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<string, string> = { active: '#10b981', suspended: '#f59e0b', terminated: '#ef4444' };
const roleColors: Record<string, string> = { owner: '#a78bfa', admin: '#60a5fa', member: '#6B7280', viewer: '#9CA3AF' };
return (
<div style={{ padding: '24px', color: 'var(--color-text-primary)' }}>
<h1 style={{ fontSize: '24px', fontWeight: 700, marginBottom: '24px' }}>多租户管理</h1>
{/* Stats cards */}
{stats && (
<div className="responsive-grid-auto" style={{ gap: '16px', marginBottom: '24px' }}>
{[
{ label: '总租户数', value: stats.total_tenants, color: '#6C63FF' },
{ 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) => (
<div key={i} style={{ background: '#FFFFFF', borderRadius: '12px', padding: '20px', border: `1px solid ${s.color}33`, boxShadow: '0 1px 3px rgba(0,0,0,0.04)' }}>
<div style={{ fontSize: '32px', fontWeight: 800, color: s.color }}>{s.value}</div>
<div style={{ fontSize: '13px', color: '#6B7280', marginTop: '4px' }}>{s.label}</div>
</div>
))}
</div>
)}
{/* Tabs */}
<div style={{ display: 'flex', gap: '8px', marginBottom: '20px' }}>
{([['list', '租户列表'], ['members', '成员管理'], ['stats', '统计详情']] as const).map(([k, label]) => (
<button key={k} onClick={() => setTab(k)}
style={{ padding: '8px 20px', borderRadius: '8px', border: 'none', cursor: 'pointer',
background: tab === k ? '#6C63FF' : '#F0F2F8', color: tab === k ? '#fff' : '#6B7280',
fontWeight: 600, fontSize: '14px' }}>{label}</button>
))}
{tab === 'list' && (
<button onClick={() => setShowCreate(true)} style={{ marginLeft: 'auto', padding: '8px 20px', borderRadius: '8px',
border: 'none', cursor: 'pointer', background: '#6C63FF', color: '#fff', fontWeight: 600, fontSize: '14px' }}>
+ 创建租户
</button>
)}
</div>
{/* Tab content */}
{tab === 'list' && (
<div>
{loading ? <div style={{ textAlign: 'center', padding: '40px', color: '#9CA3AF' }}>加载中...</div> : (
<table style={{ width: '100%', borderCollapse: 'collapse', background: '#FFFFFF', borderRadius: '12px', overflow: 'hidden', border: '1px solid rgba(0,0,0,0.08)' }}>
<thead>
<tr style={{ background: '#F8F9FC' }}>
{['租户名称', '标识', '状态', '成员数', '存储上限', 'API/天', '创建时间', '操作'].map(h => (
<th key={h} style={{ padding: '12px 16px', textAlign: 'left', fontSize: '13px', color: '#6B7280', fontWeight: 600 }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{tenants.map(t => (
<tr key={t.id} style={{ borderBottom: '1px solid rgba(0,0,0,0.08)' }}>
<td style={{ padding: '12px 16px', fontSize: '14px', fontWeight: 600 }}>{t.name}</td>
<td style={{ padding: '12px 16px', fontSize: '13px', color: '#6B7280' }}>{t.slug}</td>
<td style={{ padding: '12px 16px' }}>
<span style={{ padding: '2px 10px', borderRadius: '12px', fontSize: '12px', fontWeight: 600,
background: `${statusColors[t.status]}22`, color: statusColors[t.status] }}>{t.status}</span>
</td>
<td style={{ padding: '12px 16px', fontSize: '14px' }}>{t.member_count ?? '-'}</td>
<td style={{ padding: '12px 16px', fontSize: '13px', color: '#6B7280' }}>{(t.max_storage_mb / 1024).toFixed(1)} GB</td>
<td style={{ padding: '12px 16px', fontSize: '13px', color: '#6B7280' }}>{t.max_api_calls_per_day.toLocaleString()}</td>
<td style={{ padding: '12px 16px', fontSize: '13px', color: '#9CA3AF' }}>{t.created_at?.substring(0, 10)}</td>
<td style={{ padding: '12px 16px' }}>
<button onClick={() => { setSelectedTenant(t.id); setTab('members'); }}
style={{ padding: '4px 12px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.12)',
background: 'transparent', color: '#6B7280', cursor: 'pointer', fontSize: '12px' }}>管理</button>
</td>
</tr>
))}
</tbody>
</table>
)}
{total > 20 && (
<div className="pagination-bar" style={{ marginTop: '20px' }}>
<button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page <= 1}
style={{ padding: '6px 16px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.12)', background: '#FFFFFF', color: '#6B7280', cursor: page <= 1 ? 'not-allowed' : 'pointer' }}>上一页</button>
<span style={{ padding: '6px 16px', color: '#6B7280' }}>第 {page} 页</span>
<button onClick={() => setPage(p => p + 1)} disabled={page * 20 >= total}
style={{ padding: '6px 16px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.12)', background: '#FFFFFF', color: '#6B7280', cursor: page * 20 >= total ? 'not-allowed' : 'pointer' }}>下一页</button>
</div>
)}
</div>
)}
{tab === 'members' && (
<div>
<div style={{ marginBottom: '16px' }}>
<select value={selectedTenant} onChange={e => setSelectedTenant(e.target.value)}
style={{ padding: '8px 12px', borderRadius: '8px', background: '#FFFFFF', color: '#1A1F36', border: '1px solid rgba(0,0,0,0.12)', fontSize: '14px' }}>
<option value="">选择租户...</option>
{tenants.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
</select>
</div>
{selectedTenant && (
<table style={{ width: '100%', borderCollapse: 'collapse', background: '#FFFFFF', borderRadius: '12px', overflow: 'hidden', border: '1px solid rgba(0,0,0,0.08)' }}>
<thead>
<tr style={{ background: '#F8F9FC' }}>
{['用户名', '邮箱', '角色', '状态', '加入时间'].map(h => (
<th key={h} style={{ padding: '12px 16px', textAlign: 'left', fontSize: '13px', color: '#6B7280', fontWeight: 600 }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{members.map(m => (
<tr key={m.id} style={{ borderBottom: '1px solid rgba(0,0,0,0.08)' }}>
<td style={{ padding: '12px 16px', fontSize: '14px', fontWeight: 600 }}>{m.username || m.user_id.substring(0, 8)}</td>
<td style={{ padding: '12px 16px', fontSize: '13px', color: '#6B7280' }}>{m.email || '-'}</td>
<td style={{ padding: '12px 16px' }}>
<span style={{ padding: '2px 10px', borderRadius: '12px', fontSize: '12px', fontWeight: 600,
background: `${roleColors[m.role]}22`, color: roleColors[m.role] }}>{m.role}</span>
</td>
<td style={{ padding: '12px 16px' }}>
<span style={{ color: m.status === 'active' ? '#10b981' : '#ef4444', fontSize: '13px' }}>{m.status}</span>
</td>
<td style={{ padding: '12px 16px', fontSize: '13px', color: '#9CA3AF' }}>{m.joined_at?.substring(0, 16)}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
{tab === 'stats' && stats && (
<div style={{ background: '#FFFFFF', borderRadius: '12px', padding: '32px', border: '1px solid rgba(0,0,0,0.08)', boxShadow: '0 1px 3px rgba(0,0,0,0.04)' }}>
<div className="responsive-grid-2" style={{ gap: '24px' }}>
{Object.entries(stats).map(([k, v]) => (
<div key={k} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px', background: '#F8F9FC', borderRadius: '8px' }}>
<span style={{ fontSize: '14px', color: '#6B7280' }}>{k.replace(/_/g, ' ').toUpperCase()}</span>
<span style={{ fontSize: '24px', fontWeight: 800, color: '#6C63FF' }}>{String(v)}</span>
</div>
))}
</div>
</div>
)}
{/* Create modal */}
{showCreate && (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}
onClick={() => setShowCreate(false)}>
<div onClick={e => e.stopPropagation()} style={{ background: '#FFFFFF', borderRadius: '16px', padding: '32px', width: '480px', maxWidth: '90vw', boxShadow: '0 4px 24px rgba(0,0,0,0.12)' }}>
<h2 style={{ fontSize: '20px', fontWeight: 700, marginBottom: '24px', color: '#1A1F36' }}>创建租户</h2>
{[
{ 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 => (
<div key={f.k} style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', fontSize: '13px', color: '#6B7280', marginBottom: '6px' }}>{f.label}</label>
<input type={f.type} value={(form as any)[f.k]} onChange={e => setForm({ ...form, [f.k]: f.type === 'number' ? Number(e.target.value) : e.target.value })}
style={{ width: '100%', padding: '10px 12px', borderRadius: '8px', background: '#F8F9FC', color: '#1A1F36', border: '1px solid rgba(0,0,0,0.12)', fontSize: '14px', boxSizing: 'border-box' }} />
</div>
))}
<div style={{ display: 'flex', gap: '12px', marginTop: '24px' }}>
<button onClick={() => setShowCreate(false)} style={{ flex: 1, padding: '10px', borderRadius: '8px', border: '1px solid rgba(0,0,0,0.12)', background: 'transparent', color: '#6B7280', cursor: 'pointer', fontSize: '14px' }}>取消</button>
<button onClick={handleCreate} style={{ flex: 1, padding: '10px', borderRadius: '8px', border: 'none', background: '#6C63FF', color: '#fff', cursor: 'pointer', fontSize: '14px', fontWeight: 600 }}>创建</button>
</div>
</div>
</div>
)}
</div>
);
};
export default Tenant;