90 lines
5.5 KiB
React
90 lines
5.5 KiB
React
import { useMemo, useState } from 'react';
|
||
import { Badge, Button, Field, Input, Select } from '../components/ui.jsx';
|
||
import { Icon, PageTitle, Toolbar, Panel, ApiNotice, PageLoadingDialog, Drawer, SimpleTable, KeyValue, DateRangeFields } from '../components/layout.jsx';
|
||
import { operationLogRows } from '../fixtures/devFixtures.js';
|
||
import { dateRangeParams, lastSevenDays } from '../utils/dateRange.js';
|
||
|
||
const defaultRange = lastSevenDays();
|
||
|
||
function toneForStatus(status) {
|
||
return status === '成功' || status === 'SUCCESS' ? 'success' : status === '失败' || status === 'FAILURE' ? 'danger' : 'neutral';
|
||
}
|
||
|
||
export function OperationLogsPage({ logRows = operationLogRows, apiLoading, apiError, refreshApi }) {
|
||
const [keyword, setKeyword] = useState('');
|
||
const [moduleFilter, setModuleFilter] = useState('all');
|
||
const [resultFilter, setResultFilter] = useState('all');
|
||
const [startDate, setStartDate] = useState(defaultRange.startDate);
|
||
const [endDate, setEndDate] = useState(defaultRange.endDate);
|
||
const [detailLog, setDetailLog] = useState(null);
|
||
const modules = useMemo(() => Array.from(new Set(logRows.map((log) => log.module))), [logRows]);
|
||
const visibleLogs = useMemo(() => {
|
||
const normalized = keyword.trim().toLowerCase();
|
||
return logRows.filter((log) => {
|
||
const matchesKeyword = !normalized || [log.user, log.username, log.action, log.object, log.ip].some((value) => value.toLowerCase().includes(normalized));
|
||
const logDate = log.time.slice(0, 10);
|
||
return matchesKeyword && (moduleFilter === 'all' || log.module === moduleFilter) && (resultFilter === 'all' || log.result === resultFilter) && (!startDate || logDate >= startDate) && (!endDate || logDate <= endDate);
|
||
});
|
||
}, [endDate, keyword, logRows, moduleFilter, resultFilter, startDate]);
|
||
const queryLogs = (overrides = {}) => {
|
||
const next = { moduleFilter, resultFilter, startDate, endDate, ...overrides };
|
||
return refreshApi({
|
||
module: next.moduleFilter,
|
||
result: next.resultFilter === '成功' ? 'SUCCESS' : next.resultFilter === '失败' ? 'FAILURE' : 'all',
|
||
...dateRangeParams(next.startDate, next.endDate, 'createdFrom', 'createdTo'),
|
||
});
|
||
};
|
||
const resetFilters = () => {
|
||
setKeyword('');
|
||
setModuleFilter('all');
|
||
setResultFilter('all');
|
||
setStartDate(defaultRange.startDate);
|
||
setEndDate(defaultRange.endDate);
|
||
void queryLogs({ moduleFilter: 'all', resultFilter: 'all', ...defaultRange });
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<PageTitle title="操作日志" desc="审计登录、配置变更、敏感操作及其执行结果。" actions={<Button variant="secondary" icon={<Icon type="export" />}>导出日志</Button>} />
|
||
<ApiNotice loading={apiLoading} error={apiError} onRetry={() => void queryLogs()} />
|
||
<PageLoadingDialog loading={apiLoading} />
|
||
<Toolbar>
|
||
<Field label="关键词"><Input value={keyword} onChange={(event) => setKeyword(event.target.value)} placeholder="用户、操作对象或 IP" /></Field>
|
||
<Field label="功能模块"><Select value={moduleFilter} onChange={(event) => setModuleFilter(event.target.value)}><option value="all">全部模块</option>{modules.map((module) => <option key={module} value={module}>{module}</option>)}</Select></Field>
|
||
<Field label="执行结果"><Select value={resultFilter} onChange={(event) => setResultFilter(event.target.value)}><option value="all">全部结果</option><option value="成功">成功</option><option value="失败">失败</option></Select></Field>
|
||
<DateRangeFields startDate={startDate} endDate={endDate} onStartChange={setStartDate} onEndChange={setEndDate} />
|
||
<Button icon={<Icon type="search" />} disabled={apiLoading} onClick={() => void queryLogs()}>查询</Button>
|
||
<Button variant="outline" onClick={resetFilters}>重置</Button>
|
||
</Toolbar>
|
||
<Panel title="日志列表" aside={<Badge tone="neutral">共 {visibleLogs.length} 条</Badge>} className="wide-panel">
|
||
<SimpleTable loading={apiLoading} rows={visibleLogs} columns={[
|
||
{ key: 'time', label: '操作时间' },
|
||
{ key: 'user', label: '操作用户' },
|
||
{ key: 'module', label: '功能模块' },
|
||
{ key: 'action', label: '操作类型' },
|
||
{ key: 'object', label: '操作对象' },
|
||
{ key: 'result', label: '结果', status: true },
|
||
{ key: 'ip', label: '操作 IP' },
|
||
{ key: 'actions', label: '操作', render: (row) => <Button size="sm" variant="outline" onClick={() => setDetailLog(row)}>查看详情</Button> },
|
||
]} />
|
||
</Panel>
|
||
{detailLog ? (
|
||
<Drawer title="操作日志详情" aside={<Badge tone={toneForStatus(detailLog.result)}>{detailLog.result}</Badge>} onClose={() => setDetailLog(null)}>
|
||
<div className="detail-stack">
|
||
<KeyValue label="日志 ID" value={detailLog.id} />
|
||
<KeyValue label="操作时间" value={detailLog.time} />
|
||
<KeyValue label="操作用户" value={`${detailLog.user}(${detailLog.username})`} />
|
||
<KeyValue label="功能模块" value={detailLog.module} />
|
||
<KeyValue label="操作类型" value={detailLog.action} />
|
||
<KeyValue label="操作对象" value={detailLog.object} />
|
||
<KeyValue label="执行结果" value={detailLog.result} />
|
||
<KeyValue label="操作 IP" value={detailLog.ip} />
|
||
<KeyValue label="客户端" value={detailLog.userAgent} />
|
||
</div>
|
||
<section className="log-summary"><strong>操作摘要</strong><p>{detailLog.summary}</p></section>
|
||
</Drawer>
|
||
) : null}
|
||
</>
|
||
);
|
||
}
|