129 lines
6.2 KiB
TypeScript
129 lines
6.2 KiB
TypeScript
'use client'
|
|
import { useState, useEffect } from 'react'
|
|
import { Card, Button, Table, Badge, Modal, Input, Select } from '@/components/ui'
|
|
import { Plus, Pencil, Trash2 } from 'lucide-react'
|
|
|
|
interface User {
|
|
id: number
|
|
username: string
|
|
display_name: string
|
|
email: string | null
|
|
role: string
|
|
is_active: number
|
|
created_at: string
|
|
}
|
|
|
|
export default function UsersPage() {
|
|
const [users, setUsers] = useState<User[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [modalOpen, setModalOpen] = useState(false)
|
|
const [editUser, setEditUser] = useState<User | null>(null)
|
|
const [form, setForm] = useState({ username: '', password: '', display_name: '', email: '', role: 'viewer' })
|
|
const [error, setError] = useState('')
|
|
|
|
const fetchUsers = () => {
|
|
fetch('/api/users').then(r => r.json()).then(d => { if (d.users) setUsers(d.users) }).catch(() => {}).finally(() => setLoading(false))
|
|
}
|
|
|
|
useEffect(() => { fetchUsers() }, [])
|
|
|
|
const openCreate = () => {
|
|
setEditUser(null)
|
|
setForm({ username: '', password: '', display_name: '', email: '', role: 'viewer' })
|
|
setError('')
|
|
setModalOpen(true)
|
|
}
|
|
|
|
const openEdit = (u: User) => {
|
|
setEditUser(u)
|
|
setForm({ username: u.username, password: '', display_name: u.display_name, email: u.email || '', role: u.role })
|
|
setError('')
|
|
setModalOpen(true)
|
|
}
|
|
|
|
const handleSave = async () => {
|
|
setError('')
|
|
try {
|
|
if (editUser) {
|
|
const body: Record<string, unknown> = { display_name: form.display_name, email: form.email, role: form.role }
|
|
if (form.password) body.password = form.password
|
|
const res = await fetch(`/api/users/${editUser.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
|
if (!res.ok) { const d = await res.json(); setError(d.error || '更新失败'); return }
|
|
} else {
|
|
if (!form.username || !form.password || !form.display_name) { setError('请填写必填项'); return }
|
|
const res = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form) })
|
|
if (!res.ok) { const d = await res.json(); setError(d.error || '创建失败'); return }
|
|
}
|
|
setModalOpen(false)
|
|
fetchUsers()
|
|
} catch { setError('操作失败') }
|
|
}
|
|
|
|
const handleDelete = async (id: number) => {
|
|
if (!confirm('确定删除此用户?')) return
|
|
const res = await fetch(`/api/users/${id}`, { method: 'DELETE' })
|
|
if (res.ok) fetchUsers()
|
|
}
|
|
|
|
const handleToggleActive = async (u: User) => {
|
|
await fetch(`/api/users/${u.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_active: u.is_active ? 0 : 1 }) })
|
|
fetchUsers()
|
|
}
|
|
|
|
const roleLabel: Record<string, string> = { admin: '管理员', operator: '运维人员', viewer: '查看者' }
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">用户管理</h1>
|
|
<p className="text-slate-500 dark:text-slate-400 mt-1">系统用户与账号管理</p>
|
|
</div>
|
|
<Button size="sm" onClick={openCreate}><Plus size={16} className="mr-1" />新建用户</Button>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="text-center py-12 text-slate-500 dark:text-slate-400">加载中...</div>
|
|
) : (
|
|
<Table headers={['用户名', '显示名称', '邮箱', '角色', '状态', '创建时间', '操作']}>
|
|
{users.map(u => (
|
|
<tr key={u.id} className="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors">
|
|
<td className="px-4 py-3 font-medium text-slate-900 dark:text-slate-100">{u.username}</td>
|
|
<td className="px-4 py-3 text-slate-700 dark:text-slate-300">{u.display_name}</td>
|
|
<td className="px-4 py-3 text-slate-500 dark:text-slate-400">{u.email || '-'}</td>
|
|
<td className="px-4 py-3"><Badge variant="info">{roleLabel[u.role] || u.role}</Badge></td>
|
|
<td className="px-4 py-3">
|
|
<button onClick={() => handleToggleActive(u)}>
|
|
<Badge variant={u.is_active ? 'success' : 'danger'}>{u.is_active ? '启用' : '禁用'}</Badge>
|
|
</button>
|
|
</td>
|
|
<td className="px-4 py-3 text-sm text-slate-500 dark:text-slate-400">{u.created_at || '-'}</td>
|
|
<td className="px-4 py-3">
|
|
<div className="flex items-center gap-1">
|
|
<Button variant="ghost" size="sm" onClick={() => openEdit(u)}><Pencil size={14} /></Button>
|
|
<Button variant="ghost" size="sm" onClick={() => handleDelete(u.id)}><Trash2 size={14} className="text-red-500" /></Button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</Table>
|
|
)}
|
|
|
|
<Modal open={modalOpen} onClose={() => setModalOpen(false)} title={editUser ? '编辑用户' : '新建用户'}>
|
|
<div className="space-y-4">
|
|
{!editUser && <Input label="用户名" value={form.username} onChange={e => setForm(p => ({ ...p, username: e.target.value }))} required />}
|
|
<Input label="显示名称" value={form.display_name} onChange={e => setForm(p => ({ ...p, display_name: e.target.value }))} required />
|
|
<Input label={editUser ? '新密码(留空不修改)' : '密码'} type="password" value={form.password} onChange={e => setForm(p => ({ ...p, password: e.target.value }))} />
|
|
<Input label="邮箱" type="email" value={form.email} onChange={e => setForm(p => ({ ...p, email: e.target.value }))} />
|
|
<Select label="角色" options={[{ value: 'viewer', label: '查看者' }, { value: 'operator', label: '运维人员' }, { value: 'admin', label: '管理员' }]} value={form.role} onChange={e => setForm(p => ({ ...p, role: e.target.value }))} />
|
|
{error && <p className="text-sm text-red-500">{error}</p>}
|
|
<div className="flex gap-3">
|
|
<Button onClick={handleSave}>{editUser ? '保存' : '创建'}</Button>
|
|
<Button variant="secondary" onClick={() => setModalOpen(false)}>取消</Button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
</div>
|
|
)
|
|
}
|