import { useState, useEffect } from 'react' import { Input, Select, Button, Drawer, Form, InputNumber, message, Empty, Modal, Popconfirm, Spin, Pagination, } from 'antd' import { PlusOutlined, SearchOutlined, ReloadOutlined, PauseCircleOutlined, CloseOutlined, CalendarOutlined, } from '@ant-design/icons' import { createTenant, getPlans, getTenants, resumeTenant, suspendTenant, updateTenant, type Plan, type Tenant, } from '@/services/api' const statusMeta: Record = { normal: { text: '正常', bg: '#f0fdf4', color: '#16a34a', dot: '#16a34a' }, expiring: { text: '即将到期', bg: '#fffbeb', color: '#d97706', dot: '#d97706' }, suspended: { text: '已暂停', bg: '#f1f5f9', color: '#64748b', dot: '#94a3b8' }, expired: { text: '已过期', bg: '#fef2f2', color: '#dc2626', dot: '#dc2626' }, } const statusFilters = [ { key: undefined as string | undefined, label: '全部' }, { key: 'normal', label: '正常' }, { key: 'expiring', label: '即将到期' }, { key: 'suspended', label: '已暂停' }, { key: 'expired', label: '已过期' }, ] const avatarPalettes = [ { bg: '#dbeafe', color: '#1d4ed8' }, { bg: '#fef3c7', color: '#92400e' }, { bg: '#d1fae5', color: '#047857' }, { bg: '#ede9fe', color: '#6d28d9' }, { bg: '#ffedd5', color: '#c2410c' }, { bg: '#fce7f3', color: '#be185d' }, ] function avatarPalette(name: string) { let h = 0 for (let i = 0; i < name.length; i++) h = name.charCodeAt(i) + ((h << 5) - h) return avatarPalettes[Math.abs(h) % avatarPalettes.length] } function maskPhone(phone?: string) { if (!phone) return '—' const d = phone.replace(/\D/g, '') if (d.length === 11) return `${d.slice(0, 3)}****${d.slice(7)}` return phone } function fmtDate(iso?: string) { if (!iso) return '—' return new Date(iso).toLocaleDateString('zh-CN') } function planBadgeStyle(name: string) { if (name.includes('企业')) return { bg: '#dbeafe', color: '#2563eb' } if (name.includes('专业')) return { bg: '#f3e8ff', color: '#7c3aed' } if (name.includes('基础')) return { bg: '#f0fdf4', color: '#16a34a' } return { bg: '#f1f5f9', color: '#64748b' } } const StatusPill = ({ status }: { status: string }) => { const s = statusMeta[status] || statusMeta.normal return ( {s.text} ) } const Tenants = () => { const [tenants, setTenants] = useState([]) const [plans, setPlans] = useState([]) const [loading, setLoading] = useState(true) const [total, setTotal] = useState(0) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(10) const [search, setSearch] = useState('') const [searchInput, setSearchInput] = useState('') const [statusFilter, setStatusFilter] = useState() const [drawerOpen, setDrawerOpen] = useState(false) const [detailOpen, setDetailOpen] = useState(false) const [selectedTenant, setSelectedTenant] = useState(null) const [saving, setSaving] = useState(false) const [renewMonths, setRenewMonths] = useState(12) const [createdCreds, setCreatedCreds] = useState<{ username: string; password: string; name: string } | null>(null) const [createForm] = Form.useForm() const [editForm] = Form.useForm() useEffect(() => { loadTenants() }, [page, pageSize, search, statusFilter]) useEffect(() => { getPlans() .then(res => setPlans(Array.isArray(res.data) ? res.data.filter(p => p.status === 'active') : [])) .catch(() => setPlans([])) }, []) const loadTenants = async () => { setLoading(true) try { const res = await getTenants({ search, status: statusFilter, page, pageSize }) setTenants(res.list) setTotal(res.total) } catch { setTenants([]) } finally { setLoading(false) } } const planName = (planId: number | null) => { if (!planId) return '—' return plans.find(p => p.id === planId)?.name || `#${planId}` } const openCreate = () => { createForm.resetFields() createForm.setFieldsValue({ duration_months: 12, seat_count: 5 }) setDrawerOpen(true) } const openDetail = (record: Tenant, editFocus = false) => { setSelectedTenant(record) editForm.setFieldsValue({ edit_contact_name: record.contact_name, edit_contact_phone: record.contact_phone, edit_contact_email: record.contact_email, edit_seat_count: record.seat_count, edit_plan_id: record.plan_id, }) setRenewMonths(12) setDetailOpen(true) if (editFocus) { // detail drawer shows edit form always } } const handleCreate = async (values: { name: string; contact_name: string; contact_phone: string; contact_email?: string plan_id?: number; seat_count?: number; duration_months?: number }) => { setSaving(true) try { const res = await createTenant({ name: values.name.trim(), contact_name: values.contact_name.trim(), contact_phone: values.contact_phone.trim(), contact_email: values.contact_email?.trim() || '', plan_id: values.plan_id, seat_count: values.seat_count, duration_months: values.duration_months || 12, }) message.success('租户开通成功') setDrawerOpen(false) createForm.resetFields() setCreatedCreds({ username: res.data.admin_username, password: res.data.admin_password, name: res.data.tenant.name, }) setPage(1) await loadTenants() } catch (e) { message.error(e instanceof Error ? e.message : '开通失败') } finally { setSaving(false) } } const handleSuspend = async (id: number) => { try { await suspendTenant(id) message.success('已暂停') if (selectedTenant?.id === id) { setSelectedTenant({ ...selectedTenant, status: 'suspended' }) } await loadTenants() } catch (e) { message.error(e instanceof Error ? e.message : '暂停失败') } } const handleResume = async (id: number) => { try { await resumeTenant(id) message.success('已恢复') if (selectedTenant?.id === id) { setSelectedTenant({ ...selectedTenant, status: 'normal' }) } await loadTenants() } catch (e) { message.error(e instanceof Error ? e.message : '恢复失败') } } const handleUpdateSeats = async () => { if (!selectedTenant) return setSaving(true) try { const values = await editForm.validateFields() const res = await updateTenant(selectedTenant.id, { contact_name: values.edit_contact_name, contact_phone: values.edit_contact_phone, contact_email: values.edit_contact_email, seat_count: values.edit_seat_count, plan_id: values.edit_plan_id, }) message.success('已更新') setSelectedTenant(res.data) await loadTenants() } catch (e) { if (e instanceof Error && e.message) message.error(e.message) } finally { setSaving(false) } } const handleRenew = async () => { if (!selectedTenant || renewMonths < 1) return setSaving(true) try { const base = selectedTenant.expire_at && new Date(selectedTenant.expire_at) > new Date() ? new Date(selectedTenant.expire_at) : new Date() base.setMonth(base.getMonth() + renewMonths) const res = await updateTenant(selectedTenant.id, { expire_at: base.toISOString(), status: selectedTenant.status === 'expired' || selectedTenant.status === 'expiring' ? 'normal' : selectedTenant.status, }) message.success(`已续费 ${renewMonths} 个月`) setSelectedTenant(res.data) await loadTenants() } catch (e) { message.error(e instanceof Error ? e.message : '续费失败') } finally { setSaving(false) } } return (
{/* 顶栏 56px */}

租户管理

全平台企业租户全生命周期管理与授权配额

{/* 工具栏 */}
setSearchInput(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') { setSearch(searchInput.trim()) setPage(1) } }} className="flex-1 min-w-0 bg-transparent border-0 outline-none text-xs text-slate-800 placeholder:text-slate-400" />
{statusFilters.map(f => { const active = statusFilter === f.key || (!statusFilter && f.key === undefined) return ( ) })}
{/* 表格 */}
{loading ? (
) : tenants.length === 0 ? ( ) : (
{['公司名称', '联系人', '手机号', '套餐类型', '坐席', '开通日期', '到期日期', '状态', '操作'].map((h, i) => ( ))} {tenants.map(record => { const pal = avatarPalette(record.name) const pname = planName(record.plan_id) const pstyle = planBadgeStyle(pname) return ( openDetail(record)} > ) })}
{h}
{record.name.slice(0, 1)}
{record.name}
ID: T{String(record.id).padStart(4, '0')}
{record.contact_name || '—'} {maskPhone(record.contact_phone)} {pname} {record.seat_count} {fmtDate(record.created_at)} {fmtDate(record.expire_at)} e.stopPropagation()}>
| {record.status !== 'suspended' ? ( <> | handleSuspend(record.id)}> ) : ( <> | )}
)}
{total > 0 && (
共 {total} 个租户 { setPage(p) if (ps !== pageSize) { setPageSize(ps) setPage(1) } }} />
)}
{/* 开通抽屉 */} setDrawerOpen(false)} width={440} closable={false} styles={{ body: { padding: 0 }, header: { display: 'none' } }} >
开通新账号
{selectedTenant.status !== 'suspended' ? ( handleSuspend(selectedTenant.id)}> ) : ( )}
)} setCreatedCreds(null)} onOk={() => setCreatedCreds(null)} okText="知道了" cancelButtonProps={{ style: { display: 'none' } }} > {createdCreds && (

租户 {createdCreds.name} 已开通,请妥善保存登录信息:

管理员账号:{createdCreds.username}
初始密码:{createdCreds.password}
)}
) } export default Tenants