2026-02-12 05:43:59 +00:00
|
|
|
|
import { Card, Table, Tabs, Tag, Input, Space, Button, DatePicker, Select, Typography, Modal, Descriptions } from "antd";
|
2026-02-12 02:41:59 +00:00
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
|
|
import { fetchLogs } from "../api";
|
2026-02-12 05:43:59 +00:00
|
|
|
|
import { SearchOutlined, ReloadOutlined, InfoCircleOutlined, EyeOutlined } from "@ant-design/icons";
|
2026-02-12 02:41:59 +00:00
|
|
|
|
import dayjs from "dayjs";
|
|
|
|
|
|
|
|
|
|
|
|
const { RangePicker } = DatePicker;
|
2026-02-12 05:43:59 +00:00
|
|
|
|
const { Text, Title } = Typography;
|
2026-02-12 02:41:59 +00:00
|
|
|
|
|
|
|
|
|
|
export default function Logs() {
|
|
|
|
|
|
const [activeTab, setActiveTab] = useState("OPERATION");
|
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
const [data, setData] = useState([]);
|
|
|
|
|
|
const [total, setTotal] = useState(0);
|
|
|
|
|
|
const [params, setParams] = useState({
|
|
|
|
|
|
current: 1,
|
|
|
|
|
|
size: 10,
|
|
|
|
|
|
username: "",
|
|
|
|
|
|
status: undefined,
|
|
|
|
|
|
startDate: "",
|
|
|
|
|
|
endDate: ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-12 05:43:59 +00:00
|
|
|
|
// Modal for detail view
|
|
|
|
|
|
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
|
|
|
|
|
const [selectedLog, setSelectedLog] = useState<any>(null);
|
|
|
|
|
|
|
2026-02-12 02:41:59 +00:00
|
|
|
|
const loadData = async (currentParams = params) => {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const operationType = activeTab === "LOGIN" ? "LOGIN" : "OPERATION";
|
|
|
|
|
|
const result = await fetchLogs({ ...currentParams, operationType });
|
|
|
|
|
|
setData(result.records || []);
|
|
|
|
|
|
setTotal(result.total || 0);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
loadData();
|
|
|
|
|
|
}, [activeTab, params.current, params.size]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleSearch = () => {
|
|
|
|
|
|
setParams({ ...params, current: 1 });
|
|
|
|
|
|
loadData({ ...params, current: 1 });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleReset = () => {
|
|
|
|
|
|
const resetParams = {
|
|
|
|
|
|
current: 1,
|
|
|
|
|
|
size: 10,
|
|
|
|
|
|
username: "",
|
|
|
|
|
|
status: undefined,
|
|
|
|
|
|
startDate: "",
|
|
|
|
|
|
endDate: ""
|
|
|
|
|
|
};
|
|
|
|
|
|
setParams(resetParams);
|
|
|
|
|
|
loadData(resetParams);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-12 05:43:59 +00:00
|
|
|
|
const showDetail = (record: any) => {
|
|
|
|
|
|
setSelectedLog(record);
|
|
|
|
|
|
setDetailModalVisible(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-12 02:41:59 +00:00
|
|
|
|
const columns = [
|
|
|
|
|
|
{
|
2026-02-12 05:43:59 +00:00
|
|
|
|
title: "操作账号",
|
2026-02-12 02:41:59 +00:00
|
|
|
|
dataIndex: "username",
|
|
|
|
|
|
key: "username",
|
2026-02-12 05:43:59 +00:00
|
|
|
|
width: 140,
|
|
|
|
|
|
render: (text: string) => <Text strong>{text || "系统/访客"}</Text>
|
2026-02-12 02:41:59 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-02-12 05:43:59 +00:00
|
|
|
|
title: activeTab === "LOGIN" ? "登录模块" : "业务模块",
|
2026-02-12 02:41:59 +00:00
|
|
|
|
dataIndex: "resourceType",
|
|
|
|
|
|
key: "resourceType",
|
|
|
|
|
|
width: 150
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-02-12 05:43:59 +00:00
|
|
|
|
title: "操作描述",
|
2026-02-12 02:41:59 +00:00
|
|
|
|
dataIndex: "detail",
|
|
|
|
|
|
key: "detail",
|
2026-02-12 05:43:59 +00:00
|
|
|
|
ellipsis: true,
|
|
|
|
|
|
render: (text: string) => <Text type="secondary">{text}</Text>
|
2026-02-12 02:41:59 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-02-12 05:43:59 +00:00
|
|
|
|
title: "IP 地址",
|
2026-02-12 02:41:59 +00:00
|
|
|
|
dataIndex: "ipAddress",
|
|
|
|
|
|
key: "ipAddress",
|
2026-02-12 05:43:59 +00:00
|
|
|
|
width: 140,
|
|
|
|
|
|
className: "tabular-nums"
|
2026-02-12 02:41:59 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "状态",
|
|
|
|
|
|
dataIndex: "status",
|
|
|
|
|
|
key: "status",
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
render: (status: number) => (
|
2026-02-12 05:43:59 +00:00
|
|
|
|
<Tag color={status === 1 ? "green" : "red"} className="m-0">
|
2026-02-12 02:41:59 +00:00
|
|
|
|
{status === 1 ? "成功" : "失败"}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-02-12 05:43:59 +00:00
|
|
|
|
title: "发生时间",
|
2026-02-12 02:41:59 +00:00
|
|
|
|
dataIndex: "createdAt",
|
|
|
|
|
|
key: "createdAt",
|
|
|
|
|
|
width: 180,
|
2026-02-12 05:43:59 +00:00
|
|
|
|
className: "tabular-nums",
|
2026-02-12 02:41:59 +00:00
|
|
|
|
render: (text: string) => text?.replace('T', ' ').substring(0, 19)
|
2026-02-12 05:43:59 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "详情",
|
|
|
|
|
|
key: "action",
|
|
|
|
|
|
width: 80,
|
|
|
|
|
|
fixed: "right" as const,
|
|
|
|
|
|
render: (_: any, record: any) => (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="link"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
icon={<EyeOutlined aria-hidden="true" />}
|
|
|
|
|
|
onClick={() => showDetail(record)}
|
|
|
|
|
|
aria-label="查看详细日志信息"
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
2026-02-12 02:41:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
if (activeTab === "OPERATION") {
|
|
|
|
|
|
columns.splice(1, 0, {
|
|
|
|
|
|
title: "请求方式",
|
|
|
|
|
|
dataIndex: "operationType",
|
|
|
|
|
|
key: "operationType",
|
|
|
|
|
|
width: 100,
|
2026-02-12 05:43:59 +00:00
|
|
|
|
render: (t: string) => <Tag color="blue">{t}</Tag>
|
2026-02-12 02:41:59 +00:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="p-6">
|
2026-02-12 05:43:59 +00:00
|
|
|
|
<div className="mb-6">
|
|
|
|
|
|
<Title level={4} className="mb-1">系统日志管理</Title>
|
|
|
|
|
|
<Text type="secondary">追踪系统内的每一次重要操作,保障系统安全与可追溯性</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Card className="mb-4 shadow-sm">
|
2026-02-12 02:41:59 +00:00
|
|
|
|
<Space wrap size="middle">
|
|
|
|
|
|
<Input
|
2026-02-12 05:43:59 +00:00
|
|
|
|
placeholder="搜索用户名…"
|
|
|
|
|
|
style={{ width: 180 }}
|
2026-02-12 02:41:59 +00:00
|
|
|
|
value={params.username}
|
|
|
|
|
|
onChange={e => setParams({ ...params, username: e.target.value })}
|
2026-02-12 05:43:59 +00:00
|
|
|
|
prefix={<SearchOutlined aria-hidden="true" className="text-gray-400" />}
|
|
|
|
|
|
aria-label="搜索用户名"
|
|
|
|
|
|
allowClear
|
2026-02-12 02:41:59 +00:00
|
|
|
|
/>
|
|
|
|
|
|
<Select
|
2026-02-12 05:43:59 +00:00
|
|
|
|
placeholder="执行状态"
|
2026-02-12 02:41:59 +00:00
|
|
|
|
style={{ width: 120 }}
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
value={params.status}
|
|
|
|
|
|
onChange={v => setParams({ ...params, status: v })}
|
|
|
|
|
|
options={[
|
|
|
|
|
|
{ label: "成功", value: 1 },
|
|
|
|
|
|
{ label: "失败", value: 0 }
|
|
|
|
|
|
]}
|
2026-02-12 05:43:59 +00:00
|
|
|
|
aria-label="筛选执行状态"
|
2026-02-12 02:41:59 +00:00
|
|
|
|
/>
|
|
|
|
|
|
<RangePicker
|
|
|
|
|
|
onChange={(dates) => {
|
|
|
|
|
|
setParams({
|
|
|
|
|
|
...params,
|
|
|
|
|
|
startDate: dates ? dates[0]?.format("YYYY-MM-DD") || "" : "",
|
|
|
|
|
|
endDate: dates ? dates[1]?.format("YYYY-MM-DD") || "" : ""
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
2026-02-12 05:43:59 +00:00
|
|
|
|
aria-label="筛选时间范围"
|
2026-02-12 02:41:59 +00:00
|
|
|
|
/>
|
2026-02-12 05:43:59 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
icon={<SearchOutlined aria-hidden="true" />}
|
|
|
|
|
|
onClick={handleSearch}
|
|
|
|
|
|
>
|
|
|
|
|
|
查询
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
icon={<ReloadOutlined aria-hidden="true" />}
|
|
|
|
|
|
onClick={handleReset}
|
|
|
|
|
|
>
|
|
|
|
|
|
重置
|
|
|
|
|
|
</Button>
|
2026-02-12 02:41:59 +00:00
|
|
|
|
</Space>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
2026-02-12 05:43:59 +00:00
|
|
|
|
<Card className="shadow-sm" styles={{ body: { paddingTop: 0 } }}>
|
|
|
|
|
|
<Tabs activeKey={activeTab} onChange={setActiveTab} size="large">
|
|
|
|
|
|
<Tabs.TabPane
|
|
|
|
|
|
tab={<span><InfoCircleOutlined aria-hidden="true" />操作日志</span>}
|
|
|
|
|
|
key="OPERATION"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Tabs.TabPane
|
|
|
|
|
|
tab={<span><UserOutlined aria-hidden="true" />登录日志</span>}
|
|
|
|
|
|
key="LOGIN"
|
|
|
|
|
|
/>
|
2026-02-12 02:41:59 +00:00
|
|
|
|
</Tabs>
|
|
|
|
|
|
|
|
|
|
|
|
<Table
|
|
|
|
|
|
rowKey="id"
|
|
|
|
|
|
columns={columns}
|
|
|
|
|
|
dataSource={data}
|
|
|
|
|
|
loading={loading}
|
2026-02-12 05:43:59 +00:00
|
|
|
|
size="middle"
|
2026-02-12 02:41:59 +00:00
|
|
|
|
pagination={{
|
|
|
|
|
|
current: params.current,
|
|
|
|
|
|
pageSize: params.size,
|
|
|
|
|
|
total: total,
|
|
|
|
|
|
showSizeChanger: true,
|
|
|
|
|
|
onChange: (page, size) => setParams({ ...params, current: page, size }),
|
2026-02-12 05:43:59 +00:00
|
|
|
|
showTotal: (total) => `共 ${total} 条数据`
|
2026-02-12 02:41:59 +00:00
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
2026-02-12 05:43:59 +00:00
|
|
|
|
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title="日志详细信息"
|
|
|
|
|
|
open={detailModalVisible}
|
|
|
|
|
|
onCancel={() => setDetailModalVisible(false)}
|
|
|
|
|
|
footer={[
|
|
|
|
|
|
<Button key="close" onClick={() => setDetailModalVisible(false)}>
|
|
|
|
|
|
关闭
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
]}
|
|
|
|
|
|
width={700}
|
|
|
|
|
|
>
|
|
|
|
|
|
{selectedLog && (
|
|
|
|
|
|
<Descriptions bordered column={1} size="small">
|
|
|
|
|
|
<Descriptions.Item label="操作模块">{selectedLog.resourceType}</Descriptions.Item>
|
|
|
|
|
|
<Descriptions.Item label="请求方式">
|
|
|
|
|
|
<Tag color="blue">{selectedLog.operationType}</Tag>
|
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
|
<Descriptions.Item label="操作账号">{selectedLog.username || "系统"}</Descriptions.Item>
|
|
|
|
|
|
<Descriptions.Item label="IP 地址" className="tabular-nums">{selectedLog.ipAddress}</Descriptions.Item>
|
|
|
|
|
|
<Descriptions.Item label="User Agent">
|
|
|
|
|
|
<Text type="secondary" style={{ fontSize: '12px' }}>{selectedLog.userAgent}</Text>
|
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
|
<Descriptions.Item label="状态">
|
|
|
|
|
|
<Tag color={selectedLog.status === 1 ? "green" : "red"}>
|
|
|
|
|
|
{selectedLog.status === 1 ? "成功" : "失败"}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
|
<Descriptions.Item label="时间" className="tabular-nums">{selectedLog.createdAt?.replace('T', ' ')}</Descriptions.Item>
|
|
|
|
|
|
<Descriptions.Item label="详情/参数">
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
background: '#f5f5f5',
|
|
|
|
|
|
padding: '12px',
|
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
|
maxHeight: '200px',
|
|
|
|
|
|
overflowY: 'auto',
|
|
|
|
|
|
whiteSpace: 'pre-wrap',
|
|
|
|
|
|
wordBreak: 'break-all',
|
|
|
|
|
|
fontFamily: 'monospace'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
{selectedLog.detail}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
|
{selectedLog.errorMessage && (
|
|
|
|
|
|
<Descriptions.Item label="错误信息">
|
|
|
|
|
|
<Text type="danger">{selectedLog.errorMessage}</Text>
|
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Descriptions>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Modal>
|
2026-02-12 02:41:59 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-02-12 05:43:59 +00:00
|
|
|
|
|
|
|
|
|
|
// Ensure UserOutlined is imported
|
|
|
|
|
|
import { UserOutlined } from "@ant-design/icons";
|