imeeting/frontend/src/pages/Logs.tsx

334 lines
10 KiB
TypeScript

import { Card, Table, Tabs, Tag, Input, Space, Button, DatePicker, Select, Typography, Modal, Descriptions } from "antd";
import { useEffect, useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { fetchLogs } from "../api";
import { SearchOutlined, ReloadOutlined, InfoCircleOutlined, EyeOutlined, UserOutlined } from "@ant-design/icons";
import { SysLog, UserProfile } from "../types";
const { RangePicker } = DatePicker;
const { Text, Title } = Typography;
export default function Logs() {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState("OPERATION");
const [loading, setLoading] = useState(false);
const [data, setData] = useState<SysLog[]>([]);
const [total, setTotal] = useState(0);
const [params, setParams] = useState({
current: 1,
size: 10,
username: "",
status: undefined,
startDate: "",
endDate: "",
operation: "",
sortField: "createdAt",
sortOrder: "descend" as any
});
// Get user profile to check platform admin
const userProfile = useMemo(() => {
const stored = sessionStorage.getItem("userProfile");
if (!stored) return null;
try {
return JSON.parse(stored) as UserProfile;
} catch (e) {
return null;
}
}, []);
const isPlatformAdmin = userProfile?.isPlatformAdmin && userProfile?.tenantId === 0;
// Modal for detail view
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [selectedLog, setSelectedLog] = useState<SysLog | null>(null);
const loadData = async (currentParams = params) => {
setLoading(true);
try {
// Use logType for precise filtering
const result = await fetchLogs({ ...currentParams, logType: activeTab });
setData(result.records || []);
setTotal(result.total || 0);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
}, [activeTab, params.current, params.size, params.sortField, params.sortOrder]);
const handleTableChange = (pagination: any, filters: any, sorter: any) => {
setParams({
...params,
current: pagination.current,
size: pagination.pageSize,
sortField: sorter.field || "createdAt",
sortOrder: sorter.order || "descend"
});
};
const handleSearch = () => {
setParams({ ...params, current: 1 });
loadData({ ...params, current: 1 });
};
const handleReset = () => {
const resetParams = {
current: 1,
size: 10,
username: "",
status: undefined,
startDate: "",
endDate: "",
operation: "",
sortField: "createdAt",
sortOrder: "descend" as any
};
setParams(resetParams);
loadData(resetParams);
};
const showDetail = (record: SysLog) => {
setSelectedLog(record);
setDetailModalVisible(true);
};
const renderDuration = (ms: number) => {
if (!ms && ms !== 0) return "-";
let color = "";
if (ms > 1000) color = "#ff4d4f"; // 红色 (慢)
else if (ms > 300) color = "#faad14"; // 橘色 (中)
return (
<Text style={{ color, fontWeight: ms > 300 ? 600 : 400 }}>
{ms}ms
</Text>
);
};
const columns = [
...(isPlatformAdmin ? [{
title: t('users.tenant'),
dataIndex: "tenantName",
key: "tenantName",
width: 150,
render: (text: string) => <Text type="warning">{text || "系统平台"}</Text>
}] : []),
{
title: t('logs.opAccount'),
dataIndex: "username",
key: "username",
width: 120,
render: (text: string) => <Text strong>{text || "系统"}</Text>
},
{
title: t('logs.opDetail'),
dataIndex: "operation",
key: "operation",
ellipsis: true,
render: (text: string) => <Text type="secondary">{text}</Text>
},
{
title: t('logs.ip'),
dataIndex: "ip",
key: "ip",
width: 130,
className: "tabular-nums"
},
{
title: t('logs.duration'),
dataIndex: "duration",
key: "duration",
width: 100,
sorter: true,
sortOrder: params.sortField === 'duration' ? params.sortOrder : null,
render: renderDuration
},
{
title: t('common.status'),
dataIndex: "status",
key: "status",
width: 90,
render: (status: number) => (
<Tag color={status === 1 ? "green" : "red"} className="m-0">
{status === 1 ? "成功" : "失败"}
</Tag>
)
},
{
title: t('logs.time'),
dataIndex: "createdAt",
key: "createdAt",
width: 180,
sorter: true,
sortOrder: params.sortField === 'createdAt' ? params.sortOrder : null,
className: "tabular-nums",
render: (text: string) => text?.replace('T', ' ').substring(0, 19)
},
{
title: t('common.action'),
key: "action",
width: 60,
fixed: "right" as const,
render: (_: any, record: SysLog) => (
<Button
type="text"
size="small"
icon={<EyeOutlined aria-hidden="true" />}
onClick={() => showDetail(record)}
aria-label={t('common.view')}
/>
)
}
];
if (activeTab === "OPERATION") {
columns.splice(isPlatformAdmin ? 2 : 1, 0, {
title: t('logs.method'),
dataIndex: "method",
key: "method",
width: 180,
render: (t: string) => <Tag color="blue" style={{ fontSize: '11px' }}>{t}</Tag>
});
}
return (
<div className="p-6">
<div className="mb-6">
<Title level={4} className="mb-1">{t('logs.title')}</Title>
<Text type="secondary">{t('logs.subtitle')}</Text>
</div>
<Card className="mb-4 shadow-sm">
<Space wrap size="middle">
<Input
placeholder={t('logs.searchPlaceholder')}
style={{ width: 180 }}
value={params.operation}
onChange={e => setParams({ ...params, operation: e.target.value })}
prefix={<SearchOutlined aria-hidden="true" className="text-gray-400" />}
allowClear
/>
<Select
placeholder={t('common.status')}
style={{ width: 120 }}
allowClear
value={params.status}
onChange={v => setParams({ ...params, status: v })}
options={[
{ label: "成功", value: 1 },
{ label: "失败", value: 0 }
]}
aria-label={t('common.status')}
/>
<RangePicker
onChange={(dates) => {
setParams({
...params,
startDate: dates ? dates[0]?.format("YYYY-MM-DD") || "" : "",
endDate: dates ? dates[1]?.format("YYYY-MM-DD") || "" : ""
});
}}
aria-label="筛选时间范围"
/>
<Button
type="primary"
icon={<SearchOutlined aria-hidden="true" />}
onClick={handleSearch}
>
{t('common.search')}
</Button>
<Button
icon={<ReloadOutlined aria-hidden="true" />}
onClick={handleReset}
>
{t('common.reset')}
</Button>
</Space>
</Card>
<Card className="shadow-sm" styles={{ body: { paddingTop: 0 } }}>
<Tabs activeKey={activeTab} onChange={setActiveTab} size="large">
<Tabs.TabPane
tab={<span><InfoCircleOutlined aria-hidden="true" />{t('logs.opLog')}</span>}
key="OPERATION"
/>
<Tabs.TabPane
tab={<span><UserOutlined aria-hidden="true" />{t('logs.loginLog')}</span>}
key="LOGIN"
/>
</Tabs>
<Table
rowKey="id"
columns={columns}
dataSource={data}
loading={loading}
size="middle"
onChange={handleTableChange}
pagination={{
current: params.current,
pageSize: params.size,
total: total,
showSizeChanger: true,
showTotal: (total) => t('common.total', { total })
}}
/>
</Card>
<Modal
title={t('logs.detailTitle')}
open={detailModalVisible}
onCancel={() => setDetailModalVisible(false)}
footer={[
<Button key="close" onClick={() => setDetailModalVisible(false)}>
{t('common.cancel')}
</Button>
]}
width={700}
>
{selectedLog && (
<Descriptions bordered column={1} size="small">
{isPlatformAdmin && (
<Descriptions.Item label={t('users.tenant')}>
<Text type="warning">{selectedLog.tenantName || "系统平台"}</Text>
</Descriptions.Item>
)}
<Descriptions.Item label={t('logs.opDetail')}>{selectedLog.operation}</Descriptions.Item>
<Descriptions.Item label={t('logs.method')}>
<Tag color="blue">{selectedLog.method || "N/A"}</Tag>
</Descriptions.Item>
<Descriptions.Item label={t('logs.opAccount')}>{selectedLog.username || "系统"}</Descriptions.Item>
<Descriptions.Item label={t('logs.ip')} className="tabular-nums">{selectedLog.ip}</Descriptions.Item>
<Descriptions.Item label={t('logs.duration')}>{selectedLog.duration ? `${selectedLog.duration}ms` : "-"}</Descriptions.Item>
<Descriptions.Item label={t('common.status')}>
<Tag color={selectedLog.status === 1 ? "green" : "red"}>
{selectedLog.status === 1 ? "成功" : "失败"}
</Tag>
</Descriptions.Item>
<Descriptions.Item label={t('logs.time')} className="tabular-nums">{selectedLog.createdAt?.replace('T', ' ')}</Descriptions.Item>
<Descriptions.Item label={t('logs.params')}>
<div style={{
background: '#f5f5f5',
padding: '12px',
borderRadius: '4px',
maxHeight: '150px',
overflowY: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
fontFamily: 'monospace',
fontSize: '12px'
}}>
{selectedLog.params || "无"}
</div>
</Descriptions.Item>
</Descriptions>
)}
</Modal>
</div>
);
}