|
|
@@ -1,13 +1,335 @@
|
|
|
+"use client";
|
|
|
+
|
|
|
+import {
|
|
|
+ DownOutlined,
|
|
|
+ ReloadOutlined,
|
|
|
+ SearchOutlined,
|
|
|
+ UndoOutlined,
|
|
|
+ UpOutlined,
|
|
|
+} from "@ant-design/icons";
|
|
|
+import {
|
|
|
+ Button,
|
|
|
+ DatePicker,
|
|
|
+ Descriptions,
|
|
|
+ Form,
|
|
|
+ Input,
|
|
|
+ Space,
|
|
|
+ Table,
|
|
|
+ Typography,
|
|
|
+} from "antd";
|
|
|
+import type { ColumnsType, TablePaginationConfig } from "antd/es/table";
|
|
|
+import type { SorterResult } from "antd/es/table/interface";
|
|
|
+import dayjs from "dayjs";
|
|
|
import type React from "react";
|
|
|
+import { useEffect, useState } from "react";
|
|
|
+import { getOperationLogPage } from "@/services/operationLog";
|
|
|
+import type { OperationLog, OperationLogPageRequest } from "@/types/api";
|
|
|
+
|
|
|
+const { RangePicker } = DatePicker;
|
|
|
+const { Text, Paragraph } = Typography;
|
|
|
+
|
|
|
+const OperationLogPage: React.FC = () => {
|
|
|
+ const [form] = Form.useForm();
|
|
|
+
|
|
|
+ // State management
|
|
|
+ const [loading, setLoading] = useState(false);
|
|
|
+ const [dataSource, setDataSource] = useState<OperationLog[]>([]);
|
|
|
+ const [total, setTotal] = useState(0);
|
|
|
+ const [showMoreFilters, setShowMoreFilters] = useState(false);
|
|
|
+ const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
|
|
+ const [queryParams, setQueryParams] = useState<OperationLogPageRequest>({
|
|
|
+ pageSize: 20,
|
|
|
+ pageIndex: 1,
|
|
|
+ startDay: dayjs().format("YYYY-MM-DD"),
|
|
|
+ endDay: dayjs().format("YYYY-MM-DD"),
|
|
|
+ });
|
|
|
+
|
|
|
+ // Load page data
|
|
|
+ const loadPageData = async () => {
|
|
|
+ setLoading(true);
|
|
|
+ try {
|
|
|
+ const response = await getOperationLogPage(queryParams);
|
|
|
+ setDataSource(response.items);
|
|
|
+ setTotal(response.total);
|
|
|
+ } catch (error) {
|
|
|
+ console.error("Failed to load operation logs:", error);
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // Initial load
|
|
|
+ // biome-ignore lint/correctness/useExhaustiveDependencies: loadPageData is stable and doesn't need to be in dependencies
|
|
|
+ useEffect(() => {
|
|
|
+ loadPageData();
|
|
|
+ }, [queryParams]);
|
|
|
+
|
|
|
+ // Search handler
|
|
|
+ const handleSearch = () => {
|
|
|
+ const values = form.getFieldsValue();
|
|
|
+ const dateRange = values.dateRange;
|
|
|
+
|
|
|
+ setQueryParams({
|
|
|
+ ...queryParams,
|
|
|
+ pageIndex: 1,
|
|
|
+ adminNickname: values.adminNickname?.trim() || undefined,
|
|
|
+ operName: values.operName?.trim() || undefined,
|
|
|
+ uri: values.uri?.trim() || undefined,
|
|
|
+ requestHeaders: values.requestHeaders?.trim() || undefined,
|
|
|
+ request: values.request?.trim() || undefined,
|
|
|
+ response: values.response?.trim() || undefined,
|
|
|
+ adminUserId: values.adminUserId?.trim() || undefined,
|
|
|
+ startDay: dateRange?.[0]
|
|
|
+ ? dayjs(dateRange[0]).format("YYYY-MM-DD")
|
|
|
+ : undefined,
|
|
|
+ endDay: dateRange?.[1]
|
|
|
+ ? dayjs(dateRange[1]).format("YYYY-MM-DD")
|
|
|
+ : undefined,
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ // Reset search
|
|
|
+ const handleReset = () => {
|
|
|
+ form.resetFields();
|
|
|
+ const today = dayjs().format("YYYY-MM-DD");
|
|
|
+ setQueryParams({
|
|
|
+ pageSize: 20,
|
|
|
+ pageIndex: 1,
|
|
|
+ startDay: today,
|
|
|
+ endDay: today,
|
|
|
+ sort: undefined,
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ // Pagination change handler
|
|
|
+ const handleTableChange = (
|
|
|
+ pagination: TablePaginationConfig,
|
|
|
+ _filters: unknown,
|
|
|
+ sorter: SorterResult<OperationLog> | SorterResult<OperationLog>[],
|
|
|
+ ) => {
|
|
|
+ const newQueryParams: OperationLogPageRequest = {
|
|
|
+ ...queryParams,
|
|
|
+ pageIndex: pagination.current || 1,
|
|
|
+ pageSize: pagination.pageSize || 20,
|
|
|
+ };
|
|
|
+
|
|
|
+ // Handle sorting (only handle single sorter)
|
|
|
+ const singleSorter = Array.isArray(sorter) ? sorter[0] : sorter;
|
|
|
+ if (singleSorter?.field && singleSorter?.order) {
|
|
|
+ newQueryParams.sort = {
|
|
|
+ field: singleSorter.field as string,
|
|
|
+ order: singleSorter.order === "ascend" ? 1 : -1,
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ newQueryParams.sort = undefined;
|
|
|
+ }
|
|
|
+
|
|
|
+ setQueryParams(newQueryParams);
|
|
|
+ };
|
|
|
+
|
|
|
+ // Format datetime
|
|
|
+ const formatDateTime = (dateStr: string) => {
|
|
|
+ if (!dateStr) return "-";
|
|
|
+ return dayjs(dateStr).format("YYYY-MM-DD HH:mm:ss");
|
|
|
+ };
|
|
|
+
|
|
|
+ // Table columns
|
|
|
+ const columns: ColumnsType<OperationLog> = [
|
|
|
+ {
|
|
|
+ title: "管理员昵称",
|
|
|
+ dataIndex: "adminNickname",
|
|
|
+ key: "adminNickname",
|
|
|
+ width: 120,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "URI",
|
|
|
+ dataIndex: "uri",
|
|
|
+ key: "uri",
|
|
|
+ width: 220,
|
|
|
+ ellipsis: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "操作名称",
|
|
|
+ dataIndex: "operName",
|
|
|
+ key: "operName",
|
|
|
+ width: 240,
|
|
|
+ ellipsis: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "请求数据",
|
|
|
+ dataIndex: "request",
|
|
|
+ key: "request",
|
|
|
+ width: 300,
|
|
|
+ render: (text: string) => (
|
|
|
+ <Paragraph
|
|
|
+ ellipsis={{ rows: 2, expandable: false }}
|
|
|
+ style={{ marginBottom: 0 }}
|
|
|
+ >
|
|
|
+ {text || "-"}
|
|
|
+ </Paragraph>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "IP地址",
|
|
|
+ dataIndex: "ip",
|
|
|
+ key: "ip",
|
|
|
+ width: 140,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "IP所属运营商",
|
|
|
+ dataIndex: "isp",
|
|
|
+ key: "isp",
|
|
|
+ width: 150,
|
|
|
+ render: (text: string) => text || "-",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "请求耗时(ms)",
|
|
|
+ dataIndex: "spentTime",
|
|
|
+ key: "spentTime",
|
|
|
+ width: 130,
|
|
|
+ sorter: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "请求时间",
|
|
|
+ dataIndex: "createTime",
|
|
|
+ key: "createTime",
|
|
|
+ width: 180,
|
|
|
+ sorter: true,
|
|
|
+ render: formatDateTime,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "响应时间",
|
|
|
+ dataIndex: "completedTime",
|
|
|
+ key: "completedTime",
|
|
|
+ width: 180,
|
|
|
+ sorter: true,
|
|
|
+ render: formatDateTime,
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ // Expanded row render
|
|
|
+ const expandedRowRender = (record: OperationLog) => {
|
|
|
+ return (
|
|
|
+ <Descriptions column={1} bordered size="small">
|
|
|
+ <Descriptions.Item label="管理员ID">
|
|
|
+ {record.adminUserId}
|
|
|
+ </Descriptions.Item>
|
|
|
+ <Descriptions.Item label="请求头">
|
|
|
+ <Text style={{ whiteSpace: "pre-wrap", wordBreak: "break-all" }}>
|
|
|
+ {record.requestHeaders || "-"}
|
|
|
+ </Text>
|
|
|
+ </Descriptions.Item>
|
|
|
+ <Descriptions.Item label="请求数据">
|
|
|
+ <Text style={{ whiteSpace: "pre-wrap", wordBreak: "break-all" }}>
|
|
|
+ {record.request || "-"}
|
|
|
+ </Text>
|
|
|
+ </Descriptions.Item>
|
|
|
+ <Descriptions.Item label="响应数据">
|
|
|
+ <Text style={{ whiteSpace: "pre-wrap", wordBreak: "break-all" }}>
|
|
|
+ {record.response || "-"}
|
|
|
+ </Text>
|
|
|
+ </Descriptions.Item>
|
|
|
+ </Descriptions>
|
|
|
+ );
|
|
|
+ };
|
|
|
|
|
|
-const UserAdminListPage: React.FC = () => {
|
|
|
return (
|
|
|
- <div>
|
|
|
- <h1>后台用户列表</h1>
|
|
|
- <p>这里将展示后台管理用户列表</p>
|
|
|
+ <div className="p-6">
|
|
|
+ {/* Search Form */}
|
|
|
+ <div className="bg-white p-4 rounded-lg shadow mb-4">
|
|
|
+ <Form
|
|
|
+ form={form}
|
|
|
+ layout="inline"
|
|
|
+ initialValues={{
|
|
|
+ dateRange: [dayjs(), dayjs()],
|
|
|
+ }}
|
|
|
+ className="gap-x-2 gap-y-4"
|
|
|
+ >
|
|
|
+ <Form.Item label="管理员昵称" name="adminNickname">
|
|
|
+ <Input placeholder="请输入" allowClear style={{ width: 200 }} />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item label="操作名称" name="operName">
|
|
|
+ <Input placeholder="请输入" allowClear style={{ width: 200 }} />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item label="URI" name="uri">
|
|
|
+ <Input placeholder="请输入" allowClear style={{ width: 200 }} />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item label="请求头" name="requestHeaders">
|
|
|
+ <Input placeholder="请输入" allowClear style={{ width: 200 }} />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item label="请求数据" name="request">
|
|
|
+ <Input placeholder="请输入" allowClear style={{ width: 200 }} />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item label="响应数据" name="response">
|
|
|
+ <Input placeholder="请输入" allowClear style={{ width: 200 }} />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item label="日期范围" name="dateRange">
|
|
|
+ <RangePicker format="YYYY-MM-DD" style={{ width: 260 }} />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ {/* More filters */}
|
|
|
+ {showMoreFilters && (
|
|
|
+ <Form.Item label="管理员ID" name="adminUserId">
|
|
|
+ <Input placeholder="请输入" allowClear style={{ width: 200 }} />
|
|
|
+ </Form.Item>
|
|
|
+ )}
|
|
|
+
|
|
|
+ <Form.Item style={{ marginLeft: "auto" }}>
|
|
|
+ <Space>
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ icon={<SearchOutlined />}
|
|
|
+ onClick={handleSearch}
|
|
|
+ >
|
|
|
+ 搜索
|
|
|
+ </Button>
|
|
|
+ <Button icon={<UndoOutlined />} onClick={handleReset}>
|
|
|
+ 重置
|
|
|
+ </Button>
|
|
|
+ <Button icon={<ReloadOutlined />} onClick={() => loadPageData()}>
|
|
|
+ 刷新
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ type={showMoreFilters ? "default" : "default"}
|
|
|
+ icon={showMoreFilters ? <UpOutlined /> : <DownOutlined />}
|
|
|
+ onClick={() => setShowMoreFilters(!showMoreFilters)}
|
|
|
+ >
|
|
|
+ {showMoreFilters ? "隐藏条件" : "更多条件"}
|
|
|
+ </Button>
|
|
|
+ </Space>
|
|
|
+ </Form.Item>
|
|
|
+ </Form>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Table */}
|
|
|
+ <div className="bg-white p-4 rounded-lg shadow">
|
|
|
+ <Table
|
|
|
+ columns={columns}
|
|
|
+ dataSource={dataSource}
|
|
|
+ rowKey={(_, i) => String(i)}
|
|
|
+ loading={loading}
|
|
|
+ pagination={{
|
|
|
+ current: queryParams.pageIndex,
|
|
|
+ pageSize: queryParams.pageSize,
|
|
|
+ total: total,
|
|
|
+ showSizeChanger: true,
|
|
|
+ showTotal: (total) => `共 ${total} 条`,
|
|
|
+ pageSizeOptions: ["10", "20", "30", "40", "50", "100", "200"],
|
|
|
+ }}
|
|
|
+ onChange={handleTableChange}
|
|
|
+ scroll={{ x: 1600 }}
|
|
|
+ size="small"
|
|
|
+ bordered
|
|
|
+ expandable={{
|
|
|
+ expandedRowRender,
|
|
|
+ expandedRowKeys,
|
|
|
+ onExpandedRowsChange: (keys) =>
|
|
|
+ setExpandedRowKeys(keys as string[]),
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
-export default UserAdminListPage;
|
|
|
-
|
|
|
+export default OperationLogPage;
|