搭建用户体系

This commit is contained in:
2026-07-03 23:40:33 +08:00
parent 37a9f865ed
commit 2df19fcd02
40 changed files with 2622 additions and 0 deletions
+217
View File
@@ -0,0 +1,217 @@
import { useEffect, useState } from 'react'
import { Loader2, Plus, Trash2, Users as UsersIcon, AlertCircle } from 'lucide-react'
import { api, Role, UserInfo } from '@/lib/api'
import { canDeleteUsers, roleLabel } from '@/lib/auth'
import { cn } from '@/lib/cn'
export function Users() {
const [users, setUsers] = useState<UserInfo[]>([])
const [roles, setRoles] = useState<Role[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [formOpen, setFormOpen] = useState(false)
const [form, setForm] = useState({ username: '', email: '', password: '', role: 'user' })
const fetchData = async () => {
setLoading(true)
try {
const [u, r] = await Promise.all([api.listUsers(), api.listRoles()])
setUsers(u)
setRoles(r)
setError('')
} catch (err: any) {
setError(err?.message || '加载失败')
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchData()
}, [])
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault()
try {
await api.createUser({
username: form.username,
email: form.email || undefined,
password: form.password,
role: form.role,
})
setForm({ username: '', email: '', password: '', role: 'user' })
setFormOpen(false)
fetchData()
} catch (err: any) {
setError(err?.message || '创建失败')
}
}
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 currentUser = users.find((u) => u.username === JSON.parse(localStorage.getItem('user') || '{}')?.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={() => setFormOpen(!formOpen)}
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>
)}
{formOpen && (
<form
onSubmit={handleCreate}
className="rounded-card border border-border bg-surface p-4 grid grid-cols-1 md:grid-cols-5 gap-3"
>
<input
required
placeholder="用户名"
value={form.username}
onChange={(e) => setForm({ ...form, username: e.target.value })}
className="rounded-input bg-base border border-border px-3 py-2 text-sm focus:outline-none focus:border-accent"
/>
<input
type="email"
placeholder="邮箱"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
className="rounded-input bg-base border border-border px-3 py-2 text-sm focus:outline-none focus:border-accent"
/>
<input
required
type="password"
placeholder="密码"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
className="rounded-input bg-base border border-border px-3 py-2 text-sm focus:outline-none focus:border-accent"
/>
<select
value={form.role}
onChange={(e) => setForm({ ...form, role: e.target.value })}
className="rounded-input bg-base border border-border px-3 py-2 text-sm focus:outline-none focus:border-accent"
>
{roles.map((r) => (
<option key={r.id} value={r.name}>{roleLabel(r.name)}</option>
))}
</select>
<button
type="submit"
className="md:col-span-5 inline-flex items-center justify-center gap-2 px-4 py-2 rounded-btn bg-accent text-white text-sm font-medium hover:bg-accent/90 transition-colors"
>
</button>
</form>
)}
<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 === currentUser?.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(currentUser?.role || 'user') && u.id !== currentUser?.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>
</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'
}
}