127 lines
6.0 KiB
TypeScript
127 lines
6.0 KiB
TypeScript
'use client'
|
|
// src/app/admin/users/page.tsx — 用户管理
|
|
import { useState, useEffect, useCallback } from 'react'
|
|
import { Users, RotateCw } from 'lucide-react'
|
|
|
|
interface User {
|
|
id: number; username: string; display_name: string | null; email: string | null
|
|
role: string; is_active: number; last_login_at: string | null
|
|
}
|
|
|
|
export default function UsersPage() {
|
|
const [users, setUsers] = useState<User[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [editingId, setEditingId] = useState<number | null>(null)
|
|
const [editRole, setEditRole] = useState('')
|
|
const [saving, setSaving] = useState(false)
|
|
const [toast, setToast] = useState<{ type: 'ok' | 'err'; msg: string } | null>(null)
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true)
|
|
const res = await fetch('/api/admin/users')
|
|
if (res.ok) setUsers(await res.json())
|
|
setLoading(false)
|
|
}, [])
|
|
|
|
useEffect(() => { load() }, [load])
|
|
|
|
const showToast = (type: 'ok' | 'err', msg: string) => {
|
|
setToast({ type, msg })
|
|
setTimeout(() => setToast(null), 3000)
|
|
}
|
|
|
|
const handleSave = async (id: number) => {
|
|
setSaving(true)
|
|
try {
|
|
const res = await fetch(`/api/admin/users/${id}`, {
|
|
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ role: editRole }),
|
|
})
|
|
if (res.ok) { showToast('ok', '角色已更新'); setEditingId(null); load() }
|
|
else { const d = await res.json(); showToast('err', d.error || '保存失败') }
|
|
} catch { showToast('err', '网络错误') }
|
|
setSaving(false)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{toast && (
|
|
<div className={`fixed top-4 right-4 z-50 px-4 py-2 rounded-lg text-sm font-medium shadow-lg ${toast.type === 'ok' ? 'bg-green-600 text-white' : 'bg-red-600 text-white'}`}>
|
|
{toast.msg}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold font-[family-name:var(--font-display)]">用户管理</h1>
|
|
<p className="text-sm text-slate-400 mt-1">管理用户角色和权限</p>
|
|
</div>
|
|
<button onClick={load} className="px-4 py-2 text-sm border rounded-lg hover:bg-slate-50 dark:hover:bg-slate-800 flex items-center gap-2">
|
|
<RotateCw size={14} /> 刷新
|
|
</button>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="border-b border-slate-200 dark:border-slate-700">
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">用户名</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">显示名</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">邮箱</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">角色</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">状态</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{loading ? (
|
|
<tr><td colSpan={6} className="px-4 py-12 text-center text-slate-400">加载中...</td></tr>
|
|
) : users.map(user => (
|
|
<tr key={user.id} className="border-b border-slate-100 dark:border-slate-800">
|
|
<td className="px-4 py-3 text-sm font-medium">{user.username}</td>
|
|
<td className="px-4 py-3 text-sm text-slate-600 dark:text-slate-400">{user.display_name || '—'}</td>
|
|
<td className="px-4 py-3 text-sm text-slate-400">{user.email || '—'}</td>
|
|
<td className="px-4 py-3 text-sm">
|
|
{editingId === user.id ? (
|
|
<select value={editRole} onChange={e => setEditRole(e.target.value)}
|
|
className="px-2 py-1 rounded border text-sm">
|
|
<option value="admin">admin</option>
|
|
<option value="editor">editor</option>
|
|
<option value="viewer">viewer</option>
|
|
</select>
|
|
) : (
|
|
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
|
user.role === 'admin' ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-500/10 dark:text-indigo-400' :
|
|
user.role === 'editor' ? 'bg-blue-100 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400' :
|
|
'bg-slate-100 text-slate-700 dark:bg-slate-500/10 dark:text-slate-400'
|
|
}`}>{user.role}</span>
|
|
)}
|
|
</td>
|
|
<td className="px-4 py-3 text-sm">
|
|
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${user.is_active ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
|
|
{user.is_active ? '启用' : '禁用'}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3 text-sm">
|
|
{editingId === user.id ? (
|
|
<div className="flex gap-2">
|
|
<button onClick={() => handleSave(user.id)} disabled={saving}
|
|
className="px-3 py-1 text-xs bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50">
|
|
{saving ? '保存中' : '保存'}
|
|
</button>
|
|
<button onClick={() => setEditingId(null)} className="px-3 py-1 text-xs border rounded-lg">取消</button>
|
|
</div>
|
|
) : (
|
|
<button onClick={() => { setEditingId(user.id); setEditRole(user.role) }}
|
|
className="px-3 py-1 text-xs border rounded-lg hover:bg-slate-50 dark:hover:bg-slate-800">编辑</button>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|