Parcourir la source

Add dayjs dependency for date handling and implement operation log page with search and pagination features. Update API types for operation logs and enhance dashboard layout for improved user experience.

0es il y a 4 mois
Parent
commit
feb320a924

+ 1 - 0
package.json

@@ -16,6 +16,7 @@
     "@dnd-kit/sortable": "^10.0.0",
     "@dnd-kit/utilities": "^3.2.2",
     "antd": "^5.28.0",
+    "dayjs": "^1.11.19",
     "js-sha256": "^0.11.1",
     "next": "16.0.1",
     "react": "19.2.0",

+ 1 - 1
src/app/(dashboard)/layout.tsx

@@ -230,7 +230,7 @@ export default function DashboardLayout({
           <Content className="mx-4 pt-4">
             <Breadcrumb items={breadcrumbItems} />
             <div
-              className="p-4 mt-4 rounded-lg"
+              className="mt-4 rounded-lg"
               style={{
                 minHeight: 360,
                 background: colorBgContainer,

+ 1 - 0
src/app/(dashboard)/system/admin/user/page.tsx

@@ -446,6 +446,7 @@ const UserAdminListPage: React.FC = () => {
         confirmLoading={editLoading}
         width={800}
         destroyOnHidden
+        forceRender
       >
         <Form
           form={editForm}

+ 328 - 6
src/app/(dashboard)/system/operation-log/page.tsx

@@ -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;

+ 19 - 0
src/services/operationLog.ts

@@ -0,0 +1,19 @@
+/**
+ * Operation Log API Service
+ */
+
+import { post } from "@/lib/request";
+import type {
+  OperationLogPageRequest,
+  OperationLogPageResponse,
+} from "@/types/api";
+
+/**
+ * Get operation log page data
+ * @param params - page query parameters
+ * @returns paginated operation log list
+ */
+export async function getOperationLogPage(params: OperationLogPageRequest) {
+  return post<OperationLogPageResponse>("/ffx/operation/log/page", params);
+}
+

+ 48 - 0
src/types/api.ts

@@ -239,3 +239,51 @@ export interface AdminPermission {
   status: number;
   orderNum?: number;
 }
+
+// ============================================
+// Operation Log Types
+// ============================================
+
+// Operation log item
+export interface OperationLog {
+  uid: string;
+  adminUserId: string;
+  adminNickname: string;
+  uri: string;
+  operName: string;
+  request: string;
+  requestHeaders: string;
+  response: string;
+  ip: string;
+  isp: string;
+  spentTime: number;
+  createTime: string;
+  completedTime: string;
+}
+
+// Operation log page query request
+export interface OperationLogPageRequest {
+  pageSize: number;
+  pageIndex: number;
+  sort?: {
+    field: string;
+    order: number; // 1: ascending, -1: descending
+  };
+  adminNickname?: string;
+  adminUserId?: string;
+  operName?: string;
+  uri?: string;
+  requestHeaders?: string;
+  request?: string;
+  response?: string;
+  startDay?: string;
+  endDay?: string;
+}
+
+// Operation log page response
+export interface OperationLogPageResponse {
+  total: number;
+  pageIndex: number;
+  pageSize: number;
+  items: OperationLog[];
+}

+ 1 - 1
yarn.lock

@@ -772,7 +772,7 @@ csstype@^3.0.2, csstype@^3.1.3:
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
   integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
 
-dayjs@^1.11.11:
+dayjs@^1.11.11, dayjs@^1.11.19:
   version "1.11.19"
   resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.19.tgz#15dc98e854bb43917f12021806af897c58ae2938"
   integrity sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==