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