From cd2b7d36f3610417c268d062454b7ec43e9ca937 Mon Sep 17 00:00:00 2001
From: wsj <593216709@qq.com>
Date: Wed, 28 Oct 2026 22:51:24 +0800
Subject: [PATCH] feat: 日志页面

---
 src/pages/system/departments/types.ts                             |   13 
 src/pages/platform/audit-logs/AuditLogs.scss                      |   14 +
 src/pages/system/departments/index.tsx                            |   18 -
 CLAUDE.md                                                         |   83 ++++++
 src/pages/platform/audit-logs/index.tsx                           |   64 ++++
 .claude/skills/new-page/SKILL.md                                  |    4 
 src/pages/system/departments/columns.tsx                          |    8 
 README.md                                                         |    1 
 .claude/skills/new-page/templates/non-table-page/index.tsx        |   22 
 src/pages/platform/audit-logs/columns.tsx                         |  172 +++++++++++++
 src/pages/platform/audit-logs/types.ts                            |   70 +++++
 src/pages/system/users/columns.tsx                                |   11 
 src/router/routeConfig.tsx                                        |   16 +
 src/pages/system/users/components/UserFormDrawer.tsx              |   65 ++--
 src/pages/platform/audit-logs/components/AuditLogDetailDrawer.tsx |   82 ++++++
 src/pages/system/departments/components/DepartmentFormDrawer.tsx  |   90 ++++--
 src/pages/system/departments/server.ts                            |   14 
 src/pages/platform/audit-logs/server.ts                           |   12 
 .claude/skills/new-page/templates/table-page/index.tsx            |   21 +
 19 files changed, 679 insertions(+), 101 deletions(-)

diff --git a/.claude/skills/new-page/SKILL.md b/.claude/skills/new-page/SKILL.md
index 4e5b61c..aed1cf0 100644
--- a/.claude/skills/new-page/SKILL.md
+++ b/.claude/skills/new-page/SKILL.md
@@ -50,7 +50,7 @@
 - 使用 `ProTable`,不要手写「筛选表单 + Table」。
 - `columns` 必须放在独立 `columns.tsx`,类型为 `ProColumns<T>[]`;需要回调时导出 `createXxxColumns(options)` 工厂函数。
 - 页面中用 `useMemo` 缓存 columns,用 `useCallback` 包裹事件处理函数。
-- `request` 单独定义:在组件内声明 `const fetchList = useCallback((params: XxxListParams) => getXxxList(params), [])`,再传给 `request={fetchList}`;不要在 `<ProTable />` 标签里内联箭头函数。
+- `request` 单独定义:在组件内声明 `const fetchList = useCallback((params: XxxListParams) => getXxxList(params).then((result) => ({ data: result.data, total: result.total, success: result.success })), [])`,再传给 `request={fetchList}`;不要在 `<ProTable />` 标签里内联箭头函数,也不要用 `await` + `try/catch`。
 - 页面组件不直接写请求:列表查询、删除等接口调用统一放在同目录的 `server.ts`,组件只调用导出的方法。
 - `server.ts` 使用 `@/api/request` 提供的 `request` 发送请求,不要直接使用 axios;泛型顺序为 `request<请求参数, 响应数据>`。
 - 分页使用 `defaultPageSize`(不要用受控的 `pageSize`);需要切换每页条数时设置 `showSizeChanger: true`。
@@ -87,6 +87,7 @@
 
 - 页面级接口调用统一放在页面目录的 `server.ts`,不要写在组件内部。
 - 统一使用 `src/api/request.ts` 的 `request`,不要直接使用 axios。默认会合并相同并发请求、自动提示错误,并只返回响应 `data`。
+- 接口请求使用 `.then` / `.catch` / `.finally` 链式处理,不要使用 `await` + `try/catch`。
 - 请求选项:`merge` 控制是否合并、`showError` 控制是否提示错误、`parse: false` 返回完整 `AxiosResponse`、`signal` 用于取消。
 - 全部使用箭头函数,不新增 `function` 声明。
 - 提示与确认框使用 `AntdApp.useApp()` 提供的 `message` / `modal`,不要用静态方法。
@@ -134,6 +135,7 @@
 - [ ] 已选择正确分支并整体使用对应模板
 - [ ] 表格页使用 ProTable,columns 独立在 `columns.tsx`
 - [ ] `request` 已提取为独立的 `useCallback` 方法,未在标签内内联
+- [ ] 接口请求使用 `.then` 链式处理,没有 `await` + `try/catch`
 - [ ] 表格页无 `toolBarRender` 时已加 `cardProps={{ bodyStyle: { padding: 20 } }}`,有 `toolBarRender` 时未加
 - [ ] 非表格页未创建 `columns.tsx`,使用合适的 Ant Design 组件
 - [ ] 两个分支均已创建 `server.ts`,组件内没有直接写请求
diff --git a/.claude/skills/new-page/templates/non-table-page/index.tsx b/.claude/skills/new-page/templates/non-table-page/index.tsx
index 3830100..53cf618 100644
--- a/.claude/skills/new-page/templates/non-table-page/index.tsx
+++ b/.claude/skills/new-page/templates/non-table-page/index.tsx
@@ -9,17 +9,19 @@
   const [form] = Form.useForm<XxxFormValues>();
   const { message } = AntdApp.useApp();
 
-  const handleFinish = async (values: XxxFormValues) => {
+  const handleFinish = (values: XxxFormValues) => {
     setSubmitting(true);
-    try {
-      await saveXxx(values);
-      message.success('保存成功');
-      form.resetFields();
-    } catch (error) {
-      message.error(error instanceof Error ? error.message : '保存失败,请稍后重试');
-    } finally {
-      setSubmitting(false);
-    }
+    saveXxx(values)
+      .then(() => {
+        message.success('保存成功');
+        form.resetFields();
+      })
+      .catch((error) => {
+        message.error(error instanceof Error ? error.message : '保存失败,请稍后重试');
+      })
+      .finally(() => {
+        setSubmitting(false);
+      });
   };
 
   return (
diff --git a/.claude/skills/new-page/templates/table-page/index.tsx b/.claude/skills/new-page/templates/table-page/index.tsx
index f7aa4a7..90befcd 100644
--- a/.claude/skills/new-page/templates/table-page/index.tsx
+++ b/.claude/skills/new-page/templates/table-page/index.tsx
@@ -20,11 +20,15 @@
         okText: '删除',
         cancelText: '取消',
         okButtonProps: { danger: true },
-        onOk: async () => {
-          await deleteXxx(record.key);
-          message.success('已删除');
-          actionRef.current?.reload();
-        },
+        onOk: () =>
+          deleteXxx(record.key)
+            .then(() => {
+              message.success('已删除');
+              actionRef.current?.reload();
+            })
+            .catch(() => {
+              // 错误已由 request 统一提示
+            }),
       });
     },
     [message, modal],
@@ -36,7 +40,12 @@
   );
 
   const fetchList = useCallback(
-    (params: XxxListParams) => getXxxList(params),
+    (params: XxxListParams) =>
+      getXxxList(params).then((result) => ({
+        data: result.data,
+        total: result.total,
+        success: result.success,
+      })),
     [],
   );
 
diff --git a/CLAUDE.md b/CLAUDE.md
index 17347af..d2ca45b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -41,7 +41,7 @@
 │   ├── business/orders/
 │   ├── customers/list/
 │   └── error/NotFound.tsx
-│   # 页面目录:index.tsx、columns.tsx、types.ts、server.ts、同名 .scss(按需)
+│   # 页面目录:index.tsx、columns.tsx、types.ts、server.ts、components/(按需)、同名 .scss(按需)
 ├── router/
 │   ├── routeConfig.tsx  # 路由 + 菜单唯一数据源(含页面组件)
 │   ├── menu.tsx         # 菜单过滤与 antd Menu 结构生成
@@ -119,11 +119,58 @@
    - 登录校验:`src/router/index.tsx` 中的 `RequireAuth`
    - 全局提示 / 确认框:使用 `<App>` 提供的 `AntdApp.useApp()` 中的 `message`、`modal`,**不要**直接使用 `message.success()` / `Modal.confirm()` 静态方法。
 3. **表格优先使用 ProTable**:新增列表页时优先使用 `@ant-design/pro-components` 提供的 `ProTable`,其搜索、分页、工具栏等能力开箱即用,不要手写筛选表单 + Table 的组合。
-4. **不要手写**弹窗、Toast、下拉菜单、分页、表格、表单校验等基础交互。
-5. 不引入新的 UI 组件库;确需新组件时,先检查是否已有可复用实现,再考虑在 `src/components/` 下新增。
-6. 使用 Ant Design 5 的 API,不使用已废弃属性。例如 Card 使用 `variant="borderless"`,不要使用 `bordered={false}`。
+4. **表单内的选择控件优先使用 ProForm 组件**:`Form` 内的 Select、TreeSelect、Cascader 等选择控件,只要选项来自接口(包括父组件传入的接口数据,如组织树、角色列表),必须使用 `@ant-design/pro-components` 提供的 `ProFormSelect`、`ProFormTreeSelect`、`ProFormCascader` 等,不要用 antd 原生控件包在 `Form.Item` 中;静态枚举选项不受此限制。远程搜索使用 `request`(配合 `debounceTime`),已有选项数据通过 `options` 或 `fieldProps` 传入,校验规则直接写在组件的 `rules` 上。
+   ```tsx
+   // ✅ 选项来自接口
+   <ProFormSelect
+     name="leaderId"
+     label="负责人"
+     showSearch
+     debounceTime={300}
+     request={async ({ keyWords }) => getLeaderOptions({ keyword: keyWords })}
+   />
 
-### 3. Table columns 必须单独提取
+   // ✅ 选项由父组件传入
+   <ProFormTreeSelect
+     name="parentId"
+     label="上级组织"
+     fieldProps={{ treeData: orgTree, showSearch: true }}
+   />
+
+   // ❌ 不要这样写
+   <Form.Item name="leaderId" label="负责人">
+     <Select options={leaderOptions} />
+   </Form.Item>
+   ```
+5. **不要手写**弹窗、Toast、下拉菜单、分页、表格、表单校验等基础交互。
+6. 不引入新的 UI 组件库;确需新组件时,先检查是否已有可复用实现,再考虑在 `src/components/` 下新增。
+7. 使用 Ant Design 5 的 API,不使用已废弃属性。例如 Card 使用 `variant="borderless"`,不要使用 `bordered={false}`。
+
+### 3. 独立组件必须放到 `components/`,超长页面拆分
+
+单独封装的组件不写在页面目录根部,统一放到页面目录下的 `components/` 子目录。
+
+- Drawer、Modal 等弹层组件,以及详情卡片、表单、局部业务块等可独立成文件的组件,放到 `src/pages/<业务域>/<页面>/components/` 下。
+- 组件文件使用大驼峰命名,如 `UserFormDrawer.tsx`、`CustomerDetailDrawer.tsx`;组件私有的 `.scss` 与组件同目录,由组件自身引入。
+- 组件只接收 `props`,接口调用仍放在页面目录的 `server.ts`,由页面或组件按需调用;不要跨页面直接引用其他页面目录下的组件。
+- 单个页面文件(含 `index.tsx`)超过 300 行时必须拆分:按功能块抽取到 `components/`,`columns.tsx`、`types.ts`、`server.ts`、`utils.ts` 等继续单独维护。
+- 大于 300 行的复杂组件同样按功能拆分,不要在同一个文件里堆叠多个组件。
+
+```text
+src/pages/system/users/
+├── index.tsx                # 页面组件(尽量精简)
+├── columns.tsx              # 表格列定义
+├── types.ts
+├── server.ts
+├── utils.ts                 # 按需
+├── Users.scss
+└── components/
+    ├── UserFormDrawer.tsx
+    ├── UserDetailDrawer.tsx
+    └── UserImportModal.tsx
+```
+
+### 4. Table columns 必须单独提取
 
 所有表格的 `columns` 不得内联在 `<Table />` 的 JSX 中,也不写在页面组件内部。统一提取到页面目录单独维护。
 
@@ -231,6 +278,32 @@
 - 页面级接口调用统一放在页面目录下的 `server.ts`,页面组件通过它调用接口,不要在组件内直接写请求实现。
 - 跨页面复用的接口可放在 `src/api/` 下,按业务域拆分文件。
 - 网络请求统一使用 `src/api/request.ts` 的 `request`(基于 axios),不要在页面或 `server.ts` 中直接使用 axios。
+- **接口调用使用 `.then` 链式处理,不要使用 `await` + `try/catch`**:成功逻辑写在 `.then`,失败逻辑写在 `.catch`(错误已由 `request` 统一提示时可只保留空 `catch`),收尾逻辑写在 `.finally`。禁止写成 `try { const data = await xxx() } catch {}`。(存量代码在后续迭代中逐步迁移。)
+
+```ts
+// ✅ 使用 .then 链式处理
+getOrgList({ page: 1, size: 20 })
+  .then((data) => {
+    setDataSource(data.records);
+  })
+  .catch(() => {
+    // 错误已由 request 统一提示
+  })
+  .finally(() => {
+    setLoading(false);
+  });
+
+// ❌ 不要使用 await + try/catch
+try {
+  const data = await getOrgList({ page: 1, size: 20 });
+  setDataSource(data.records);
+} catch {
+  // ...
+} finally {
+  setLoading(false);
+}
+```
+
 - 开发环境通过 Vite 代理 `/proxy-api` 转发到 `VITE_API_PROXY_TARGET`(未配置时取 `VITE_API_BASE_URL`),因此浏览器端不会产生跨域请求;生产环境需后端配置 CORS,或使用同域反向代理。
   - 泛型:`request<TReq, TRes>(config, options)`,分别表示请求参数/请求体与响应 `data` 的类型。
   - 默认只返回响应 `data`;传 `parse: false` 时返回完整 `AxiosResponse`。
diff --git a/README.md b/README.md
index 1c7ecde..1524946 100644
--- a/README.md
+++ b/README.md
@@ -105,6 +105,7 @@
 | 部门管理 | `/system/departments`   | 组织树、增删改、启用 / 停用        |
 | 订单管理 | `/business/orders`      | 筛选、处理订单、导出               |
 | 客户列表 | `/customers/list`       | 接口数据,按归属销售员筛选         |
+| 审计日志 | `/platform/audit-logs`  | 接口数据,多条件筛选、详情查看     |
 | 404      | 任意未知地址            | 页面不存在提示                     |
 
 ## 目录结构
diff --git a/src/pages/platform/audit-logs/AuditLogs.scss b/src/pages/platform/audit-logs/AuditLogs.scss
new file mode 100644
index 0000000..0059734
--- /dev/null
+++ b/src/pages/platform/audit-logs/AuditLogs.scss
@@ -0,0 +1,14 @@
+.audit-log-page {
+  &__change-detail {
+    max-height: 240px;
+    margin: 0;
+    padding: 8px 12px;
+    overflow: auto;
+    font-size: 12px;
+    line-height: 1.6;
+    white-space: pre-wrap;
+    word-break: break-all;
+    background: var(--app-bg);
+    border-radius: 6px;
+  }
+}
diff --git a/src/pages/platform/audit-logs/columns.tsx b/src/pages/platform/audit-logs/columns.tsx
new file mode 100644
index 0000000..2ee7092
--- /dev/null
+++ b/src/pages/platform/audit-logs/columns.tsx
@@ -0,0 +1,172 @@
+import { Button, Space, Tag } from 'antd';
+import type { ProColumns } from '@ant-design/pro-components';
+import dayjs from 'dayjs';
+import type { AuditLogResultStatus, AuditLogVO } from './types';
+
+/** 业务模块(后端可能扩展,未命中的直接展示原值)。 */
+export const moduleText: Record<string, string> = {
+  EMPLOYEE: '员工',
+  ORG: '组织',
+  ROLE: '角色',
+  CUSTOMER: '客户',
+  FOLLOW_UP: '跟进',
+  TENANT: '租户',
+  SYSTEM: '系统',
+};
+
+/** 操作动作(后端可能扩展,未命中的直接展示原值)。 */
+export const actionText: Record<string, string> = {
+  CREATE: '新增',
+  UPDATE: '修改',
+  DELETE: '删除',
+  IMPORT: '导入',
+  EXPORT: '导出',
+  LOGIN: '登录',
+  LOGOUT: '退出',
+};
+
+export const resultStatusMeta: Record<AuditLogResultStatus, { text: string; color: string }> = {
+  SUCCESS: { text: '成功', color: 'success' },
+  FAILURE: { text: '失败', color: 'error' },
+};
+
+const toValueEnum = (meta: Record<string, string>) =>
+  Object.fromEntries(Object.entries(meta).map(([value, text]) => [value, { text }]));
+
+export const getModuleText = (value?: string) => (value ? moduleText[value] ?? value : '-');
+
+export const getActionText = (value?: string) => (value ? actionText[value] ?? value : '-');
+
+export const createAuditLogColumns = (options: {
+  onDetail: (record: AuditLogVO) => void;
+}): ProColumns<AuditLogVO>[] => [
+  {
+    title: '日志 ID',
+    dataIndex: 'logId',
+    width: 100,
+    search: false,
+  },
+  {
+    title: '操作时间',
+    dataIndex: 'createdAt',
+    valueType: 'dateTime',
+    width: 170,
+    search: false,
+  },
+  {
+    title: '操作人',
+    dataIndex: 'operatorName',
+    width: 140,
+    search: false,
+    render: (_value, record) => (
+      <Space direction="vertical" size={0}>
+        <span>{record.operatorName || '-'}</span>
+        {record.operatorRole && <span className="text-muted">{record.operatorRole}</span>}
+      </Space>
+    ),
+  },
+  {
+    title: '操作人 ID',
+    dataIndex: 'operatorId',
+    valueType: 'digit',
+    hideInTable: true,
+    fieldProps: { placeholder: '请输入操作人 ID' },
+  },
+  {
+    title: '业务模块',
+    dataIndex: 'module',
+    width: 100,
+    valueEnum: toValueEnum(moduleText),
+    render: (_value, record) => getModuleText(record.module),
+  },
+  {
+    title: '操作动作',
+    dataIndex: 'action',
+    width: 100,
+    valueEnum: toValueEnum(actionText),
+    render: (_value, record) => getActionText(record.action),
+  },
+  {
+    title: '操作描述',
+    dataIndex: 'operationDesc',
+    width: 120,
+    search: false,
+    ellipsis: true,
+    render: (_value, record) => record.operationDesc || '-',
+  },
+  {
+    title: '操作对象',
+    dataIndex: 'entityName',
+    width: 180,
+    search: false,
+    ellipsis: true,
+    render: (_value, record) => record.entityName || '-',
+  },
+  {
+    title: '实体类型',
+    dataIndex: 'entityType',
+    hideInTable: true,
+    fieldProps: { placeholder: '请输入实体类型' },
+  },
+  {
+    title: '请求',
+    dataIndex: 'requestPath',
+    width: 220,
+    search: false,
+    ellipsis: true,
+    render: (_value, record) => (
+      <Space size={4}>
+        <Tag bordered={false}>{record.requestMethod || '-'}</Tag>
+        <span>{record.requestPath || '-'}</span>
+      </Space>
+    ),
+  },
+  {
+    title: '来源 IP',
+    dataIndex: 'requestIp',
+    width: 130,
+    search: false,
+  },
+  {
+    title: '结果',
+    dataIndex: 'resultStatus',
+    width: 90,
+    valueEnum: {
+      SUCCESS: { text: resultStatusMeta.SUCCESS.text },
+      FAILURE: { text: resultStatusMeta.FAILURE.text },
+    },
+    render: (_value, record) => {
+      const meta = record.resultStatus ? resultStatusMeta[record.resultStatus] : undefined;
+      return meta ? (
+        <Tag color={meta.color} bordered={false}>
+          {meta.text}
+        </Tag>
+      ) : (
+        '-'
+      );
+    },
+  },
+  {
+    title: '操作',
+    key: 'option',
+    valueType: 'option',
+    width: 80,
+    render: (_value, record) => (
+      <Button type="link" size="small" onClick={() => options.onDetail(record)}>
+        详情
+      </Button>
+    ),
+  },
+  {
+    title: '操作时间',
+    dataIndex: 'timeRange',
+    valueType: 'dateRange',
+    hideInTable: true,
+    search: {
+      transform: (value: [string, string]) => ({
+        start: value?.[0] ? dayjs(value[0]).format('YYYY-MM-DD 00:00:00') : undefined,
+        end: value?.[1] ? dayjs(value[1]).format('YYYY-MM-DD 23:59:59') : undefined,
+      }),
+    },
+  },
+];
diff --git a/src/pages/platform/audit-logs/components/AuditLogDetailDrawer.tsx b/src/pages/platform/audit-logs/components/AuditLogDetailDrawer.tsx
new file mode 100644
index 0000000..a5ebb39
--- /dev/null
+++ b/src/pages/platform/audit-logs/components/AuditLogDetailDrawer.tsx
@@ -0,0 +1,82 @@
+import { useMemo } from 'react';
+import { Descriptions, Drawer, Tag } from 'antd';
+import type { DescriptionsProps } from 'antd';
+import dayjs from 'dayjs';
+import { getActionText, getModuleText, resultStatusMeta } from '../columns';
+import type { AuditLogVO } from '../types';
+import '../AuditLogs.scss';
+
+interface AuditLogDetailDrawerProps {
+  open: boolean;
+  /** 待展示的审计日志,关闭时为 null。 */
+  record: AuditLogVO | null;
+  onClose: () => void;
+}
+
+const renderTag = (meta?: { text: string; color: string }) =>
+  meta ? (
+    <Tag color={meta.color} bordered={false}>
+      {meta.text}
+    </Tag>
+  ) : (
+    '-'
+  );
+
+const AuditLogDetailDrawer = ({ open, record, onClose }: AuditLogDetailDrawerProps) => {
+  const items = useMemo<DescriptionsProps['items']>(() => {
+    if (!record) return [];
+
+    return [
+      { key: 'logId', label: '日志 ID', children: record.logId },
+      {
+        key: 'createdAt',
+        label: '操作时间',
+        children: record.createdAt ? dayjs(record.createdAt).format('YYYY-MM-DD HH:mm:ss') : '-',
+      },
+      {
+        key: 'operator',
+        label: '操作人',
+        children: `${record.operatorName || '-'}${record.operatorRole ? `(${record.operatorRole})` : ''}`,
+      },
+      { key: 'operatorId', label: '操作人 ID', children: record.operatorId ?? '-' },
+      { key: 'tenantId', label: '租户 ID', children: record.tenantId || '-' },
+      { key: 'module', label: '业务模块', children: getModuleText(record.module) },
+      { key: 'action', label: '操作动作', children: getActionText(record.action) },
+      { key: 'operationDesc', label: '操作描述', children: record.operationDesc || '-' },
+      { key: 'entityType', label: '实体类型', children: record.entityType || '-' },
+      { key: 'entityId', label: '实体 ID', children: record.entityId || '-' },
+      { key: 'entityName', label: '实体名称', children: record.entityName || '-' },
+      {
+        key: 'resultStatus',
+        label: '结果状态',
+        children: renderTag(record.resultStatus ? resultStatusMeta[record.resultStatus] : undefined),
+      },
+      { key: 'errorMessage', label: '错误信息', children: record.errorMessage || '-' },
+      { key: 'requestMethod', label: '请求方法', children: record.requestMethod || '-' },
+      { key: 'requestPath', label: '请求路径', children: record.requestPath || '-' },
+      { key: 'requestIp', label: '来源 IP', children: record.requestIp || '-' },
+      {
+        key: 'userAgent',
+        label: 'User-Agent',
+        children: <span className="text-muted">{record.userAgent || '-'}</span>,
+      },
+      {
+        key: 'changeDetail',
+        label: '变更明细',
+        children: record.changeDetail ? (
+          <pre className="audit-log-page__change-detail">{record.changeDetail}</pre>
+        ) : (
+          '-'
+        ),
+      },
+    ];
+  }, [record]);
+
+  return (
+    <Drawer title="审计日志详情" width={560} open={open} onClose={onClose}>
+      {record && <Descriptions column={1} bordered size="small" items={items} />}
+    </Drawer>
+  );
+};
+
+export default AuditLogDetailDrawer;
diff --git a/src/pages/platform/audit-logs/index.tsx b/src/pages/platform/audit-logs/index.tsx
new file mode 100644
index 0000000..e1332d7
--- /dev/null
+++ b/src/pages/platform/audit-logs/index.tsx
@@ -0,0 +1,64 @@
+import { useCallback, useMemo, useState } from 'react';
+import { ProTable } from '@ant-design/pro-components';
+import { createAuditLogColumns } from './columns';
+import AuditLogDetailDrawer from './components/AuditLogDetailDrawer';
+import { getAuditLogList } from './server';
+import type { AuditLogListQuery, AuditLogVO } from './types';
+
+const AuditLogs = () => {
+  const [detail, setDetail] = useState<AuditLogVO | null>(null);
+
+  const openDetail = useCallback((record: AuditLogVO) => {
+    setDetail(record);
+  }, []);
+
+  const closeDetail = useCallback(() => {
+    setDetail(null);
+  }, []);
+
+  const columns = useMemo(() => createAuditLogColumns({ onDetail: openDetail }), [openDetail]);
+
+  const fetchList = useCallback(
+    (params: AuditLogListQuery) =>
+      getAuditLogList({
+        page: params.current,
+        size: params.pageSize,
+        module: params.module,
+        action: params.action,
+        entityType: params.entityType,
+        resultStatus: params.resultStatus,
+        operatorId: params.operatorId,
+        start: params.start,
+        end: params.end,
+      }).then((result) => ({
+        data: result.records ?? [],
+        total: result.total ?? 0,
+        success: true,
+      })),
+    [],
+  );
+
+  return (
+    <div className="audit-log-page">
+      <ProTable<AuditLogVO, AuditLogListQuery>
+        rowKey="logId"
+        columns={columns}
+        cardProps={{ bodyStyle: { padding: 20 } }}
+        search={{ labelWidth: 'auto', defaultCollapsed: false }}
+        scroll={{ x: 1430 }}
+        options={false}
+        pagination={{
+          defaultPageSize: 10,
+          showSizeChanger: true,
+          hideOnSinglePage: false,
+          showTotal: (total) => `共 ${total} 条`,
+        }}
+        request={fetchList}
+      />
+
+      <AuditLogDetailDrawer open={detail !== null} record={detail} onClose={closeDetail} />
+    </div>
+  );
+};
+
+export default AuditLogs;
diff --git a/src/pages/platform/audit-logs/server.ts b/src/pages/platform/audit-logs/server.ts
new file mode 100644
index 0000000..b75ab32
--- /dev/null
+++ b/src/pages/platform/audit-logs/server.ts
@@ -0,0 +1,12 @@
+import { getAuthHeaders } from '@/api/authHeaders';
+import { request } from '@/api/request';
+import type { TenantAuditLogListRequest, TenantAuditLogListResponse } from './types';
+
+/** 查询本租户审计日志(分页)。 */
+export const getAuditLogList = (params: TenantAuditLogListRequest) =>
+  request<TenantAuditLogListRequest, TenantAuditLogListResponse>({
+    url: '/tenant/audit-logs',
+    method: 'GET',
+    params,
+    headers: getAuthHeaders(),
+  });
diff --git a/src/pages/platform/audit-logs/types.ts b/src/pages/platform/audit-logs/types.ts
new file mode 100644
index 0000000..258d453
--- /dev/null
+++ b/src/pages/platform/audit-logs/types.ts
@@ -0,0 +1,70 @@
+import type { PageResult } from '@/types/api';
+
+/** 审计日志结果状态。 */
+export type AuditLogResultStatus = 'SUCCESS' | 'FAILURE';
+
+/** 审计日志(AuditLogVO)。 */
+export interface AuditLogVO {
+  logId: number;
+  tenantId?: string;
+  operatorId?: number;
+  operatorName?: string;
+  operatorRole?: string;
+  /** 业务模块,如 EMPLOYEE / ORG / ROLE / CUSTOMER / FOLLOW_UP / TENANT / SYSTEM。 */
+  module?: string;
+  /** 操作动作,如 CREATE / UPDATE / DELETE / IMPORT。 */
+  action?: string;
+  operationDesc?: string;
+  entityType?: string;
+  entityId?: string;
+  entityName?: string;
+  changeDetail?: string;
+  requestIp?: string;
+  userAgent?: string;
+  requestPath?: string;
+  requestMethod?: string;
+  resultStatus?: AuditLogResultStatus;
+  errorMessage?: string;
+  createdAt?: string;
+}
+
+/** 审计日志分页查询参数(AuditLogPageQuery)。 */
+export interface AuditLogPageQuery {
+  /** 页码。 */
+  page?: number;
+  /** 每页条数。 */
+  size?: number;
+  /** 业务模块。 */
+  module?: string;
+  /** 操作动作。 */
+  action?: string;
+  /** 实体类型。 */
+  entityType?: string;
+  /** 结果状态。 */
+  resultStatus?: AuditLogResultStatus;
+  /** 操作人 ID。 */
+  operatorId?: number;
+  /** 开始时间(yyyy-MM-dd HH:mm)。 */
+  start?: string;
+  /** 结束时间(yyyy-MM-dd HH:mm)。 */
+  end?: string;
+}
+
+/** 审计日志列表请求参数。 */
+export interface TenantAuditLogListRequest extends AuditLogPageQuery {}
+
+/** 审计日志列表响应数据(ResultPageResultAuditLogVO 的 data)。 */
+export type TenantAuditLogListResponse = PageResult<AuditLogVO>;
+
+/** ProTable 搜索表单参数,字段与请求参数中可筛选的字段保持一致。 */
+export interface AuditLogListQuery {
+  current?: number;
+  pageSize?: number;
+  module?: string;
+  action?: string;
+  entityType?: string;
+  resultStatus?: AuditLogResultStatus;
+  operatorId?: number;
+  start?: string;
+  end?: string;
+}
diff --git a/src/pages/system/departments/columns.tsx b/src/pages/system/departments/columns.tsx
index ba67b61..766c638 100644
--- a/src/pages/system/departments/columns.tsx
+++ b/src/pages/system/departments/columns.tsx
@@ -101,7 +101,13 @@
         <Button type="link" size="small" onClick={() => options.onCreateChild(record)}>
           新增子部门
         </Button>
-        <Button type="link" size="small" onClick={() => options.onEdit(record)}>
+        {/* 总部为租户根组织,不允许编辑 */}
+        <Button
+          type="link"
+          size="small"
+          disabled={record.orgType === 'HQ'}
+          onClick={() => options.onEdit(record)}
+        >
           编辑
         </Button>
         <Button type="link" size="small" onClick={() => options.onToggle(record)}>
diff --git a/src/pages/system/departments/components/DepartmentFormDrawer.tsx b/src/pages/system/departments/components/DepartmentFormDrawer.tsx
index b159038..126ee40 100644
--- a/src/pages/system/departments/components/DepartmentFormDrawer.tsx
+++ b/src/pages/system/departments/components/DepartmentFormDrawer.tsx
@@ -1,4 +1,5 @@
 import { useCallback, useEffect, useMemo, useState } from 'react';
+import { ProFormSelect, ProFormTreeSelect } from '@ant-design/pro-components';
 import {
   App as AntdApp,
   Button,
@@ -8,9 +9,8 @@
   InputNumber,
   Select,
   Space,
-  TreeSelect,
 } from 'antd';
-import { createOrg, updateOrg } from '../server';
+import { createOrg, getLeaderDetail, getLeaderOptions, updateOrg } from '../server';
 import type { DepartmentFormValues, OrgVO } from '../types';
 import { toOrgTreeSelectData } from '../utils';
 
@@ -23,8 +23,6 @@
   parentId?: number;
   /** 组织树,用于上级组织选择。 */
   orgTree: OrgVO[];
-  /** 负责人员工下拉选项。 */
-  leaderOptions: { label: string; value: number }[];
   onClose: () => void;
   onSuccess: () => void;
 }
@@ -35,7 +33,6 @@
   record,
   parentId,
   orgTree,
-  leaderOptions,
   onClose,
   onSuccess,
 }: DepartmentFormDrawerProps) => {
@@ -43,10 +40,46 @@
   const [submitting, setSubmitting] = useState(false);
   const { message } = AntdApp.useApp();
   const isEdit = mode === 'edit';
+  /** 新增入口只有「新增子部门」,上级组织已确定,不允许再选择。 */
+  const parentDisabled = !isEdit;
 
   const parentOptions = useMemo(
     () => toOrgTreeSelectData(orgTree, isEdit ? record?.id : undefined),
     [isEdit, orgTree, record],
+  );
+
+  /** 负责人远程搜索:按姓名 / 手机号模糊查询员工。 */
+  const requestLeaderOptions = useCallback(
+    async ({ keyWords }: { keyWords?: string }) => {
+      const keyword = keyWords?.trim();
+      const result = await getLeaderOptions({
+        page: 1,
+        size: 50,
+        keyword: keyword || undefined,
+      });
+      const options = (result.records ?? []).map((employee) => ({
+        label: employee.name,
+        value: employee.id,
+      }));
+
+      // 编辑时把当前负责人补进首屏选项,避免回填后显示为员工 ID。
+      const currentLeaderId = isEdit ? record?.leaderId : undefined;
+      if (
+        !keyword &&
+        currentLeaderId &&
+        !options.some((option) => option.value === currentLeaderId)
+      ) {
+        try {
+          const leader = await getLeaderDetail({ id: currentLeaderId });
+          options.unshift({ label: leader.name, value: leader.id });
+        } catch {
+          // 错误已由 request 统一提示
+        }
+      }
+
+      return options;
+    },
+    [isEdit, record],
   );
 
   useEffect(() => {
@@ -97,7 +130,7 @@
     }
   }, [form, isEdit, message, onSuccess, record]);
 
-  const title = isEdit ? '编辑部门' : parentId ? '新增子部门' : '新增部门';
+  const title = isEdit ? '编辑部门' : '新增子部门';
 
   return (
     <Drawer
@@ -140,26 +173,31 @@
             ]}
           />
         </Form.Item>
-        <Form.Item name="parentId" label="上级组织">
-          <TreeSelect
-            treeData={parentOptions}
-            treeDefaultExpandAll
-            allowClear
-            showSearch
-            treeNodeFilterProp="title"
-            placeholder="不选则作为根组织"
-            style={{ width: '100%' }}
-          />
-        </Form.Item>
-        <Form.Item name="leaderId" label="负责人">
-          <Select
-            options={leaderOptions}
-            showSearch
-            optionFilterProp="label"
-            allowClear
-            placeholder="请选择负责人"
-          />
-        </Form.Item>
+        <ProFormTreeSelect
+          name="parentId"
+          label="上级组织"
+          placeholder="不选则作为根组织"
+          disabled={parentDisabled}
+          fieldProps={{
+            treeData: parentOptions,
+            treeDefaultExpandAll: true,
+            allowClear: true,
+            showSearch: true,
+            treeNodeFilterProp: 'title',
+            style: { width: '100%' },
+          }}
+        />
+        <ProFormSelect
+          name="leaderId"
+          label="负责人"
+          rules={isEdit ? undefined : [{ required: true, message: '请选择负责人' }]}
+          placeholder="请输入姓名搜索负责人"
+          showSearch
+          allowClear
+          debounceTime={300}
+          request={requestLeaderOptions}
+          fieldProps={{ filterOption: false, style: { width: '100%' } }}
+        />
         <Form.Item name="region" label="所在地区">
           <Input placeholder="请输入所在地区" maxLength={50} />
         </Form.Item>
diff --git a/src/pages/system/departments/index.tsx b/src/pages/system/departments/index.tsx
index c62577d..781f807 100644
--- a/src/pages/system/departments/index.tsx
+++ b/src/pages/system/departments/index.tsx
@@ -1,9 +1,8 @@
 import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
 import type { Key } from 'react';
-import { PlusOutlined } from '@ant-design/icons';
 import { ProTable } from '@ant-design/pro-components';
 import type { ActionType } from '@ant-design/pro-components';
-import { App as AntdApp, Button } from 'antd';
+import { App as AntdApp } from 'antd';
 import { createDepartmentColumns } from './columns';
 import DepartmentFormDrawer from './components/DepartmentFormDrawer';
 import { deleteOrg, getLeaderOptions, getOrgTree, toggleOrgStatus } from './server';
@@ -26,7 +25,7 @@
 
     const loadLeaders = async () => {
       try {
-        const result = await getLeaderOptions();
+        const result = await getLeaderOptions({ page: 1, size: 200 });
         if (!active) return;
 
         setLeaderOptions(
@@ -59,13 +58,6 @@
 
     const data = filterOrgTree(list, params);
     return { data, total: data.length, success: true };
-  }, []);
-
-  const openCreateRoot = useCallback(() => {
-    setActiveOrg(null);
-    setParentId(undefined);
-    setFormMode('create');
-    setFormOpen(true);
   }, []);
 
   const openCreateChild = useCallback((record: OrgVO) => {
@@ -155,11 +147,6 @@
           expandedRowKeys,
           onExpandedRowsChange: (keys) => setExpandedRowKeys([...keys]),
         }}
-        toolBarRender={() => [
-          <Button key="create" type="primary" icon={<PlusOutlined />} onClick={openCreateRoot}>
-            新增部门
-          </Button>,
-        ]}
         request={fetchDepartments}
       />
 
@@ -169,7 +156,6 @@
         record={activeOrg}
         parentId={parentId}
         orgTree={orgTree}
-        leaderOptions={leaderOptions}
         onClose={closeForm}
         onSuccess={handleFormSuccess}
       />
diff --git a/src/pages/system/departments/server.ts b/src/pages/system/departments/server.ts
index cf003c6..64594ef 100644
--- a/src/pages/system/departments/server.ts
+++ b/src/pages/system/departments/server.ts
@@ -1,6 +1,8 @@
 import { getAuthHeaders } from '@/api/authHeaders';
 import { request } from '@/api/request';
 import type {
+  DepartmentLeaderDetailRequest,
+  DepartmentLeaderDetailResponse,
   TenantEmployeeOptionListRequest,
   TenantEmployeeOptionListResponse,
   TenantOrgCreateRequest,
@@ -59,11 +61,19 @@
     headers: getAuthHeaders(),
   });
 
-/** 员工列表,用于负责人下拉选项。 */
-export const getLeaderOptions = (params: TenantEmployeeOptionListRequest = { page: 1, size: 200 }) =>
+/** 员工列表,用于负责人下拉远程搜索。 */
+export const getLeaderOptions = (params: TenantEmployeeOptionListRequest) =>
   request<TenantEmployeeOptionListRequest, TenantEmployeeOptionListResponse>({
     url: '/tenant/employees',
     method: 'GET',
     params,
     headers: getAuthHeaders(),
   });
+
+/** 员工详情,用于编辑时回填负责人姓名。 */
+export const getLeaderDetail = ({ id }: DepartmentLeaderDetailRequest) =>
+  request<DepartmentLeaderDetailRequest, DepartmentLeaderDetailResponse>({
+    url: `/tenant/employees/${id}`,
+    method: 'GET',
+    headers: getAuthHeaders(),
+  });
diff --git a/src/pages/system/departments/types.ts b/src/pages/system/departments/types.ts
index c56653a..8f53413 100644
--- a/src/pages/system/departments/types.ts
+++ b/src/pages/system/departments/types.ts
@@ -72,10 +72,12 @@
 /** PUT /tenant/organizations/{id}/toggle-status 响应数据(ResultVoid 的 data)。 */
 export type TenantOrgToggleStatusResponse = void;
 
-/** GET /tenant/employees 请求参数(仅用于负责人下拉)。 */
+/** GET /tenant/employees 请求参数(负责人下拉远程搜索)。 */
 export interface TenantEmployeeOptionListRequest {
   page?: number;
   size?: number;
+  /** 姓名 / 手机号模糊搜索。 */
+  keyword?: string;
 }
 
 /** 负责人选项(EmployeeVO 子集)。 */
@@ -88,6 +90,15 @@
 /** GET /tenant/employees 响应数据(ResultPageResultEmployeeVO 的 data)。 */
 export type TenantEmployeeOptionListResponse = PageResult<DepartmentLeaderOption>;
 
+/** GET /tenant/employees/{id} 请求参数(编辑时回填负责人姓名)。 */
+export interface DepartmentLeaderDetailRequest {
+  /** 员工 ID(路径参数)。 */
+  id: number;
+}
+
+/** GET /tenant/employees/{id} 响应数据(EmployeeVO 子集)。 */
+export type DepartmentLeaderDetailResponse = DepartmentLeaderOption;
+
 /** ProTable 筛选参数:组织树接口没有查询参数,这些字段仅用于前端本地过滤。 */
 export interface DepartmentListQuery {
   orgName?: string;
diff --git a/src/pages/system/users/columns.tsx b/src/pages/system/users/columns.tsx
index 8640ce5..75f4180 100644
--- a/src/pages/system/users/columns.tsx
+++ b/src/pages/system/users/columns.tsx
@@ -7,10 +7,13 @@
   DISABLED: { text: '禁用', color: 'default' },
 };
 
-/** 兼容后端返回未知或小写状态,避免直接索引得到 undefined。 */
-export const getEmployeeStatusMeta = (status?: string) => {
-  const key = String(status ?? '').toUpperCase() as EmployeeStatus;
-  return employeeStatusMeta[key] ?? { text: status || '-', color: 'default' };
+/** 兼容后端返回 null、小写或未知状态:ACTIVE 与空值均为启用,仅 DISABLED 为禁用。 */
+export const getEmployeeStatusMeta = (status?: string | null) => {
+  const key = String(status ?? '').toUpperCase();
+
+  if (!key || key === 'ACTIVE') return employeeStatusMeta.ACTIVE;
+  if (key === 'DISABLED') return employeeStatusMeta.DISABLED;
+  return { text: status || '-', color: 'default' };
 };
 
 export const createUserColumns = (options: {
diff --git a/src/pages/system/users/components/UserFormDrawer.tsx b/src/pages/system/users/components/UserFormDrawer.tsx
index b71a3f6..4c5ffd2 100644
--- a/src/pages/system/users/components/UserFormDrawer.tsx
+++ b/src/pages/system/users/components/UserFormDrawer.tsx
@@ -1,13 +1,12 @@
 import { useCallback, useEffect, useMemo, useState } from 'react';
+import { ProFormCascader, ProFormSelect } from '@ant-design/pro-components';
 import {
   App as AntdApp,
   Button,
-  Cascader,
   Drawer,
   Form,
   Input,
   InputNumber,
-  Select,
   Space,
   Spin,
 } from 'antd';
@@ -24,6 +23,10 @@
   label: string;
   children?: OrgCascaderOption[];
 }
+
+/** 去掉最外层公司节点,只保留公司下的组织用于选择。 */
+const getSelectableOrgNodes = (nodes: OrgTreeNode[]): OrgTreeNode[] =>
+  nodes.flatMap((node) => (node.children?.length ? node.children : [node]));
 
 /** 组织树转为级联选择选项,按层级展示。 */
 const toCascaderOptions = (nodes: OrgTreeNode[]): OrgCascaderOption[] =>
@@ -65,7 +68,7 @@
 ): EmployeeFormValues => ({
   name: employee.name,
   phone: employee.phone,
-  orgPath: findOrgPath(orgTree, employee.orgId) ?? [],
+  orgPath: findOrgPath(getSelectableOrgNodes(orgTree), employee.orgId) ?? [],
   roleId: employee.roleId,
   position: employee.position,
   email: employee.email,
@@ -87,7 +90,10 @@
   const [loading, setLoading] = useState(false);
   const { message } = AntdApp.useApp();
   const isDetail = mode === 'detail';
-  const orgCascaderOptions = useMemo(() => toCascaderOptions(orgTree), [orgTree]);
+  const orgCascaderOptions = useMemo(
+    () => toCascaderOptions(getSelectableOrgNodes(orgTree)),
+    [orgTree],
+  );
 
   useEffect(() => {
     if (!open) return;
@@ -196,30 +202,39 @@
           >
             <Input placeholder="请输入手机号" maxLength={11} disabled={isDetail} />
           </Form.Item>
-          <Form.Item
+          <ProFormCascader
             name="orgPath"
             label="所属组织"
             rules={[{ required: true, message: '请选择所属组织' }]}
-          >
-            <Cascader
-              options={orgCascaderOptions}
-              changeOnSelect
-              showSearch
-              placeholder="请选择所属组织"
-              disabled={isDetail}
-              style={{ width: '100%' }}
-            />
-          </Form.Item>
-          <Form.Item
+            placeholder="请选择所属组织"
+            disabled={isDetail}
+            fieldProps={{
+              options: orgCascaderOptions,
+              changeOnSelect: true,
+              showSearch: true,
+              style: { width: '100%' },
+            }}
+          />
+          <ProFormSelect
             name="roleId"
             label="角色"
             rules={[{ required: true, message: '请选择角色' }]}
+            placeholder="请选择角色"
+            disabled={isDetail}
+            showSearch
+            options={roleOptions}
+            fieldProps={{ optionFilterProp: 'label', style: { width: '100%' } }}
+          />
+          <Form.Item
+            name="holdLimit"
+            label="客户持有上限"
+            rules={[{ required: true, message: '请输入客户持有上限' }]}
           >
-            <Select
-              showSearch
-              optionFilterProp="label"
-              options={roleOptions}
-              placeholder="请选择角色"
+            <InputNumber
+              min={0}
+              precision={0}
+              placeholder="请输入客户持有上限"
+              style={{ width: '100%' }}
               disabled={isDetail}
             />
           </Form.Item>
@@ -228,14 +243,6 @@
           </Form.Item>
           <Form.Item name="region" label="负责区域">
             <Input placeholder="请输入负责区域" disabled={isDetail} />
-          </Form.Item>
-          <Form.Item name="holdLimit" label="客户持有上限">
-            <InputNumber
-              min={0}
-              precision={0}
-              style={{ width: '100%' }}
-              disabled={isDetail}
-            />
           </Form.Item>
           <Form.Item
             name="email"
diff --git a/src/router/routeConfig.tsx b/src/router/routeConfig.tsx
index 27ce482..dbb62a7 100644
--- a/src/router/routeConfig.tsx
+++ b/src/router/routeConfig.tsx
@@ -2,17 +2,20 @@
 import {
   ApartmentOutlined,
   AppstoreOutlined,
+  AuditOutlined,
   ContactsOutlined,
   DashboardOutlined,
   ProfileOutlined,
   SafetyCertificateOutlined,
   SafetyOutlined,
+  SettingOutlined,
   TeamOutlined,
   UnorderedListOutlined,
 } from '@ant-design/icons';
 import Orders from '@/pages/business/orders';
 import CustomerList from '@/pages/customers/list';
 import Dashboard from '@/pages/dashboard';
+import AuditLogs from '@/pages/platform/audit-logs';
 import Departments from '@/pages/system/departments';
 import Roles from '@/pages/system/roles';
 import Users from '@/pages/system/users';
@@ -90,6 +93,19 @@
       },
     ],
   },
+  {
+    path: '/platform',
+    title: '平台管理',
+    icon: <SettingOutlined />,
+    children: [
+      {
+        path: '/platform/audit-logs',
+        title: '审计日志',
+        icon: <AuditOutlined />,
+        element: <AuditLogs />,
+      },
+    ],
+  },
 ];
 
 /** 从路由树中找到某个路径的完整面包屑链路。 */

--
Gitblit v1.9.3