feat: frontend auth flow (login, user management, audit logs, route guards)
This commit is contained in:
@@ -1,3 +1,160 @@
|
||||
export default function UserManagementPage() {
|
||||
return <div><h2 style={{ color: 'var(--text)', fontSize: 18, fontWeight: 600 }}>用户管理</h2><p style={{ color: 'var(--text-dim)', marginTop: 8 }}>用户管理与权限</p></div>
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { useAuth } from '../hooks/useAuth'
|
||||
import { getUsers, updateUser, approveUser } from '../api/users'
|
||||
import Table, { type TableColumn } from '../components/ui/Table'
|
||||
import Button from '../components/ui/Button'
|
||||
import Badge from '../components/ui/Badge'
|
||||
import toast from '../components/ui/Toast'
|
||||
|
||||
interface UserRecord {
|
||||
id: string
|
||||
username: string
|
||||
email: string
|
||||
role: string
|
||||
is_active: boolean
|
||||
is_approved: boolean
|
||||
created_at: string
|
||||
}
|
||||
|
||||
const ROLE_OPTIONS = ['owner', 'operator', 'viewer']
|
||||
|
||||
export default function UserManagementPage() {
|
||||
const { user: me } = useAuth()
|
||||
const [users, setUsers] = useState<UserRecord[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const fetchUsers = useCallback(async (p: number) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getUsers({ page: p, page_size: 20 })
|
||||
const data: UserRecord[] = res.data
|
||||
setUsers(data)
|
||||
setHasMore(data.length === 20)
|
||||
} catch {
|
||||
toast.error('加载用户列表失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers(page)
|
||||
}, [page, fetchUsers])
|
||||
|
||||
const handleRoleChange = async (userId: string, newRole: string) => {
|
||||
try {
|
||||
await updateUser(userId, { role: newRole })
|
||||
toast.success('角色已更新')
|
||||
fetchUsers(page)
|
||||
} catch {
|
||||
toast.error('更新角色失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleApprove = async (userId: string) => {
|
||||
try {
|
||||
await approveUser(userId)
|
||||
toast.success('用户已审批通过')
|
||||
fetchUsers(page)
|
||||
} catch {
|
||||
toast.error('审批失败')
|
||||
}
|
||||
}
|
||||
|
||||
const isOwner = me?.role === 'owner'
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{ key: 'username', label: '用户名' },
|
||||
{ key: 'email', label: '邮箱' },
|
||||
{ key: 'role', label: '角色' },
|
||||
{ key: 'status', label: '状态' },
|
||||
{ key: 'approve', label: '审批状态' },
|
||||
{ key: 'created_at', label: '创建时间' },
|
||||
...(isOwner ? [{ key: 'actions', label: '操作' }] : []),
|
||||
]
|
||||
|
||||
const data = users.map((u) => ({
|
||||
username: u.username,
|
||||
email: u.email,
|
||||
role: isOwner ? (
|
||||
<select
|
||||
value={u.role}
|
||||
onChange={(e) => handleRoleChange(u.id, e.target.value)}
|
||||
className="px-2 py-1 rounded text-[12px] outline-none cursor-pointer"
|
||||
style={{
|
||||
background: 'var(--bg-root)',
|
||||
color: 'var(--text)',
|
||||
border: '1px solid var(--border)',
|
||||
}}
|
||||
>
|
||||
{ROLE_OPTIONS.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{r}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<Badge variant={u.role === 'owner' ? 'honey' : u.role === 'operator' ? 'info' : 'outline'}>
|
||||
{u.role}
|
||||
</Badge>
|
||||
),
|
||||
status: u.is_active ? (
|
||||
<Badge variant="success">active</Badge>
|
||||
) : (
|
||||
<Badge variant="danger">inactive</Badge>
|
||||
),
|
||||
approve: u.is_approved ? (
|
||||
<Badge variant="success">已审批</Badge>
|
||||
) : (
|
||||
<Badge variant="warning">待审批</Badge>
|
||||
),
|
||||
created_at: new Date(u.created_at).toLocaleString('zh-CN'),
|
||||
actions: isOwner ? (
|
||||
<div className="flex items-center gap-2">
|
||||
{!u.is_approved && (
|
||||
<Button size="sm" onClick={() => handleApprove(u.id)}>
|
||||
审批
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : null,
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ color: 'var(--text)', fontSize: 18, fontWeight: 600 }}>用户管理</h2>
|
||||
<p style={{ color: 'var(--text-dim)', marginTop: 4, marginBottom: 16, fontSize: 13 }}>
|
||||
用户管理与权限
|
||||
</p>
|
||||
|
||||
<Table columns={columns} data={data} rowKey="username" />
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="flex items-center justify-between mt-4">
|
||||
<span style={{ color: 'var(--text-dim)', fontSize: 13 }}>
|
||||
第 {page} 页
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={page <= 1}
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={!hasMore || loading}
|
||||
onClick={() => setPage((p) => p + 1)}
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user