imeeting/frontend/src/pages/system/logs/index.tsx

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>
);
}