304 lines
12 KiB
TypeScript
304 lines
12 KiB
TypeScript
import { Button, Card, DatePicker, Descriptions, Input, Modal, Select, Space, Tabs, Tag, Typography } from "antd";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { EyeOutlined, InfoCircleOutlined, ReloadOutlined, SearchOutlined, UserOutlined } from "@ant-design/icons";
|
|
import { fetchLogModules, fetchLogs } from "@/api";
|
|
import { useDict } from "@/hooks/useDict";
|
|
import PageHeader from "@/components/shared/PageHeader";
|
|
import ListTable from "@/components/shared/ListTable/ListTable";
|
|
import { getStandardPagination } from "@/utils/pagination";
|
|
import type { SysLog, UserProfile } from "@/types";
|
|
|
|
const { RangePicker } = DatePicker;
|
|
const { Text } = 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 [moduleOptions, setModuleOptions] = useState<string[]>([]);
|
|
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
|
const [selectedLog, setSelectedLog] = useState<SysLog | null>(null);
|
|
const [params, setParams] = useState({
|
|
current: 1,
|
|
size: 20,
|
|
username: "",
|
|
moduleName: "",
|
|
status: undefined as number | undefined,
|
|
startDate: "",
|
|
endDate: "",
|
|
operation: "",
|
|
sortField: "createdAt",
|
|
sortOrder: "descend" as any
|
|
});
|
|
const { items: logTypeDict } = useDict("sys_log_type");
|
|
const { items: logStatusDict } = useDict("sys_log_status");
|
|
|
|
const userProfile = useMemo(() => {
|
|
const stored = sessionStorage.getItem("userProfile");
|
|
if (!stored) return null;
|
|
try {
|
|
return JSON.parse(stored) as UserProfile;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}, []);
|
|
|
|
const isPlatformAdmin = Boolean(userProfile?.isPlatformAdmin);
|
|
|
|
const loadData = async (currentParams = params) => {
|
|
setLoading(true);
|
|
try {
|
|
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]);
|
|
|
|
useEffect(() => {
|
|
if (activeTab !== "OPERATION") {
|
|
setModuleOptions([]);
|
|
return;
|
|
}
|
|
fetchLogModules().then((items) => setModuleOptions(items || [])).catch(() => setModuleOptions([]));
|
|
}, [activeTab]);
|
|
|
|
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 = () => {
|
|
const nextParams = { ...params, current: 1 };
|
|
setParams(nextParams);
|
|
loadData(nextParams);
|
|
};
|
|
|
|
const handleReset = () => {
|
|
const resetParams = {
|
|
current: 1,
|
|
size: 20,
|
|
username: "",
|
|
moduleName: "",
|
|
status: undefined,
|
|
startDate: "",
|
|
endDate: "",
|
|
operation: "",
|
|
sortField: "createdAt",
|
|
sortOrder: "descend" as any
|
|
};
|
|
setParams(resetParams);
|
|
loadData(resetParams);
|
|
};
|
|
|
|
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: any[] = [
|
|
...(isPlatformAdmin
|
|
? [{ title: t("users.tenant"), dataIndex: "tenantName", key: "tenantName", width: 150, render: (text: string) => <Text>{text || t("logsExt.platform")}</Text> }]
|
|
: []),
|
|
{
|
|
title: t("logs.opAccount"),
|
|
dataIndex: "username",
|
|
key: "username",
|
|
width: 120,
|
|
render: (text: string) => <Text strong>{text || t("logsExt.system")}</Text>
|
|
},
|
|
{
|
|
title: activeTab === "OPERATION" ? t("logsExt.actionLabel") : t("logs.opDetail"),
|
|
dataIndex: activeTab === "OPERATION" ? "actionName" : "operation",
|
|
key: activeTab === "OPERATION" ? "actionName" : "operation",
|
|
ellipsis: true,
|
|
render: (_: string, record: SysLog) => <Text type="secondary">{record.actionName || record.operation}</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) => {
|
|
const item = logStatusDict.find((dictItem) => dictItem.itemValue === String(status));
|
|
return <Tag color={status === 1 ? "green" : "red"} className="m-0">{item ? item.itemLabel : status === 1 ? t("logsExt.success") : t("logsExt.failed")}</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={() => { setSelectedLog(record); setDetailModalVisible(true); }} aria-label={t("common.view")} />
|
|
}
|
|
];
|
|
|
|
if (activeTab === "OPERATION") {
|
|
columns.splice(isPlatformAdmin ? 2 : 1, 0, {
|
|
title: t("logsExt.module"),
|
|
dataIndex: "moduleName",
|
|
key: "moduleName",
|
|
width: 140,
|
|
render: (text: string) => <Tag color="processing">{text || t("logsExt.uncategorized")}</Tag>
|
|
});
|
|
columns.splice(isPlatformAdmin ? 3 : 2, 0, {
|
|
title: t("logs.method"),
|
|
dataIndex: "method",
|
|
key: "method",
|
|
width: 180,
|
|
ellipsis: true,
|
|
render: (method: string) => (
|
|
<Tag
|
|
color="blue"
|
|
style={{
|
|
fontSize: "11px",
|
|
maxWidth: "100%",
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
whiteSpace: "nowrap",
|
|
verticalAlign: "middle"
|
|
}}
|
|
title={method}
|
|
>
|
|
{method}
|
|
</Tag>
|
|
)
|
|
});
|
|
}
|
|
|
|
return (
|
|
<div className="app-page">
|
|
<PageHeader title={t("logs.title")} subtitle={t("logs.subtitle")} />
|
|
|
|
<Card className="app-page__filter-card" styles={{ body: { padding: "16px" } }}>
|
|
<Space wrap size="middle" className="app-page__toolbar">
|
|
<Input
|
|
placeholder={t("logs.searchPlaceholder")}
|
|
style={{ width: 180 }}
|
|
value={params.operation}
|
|
onChange={(event) => setParams({ ...params, operation: event.target.value })}
|
|
prefix={<SearchOutlined aria-hidden="true" className="text-gray-400" />}
|
|
allowClear
|
|
/>
|
|
{activeTab === "OPERATION" && (
|
|
<Select
|
|
placeholder={t("logsExt.filterModule")}
|
|
style={{ width: 160 }}
|
|
value={params.moduleName || undefined}
|
|
onChange={(value) => setParams({ ...params, moduleName: value || "" })}
|
|
options={moduleOptions.map((item) => ({ label: item, value: item }))}
|
|
allowClear
|
|
/>
|
|
)}
|
|
<Select
|
|
placeholder={t("common.status")}
|
|
style={{ width: 120 }}
|
|
allowClear
|
|
value={params.status}
|
|
onChange={(value) => setParams({ ...params, status: value })}
|
|
options={logStatusDict.map((item) => ({ label: item.itemLabel, value: Number(item.itemValue) }))}
|
|
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="Filter date range"
|
|
/>
|
|
<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="app-page__content-card flex-1 flex flex-col overflow-hidden" styles={{ body: { paddingTop: 0, paddingBottom: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}>
|
|
<Tabs activeKey={activeTab} onChange={(key) => { setActiveTab(key); setParams((prev) => ({ ...prev, current: 1, moduleName: "" })); }} size="large" className="flex-shrink-0">
|
|
{logTypeDict.length > 0
|
|
? logTypeDict.map((item) => <Tabs.TabPane tab={<span>{item.itemValue === "OPERATION" ? <InfoCircleOutlined aria-hidden="true" /> : <UserOutlined aria-hidden="true" />}{item.itemLabel}</span>} key={item.itemValue} />)
|
|
: <><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>
|
|
|
|
<div className="flex-1 min-h-0 h-full">
|
|
<ListTable
|
|
rowKey="id"
|
|
columns={columns}
|
|
dataSource={data}
|
|
loading={loading}
|
|
onChange={handleTableChange}
|
|
totalCount={total}
|
|
scroll={{ y: "calc(100vh - 540px)" }}
|
|
pagination={getStandardPagination(total, params.current, params.size)}
|
|
/>
|
|
</div>
|
|
</Card>
|
|
|
|
<Modal title={t("logs.detailTitle")} open={detailModalVisible} onCancel={() => setDetailModalVisible(false)} footer={[<Button key="close" onClick={() => setDetailModalVisible(false)}>{t("logsExt.close")}</Button>]} width={700}>
|
|
{selectedLog && (
|
|
<Descriptions bordered column={1} size="small">
|
|
{isPlatformAdmin && <Descriptions.Item label={t("users.tenant")}><Text>{selectedLog.tenantName || t("logsExt.platform")}</Text></Descriptions.Item>}
|
|
{selectedLog.logType === "OPERATION" && <Descriptions.Item label={t("logsExt.module")}>{selectedLog.moduleName || t("logsExt.uncategorized")}</Descriptions.Item>}
|
|
{selectedLog.logType === "OPERATION" && <Descriptions.Item label={t("logsExt.actionLabel")}>{selectedLog.actionName || selectedLog.operation}</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 || t("logsExt.system")}</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"}>{logStatusDict.find((item) => item.itemValue === String(selectedLog.status))?.itemLabel || (selectedLog.status === 1 ? t("logsExt.success") : t("logsExt.failed"))}</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>
|
|
);
|
|
}
|