Files
TurboHR/frontend/src/pages/platform/PlatformUsers.tsx
T
freedakgmail 2968484d2d 优化: 大文件拆分+代码分割+按需加载+console清理+any类型替换
- Money.tsx (2260行→54行): 拆分为 money/ 子目录4个组件, React.lazy二级分割
- AIAssistant.tsx (2038行→63行): 拆分为 ai-assistant/ 子目录6个组件, React.lazy二级分割
- xlsx改为动态导入, OvertimeTab从345KB降至12.7KB
- api-services.ts: 请求参数 any→Record<string,unknown>
- 移除前端3处console.log残留
- 后端console替换为pino logger
- 前后端未使用import/变量清理
- Zod schema验证: termination/platform/special-status/work-process
- 新增 leave.routes.ts, acceptance-test.routes.ts
- UI组件: PageGuide, QueryError, Stepper
2026-08-04 07:53:37 +08:00

168 lines
6.7 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 { toast } from 'sonner'
import { platformApi } from '../../lib/api-services'
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 platformApi.users(params) as any
setUsers(res.list)
setTotal(res.total)
} finally {
setLoading(false)
}
}
useEffect(() => {
platformApi.orgs({ pageSize: 200 }).then((res: any) => {
setOrgs(res.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 platformApi.toggleUser(user.id)
fetchUsers()
} catch (err: any) {
toast.error(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-gray-900"></h1>
<p className="text-sm text-gray-500 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="pl-10"
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && fetchUsers()}
/>
</div>
<Select
className="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}>
</Button>
</div>
{/* 表格 */}
<div className="bg-white border border-gray-200 rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-200 text-gray-500 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-gray-400">...</td></tr>
) : users.length === 0 ? (
<tr><td colSpan={7} className="text-center py-12 text-gray-400"></td></tr>
) : users.map((user) => (
<tr key={user.id} className="border-b border-gray-100 hover:bg-gray-50">
<td className="px-4 py-3 text-gray-900 font-medium">{user.name}</td>
<td className="px-4 py-3 text-gray-700">{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-gray-700">{user.orgName || '-'}</td>
<td className="px-4 py-3">
{user.disabled
? <span className="text-xs text-red-500"></span>
: <span className="text-xs text-emerald-600"></span>}
</td>
<td className="px-4 py-3 text-gray-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-600 hover:bg-gray-100'
: 'text-red-500 hover:bg-gray-100'
}`}
>
{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-gray-200">
<span className="text-xs text-gray-500"> {page} / {totalPages} {total} </span>
<div className="flex gap-2">
<Button size="sm" variant="secondary" disabled={page <= 1} onClick={() => setPage(page - 1)}></Button>
<Button size="sm" variant="secondary" disabled={page >= totalPages} onClick={() => setPage(page + 1)}></Button>
</div>
</div>
)}
</div>
</div>
)
}