feat(logs): 日志管理增加筛选维度(操作人/角色/请求方法)

- 后端 querySystemLogs 支持 role/method 过滤;新增 distinctRoles/distinctActors
- 关键词搜索补充 target_name 匹配
- /api/system-logs 返回 roles 与 actors 供前端下拉
- 前端独立筛选工具条:操作人/角色/动作/方法/结果/日期范围 + 清除筛选
- 结束日期改为当日 23:59:59 含当天
This commit is contained in:
freedakgmail
2026-06-14 10:27:24 +08:00
parent 3716564b58
commit f42c04da8b
4 changed files with 94 additions and 26 deletions
+6 -1
View File
@@ -1008,16 +1008,21 @@ export interface SystemLogPage {
page: number;
pageSize: number;
actions: string[];
roles: string[];
actors: Array<{ id: string; name: string }>;
}
/** 查询系统操作审计日志(系统管理员)。 */
export async function fetchSystemLogs(params: {
page: number; pageSize: number; actorId?: string; action?: string; q?: string; from?: string; to?: string; success?: 'true' | 'false';
page: number; pageSize: number; actorId?: string; action?: string; role?: string; method?: string; q?: string; from?: string; to?: string; success?: 'true' | 'false';
}): Promise<SystemLogPage> {
const sp = new URLSearchParams();
sp.set('page', String(params.page));
sp.set('pageSize', String(params.pageSize));
if (params.action) sp.set('action', params.action);
if (params.actorId) sp.set('actorId', params.actorId);
if (params.role) sp.set('role', params.role);
if (params.method) sp.set('method', params.method);
if (params.q) sp.set('q', params.q);
if (params.from) sp.set('from', params.from);
if (params.to) sp.set('to', params.to);
+53 -21
View File
@@ -23,7 +23,12 @@ export function SystemLogs(): JSX.Element {
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [actions, setActions] = useState<string[]>([]);
const [roles, setRoles] = useState<string[]>([]);
const [actors, setActors] = useState<Array<{ id: string; name: string }>>([]);
const [action, setAction] = useState('');
const [role, setRole] = useState('');
const [method, setMethod] = useState('');
const [actorId, setActorId] = useState('');
const [success, setSuccess] = useState<'' | 'true' | 'false'>('');
const [q, setQ] = useState('');
const [qInput, setQInput] = useState('');
@@ -43,17 +48,26 @@ export function SystemLogs(): JSX.Element {
fetchSystemLogs({
page, pageSize,
...(action ? { action } : {}),
...(role ? { role } : {}),
...(method ? { method } : {}),
...(actorId ? { actorId } : {}),
...(q ? { q } : {}),
...(from ? { from: new Date(from).toISOString() } : {}),
...(to ? { to: new Date(to).toISOString() } : {}),
...(to ? { to: new Date(`${to}T23:59:59`).toISOString() } : {}),
...(success ? { success } : {}),
})
.then((res) => { setItems(res.items); setTotal(res.total); setActions(res.actions); setError(null); })
.then((res) => { setItems(res.items); setTotal(res.total); setActions(res.actions); setRoles(res.roles); setActors(res.actors); setError(null); })
.catch((e: unknown) => setError(e instanceof Error ? e.message : '加载失败'))
.finally(() => setLoading(false));
}, [page, pageSize, action, q, from, to, success]);
}, [page, pageSize, action, role, method, actorId, q, from, to, success]);
useEffect(() => { load(); }, [load]);
const hasFilter = action !== '' || role !== '' || method !== '' || actorId !== '' || success !== '' || qInput !== '' || from !== '' || to !== '';
const resetFilters = (): void => {
setAction(''); setRole(''); setMethod(''); setActorId(''); setSuccess('');
setQInput(''); setQ(''); setFrom(''); setTo(''); setPage(1);
};
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const input: React.CSSProperties = {
padding: `${space(1)}px ${space(2)}px`, border: `1px solid ${colorVar('color.border.default')}`,
@@ -72,25 +86,43 @@ export function SystemLogs(): JSX.Element {
</p>
</div>
<Card title={
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: `${space(2)}px` }}>
<span>{total}</span>
<div style={{ display: 'flex', gap: `${space(2)}px`, flexWrap: 'wrap', alignItems: 'center' }}>
<input style={{ ...input, width: 200 }} placeholder="搜索 操作人/动作/路径/目标" value={qInput} onChange={(e) => setQInput(e.target.value)} />
<select style={input} value={action} onChange={(e) => { setAction(e.target.value); setPage(1); }}>
<option value=""></option>
{actions.map((a) => <option key={a} value={a}>{a}</option>)}
</select>
<select style={input} value={success} onChange={(e) => { setSuccess(e.target.value as '' | 'true' | 'false'); setPage(1); }}>
<option value=""></option>
<option value="true"></option>
<option value="false"></option>
</select>
<input style={input} type="date" value={from} onChange={(e) => { setFrom(e.target.value); setPage(1); }} title="起始日期" />
<input style={input} type="date" value={to} onChange={(e) => { setTo(e.target.value); setPage(1); }} title="结束日期" />
</div>
<Card title={`操作日志(${total}`}>
{/* 筛选工具条 */}
<div style={{ display: 'flex', gap: `${space(2)}px`, flexWrap: 'wrap', alignItems: 'center', marginBottom: `${space(3)}px`, paddingBottom: `${space(3)}px`, borderBottom: `1px solid ${colorVar('color.border.default')}` }}>
<input style={{ ...input, width: 220 }} placeholder="搜索 操作人/动作/路径/目标" value={qInput} onChange={(e) => setQInput(e.target.value)} />
<select style={input} value={actorId} onChange={(e) => { setActorId(e.target.value); setPage(1); }} title="按操作人筛选">
<option value=""></option>
{actors.map((a) => <option key={a.id} value={a.id}>{a.name}</option>)}
</select>
<select style={input} value={role} onChange={(e) => { setRole(e.target.value); setPage(1); }} title="按角色筛选">
<option value=""></option>
{roles.map((r) => <option key={r} value={r}>{r}</option>)}
</select>
<select style={input} value={action} onChange={(e) => { setAction(e.target.value); setPage(1); }} title="按动作筛选">
<option value=""></option>
{actions.map((a) => <option key={a} value={a}>{a}</option>)}
</select>
<select style={input} value={method} onChange={(e) => { setMethod(e.target.value); setPage(1); }} title="按请求方法筛选">
<option value=""></option>
{['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].map((m) => <option key={m} value={m}>{m}</option>)}
</select>
<select style={input} value={success} onChange={(e) => { setSuccess(e.target.value as '' | 'true' | 'false'); setPage(1); }} title="按结果筛选">
<option value=""></option>
<option value="true"></option>
<option value="false"></option>
</select>
<label style={{ ...typographyStyle('caption'), color: colorVar('color.text.secondary'), display: 'inline-flex', alignItems: 'center', gap: 4 }}>
<input style={input} type="date" value={from} onChange={(e) => { setFrom(e.target.value); setPage(1); }} title="起始日期" />
</label>
<label style={{ ...typographyStyle('caption'), color: colorVar('color.text.secondary'), display: 'inline-flex', alignItems: 'center', gap: 4 }}>
<input style={input} type="date" value={to} onChange={(e) => { setTo(e.target.value); setPage(1); }} title="结束日期" />
</label>
{hasFilter && (
<button type="button" onClick={resetFilters} style={{ ...pagerBtn, display: 'inline-flex', alignItems: 'center', gap: 4, color: colorVar('color.brand.primary'), borderColor: colorVar('color.brand.primary') }}>
<Icon name="close" size={12} />
</button>
)}
</div>
}>
{error !== null && <div style={{ color: colorVar('color.risk.critical'), marginBottom: `${space(2)}px` }}>{error}</div>}
{loading ? <p style={{ color: colorVar('color.text.secondary') }}></p> : (
<div style={{ overflowX: 'auto' }}>