issue-ai/src/app/(app)/settings/users/page.tsx

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