feat: frontend auth flow (login, user management, audit logs, route guards)
This commit is contained in:
@@ -1,3 +1,139 @@
|
||||
export default function AuditLogPage() {
|
||||
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 { getAuditLogs } from '../api/audit'
|
||||
import Table, { type TableColumn } from '../components/ui/Table'
|
||||
import Button from '../components/ui/Button'
|
||||
import toast from '../components/ui/Toast'
|
||||
|
||||
interface AuditRecord {
|
||||
id: string
|
||||
username: string
|
||||
action: string
|
||||
target: string
|
||||
detail: string | null
|
||||
ip_address: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
const ACTION_OPTIONS = [
|
||||
'',
|
||||
'keys.batch_overwrite',
|
||||
'gateway.toggle',
|
||||
'cron.trigger',
|
||||
'user.login',
|
||||
'user.update_role',
|
||||
'user.approve',
|
||||
'keys.generate',
|
||||
'kaban.create',
|
||||
'kaban.update',
|
||||
]
|
||||
|
||||
export default function AuditLogPage() {
|
||||
const [logs, setLogs] = useState<AuditRecord[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [actionFilter, setActionFilter] = useState('')
|
||||
|
||||
const fetchLogs = useCallback(async (p: number, action?: string) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const params: Record<string, unknown> = { page: p, page_size: 20 }
|
||||
if (action) params.action = action
|
||||
const res = await getAuditLogs(params)
|
||||
const data: AuditRecord[] = res.data
|
||||
setLogs(data)
|
||||
setHasMore(data.length === 20)
|
||||
} catch {
|
||||
toast.error('加载审计日志失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchLogs(page, actionFilter)
|
||||
}, [page, fetchLogs])
|
||||
|
||||
const handleFilterChange = (val: string) => {
|
||||
setActionFilter(val)
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{ key: 'created_at', label: '时间', width: 180 },
|
||||
{ key: 'username', label: '用户', width: 100 },
|
||||
{ key: 'action', label: '操作', width: 160 },
|
||||
{ key: 'target', label: '目标', width: 200 },
|
||||
{ key: 'detail', label: '详情' },
|
||||
{ key: 'ip_address', label: 'IP', width: 140 },
|
||||
]
|
||||
|
||||
const data = logs.map((l) => ({
|
||||
created_at: new Date(l.created_at).toLocaleString('zh-CN'),
|
||||
username: l.username,
|
||||
action: l.action,
|
||||
target: l.target,
|
||||
detail: l.detail ?? '-',
|
||||
ip_address: l.ip_address ?? '-',
|
||||
}))
|
||||
|
||||
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>
|
||||
|
||||
{/* Filter */}
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<label className="text-[12px] font-semibold" style={{ color: 'var(--text-dim)' }}>
|
||||
操作筛选:
|
||||
</label>
|
||||
<select
|
||||
value={actionFilter}
|
||||
onChange={(e) => handleFilterChange(e.target.value)}
|
||||
className="px-3 py-[7px] rounded-[var(--radius-sm)] text-[13px] outline-none"
|
||||
style={{
|
||||
background: 'var(--bg-root)',
|
||||
color: 'var(--text)',
|
||||
border: '1px solid var(--border)',
|
||||
}}
|
||||
>
|
||||
<option value="">全部</option>
|
||||
{ACTION_OPTIONS.filter(Boolean).map((a) => (
|
||||
<option key={a} value={a}>
|
||||
{a}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<Table columns={columns} data={data} rowKey="created_at" />
|
||||
|
||||
{/* 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