From 4a7848a4bec14542b8574d250684dd631a388c1a Mon Sep 17 00:00:00 2001
From: wsj <593216709@qq.com>
Date: Mon, 26 Oct 2026 23:27:46 +0800
Subject: [PATCH] feat: 用户管理接入员工管理接口

---
 src/types/api.ts                   |    8 
 src/pages/system/roles/columns.tsx |   24 
 src/pages/system/roles/index.tsx   |   10 
 src/pages/system/users/columns.tsx |  141 +++++++
 src/pages/system/users/types.ts    |  167 ++++++++
 src/pages/system/users/index.tsx   |  721 +++++++++++++++++++----------------
 src/pages/system/users/server.ts   |  112 +++++
 README.md                          |    2 
 src/pages/customers/list/types.ts  |   11 
 9 files changed, 854 insertions(+), 342 deletions(-)

diff --git a/README.md b/README.md
index 948fb19..b279907 100644
--- a/README.md
+++ b/README.md
@@ -100,7 +100,7 @@
 | 页面     | 路由                    | 说明                               |
 | -------- | ----------------------- | ---------------------------------- |
 | 工作台   | `/dashboard`            | 数据卡片、快捷入口、动态、最近订单 |
-| 用户管理 | `/system/users`         | 搜索、新增 / 编辑 / 删除用户       |
+| 用户管理 | `/system/users`         | 员工列表、详情、增删改、导入 / 导出 |
 | 角色管理 | `/system/roles`         | 角色列表、详情、启用 / 停用        |
 | 订单管理 | `/business/orders`      | 筛选、处理订单、导出               |
 | 客户列表 | `/customers/list`       | 接口数据,按归属销售员筛选         |
diff --git a/src/pages/customers/list/types.ts b/src/pages/customers/list/types.ts
index 9b98499..7851cda 100644
--- a/src/pages/customers/list/types.ts
+++ b/src/pages/customers/list/types.ts
@@ -1,3 +1,5 @@
+import type { PageResult } from '@/types/api';
+
 /** 客户类型。 */
 export type CustomerType = 'ENTERPRISE' | 'PERSONAL';
 
@@ -58,15 +60,6 @@
   dealAt?: string;
   lostAt?: string;
   lostReason?: string;
-}
-
-/** 通用分页响应(PageResult)。 */
-export interface PageResult<T> {
-  page: number;
-  size: number;
-  total: number;
-  totalPages: number;
-  records: T[];
 }
 
 /** 客户列表响应数据(ResultPageResultCustomerVO 的 data)。 */
diff --git a/src/pages/system/roles/columns.tsx b/src/pages/system/roles/columns.tsx
index 5253c0b..d7c6eb6 100644
--- a/src/pages/system/roles/columns.tsx
+++ b/src/pages/system/roles/columns.tsx
@@ -13,6 +13,17 @@
   DELETED: { text: '已删除', color: 'red' },
 };
 
+/** 兼容后端返回未知或小写枚举,避免直接索引得到 undefined。 */
+export const getRoleTypeMeta = (roleType?: string) => {
+  const key = String(roleType ?? '').toUpperCase() as RoleType;
+  return roleTypeMeta[key] ?? { text: roleType || '-', color: 'default' };
+};
+
+export const getRoleStatusMeta = (status?: string) => {
+  const key = String(status ?? '').toUpperCase() as RoleStatus;
+  return statusMeta[key] ?? { text: status || '-', color: 'default' };
+};
+
 export const createRoleColumns = (options: {
   onDetail: (record: RoleVO) => void;
   onToggle: (record: RoleVO) => void;
@@ -49,11 +60,14 @@
       DISABLED: { text: '禁用' },
       DELETED: { text: '已删除' },
     },
-    render: (_value, record) => (
-      <Tag color={statusMeta[record.status].color} bordered={false}>
-        {statusMeta[record.status].text}
-      </Tag>
-    ),
+    render: (_value, record) => {
+      const meta = getRoleStatusMeta(record.status);
+      return (
+        <Tag color={meta.color} bordered={false}>
+          {meta.text}
+        </Tag>
+      );
+    },
   },
   {
     title: '操作',
diff --git a/src/pages/system/roles/index.tsx b/src/pages/system/roles/index.tsx
index 694b1d6..28342f5 100644
--- a/src/pages/system/roles/index.tsx
+++ b/src/pages/system/roles/index.tsx
@@ -2,7 +2,7 @@
 import { ProTable } from '@ant-design/pro-components';
 import type { ActionType } from '@ant-design/pro-components';
 import { App as AntdApp, Descriptions, Drawer, Tag } from 'antd';
-import { createRoleColumns, roleTypeMeta, statusMeta } from './columns';
+import { createRoleColumns, getRoleStatusMeta, getRoleTypeMeta } from './columns';
 import { getRoleDetail, getRoleList, toggleRoleStatus } from './server';
 import type { RoleListQuery, RoleVO } from './types';
 
@@ -106,13 +106,13 @@
             <Descriptions.Item label="角色名称">{detail.roleName}</Descriptions.Item>
             <Descriptions.Item label="角色编码">{detail.roleCode}</Descriptions.Item>
             <Descriptions.Item label="角色类型">
-              <Tag color={roleTypeMeta[detail.roleType].color} bordered={false}>
-                {roleTypeMeta[detail.roleType].text}
+              <Tag color={getRoleTypeMeta(detail.roleType).color} bordered={false}>
+                {getRoleTypeMeta(detail.roleType).text}
               </Tag>
             </Descriptions.Item>
             <Descriptions.Item label="状态">
-              <Tag color={statusMeta[detail.status].color} bordered={false}>
-                {statusMeta[detail.status].text}
+              <Tag color={getRoleStatusMeta(detail.status).color} bordered={false}>
+                {getRoleStatusMeta(detail.status).text}
               </Tag>
             </Descriptions.Item>
             <Descriptions.Item label="描述">{detail.description || '-'}</Descriptions.Item>
diff --git a/src/pages/system/users/columns.tsx b/src/pages/system/users/columns.tsx
new file mode 100644
index 0000000..8640ce5
--- /dev/null
+++ b/src/pages/system/users/columns.tsx
@@ -0,0 +1,141 @@
+import { Avatar, Button, Space, Tag } from 'antd';
+import type { ProColumns } from '@ant-design/pro-components';
+import type { EmployeeStatus, EmployeeVO, SelectOption } from './types';
+
+export const employeeStatusMeta: Record<EmployeeStatus, { text: string; color: string }> = {
+  ACTIVE: { text: '启用', color: 'green' },
+  DISABLED: { text: '禁用', color: 'default' },
+};
+
+/** 兼容后端返回未知或小写状态,避免直接索引得到 undefined。 */
+export const getEmployeeStatusMeta = (status?: string) => {
+  const key = String(status ?? '').toUpperCase() as EmployeeStatus;
+  return employeeStatusMeta[key] ?? { text: status || '-', color: 'default' };
+};
+
+export const createUserColumns = (options: {
+  orgOptions: SelectOption[];
+  roleOptions: SelectOption[];
+  orgNameMap: Map<number, string>;
+  roleNameMap: Map<number, string>;
+  onDetail: (record: EmployeeVO) => void;
+  onEdit: (record: EmployeeVO) => void;
+  onDelete: (record: EmployeeVO) => void;
+}): ProColumns<EmployeeVO>[] => [
+  {
+    title: '员工',
+    dataIndex: 'name',
+    width: 220,
+    ellipsis: true,
+    fieldProps: { placeholder: '姓名 / 手机号' },
+    search: {
+      transform: (value) => ({ keyword: value }),
+    },
+    render: (_value, record) => (
+      <div className="user-cell">
+        <Avatar className="user-cell__avatar" src={record.avatarUrl} size={36}>
+          {record.name?.charAt(0)}
+        </Avatar>
+        <div className="user-cell__meta">
+          <span className="user-cell__name">{record.name}</span>
+          <span className="user-cell__username">{record.phone}</span>
+        </div>
+      </div>
+    ),
+  },
+  {
+    title: '所属组织',
+    dataIndex: 'orgId',
+    width: 170,
+    valueType: 'select',
+    fieldProps: {
+      options: options.orgOptions,
+      showSearch: true,
+      optionFilterProp: 'label',
+      allowClear: true,
+      placeholder: '请选择组织',
+    },
+    render: (_value, record) =>
+      options.orgNameMap.get(record.orgId) ?? (record.orgId ? `#${record.orgId}` : '-'),
+  },
+  {
+    title: '角色',
+    dataIndex: 'roleId',
+    width: 150,
+    valueType: 'select',
+    fieldProps: {
+      options: options.roleOptions,
+      showSearch: true,
+      optionFilterProp: 'label',
+      allowClear: true,
+      placeholder: '请选择角色',
+    },
+    render: (_value, record) =>
+      options.roleNameMap.get(record.roleId) ?? (record.roleId ? `#${record.roleId}` : '-'),
+  },
+  {
+    title: '职位',
+    dataIndex: 'position',
+    width: 120,
+    search: false,
+    render: (_value, record) => record.position || '-',
+  },
+  {
+    title: '负责区域',
+    dataIndex: 'region',
+    width: 120,
+    search: false,
+    render: (_value, record) => record.region || '-',
+  },
+  {
+    title: '客户上限',
+    dataIndex: 'holdLimit',
+    width: 100,
+    search: false,
+    render: (_value, record) => record.holdLimit ?? '-',
+  },
+  {
+    title: '状态',
+    dataIndex: 'status',
+    width: 100,
+    valueType: 'select',
+    valueEnum: {
+      ACTIVE: { text: '启用' },
+      DISABLED: { text: '禁用' },
+    },
+    render: (_value, record) => {
+      const meta = getEmployeeStatusMeta(record.status);
+      return (
+        <Tag color={meta.color} bordered={false}>
+          {meta.text}
+        </Tag>
+      );
+    },
+  },
+  {
+    title: '入职日期',
+    dataIndex: 'hireDate',
+    width: 130,
+    search: false,
+    render: (_value, record) => record.hireDate || '-',
+  },
+  {
+    title: '操作',
+    key: 'action',
+    valueType: 'option',
+    width: 180,
+    render: (_value, record) => (
+      <Space size={0}>
+        <Button type="link" size="small" onClick={() => options.onDetail(record)}>
+          详情
+        </Button>
+        <Button type="link" size="small" onClick={() => options.onEdit(record)}>
+          编辑
+        </Button>
+        <Button type="link" size="small" danger onClick={() => options.onDelete(record)}>
+          删除
+        </Button>
+      </Space>
+    ),
+  },
+];
diff --git a/src/pages/system/users/index.tsx b/src/pages/system/users/index.tsx
index 5b31a97..15711c8 100644
--- a/src/pages/system/users/index.tsx
+++ b/src/pages/system/users/index.tsx
@@ -1,373 +1,450 @@
-import { useMemo, useState } from 'react';
-import { PlusOutlined, SearchOutlined } from '@ant-design/icons';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import {
+  DownloadOutlined,
+  InboxOutlined,
+  PlusOutlined,
+  UploadOutlined,
+} from '@ant-design/icons';
+import { ProTable } from '@ant-design/pro-components';
+import type { ActionType } from '@ant-design/pro-components';
 import {
   App as AntdApp,
-  Avatar,
   Button,
-  Card,
+  Descriptions,
+  Drawer,
   Form,
   Input,
+  InputNumber,
   Modal,
   Select,
   Space,
-  Table,
   Tag,
+  Upload,
 } from 'antd';
-import type { TableProps } from 'antd';
-import dayjs from 'dayjs';
+import type { UploadFile } from 'antd';
+import { createUserColumns, getEmployeeStatusMeta } from './columns';
+import {
+  createEmployee,
+  deleteEmployee,
+  exportEmployees,
+  getEmployeeDetail,
+  getEmployeeList,
+  getOrgTree,
+  getRoleOptions,
+  importEmployees,
+  updateEmployee,
+} from './server';
+import type {
+  EmployeeFormValues,
+  EmployeeListQuery,
+  EmployeeVO,
+  OrgTreeNode,
+  SelectOption,
+  TenantEmployeeListRequest,
+} from './types';
 import './Users.scss';
 
-type UserStatus = 'active' | 'disabled';
-
-interface UserRecord {
-  key: number;
-  username: string;
-  nickname: string;
-  role: string;
-  status: UserStatus;
-  createdAt: string;
-}
-
-interface UserFormValues {
-  username: string;
-  nickname: string;
-  role: string;
-  status: UserStatus;
-}
-
-const roleOptions = [
-  { value: '超级管理员', label: '超级管理员' },
-  { value: '运营管理员', label: '运营管理员' },
-  { value: '销售专员', label: '销售专员' },
-  { value: '访客', label: '访客' },
-];
-
-const roleColor: Record<string, string> = {
-  超级管理员: 'purple',
-  运营管理员: 'blue',
-  销售专员: 'cyan',
-  访客: 'default',
-};
-
-const statusMeta: Record<UserStatus, { label: string; color: string }> = {
-  active: { label: '启用', color: 'green' },
-  disabled: { label: '禁用', color: 'default' },
-};
-
-const initialUsers: UserRecord[] = [
-  {
-    key: 1,
-    username: 'admin',
-    nickname: '陈一鸣',
-    role: '超级管理员',
-    status: 'active',
-    createdAt: '2026-01-08 09:30',
-  },
-  {
-    key: 2,
-    username: 'lin.xia',
-    nickname: '林夏',
-    role: '运营管理员',
-    status: 'active',
-    createdAt: '2026-03-12 14:20',
-  },
-  {
-    key: 3,
-    username: 'zhou.yang',
-    nickname: '周洋',
-    role: '销售专员',
-    status: 'active',
-    createdAt: '2026-04-02 10:05',
-  },
-  {
-    key: 4,
-    username: 'su.wen',
-    nickname: '苏雯',
-    role: '销售专员',
-    status: 'active',
-    createdAt: '2026-05-18 16:42',
-  },
-  {
-    key: 5,
-    username: 'he.jun',
-    nickname: '何俊',
-    role: '运营管理员',
-    status: 'disabled',
-    createdAt: '2026-06-09 11:18',
-  },
-  {
-    key: 6,
-    username: 'yang.li',
-    nickname: '杨莉',
-    role: '销售专员',
-    status: 'active',
-    createdAt: '2026-07-21 15:54',
-  },
-  {
-    key: 7,
-    username: 'wang.qi',
-    nickname: '王琪',
-    role: '访客',
-    status: 'disabled',
-    createdAt: '2026-08-03 09:12',
-  },
-  {
-    key: 8,
-    username: 'zhao.min',
-    nickname: '赵敏',
-    role: '销售专员',
-    status: 'active',
-    createdAt: '2026-09-15 13:26',
-  },
-];
+const flattenOrgOptions = (nodes: OrgTreeNode[]): SelectOption[] =>
+  nodes.flatMap((node) => [
+    { label: node.orgName, value: node.id },
+    ...flattenOrgOptions(node.children ?? []),
+  ]);
 
 const Users = () => {
-  const [users, setUsers] = useState<UserRecord[]>(initialUsers);
-  const [keyword, setKeyword] = useState('');
-  const [status, setStatus] = useState<UserStatus | 'all'>('all');
-  const [isModalOpen, setIsModalOpen] = useState(false);
-  const [editingUser, setEditingUser] = useState<UserRecord | null>(null);
-  const [form] = Form.useForm<UserFormValues>();
+  const actionRef = useRef<ActionType>();
+  const lastQueryRef = useRef<TenantEmployeeListRequest>({});
   const { message, modal } = AntdApp.useApp();
+  const [form] = Form.useForm<EmployeeFormValues>();
+  const [orgOptions, setOrgOptions] = useState<SelectOption[]>([]);
+  const [roleOptions, setRoleOptions] = useState<SelectOption[]>([]);
+  const [formOpen, setFormOpen] = useState(false);
+  const [editing, setEditing] = useState<EmployeeVO | null>(null);
+  const [submitting, setSubmitting] = useState(false);
+  const [detail, setDetail] = useState<EmployeeVO | null>(null);
+  const [detailOpen, setDetailOpen] = useState(false);
+  const [detailLoading, setDetailLoading] = useState(false);
+  const [importOpen, setImportOpen] = useState(false);
+  const [importFileList, setImportFileList] = useState<UploadFile[]>([]);
+  const [importing, setImporting] = useState(false);
+  const [exporting, setExporting] = useState(false);
 
-  const filteredUsers = useMemo(() => {
-    const normalizedKeyword = keyword.trim().toLowerCase();
-    return users.filter((user) => {
-      const matchesKeyword =
-        !normalizedKeyword ||
-        user.username.toLowerCase().includes(normalizedKeyword) ||
-        user.nickname.toLowerCase().includes(normalizedKeyword);
-      const matchesStatus = status === 'all' || user.status === status;
-      return matchesKeyword && matchesStatus;
-    });
-  }, [users, keyword, status]);
+  useEffect(() => {
+    let active = true;
 
-  const openCreateModal = () => {
-    setEditingUser(null);
+    const loadOptions = async () => {
+      try {
+        const [orgs, roles] = await Promise.all([getOrgTree(), getRoleOptions()]);
+        if (!active) return;
+
+        setOrgOptions(flattenOrgOptions(orgs));
+        setRoleOptions(roles.map((role) => ({ label: role.roleName, value: role.id })));
+      } catch {
+        // 错误已由 request 统一提示
+      }
+    };
+
+    void loadOptions();
+
+    return () => {
+      active = false;
+    };
+  }, []);
+
+  const orgNameMap = useMemo(
+    () => new Map(orgOptions.map((option) => [option.value, option.label])),
+    [orgOptions],
+  );
+
+  const roleNameMap = useMemo(
+    () => new Map(roleOptions.map((option) => [option.value, option.label])),
+    [roleOptions],
+  );
+
+  const openCreate = useCallback(() => {
+    setEditing(null);
     form.resetFields();
-    form.setFieldsValue({ status: 'active', role: '销售专员' });
-    setIsModalOpen(true);
-  };
+    setFormOpen(true);
+  }, [form]);
 
-  const openEditModal = (record: UserRecord) => {
-    setEditingUser(record);
-    form.setFieldsValue({
-      username: record.username,
-      nickname: record.nickname,
-      role: record.role,
-      status: record.status,
-    });
-    setIsModalOpen(true);
-  };
+  const closeForm = useCallback(() => {
+    setFormOpen(false);
+    setEditing(null);
+  }, []);
 
-  const closeModal = () => {
-    setIsModalOpen(false);
-    form.resetFields();
-  };
+  const openEdit = useCallback(
+    (record: EmployeeVO) => {
+      setEditing(record);
+      form.setFieldsValue({
+        name: record.name,
+        phone: record.phone,
+        orgId: record.orgId,
+        roleId: record.roleId,
+        position: record.position,
+        email: record.email,
+        holdLimit: record.holdLimit,
+        region: record.region,
+      });
+      setFormOpen(true);
+    },
+    [form],
+  );
 
-  const handleSubmit = async () => {
+  const handleDetail = useCallback(async (record: EmployeeVO) => {
+    setDetail(null);
+    setDetailOpen(true);
+    setDetailLoading(true);
+
     try {
-      const values = await form.validateFields();
-      const duplicate = users.some(
-        (user) => user.username === values.username && user.key !== editingUser?.key,
-      );
-      if (duplicate) {
-        message.error('用户名已存在,请更换一个');
-        return;
-      }
-
-      if (editingUser) {
-        setUsers((prev) =>
-          prev.map((user) =>
-            user.key === editingUser.key ? { ...user, ...values } : user,
-          ),
-        );
-        message.success('用户信息已更新');
-      } else {
-        setUsers((prev) => [
-          {
-            key: Date.now(),
-            ...values,
-            createdAt: dayjs().format('YYYY-MM-DD HH:mm'),
-          },
-          ...prev,
-        ]);
-        message.success('用户创建成功');
-      }
-      closeModal();
+      const data = await getEmployeeDetail({ id: record.id });
+      setDetail(data);
     } catch {
-      // 表单校验失败时由 antd 自动展示错误信息
+      // 错误已由 request 统一提示
+      setDetailOpen(false);
+    } finally {
+      setDetailLoading(false);
     }
-  };
+  }, []);
 
-  const handleDelete = (record: UserRecord) => {
-    modal.confirm({
-      title: '删除用户',
-      content: `确定要删除用户「${record.nickname}」吗?删除后不可恢复。`,
-      okText: '删除',
-      cancelText: '取消',
-      okButtonProps: { danger: true },
-      onOk: () => {
-        setUsers((prev) => prev.filter((user) => user.key !== record.key));
-        message.success('用户已删除');
-      },
-    });
-  };
+  const handleDelete = useCallback(
+    (record: EmployeeVO) => {
+      modal.confirm({
+        title: '删除员工',
+        content: `确定要删除员工「${record.name}」吗?删除后不可恢复。`,
+        okText: '删除',
+        cancelText: '取消',
+        okButtonProps: { danger: true },
+        onOk: async () => {
+          await deleteEmployee({ id: record.id });
+          message.success('员工已删除');
+          actionRef.current?.reload();
+        },
+      });
+    },
+    [message, modal],
+  );
 
-  const columns: TableProps<UserRecord>['columns'] = [
-    {
-      title: '用户',
-      dataIndex: 'nickname',
-      width: 220,
-      render: (_value, record) => (
-        <div className="user-cell">
-          <Avatar className="user-cell__avatar">{record.nickname.charAt(0)}</Avatar>
-          <div className="user-cell__meta">
-            <span className="user-cell__name">{record.nickname}</span>
-            <span className="user-cell__username">@{record.username}</span>
-          </div>
-        </div>
-      ),
-    },
-    {
-      title: '角色',
-      dataIndex: 'role',
-      width: 140,
-      render: (value: string) => (
-        <Tag color={roleColor[value] ?? 'blue'} bordered={false}>
-          {value}
-        </Tag>
-      ),
-    },
-    {
-      title: '状态',
-      dataIndex: 'status',
-      width: 100,
-      render: (value: UserStatus) => (
-        <Tag color={statusMeta[value].color} bordered={false}>
-          {statusMeta[value].label}
-        </Tag>
-      ),
-    },
-    { title: '创建时间', dataIndex: 'createdAt', width: 170 },
-    {
-      title: '操作',
-      key: 'action',
-      width: 150,
-      render: (_value, record) => (
-        <Space size={0}>
-          <Button type="link" size="small" onClick={() => openEditModal(record)}>
-            编辑
-          </Button>
-          <Button type="link" size="small" danger onClick={() => handleDelete(record)}>
-            删除
-          </Button>
-        </Space>
-      ),
-    },
-  ];
+  const handleSubmit = useCallback(async () => {
+    let values: EmployeeFormValues;
+    try {
+      values = await form.validateFields();
+    } catch {
+      return;
+    }
+
+    setSubmitting(true);
+    try {
+      if (editing) {
+        await updateEmployee({ id: editing.id, ...values });
+        message.success('员工信息已更新');
+      } else {
+        await createEmployee(values);
+        message.success('员工创建成功');
+      }
+
+      setFormOpen(false);
+      setEditing(null);
+      form.resetFields();
+      actionRef.current?.reload();
+    } catch {
+      // 错误已由 request 统一提示
+    } finally {
+      setSubmitting(false);
+    }
+  }, [editing, form, message]);
+
+  const handleExport = useCallback(async () => {
+    setExporting(true);
+    try {
+      const blob = await exportEmployees({ orgId: lastQueryRef.current.orgId });
+      const url = URL.createObjectURL(blob);
+      const link = document.createElement('a');
+      link.href = url;
+      link.download = 'employees.xlsx';
+      document.body.appendChild(link);
+      link.click();
+      document.body.removeChild(link);
+      window.setTimeout(() => URL.revokeObjectURL(url), 0);
+      message.success('导出成功');
+    } catch {
+      // 错误已由 request 统一提示
+    } finally {
+      setExporting(false);
+    }
+  }, [message]);
+
+  const handleImport = useCallback(async () => {
+    const file = importFileList[0]?.originFileObj;
+    if (!file) {
+      message.warning('请先选择要导入的 Excel 文件');
+      return;
+    }
+
+    setImporting(true);
+    try {
+      const count = await importEmployees({ file });
+      message.success(`成功导入 ${count} 条员工`);
+      setImportOpen(false);
+      setImportFileList([]);
+      actionRef.current?.reload();
+    } catch {
+      // 错误已由 request 统一提示
+    } finally {
+      setImporting(false);
+    }
+  }, [importFileList, message]);
+
+  const columns = useMemo(
+    () =>
+      createUserColumns({
+        orgOptions,
+        roleOptions,
+        orgNameMap,
+        roleNameMap,
+        onDetail: handleDetail,
+        onEdit: openEdit,
+        onDelete: handleDelete,
+      }),
+    [orgOptions, roleOptions, orgNameMap, roleNameMap, handleDetail, openEdit, handleDelete],
+  );
 
   return (
     <div className="users-page">
-      <div className="page-header">
-        <div>
-          <h1 className="page-header__title">用户管理</h1>
-          <p className="page-header__desc">
-            维护平台账号、角色与启用状态。
-          </p>
-        </div>
-        <div className="page-header__actions">
-          <Button type="primary" icon={<PlusOutlined />} onClick={openCreateModal}>
-            新增用户
-          </Button>
-        </div>
-      </div>
+      <ProTable<EmployeeVO, EmployeeListQuery>
+        rowKey="id"
+        actionRef={actionRef}
+        columns={columns}
+        cardProps={{ className: 'table-card' }}
+        search={{ labelWidth: 'auto', defaultCollapsed: false }}
+        scroll={{ x: 1290 }}
+        options={false}
+        pagination={{
+          defaultPageSize: 10,
+          showSizeChanger: true,
+          hideOnSinglePage: false,
+          showTotal: (total) => `共 ${total} 条`,
+        }}
+        toolBarRender={() => [
+          <Button
+            key="import"
+            icon={<UploadOutlined />}
+            onClick={() => {
+              setImportFileList([]);
+              setImportOpen(true);
+            }}
+          >
+            导入
+          </Button>,
+          <Button
+            key="export"
+            icon={<DownloadOutlined />}
+            loading={exporting}
+            onClick={handleExport}
+          >
+            导出
+          </Button>,
+          <Button key="create" type="primary" icon={<PlusOutlined />} onClick={openCreate}>
+            新增员工
+          </Button>,
+        ]}
+        request={async (params) => {
+          const query: TenantEmployeeListRequest = {
+            page: params.current,
+            size: params.pageSize,
+            keyword: params.keyword?.trim() || undefined,
+            status: params.status,
+            orgId: params.orgId,
+            roleId: params.roleId,
+          };
+          lastQueryRef.current = query;
 
-      <Card className="filter-card" variant="borderless">
-        <div className="page-toolbar">
-          <Input
-            allowClear
-            prefix={<SearchOutlined />}
-            placeholder="搜索用户名 / 昵称"
-            value={keyword}
-            onChange={(event) => setKeyword(event.target.value)}
-            style={{ width: 260 }}
-          />
-          <Select<UserStatus | 'all'>
-            value={status}
-            onChange={setStatus}
-            style={{ width: 150 }}
-            options={[
-              { value: 'all', label: '全部状态' },
-              { value: 'active', label: '启用' },
-              { value: 'disabled', label: '禁用' },
-            ]}
-          />
-          <span className="text-muted">共 {filteredUsers.length} 条记录</span>
-        </div>
-      </Card>
+          const result = await getEmployeeList(query);
 
-      <Card className="table-card" variant="borderless">
-        <Table<UserRecord>
-          rowKey="key"
-          columns={columns}
-          dataSource={filteredUsers}
-          scroll={{ x: 820 }}
-          pagination={{
-            pageSize: 8,
-            showSizeChanger: false,
-            showTotal: (total) => `共 ${total} 条`,
-          }}
-        />
-      </Card>
+          return {
+            data: result.records ?? [],
+            total: result.total ?? 0,
+            success: true,
+          };
+        }}
+      />
 
-      <Modal
-        title={editingUser ? '编辑用户' : '新增用户'}
-        open={isModalOpen}
-        onOk={handleSubmit}
-        onCancel={closeModal}
-        okText={editingUser ? '保存' : '创建'}
-        cancelText="取消"
+      <Drawer
+        title={editing ? '编辑员工' : '新增员工'}
+        width={420}
+        open={formOpen}
         forceRender
+        onClose={closeForm}
+        footer={
+          <Space style={{ display: 'flex', justifyContent: 'flex-end' }}>
+            <Button onClick={closeForm}>取消</Button>
+            <Button type="primary" loading={submitting} onClick={handleSubmit}>
+              {editing ? '保存' : '创建'}
+            </Button>
+          </Space>
+        }
       >
         <Form form={form} layout="vertical" preserve={false}>
           <Form.Item
-            name="username"
-            label="用户名"
-            rules={[
-              { required: true, message: '请输入用户名' },
-              { pattern: /^[a-zA-Z][a-zA-Z0-9._-]{2,19}$/, message: '3-20 位,字母开头' },
-            ]}
-          >
-            <Input placeholder="请输入登录用户名" disabled={Boolean(editingUser)} />
-          </Form.Item>
-          <Form.Item
-            name="nickname"
+            name="name"
             label="姓名"
             rules={[{ required: true, message: '请输入姓名' }]}
           >
-            <Input placeholder="请输入用户姓名" />
+            <Input placeholder="请输入员工姓名" />
           </Form.Item>
           <Form.Item
-            name="role"
+            name="phone"
+            label="手机号"
+            rules={[
+              { required: true, message: '请输入手机号' },
+              { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' },
+            ]}
+          >
+            <Input placeholder="请输入手机号" maxLength={11} />
+          </Form.Item>
+          <Form.Item
+            name="orgId"
+            label="所属组织"
+            rules={[{ required: true, message: '请选择所属组织' }]}
+          >
+            <Select
+              showSearch
+              optionFilterProp="label"
+              options={orgOptions}
+              placeholder="请选择所属组织"
+            />
+          </Form.Item>
+          <Form.Item
+            name="roleId"
             label="角色"
             rules={[{ required: true, message: '请选择角色' }]}
           >
-            <Select placeholder="请选择角色" options={roleOptions} />
-          </Form.Item>
-          <Form.Item
-            name="status"
-            label="状态"
-            rules={[{ required: true, message: '请选择状态' }]}
-          >
             <Select
-              options={[
-                { value: 'active', label: '启用' },
-                { value: 'disabled', label: '禁用' },
-              ]}
+              showSearch
+              optionFilterProp="label"
+              options={roleOptions}
+              placeholder="请选择角色"
             />
           </Form.Item>
+          <Form.Item name="position" label="职位">
+            <Input placeholder="请输入职位" />
+          </Form.Item>
+          <Form.Item name="region" label="负责区域">
+            <Input placeholder="请输入负责区域" />
+          </Form.Item>
+          <Form.Item name="holdLimit" label="客户持有上限">
+            <InputNumber min={0} precision={0} style={{ width: '100%' }} />
+          </Form.Item>
+          <Form.Item
+            name="email"
+            label="邮箱"
+            rules={[{ type: 'email', message: '请输入正确的邮箱' }]}
+          >
+            <Input placeholder="请输入邮箱" />
+          </Form.Item>
         </Form>
+      </Drawer>
+
+      <Drawer
+        title="员工详情"
+        width={480}
+        open={detailOpen}
+        loading={detailLoading}
+        onClose={() => {
+          setDetailOpen(false);
+          setDetail(null);
+        }}
+      >
+        {detail && (
+          <Descriptions column={1} bordered size="small">
+            <Descriptions.Item label="姓名">{detail.name}</Descriptions.Item>
+            <Descriptions.Item label="手机号">{detail.phone}</Descriptions.Item>
+            <Descriptions.Item label="所属组织">
+              {orgNameMap.get(detail.orgId) ?? `#${detail.orgId}`}
+            </Descriptions.Item>
+            <Descriptions.Item label="角色">
+              {roleNameMap.get(detail.roleId) ?? `#${detail.roleId}`}
+            </Descriptions.Item>
+            <Descriptions.Item label="职位">{detail.position || '-'}</Descriptions.Item>
+            <Descriptions.Item label="邮箱">{detail.email || '-'}</Descriptions.Item>
+            <Descriptions.Item label="负责区域">{detail.region || '-'}</Descriptions.Item>
+            <Descriptions.Item label="客户持有上限">
+              {detail.holdLimit ?? '-'}
+            </Descriptions.Item>
+            <Descriptions.Item label="状态">
+              <Tag color={getEmployeeStatusMeta(detail.status).color} bordered={false}>
+                {getEmployeeStatusMeta(detail.status).text}
+              </Tag>
+            </Descriptions.Item>
+            <Descriptions.Item label="入职日期">{detail.hireDate || '-'}</Descriptions.Item>
+            <Descriptions.Item label="创建时间">{detail.createdAt || '-'}</Descriptions.Item>
+          </Descriptions>
+        )}
+      </Drawer>
+
+      <Modal
+        title="导入员工"
+        open={importOpen}
+        onOk={handleImport}
+        onCancel={() => {
+          setImportOpen(false);
+          setImportFileList([]);
+        }}
+        confirmLoading={importing}
+        okText="导入"
+        cancelText="取消"
+      >
+        <Upload.Dragger
+          accept=".xlsx,.xls"
+          maxCount={1}
+          fileList={importFileList}
+          beforeUpload={() => false}
+          onChange={({ fileList }) => setImportFileList(fileList)}
+        >
+          <p className="ant-upload-drag-icon">
+            <InboxOutlined />
+          </p>
+          <p className="ant-upload-text">点击或拖拽 Excel 文件到此处</p>
+          <p className="ant-upload-hint">仅支持 .xlsx / .xls 格式</p>
+        </Upload.Dragger>
       </Modal>
     </div>
   );
diff --git a/src/pages/system/users/server.ts b/src/pages/system/users/server.ts
new file mode 100644
index 0000000..9b664df
--- /dev/null
+++ b/src/pages/system/users/server.ts
@@ -0,0 +1,112 @@
+import { getAuthHeaders } from '@/api/authHeaders';
+import { request } from '@/api/request';
+import type {
+  TenantEmployeeCreateRequest,
+  TenantEmployeeCreateResponse,
+  TenantEmployeeDeleteRequest,
+  TenantEmployeeDeleteResponse,
+  TenantEmployeeDetailRequest,
+  TenantEmployeeDetailResponse,
+  TenantEmployeeExportRequest,
+  TenantEmployeeExportResponse,
+  TenantEmployeeImportRequest,
+  TenantEmployeeImportResponse,
+  TenantEmployeeListRequest,
+  TenantEmployeeListResponse,
+  TenantEmployeeUpdateBody,
+  TenantEmployeeUpdateRequest,
+  TenantEmployeeUpdateResponse,
+  TenantOrgTreeRequest,
+  TenantOrgTreeResponse,
+  TenantRoleOptionRequest,
+  TenantRoleOptionResponse,
+} from './types';
+
+/** 员工分页列表。 */
+export const getEmployeeList = (params: TenantEmployeeListRequest) =>
+  request<TenantEmployeeListRequest, TenantEmployeeListResponse>({
+    url: '/tenant/employees',
+    method: 'GET',
+    params,
+    headers: getAuthHeaders(),
+  });
+
+/** 创建员工。 */
+export const createEmployee = (data: TenantEmployeeCreateRequest) =>
+  request<TenantEmployeeCreateRequest, TenantEmployeeCreateResponse>({
+    url: '/tenant/employees',
+    method: 'POST',
+    data,
+    headers: getAuthHeaders(),
+  });
+
+/** 员工详情。 */
+export const getEmployeeDetail = ({ id }: TenantEmployeeDetailRequest) =>
+  request<TenantEmployeeDetailRequest, TenantEmployeeDetailResponse>({
+    url: `/tenant/employees/${id}`,
+    method: 'GET',
+    headers: getAuthHeaders(),
+  });
+
+/** 更新员工基本信息。 */
+export const updateEmployee = ({ id, ...data }: TenantEmployeeUpdateRequest) =>
+  request<TenantEmployeeUpdateBody, TenantEmployeeUpdateResponse>({
+    url: `/tenant/employees/${id}`,
+    method: 'PUT',
+    data,
+    headers: getAuthHeaders(),
+  });
+
+/** 删除员工。 */
+export const deleteEmployee = ({ id }: TenantEmployeeDeleteRequest) =>
+  request<TenantEmployeeDeleteRequest, TenantEmployeeDeleteResponse>({
+    url: `/tenant/employees/${id}`,
+    method: 'DELETE',
+    headers: getAuthHeaders(),
+  });
+
+/** 导出员工 Excel 文件流。 */
+export const exportEmployees = (params: TenantEmployeeExportRequest = {}) =>
+  request<TenantEmployeeExportRequest, TenantEmployeeExportResponse>({
+    url: '/tenant/employees/export',
+    method: 'GET',
+    params,
+    responseType: 'blob',
+    headers: getAuthHeaders(),
+  });
+
+/** 批量导入员工 Excel。 */
+export const importEmployees = ({ file }: TenantEmployeeImportRequest) => {
+  const formData = new FormData();
+  formData.append('file', file);
+
+  return request<FormData, TenantEmployeeImportResponse>(
+    {
+      url: '/tenant/employees/import',
+      method: 'POST',
+      data: formData,
+      headers: getAuthHeaders(),
+    },
+    {
+      // 不同文件请求体无法通过 FormData 区分,关闭合并。
+      merge: false,
+    },
+  );
+};
+
+/** 组织树,用于组织下拉选项。 */
+export const getOrgTree = (params: TenantOrgTreeRequest = {}) =>
+  request<TenantOrgTreeRequest, TenantOrgTreeResponse>({
+    url: '/tenant/organizations/tree',
+    method: 'GET',
+    params,
+    headers: getAuthHeaders(),
+  });
+
+/** 角色列表,用于角色下拉选项。 */
+export const getRoleOptions = () =>
+  request<TenantRoleOptionRequest, TenantRoleOptionResponse>({
+    url: '/tenant/roles',
+    method: 'GET',
+    headers: getAuthHeaders(),
+  });
diff --git a/src/pages/system/users/types.ts b/src/pages/system/users/types.ts
new file mode 100644
index 0000000..1e08839
--- /dev/null
+++ b/src/pages/system/users/types.ts
@@ -0,0 +1,167 @@
+import type { PageResult } from '@/types/api';
+
+/** 员工状态。 */
+export type EmployeeStatus = 'ACTIVE' | 'DISABLED';
+
+/** 员工响应体(EmployeeVO)。 */
+export interface EmployeeVO {
+  id: number;
+  createdAt?: string;
+  updatedAt?: string;
+  createdBy?: string;
+  updatedBy?: string;
+  tenantId?: string;
+  deleted?: number;
+  name: string;
+  phone: string;
+  orgId: number;
+  roleId: number;
+  position?: string;
+  email?: string;
+  avatarUrl?: string;
+  status: EmployeeStatus;
+  holdLimit?: number;
+  region?: string;
+  hireDate?: string;
+}
+
+/** GET /tenant/employees 请求参数(EmployeePageQuery)。 */
+export interface TenantEmployeeListRequest {
+  page?: number;
+  size?: number;
+  sortBy?: string;
+  sortDir?: string;
+  orgId?: number;
+  roleId?: number;
+  status?: EmployeeStatus;
+  keyword?: string;
+}
+
+/** GET /tenant/employees 响应数据(ResultPageResultEmployeeVO 的 data)。 */
+export type TenantEmployeeListResponse = PageResult<EmployeeVO>;
+
+/** POST /tenant/employees 请求体(EmployeeCreateDTO)。 */
+export interface TenantEmployeeCreateRequest {
+  name: string;
+  phone: string;
+  orgId: number;
+  roleId: number;
+  position?: string;
+  email?: string;
+  avatarUrl?: string;
+  holdLimit?: number;
+  region?: string;
+}
+
+/** POST /tenant/employees 响应数据(ResultEmployeeVO 的 data)。 */
+export type TenantEmployeeCreateResponse = EmployeeVO;
+
+/** GET /tenant/employees/{id} 请求参数。 */
+export interface TenantEmployeeDetailRequest {
+  /** 员工 ID(路径参数)。 */
+  id: number;
+}
+
+/** GET /tenant/employees/{id} 响应数据(ResultEmployeeVO 的 data)。 */
+export type TenantEmployeeDetailResponse = EmployeeVO;
+
+/** PUT /tenant/employees/{id} 请求参数(路径参数 + EmployeeUpdateDTO)。 */
+export interface TenantEmployeeUpdateRequest {
+  /** 员工 ID(路径参数)。 */
+  id: number;
+  name?: string;
+  phone?: string;
+  orgId?: number;
+  roleId?: number;
+  position?: string;
+  email?: string;
+  holdLimit?: number;
+  region?: string;
+}
+
+/** PUT /tenant/employees/{id} 请求体。 */
+export type TenantEmployeeUpdateBody = Omit<TenantEmployeeUpdateRequest, 'id'>;
+
+/** PUT /tenant/employees/{id} 响应数据(ResultEmployeeVO 的 data)。 */
+export type TenantEmployeeUpdateResponse = EmployeeVO;
+
+/** DELETE /tenant/employees/{id} 请求参数。 */
+export interface TenantEmployeeDeleteRequest {
+  /** 员工 ID(路径参数)。 */
+  id: number;
+}
+
+/** DELETE /tenant/employees/{id} 响应数据(ResultVoid 的 data)。 */
+export type TenantEmployeeDeleteResponse = void;
+
+/** GET /tenant/employees/export 请求参数。 */
+export interface TenantEmployeeExportRequest {
+  /** 不填导出全部,填则仅导出指定组织。 */
+  orgId?: number;
+}
+
+/** GET /tenant/employees/export 响应数据(Excel 文件流)。 */
+export type TenantEmployeeExportResponse = Blob;
+
+/** POST /tenant/employees/import 请求参数(multipart/form-data,字段名 file)。 */
+export interface TenantEmployeeImportRequest {
+  file: File;
+}
+
+/** POST /tenant/employees/import 响应数据(成功导入行数)。 */
+export type TenantEmployeeImportResponse = number;
+
+/** GET /tenant/organizations/tree 请求参数。 */
+export interface TenantOrgTreeRequest {
+  orgId?: number;
+}
+
+/** 组织树节点(OrgVO 子集)。 */
+export interface OrgTreeNode {
+  id: number;
+  orgName: string;
+  status?: boolean;
+  children?: OrgTreeNode[];
+}
+
+/** GET /tenant/organizations/tree 响应数据(ResultListOrgVO 的 data)。 */
+export type TenantOrgTreeResponse = OrgTreeNode[];
+
+/** GET /tenant/roles 请求参数:该接口未定义查询参数。 */
+export type TenantRoleOptionRequest = Record<string, never>;
+
+/** 角色选项(RoleVO 子集)。 */
+export interface RoleOption {
+  id: number;
+  roleName: string;
+  status?: string;
+}
+
+/** GET /tenant/roles 响应数据(ResultListRoleVO 的 data)。 */
+export type TenantRoleOptionResponse = RoleOption[];
+
+/** ProTable 搜索参数,与 EmployeePageQuery 中可筛选字段一致。 */
+export interface EmployeeListQuery {
+  keyword?: string;
+  status?: EmployeeStatus;
+  orgId?: number;
+  roleId?: number;
+}
+
+/** 新增 / 编辑员工表单值。 */
+export interface EmployeeFormValues {
+  name: string;
+  phone: string;
+  orgId: number;
+  roleId: number;
+  position?: string;
+  email?: string;
+  holdLimit?: number;
+  region?: string;
+}
+
+/** 下拉选项。 */
+export interface SelectOption {
+  label: string;
+  value: number;
+}
diff --git a/src/types/api.ts b/src/types/api.ts
new file mode 100644
index 0000000..c7a7994
--- /dev/null
+++ b/src/types/api.ts
@@ -0,0 +1,8 @@
+/** 后端通用分页响应结构(PageResult)。 */
+export interface PageResult<T> {
+  page: number;
+  size: number;
+  total: number;
+  totalPages: number;
+  records: T[];
+}

--
Gitblit v1.9.3