247 lines
12 KiB
TypeScript
247 lines
12 KiB
TypeScript
/**
|
||
* 企业租户管理页 — 列表、搜索、查看详情、编辑套餐、删除
|
||
*/
|
||
import { useEffect, useState } from 'react'
|
||
import { Search, Building2, Eye, Trash2, Edit2 } from 'lucide-react'
|
||
import api from '../../lib/api'
|
||
import { Input, Select, Label } from '../../components/ui/Input'
|
||
import Button from '../../components/ui/Button'
|
||
|
||
interface Org {
|
||
id: string; name: string; plan: string; maxEmployees: number
|
||
city: string | null; contactName: string | null; contactPhone: string | null
|
||
payrollFrequency: number; retirementReminderEnabled: boolean
|
||
createdAt: string; updatedAt: string
|
||
employeeCount: number; userCount: number; contractCount: number; payslipCount: number
|
||
}
|
||
|
||
const PLAN_LABELS: Record<string, string> = { FREE: '免费版', PRO: '专业版', ENTERPRISE: '企业版' }
|
||
const PLAN_COLORS: Record<string, string> = { FREE: 'bg-gray-100 text-gray-700', PRO: 'bg-blue-50 text-blue-700', ENTERPRISE: 'bg-purple-50 text-purple-700' }
|
||
|
||
export default function PlatformOrgs() {
|
||
const [orgs, setOrgs] = useState<Org[]>([])
|
||
const [total, setTotal] = useState(0)
|
||
const [page, setPage] = useState(1)
|
||
const [pageSize] = useState(20)
|
||
const [search, setSearch] = useState('')
|
||
const [planFilter, setPlanFilter] = useState('')
|
||
const [loading, setLoading] = useState(true)
|
||
const [editOrg, setEditOrg] = useState<Org | null>(null)
|
||
const [deleteOrg, setDeleteOrg] = useState<Org | null>(null)
|
||
|
||
const fetchOrgs = async () => {
|
||
setLoading(true)
|
||
try {
|
||
const params: any = { page, pageSize }
|
||
if (search) params.search = search
|
||
if (planFilter) params.plan = planFilter
|
||
const res = await api.get('/platform/orgs', { params }) as any
|
||
setOrgs(res.data.list)
|
||
setTotal(res.data.total)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
useEffect(() => { fetchOrgs() }, [page, planFilter])
|
||
useEffect(() => { setPage(1) }, [search, planFilter])
|
||
|
||
const totalPages = Math.ceil(total / pageSize)
|
||
|
||
const handleSaveEdit = async () => {
|
||
if (!editOrg) return
|
||
try {
|
||
await api.put(`/platform/orgs/${editOrg.id}`, {
|
||
name: editOrg.name,
|
||
plan: editOrg.plan,
|
||
maxEmployees: editOrg.maxEmployees,
|
||
city: editOrg.city,
|
||
contactName: editOrg.contactName,
|
||
contactPhone: editOrg.contactPhone,
|
||
})
|
||
setEditOrg(null)
|
||
fetchOrgs()
|
||
} catch (err: any) {
|
||
alert(err.response?.data?.error?.message || '保存失败')
|
||
}
|
||
}
|
||
|
||
const handleDelete = async () => {
|
||
if (!deleteOrg) return
|
||
try {
|
||
await api.delete(`/platform/orgs/${deleteOrg.id}`)
|
||
setDeleteOrg(null)
|
||
fetchOrgs()
|
||
} catch (err: any) {
|
||
alert(err.response?.data?.error?.message || '删除失败')
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-white">企业租户管理</h1>
|
||
<p className="text-sm text-slate-400 mt-1">共 {total} 家企业</p>
|
||
</div>
|
||
|
||
{/* 搜索栏 */}
|
||
<div className="flex flex-wrap gap-3">
|
||
<div className="relative flex-1 min-w-[200px]">
|
||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
||
<Input
|
||
placeholder="搜索企业名称、城市、联系人..."
|
||
className="bg-slate-900 border-slate-700 text-white placeholder:text-slate-500 pl-10"
|
||
value={search}
|
||
onChange={(e) => setSearch(e.target.value)}
|
||
onKeyDown={(e) => e.key === 'Enter' && fetchOrgs()}
|
||
/>
|
||
</div>
|
||
<Select
|
||
className="bg-slate-900 border-slate-700 text-white w-32"
|
||
value={planFilter}
|
||
onChange={(e) => setPlanFilter(e.target.value)}
|
||
>
|
||
<option value="">全部套餐</option>
|
||
<option value="FREE">免费版</option>
|
||
<option value="PRO">专业版</option>
|
||
<option value="ENTERPRISE">企业版</option>
|
||
</Select>
|
||
<Button variant="secondary" onClick={fetchOrgs} className="bg-slate-800 text-slate-200 border border-slate-700 hover:bg-slate-700">
|
||
搜索
|
||
</Button>
|
||
</div>
|
||
|
||
{/* 表格 */}
|
||
<div className="bg-slate-900 border border-slate-800 rounded-lg overflow-hidden">
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="border-b border-slate-800 text-slate-400 text-xs">
|
||
<th className="text-left px-4 py-3 font-medium">企业名称</th>
|
||
<th className="text-left px-4 py-3 font-medium">套餐</th>
|
||
<th className="text-left px-4 py-3 font-medium">城市</th>
|
||
<th className="text-right px-4 py-3 font-medium">员工数</th>
|
||
<th className="text-right px-4 py-3 font-medium">用户数</th>
|
||
<th className="text-right px-4 py-3 font-medium">合同数</th>
|
||
<th className="text-left px-4 py-3 font-medium">注册时间</th>
|
||
<th className="text-center px-4 py-3 font-medium">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{loading ? (
|
||
<tr><td colSpan={8} className="text-center py-12 text-slate-400">加载中...</td></tr>
|
||
) : orgs.length === 0 ? (
|
||
<tr><td colSpan={8} className="text-center py-12 text-slate-400">暂无数据</td></tr>
|
||
) : orgs.map((org) => (
|
||
<tr key={org.id} className="border-b border-slate-800/50 hover:bg-slate-800/30">
|
||
<td className="px-4 py-3">
|
||
<div className="text-white font-medium">{org.name}</div>
|
||
<div className="text-xs text-slate-500">{org.contactName || '-'} {org.contactPhone || ''}</div>
|
||
</td>
|
||
<td className="px-4 py-3">
|
||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${PLAN_COLORS[org.plan] || 'bg-gray-100 text-gray-700'}`}>
|
||
{PLAN_LABELS[org.plan] || org.plan}
|
||
</span>
|
||
</td>
|
||
<td className="px-4 py-3 text-slate-300">{org.city || '-'}</td>
|
||
<td className="px-4 py-3 text-right text-slate-300">{org.employeeCount}</td>
|
||
<td className="px-4 py-3 text-right text-slate-300">{org.userCount}</td>
|
||
<td className="px-4 py-3 text-right text-slate-300">{org.contractCount}</td>
|
||
<td className="px-4 py-3 text-slate-400 text-xs">{new Date(org.createdAt).toLocaleDateString('zh-CN')}</td>
|
||
<td className="px-4 py-3">
|
||
<div className="flex items-center justify-center gap-1">
|
||
<button
|
||
onClick={() => setEditOrg(org)}
|
||
className="p-1.5 rounded text-slate-400 hover:text-amber-400 hover:bg-slate-800"
|
||
title="编辑"
|
||
>
|
||
<Edit2 className="w-4 h-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => setDeleteOrg(org)}
|
||
className="p-1.5 rounded text-slate-400 hover:text-red-400 hover:bg-slate-800"
|
||
title="删除"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
|
||
{/* 分页 */}
|
||
{totalPages > 1 && (
|
||
<div className="flex items-center justify-between px-4 py-3 border-t border-slate-800">
|
||
<span className="text-xs text-slate-400">第 {page} / {totalPages} 页,共 {total} 条</span>
|
||
<div className="flex gap-2">
|
||
<Button size="sm" variant="secondary" disabled={page <= 1} onClick={() => setPage(page - 1)} className="bg-slate-800 text-slate-200 border border-slate-700">上一页</Button>
|
||
<Button size="sm" variant="secondary" disabled={page >= totalPages} onClick={() => setPage(page + 1)} className="bg-slate-800 text-slate-200 border border-slate-700">下一页</Button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 编辑弹窗 */}
|
||
{editOrg && (
|
||
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" onClick={() => setEditOrg(null)}>
|
||
<div className="bg-slate-900 border border-slate-700 rounded-lg p-6 w-full max-w-md" onClick={(e) => e.stopPropagation()}>
|
||
<h2 className="text-lg font-semibold text-white mb-4">编辑企业</h2>
|
||
<div className="space-y-3">
|
||
<div>
|
||
<Label className="text-slate-300">企业名称</Label>
|
||
<Input className="bg-slate-800 border-slate-700 text-white" value={editOrg.name} onChange={(e) => setEditOrg({ ...editOrg, name: e.target.value })} />
|
||
</div>
|
||
<div>
|
||
<Label className="text-slate-300">套餐</Label>
|
||
<Select className="bg-slate-800 border-slate-700 text-white" value={editOrg.plan} onChange={(e) => setEditOrg({ ...editOrg, plan: e.target.value })}>
|
||
<option value="FREE">免费版</option>
|
||
<option value="PRO">专业版</option>
|
||
<option value="ENTERPRISE">企业版</option>
|
||
</Select>
|
||
</div>
|
||
<div>
|
||
<Label className="text-slate-300">员工上限</Label>
|
||
<Input type="number" className="bg-slate-800 border-slate-700 text-white" value={editOrg.maxEmployees} onChange={(e) => setEditOrg({ ...editOrg, maxEmployees: parseInt(e.target.value) || 0 })} />
|
||
</div>
|
||
<div>
|
||
<Label className="text-slate-300">城市</Label>
|
||
<Input className="bg-slate-800 border-slate-700 text-white" value={editOrg.city || ''} onChange={(e) => setEditOrg({ ...editOrg, city: e.target.value })} />
|
||
</div>
|
||
<div className="flex gap-2 pt-2">
|
||
<Button className="bg-amber-400 text-slate-950 hover:bg-amber-300 flex-1" onClick={handleSaveEdit}>保存</Button>
|
||
<Button variant="secondary" className="bg-slate-800 text-slate-200 border border-slate-700" onClick={() => setEditOrg(null)}>取消</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 删除确认 */}
|
||
{deleteOrg && (
|
||
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" onClick={() => setDeleteOrg(null)}>
|
||
<div className="bg-slate-900 border border-slate-700 rounded-lg p-6 w-full max-w-sm" onClick={(e) => e.stopPropagation()}>
|
||
<div className="flex items-center gap-3 mb-4">
|
||
<div className="w-10 h-10 rounded-full bg-red-950 flex items-center justify-center">
|
||
<Trash2 className="w-5 h-5 text-red-400" />
|
||
</div>
|
||
<div>
|
||
<h2 className="text-lg font-semibold text-white">确认删除</h2>
|
||
<p className="text-xs text-slate-400">此操作不可撤销,将级联删除所有数据</p>
|
||
</div>
|
||
</div>
|
||
<p className="text-sm text-slate-300 mb-4">
|
||
确定要删除企业「<span className="text-white font-medium">{deleteOrg.name}</span>」吗?
|
||
该企业下有 {deleteOrg.employeeCount} 名员工、{deleteOrg.contractCount} 份合同、{deleteOrg.payslipCount} 条工资条,都将被永久删除。
|
||
</p>
|
||
<div className="flex gap-2">
|
||
<Button variant="danger" className="flex-1" onClick={handleDelete}>确认删除</Button>
|
||
<Button variant="secondary" className="bg-slate-800 text-slate-200 border border-slate-700" onClick={() => setDeleteOrg(null)}>取消</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|