Files
TurboHR/frontend/src/pages/platform/PlatformUsers.tsx
T

167 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 用户管理页 — 查看所有企业用户、搜索、启用/禁用
*/
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>
)
}