monitor-ai/src/app/admin/users/page.tsx

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>
)
}