167 lines
7.0 KiB
TypeScript
167 lines
7.0 KiB
TypeScript
/**
|
||
* 用户管理页 — 查看所有企业用户、搜索、启用/禁用
|
||
*/
|
||
import { useEffect, useState } from 'react'
|
||
import { Search, Ban, CheckCircle } from 'lucide-react'
|
||
import api from '../../lib/api'
|
||
import { Input, Select } from '../../components/ui/Input'
|
||
import Button from '../../components/ui/Button'
|
||
|
||
interface UserItem {
|
||
id: string; name: string; phone: string; role: string
|
||
disabled: boolean; lastLoginAt: string | null; createdAt: string
|
||
orgName: string | null
|
||
}
|
||
|
||
const ROLE_LABELS: Record<string, string> = {
|
||
SUPER_ADMIN: '超级管理员', ADMIN: '管理员', HR: 'HR', VIEWER: '只读',
|
||
}
|
||
const ROLE_COLORS: Record<string, string> = {
|
||
SUPER_ADMIN: 'bg-amber-100 text-amber-800', ADMIN: 'bg-blue-50 text-blue-700',
|
||
HR: 'bg-emerald-50 text-emerald-700', VIEWER: 'bg-gray-100 text-gray-600',
|
||
}
|
||
|
||
export default function PlatformUsers() {
|
||
const [users, setUsers] = useState<UserItem[]>([])
|
||
const [total, setTotal] = useState(0)
|
||
const [page, setPage] = useState(1)
|
||
const [pageSize] = useState(20)
|
||
const [search, setSearch] = useState('')
|
||
const [orgFilter, setOrgFilter] = useState('')
|
||
const [loading, setLoading] = useState(true)
|
||
const [orgs, setOrgs] = useState<{ id: string; name: string }[]>([])
|
||
|
||
const fetchUsers = async () => {
|
||
setLoading(true)
|
||
try {
|
||
const params: any = { page, pageSize }
|
||
if (search) params.search = search
|
||
if (orgFilter) params.orgId = orgFilter
|
||
const res = await api.get('/platform/users', { params }) as any
|
||
setUsers(res.data.list)
|
||
setTotal(res.data.total)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
api.get('/platform/orgs', { params: { pageSize: 200 } }).then((res: any) => {
|
||
setOrgs(res.data.list.map((o: any) => ({ id: o.id, name: o.name })))
|
||
})
|
||
}, [])
|
||
|
||
useEffect(() => { fetchUsers() }, [page, orgFilter])
|
||
useEffect(() => { setPage(1) }, [search, orgFilter])
|
||
|
||
const handleToggle = async (user: UserItem) => {
|
||
try {
|
||
await api.put(`/platform/users/${user.id}/toggle`)
|
||
fetchUsers()
|
||
} catch (err: any) {
|
||
alert(err.response?.data?.error?.message || '操作失败')
|
||
}
|
||
}
|
||
|
||
const totalPages = Math.ceil(total / pageSize)
|
||
|
||
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' && fetchUsers()}
|
||
/>
|
||
</div>
|
||
<Select
|
||
className="bg-slate-900 border-slate-700 text-white w-40"
|
||
value={orgFilter}
|
||
onChange={(e) => setOrgFilter(e.target.value)}
|
||
>
|
||
<option value="">全部企业</option>
|
||
{orgs.map((o) => <option key={o.id} value={o.id}>{o.name}</option>)}
|
||
</Select>
|
||
<Button variant="secondary" onClick={fetchUsers} 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-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-center px-4 py-3 font-medium">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{loading ? (
|
||
<tr><td colSpan={7} className="text-center py-12 text-slate-400">加载中...</td></tr>
|
||
) : users.length === 0 ? (
|
||
<tr><td colSpan={7} className="text-center py-12 text-slate-400">暂无数据</td></tr>
|
||
) : users.map((user) => (
|
||
<tr key={user.id} className="border-b border-slate-800/50 hover:bg-slate-800/30">
|
||
<td className="px-4 py-3 text-white font-medium">{user.name}</td>
|
||
<td className="px-4 py-3 text-slate-300">{user.phone}</td>
|
||
<td className="px-4 py-3">
|
||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${ROLE_COLORS[user.role] || 'bg-gray-100 text-gray-600'}`}>
|
||
{ROLE_LABELS[user.role] || user.role}
|
||
</span>
|
||
</td>
|
||
<td className="px-4 py-3 text-slate-300">{user.orgName || '-'}</td>
|
||
<td className="px-4 py-3">
|
||
{user.disabled
|
||
? <span className="text-xs text-red-400">已禁用</span>
|
||
: <span className="text-xs text-emerald-400">正常</span>}
|
||
</td>
|
||
<td className="px-4 py-3 text-slate-400 text-xs">
|
||
{user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString('zh-CN') : '从未登录'}
|
||
</td>
|
||
<td className="px-4 py-3 text-center">
|
||
<button
|
||
onClick={() => handleToggle(user)}
|
||
className={`inline-flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${
|
||
user.disabled
|
||
? 'text-emerald-400 hover:bg-slate-800'
|
||
: 'text-red-400 hover:bg-slate-800'
|
||
}`}
|
||
>
|
||
{user.disabled ? <><CheckCircle className="w-3 h-3" /> 启用</> : <><Ban className="w-3 h-3" /> 禁用</>}
|
||
</button>
|
||
</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>
|
||
</div>
|
||
)
|
||
}
|