feat: frontend auth flow (login, user management, audit logs, route guards)

This commit is contained in:
enzo
2026-07-22 16:55:11 +08:00
parent e13be8eb1c
commit 10404be62d
11 changed files with 672 additions and 70 deletions
+159 -2
View File
@@ -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>
)
}