173 lines
5.8 KiB
TypeScript
173 lines
5.8 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { Plus, Trash2, Users as UsersIcon, AlertCircle, Loader2 } from 'lucide-react'
|
|
import { api, UserInfo } from '@/lib/api'
|
|
import { canDeleteUsers, getStoredUser, roleLabel, RoleName } from '@/lib/auth'
|
|
import { CreateUserDialog } from '@/components/CreateUserDialog'
|
|
import { cn } from '@/lib/cn'
|
|
|
|
const creatableRoles: Record<RoleName, RoleName[]> = {
|
|
system_admin: ['admin', 'user'],
|
|
admin: ['user'],
|
|
user: [],
|
|
}
|
|
|
|
export function Users() {
|
|
const [users, setUsers] = useState<UserInfo[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState('')
|
|
const [dialogOpen, setDialogOpen] = useState(false)
|
|
|
|
const currentUser = getStoredUser()
|
|
const allowedRoles = currentUser ? creatableRoles[currentUser.role] : []
|
|
|
|
const fetchData = async () => {
|
|
setLoading(true)
|
|
try {
|
|
const u = await api.listUsers()
|
|
setUsers(u)
|
|
setError('')
|
|
} catch (err: any) {
|
|
setError(err?.message || '加载失败')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchData()
|
|
}, [])
|
|
|
|
const handleDelete = async (id: string) => {
|
|
if (!confirm('确定删除该用户?')) return
|
|
try {
|
|
await api.deleteUser(id)
|
|
fetchData()
|
|
} catch (err: any) {
|
|
setError(err?.message || '删除失败')
|
|
}
|
|
}
|
|
|
|
const handleStatusChange = async (user: UserInfo, status: string) => {
|
|
try {
|
|
await api.updateUser(user.id, { status })
|
|
fetchData()
|
|
} catch (err: any) {
|
|
setError(err?.message || '更新失败')
|
|
}
|
|
}
|
|
|
|
const current = users.find((u) => u.username === currentUser?.username)
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 rounded-btn bg-accent/10 text-accent">
|
|
<UsersIcon className="h-5 w-5" />
|
|
</div>
|
|
<h1 className="text-xl font-bold text-foreground">用户管理</h1>
|
|
</div>
|
|
<button
|
|
onClick={() => setDialogOpen(true)}
|
|
className="inline-flex items-center gap-2 px-3 py-2 rounded-btn bg-accent text-white text-sm font-medium hover:bg-accent/90 transition-colors"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
新建用户
|
|
</button>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="flex items-start gap-2 rounded-btn border border-danger/30 bg-danger/10 px-3 py-2.5 text-xs text-danger">
|
|
<AlertCircle className="h-3.5 w-3.5 mt-px shrink-0" />
|
|
<span>{error}</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="rounded-card border border-border bg-surface overflow-hidden">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-elevated/50 text-secondary">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left font-medium">用户名</th>
|
|
<th className="px-4 py-3 text-left font-medium">角色</th>
|
|
<th className="px-4 py-3 text-left font-medium">状态</th>
|
|
<th className="px-4 py-3 text-left font-medium">创建时间</th>
|
|
<th className="px-4 py-3 text-right font-medium">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-border">
|
|
{loading ? (
|
|
<tr>
|
|
<td colSpan={5} className="px-4 py-8 text-center text-muted">
|
|
<Loader2 className="h-5 w-5 animate-spin mx-auto" />
|
|
</td>
|
|
</tr>
|
|
) : users.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={5} className="px-4 py-8 text-center text-muted">暂无用户</td>
|
|
</tr>
|
|
) : (
|
|
users.map((u) => (
|
|
<tr key={u.id} className="hover:bg-elevated/30">
|
|
<td className="px-4 py-3 text-foreground">{u.username}</td>
|
|
<td className="px-4 py-3">
|
|
<span className={cn('text-xs px-2 py-0.5 rounded', roleBadge(u.role))}>
|
|
{roleLabel(u.role)}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<select
|
|
value={u.status}
|
|
onChange={(e) => handleStatusChange(u, e.target.value)}
|
|
disabled={u.id === current?.id}
|
|
className="bg-base border border-border rounded-input px-2 py-1 text-xs disabled:opacity-50"
|
|
>
|
|
<option value="active">启用</option>
|
|
<option value="disabled">禁用</option>
|
|
</select>
|
|
</td>
|
|
<td className="px-4 py-3 text-muted text-xs">
|
|
{new Date(u.created_at).toLocaleString()}
|
|
</td>
|
|
<td className="px-4 py-3 text-right">
|
|
{canDeleteUsers(current?.role || 'user') && u.id !== current?.id && (
|
|
<button
|
|
onClick={() => handleDelete(u.id)}
|
|
className="p-1.5 rounded-btn text-danger hover:bg-danger/10 transition-colors"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</button>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<CreateUserDialog
|
|
open={dialogOpen}
|
|
onClose={() => setDialogOpen(false)}
|
|
onSuccess={() => {
|
|
setDialogOpen(false)
|
|
fetchData()
|
|
}}
|
|
allowedRoles={allowedRoles}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function roleBadge(role: string) {
|
|
switch (role) {
|
|
case 'system_admin':
|
|
return 'bg-accent/10 text-accent'
|
|
case 'admin':
|
|
return 'bg-purple-500/10 text-purple-400'
|
|
case 'user':
|
|
return 'bg-bear/10 text-bear'
|
|
default:
|
|
return 'bg-muted/10 text-muted'
|
|
}
|
|
}
|