实现租户管理页面:租户列表、筛选、开通新账号、查看详情、暂停/恢复

This commit is contained in:
yml2213
2026-07-14 11:15:54 +08:00
parent a0b9271707
commit 312ae17cb9
+130 -5
View File
@@ -1,6 +1,131 @@
const Tenants = () => (
<div className="h-full flex items-center justify-center text-neutral-400 text-center">
<div><div className="text-lg mb-1"></div><div className="text-sm">...</div></div>
</div>
)
import { useState } from 'react'
import { Table, Input, Select, Tag, Button, Drawer, Form, InputNumber, Switch, Space, message, Popconfirm, Descriptions } from 'antd'
import { PlusOutlined, SearchOutlined, EditOutlined, ReloadOutlined, PauseCircleOutlined, EyeOutlined } from '@ant-design/icons'
interface Tenant {
id: string
name: string
contact: string
phone: string
email: string
plan: string
seats: number
startDate: string
expireDate: string
status: 'normal' | 'expiring' | 'suspended' | 'expired'
}
const mockTenants: Tenant[] = [
{ id: '1', name: '赵六科技', contact: '赵总', phone: '138****1111', email: 'zhao@tech.com', plan: '企业版', seats: 30, startDate: '2026-01-15', expireDate: '2027-01-15', status: 'normal' },
{ id: '2', name: '李四商贸', contact: '李经理', phone: '139****2222', email: 'li@trade.com', plan: '专业版', seats: 10, startDate: '2026-03-01', expireDate: '2026-09-01', status: 'normal' },
{ id: '3', name: '王五集团', contact: '王总', phone: '137****3333', email: 'wang@group.com', plan: '企业版', seats: 50, startDate: '2025-06-01', expireDate: '2026-07-20', status: 'expiring' },
{ id: '4', name: '孙八信息', contact: '孙经理', phone: '136****4444', email: 'sun@info.com', plan: '基础版', seats: 2, startDate: '2026-04-01', expireDate: '2027-04-01', status: 'suspended' },
{ id: '5', name: '周九科技', contact: '周总', phone: '135****5555', email: 'zhou@tech.com', plan: '专业版', seats: 8, startDate: '2025-01-01', expireDate: '2026-06-30', status: 'expired' },
{ id: '6', name: '吴十教育', contact: '吴校长', phone: '134****6666', email: 'wu@edu.com', plan: '基础版', seats: 3, startDate: '2026-07-01', expireDate: '2027-07-01', status: 'normal' },
{ id: '7', name: '郑十一医疗', contact: '郑主任', phone: '133****7777', email: 'zheng@med.com', plan: '企业版', seats: 20, startDate: '2026-05-15', expireDate: '2026-08-15', status: 'expiring' },
]
const statusConfig: Record<string, { color: string; text: string }> = {
normal: { color: 'green', text: '正常' },
expiring: { color: 'orange', text: '即将到期' },
suspended: { color: 'red', text: '已暂停' },
expired: { color: 'default', text: '已过期' },
}
const Tenants = () => {
const [search, setSearch] = useState('')
const [statusFilter, setStatusFilter] = useState<string>()
const [planFilter, setPlanFilter] = useState<string>()
const [drawerOpen, setDrawerOpen] = useState(false)
const [detailOpen, setDetailOpen] = useState(false)
const [selectedTenant, setSelectedTenant] = useState<Tenant | null>(null)
const [form] = Form.useForm()
const filtered = mockTenants.filter(t => {
if (search && !t.name.includes(search) && !t.contact.includes(search)) return false
if (statusFilter && t.status !== statusFilter) return false
if (planFilter && t.plan !== planFilter) return false
return true
})
const columns = [
{ title: '公司名称', dataIndex: 'name', key: 'name', render: (text: string) => <span className="text-sm font-medium text-neutral-800">{text}</span> },
{ title: '联系人', dataIndex: 'contact', key: 'contact' },
{ title: '手机号', dataIndex: 'phone', key: 'phone', render: (t: string) => <span className="text-neutral-500">{t}</span> },
{ title: '套餐', dataIndex: 'plan', key: 'plan', render: (p: string) => <Tag color={p === '企业版' ? 'purple' : p === '专业版' ? 'blue' : 'default'}>{p}</Tag> },
{ title: '坐席数', dataIndex: 'seats', key: 'seats', align: 'center' as const },
{ title: '开通日期', dataIndex: 'startDate', key: 'startDate', render: (t: string) => <span className="text-neutral-500 text-xs">{t}</span> },
{ title: '到期日期', dataIndex: 'expireDate', key: 'expireDate', render: (t: string) => <span className="text-neutral-500 text-xs">{t}</span> },
{ title: '状态', dataIndex: 'status', key: 'status', render: (s: string) => <Tag color={statusConfig[s].color}>{statusConfig[s].text}</Tag> },
{ title: '操作', key: 'actions', width: 220, render: (_: unknown, record: Tenant) => (
<Space size={0}>
<Button type="link" size="small" icon={<EyeOutlined />} onClick={(e) => { e.stopPropagation(); setSelectedTenant(record); setDetailOpen(true) }}></Button>
<Button type="link" size="small" icon={<EditOutlined />} onClick={(e) => e.stopPropagation()}></Button>
{record.status === 'normal' && (
<Popconfirm title="确定暂停该租户?" onConfirm={(e) => e?.stopPropagation()} onCancel={(e) => e?.stopPropagation()}>
<Button type="link" size="small" danger icon={<PauseCircleOutlined />} onClick={(e) => e.stopPropagation()}></Button>
</Popconfirm>
)}
{(record.status === 'suspended' || record.status === 'expired') && (
<Button type="link" size="small" icon={<ReloadOutlined />} onClick={(e) => { e.stopPropagation(); message.success('已恢复') }}></Button>
)}
</Space>
)},
]
return (
<div>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-neutral-800"></h2>
<Button type="primary" icon={<PlusOutlined />} onClick={() => { form.resetFields(); setDrawerOpen(true) }}></Button>
</div>
<div className="flex gap-3 mb-3 flex-wrap">
<Input prefix={<SearchOutlined />} placeholder="搜索公司名称或联系人" value={search} onChange={e => setSearch(e.target.value)} className="w-56" allowClear />
<Select placeholder="状态筛选" value={statusFilter} onChange={setStatusFilter} allowClear className="w-28" options={Object.entries(statusConfig).map(([k, v]) => ({ value: k, label: v.text }))} />
<Select placeholder="套餐筛选" value={planFilter} onChange={setPlanFilter} allowClear className="w-28" options={['基础版', '专业版', '企业版'].map(v => ({ value: v, label: v }))} />
</div>
<div className="bg-white rounded-lg border border-neutral-200 overflow-hidden">
<Table dataSource={filtered} columns={columns} rowKey="id" size="middle" pagination={{ pageSize: 10, showTotal: t => `${t} 个租户` }} />
</div>
{/* 开通新账号 */}
<Drawer title="开通新账号" open={drawerOpen} onClose={() => setDrawerOpen(false)} width={480}>
<Form form={form} layout="vertical" onFinish={_ => { setDrawerOpen(false); message.success('租户开通成功') }}>
<Form.Item name="company" label="公司名称" rules={[{ required: true }]}><Input placeholder="2-100字符" maxLength={100} /></Form.Item>
<Form.Item name="contact" label="联系人" rules={[{ required: true }]}><Input placeholder="2-30字符" maxLength={30} /></Form.Item>
<Form.Item name="phone" label="手机号" rules={[{ required: true, pattern: /^\d{11}$/, message: '请输入11位手机号' }]}><Input placeholder="11位手机号" maxLength={11} /></Form.Item>
<Form.Item name="email" label="邮箱" rules={[{ required: true, type: 'email' }]}><Input placeholder="接收登录信息" /></Form.Item>
<Form.Item name="plan" label="套餐" rules={[{ required: true }]}>
<Select options={[{ value: 'basic', label: '基础版 ¥299/月' }, { value: 'pro', label: '专业版 ¥699/月' }, { value: 'enterprise', label: '企业版 ¥1999/月' }]} />
</Form.Item>
<Form.Item name="seats" label="坐席数量" rules={[{ required: true }]}><InputNumber min={1} max={100} className="w-full" /></Form.Item>
<Form.Item name="duration" label="开通时长(月)" rules={[{ required: true }]}><InputNumber min={1} max={36} className="w-full" /></Form.Item>
<Form.Item name="autoRenew" label="自动续费" valuePropName="checked"><Switch /></Form.Item>
<Form.Item name="note" label="备注"><Input.TextArea rows={2} maxLength={500} /></Form.Item>
<Form.Item><Button type="primary" htmlType="submit" block></Button></Form.Item>
</Form>
</Drawer>
{/* 查看详情 */}
<Drawer title="租户详情" open={detailOpen} onClose={() => setDetailOpen(false)} width={400}>
{selectedTenant && (
<Descriptions column={1} size="small" colon={false} className="mt-4">
<Descriptions.Item label="公司名称">{selectedTenant.name}</Descriptions.Item>
<Descriptions.Item label="联系人">{selectedTenant.contact}</Descriptions.Item>
<Descriptions.Item label="手机号">{selectedTenant.phone}</Descriptions.Item>
<Descriptions.Item label="邮箱">{selectedTenant.email}</Descriptions.Item>
<Descriptions.Item label="套餐"><Tag color={selectedTenant.plan === '企业版' ? 'purple' : selectedTenant.plan === '专业版' ? 'blue' : 'default'}>{selectedTenant.plan}</Tag></Descriptions.Item>
<Descriptions.Item label="坐席数">{selectedTenant.seats}</Descriptions.Item>
<Descriptions.Item label="开通日期">{selectedTenant.startDate}</Descriptions.Item>
<Descriptions.Item label="到期日期">{selectedTenant.expireDate}</Descriptions.Item>
<Descriptions.Item label="状态"><Tag color={statusConfig[selectedTenant.status].color}>{statusConfig[selectedTenant.status].text}</Tag></Descriptions.Item>
</Descriptions>
)}
</Drawer>
</div>
)
}
export default Tenants