143 lines
5.4 KiB
TypeScript
143 lines
5.4 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { CalendarDays, Download, FileText, Search } from 'lucide-react';
|
|
import { Button, Input, Pagination, Select, Table, Tag, type TableColumn } from '@/components/ui';
|
|
import { adminApi, type OperationLogItem } from '@/api/adminApi';
|
|
|
|
type LogLevel = 'info' | 'success' | 'warning' | 'error';
|
|
|
|
const levelLabelMap: Record<LogLevel, string> = {
|
|
info: '信息',
|
|
success: '成功',
|
|
warning: '警告',
|
|
error: '错误',
|
|
};
|
|
|
|
const levelToneMap: Record<LogLevel, 'info' | 'success' | 'warning' | 'danger'> = {
|
|
info: 'info',
|
|
success: 'success',
|
|
warning: 'warning',
|
|
error: 'danger',
|
|
};
|
|
|
|
export function AdminSystemLogsPage() {
|
|
const [keyword, setKeyword] = useState('');
|
|
const [level, setLevel] = useState('all');
|
|
const [module, setModule] = useState('all');
|
|
const [range, setRange] = useState('today');
|
|
const [page, setPage] = useState(1);
|
|
const pageSize = 5;
|
|
const [logs, setLogs] = useState<OperationLogItem[]>([]);
|
|
const [modules, setModules] = useState<string[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [error, setError] = useState('');
|
|
|
|
useEffect(() => {
|
|
adminApi.listSystemLogs({ keyword, level, module, range, page, pageSize })
|
|
.then((data) => {
|
|
setLogs(data.items);
|
|
setModules(data.modules);
|
|
setTotal(data.total);
|
|
setError('');
|
|
})
|
|
.catch((err) => {
|
|
setLogs([]);
|
|
setTotal(0);
|
|
setError(err instanceof Error ? err.message : '系统日志加载失败');
|
|
});
|
|
}, [keyword, level, module, range, page]);
|
|
|
|
const moduleOptions = useMemo(() => {
|
|
return [{ label: '全部模块', value: 'all' }, ...modules.map((item) => ({ label: item, value: item }))];
|
|
}, [modules]);
|
|
|
|
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
const currentPage = Math.min(page, totalPages);
|
|
|
|
const columns = useMemo<Array<TableColumn<OperationLogItem>>>(() => [
|
|
{ key: 'time', title: '时间', width: '180px', render: (record) => <span className="muted">{new Date(record.time).toLocaleString('zh-CN')}</span> },
|
|
{ key: 'level', title: '级别', width: '100px', render: (record) => <Tag tone={levelToneMap[record.level]}>{levelLabelMap[record.level]}</Tag> },
|
|
{ key: 'tenant', title: '企业', width: '190px', render: (record) => <strong>{record.tenant}</strong> },
|
|
{ key: 'module', title: '模块', width: '130px', render: (record) => record.module },
|
|
{ key: 'operator', title: '操作人', width: '120px', render: (record) => <strong>{record.operator}</strong> },
|
|
{ key: 'action', title: '动作', width: '140px', render: (record) => record.action },
|
|
{ key: 'resourceId', title: '资源ID', width: '150px', render: (record) => <span className="muted">{record.resourceId}</span> },
|
|
{
|
|
key: 'detail',
|
|
title: '详情',
|
|
width: '320px',
|
|
render: (record) => (
|
|
<div className="system-log-detail-card">
|
|
<strong>{record.action}</strong>
|
|
<span>{JSON.stringify(record.detail)}</span>
|
|
<small>{record.resourceId}</small>
|
|
</div>
|
|
),
|
|
},
|
|
{ key: 'ip', title: 'IP', width: '120px', render: (record) => <span className="muted">{record.ip}</span> },
|
|
], []);
|
|
|
|
return (
|
|
<section className="page-stack system-page">
|
|
<div className="system-page-toolbar">
|
|
<div className="sms-send-title">
|
|
<span className="sms-send-title__icon"><FileText size={22} /></span>
|
|
<h1>系统日志</h1>
|
|
</div>
|
|
<Button icon={<Download size={17} />} variant="secondary">导出日志</Button>
|
|
</div>
|
|
|
|
<div className="system-log-filters">
|
|
<Input
|
|
onChange={(event) => { setKeyword(event.target.value); setPage(1); }}
|
|
placeholder="搜索企业、操作人、动作、资源ID或详情"
|
|
prefix={<Search size={16} />}
|
|
value={keyword}
|
|
/>
|
|
<Select
|
|
onChange={(event) => { setLevel(event.target.value); setPage(1); }}
|
|
options={[
|
|
{ label: '全部级别', value: 'all' },
|
|
{ label: '信息', value: 'info' },
|
|
{ label: '成功', value: 'success' },
|
|
{ label: '警告', value: 'warning' },
|
|
{ label: '错误', value: 'error' },
|
|
]}
|
|
value={level}
|
|
/>
|
|
<Select onChange={(event) => { setModule(event.target.value); setPage(1); }} options={moduleOptions} value={module} />
|
|
</div>
|
|
|
|
<div className="system-log-range">
|
|
<span><CalendarDays size={18} /> 时间范围:</span>
|
|
{[
|
|
{ label: '今天', value: 'today' },
|
|
{ label: '近7天', value: '7d' },
|
|
{ label: '近30天', value: '30d' },
|
|
{ label: '全部', value: 'all' },
|
|
].map((item) => (
|
|
<Button
|
|
key={item.value}
|
|
onClick={() => setRange(item.value)}
|
|
size="sm"
|
|
variant={range === item.value ? 'primary' : 'secondary'}
|
|
>
|
|
{item.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="surface system-table-card">
|
|
<Table columns={columns} data={logs} emptyText={error || '暂无系统日志'} rowKey="id" />
|
|
<Pagination
|
|
nextDisabled={currentPage >= totalPages}
|
|
onNext={() => setPage((value) => Math.min(totalPages, value + 1))}
|
|
onPrevious={() => setPage((value) => Math.max(1, value - 1))}
|
|
page={currentPage}
|
|
previousDisabled={currentPage <= 1}
|
|
total={total}
|
|
/>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|