|
|
@@ -1,8 +1,5 @@
|
|
|
-import React, { useState, useEffect, useImperativeHandle, forwardRef } from 'react';
|
|
|
-import { Button } from '../ui/button';
|
|
|
-import { Input } from '../ui/input';
|
|
|
-import { Label } from '../ui/label';
|
|
|
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
|
|
+import React, { useState, useImperativeHandle, forwardRef } from 'react';
|
|
|
+import { Modal, Form, Input, Select, Spin } from 'antd';
|
|
|
import { deptApi, DeptVO } from '../../api/dept';
|
|
|
import { userApi } from '../../api/user';
|
|
|
import { UserVO } from '../../types';
|
|
|
@@ -24,15 +21,8 @@ const DeptForm = forwardRef<DeptFormRef, DeptFormProps>(({ onSuccess }, ref) =>
|
|
|
const [dialogTitle, setDialogTitle] = useState('');
|
|
|
const [formLoading, setFormLoading] = useState(false);
|
|
|
const [formType, setFormType] = useState<'create' | 'update'>('create');
|
|
|
- const [formData, setFormData] = useState<Partial<DeptVO>>({
|
|
|
- name: '',
|
|
|
- parentId: 0,
|
|
|
- sort: 0,
|
|
|
- leaderUserId: undefined,
|
|
|
- phone: '',
|
|
|
- email: '',
|
|
|
- status: CommonStatusEnum.ENABLE,
|
|
|
- });
|
|
|
+ const [currentId, setCurrentId] = useState<number | undefined>();
|
|
|
+ const [form] = Form.useForm();
|
|
|
const [deptTree, setDeptTree] = useState<TreeNode[]>([]);
|
|
|
const [userList, setUserList] = useState<UserVO[]>([]);
|
|
|
const [statusOptions, setStatusOptions] = useState(getIntDictOptions(DICT_TYPE.COMMON_STATUS));
|
|
|
@@ -42,7 +32,8 @@ const DeptForm = forwardRef<DeptFormRef, DeptFormProps>(({ onSuccess }, ref) =>
|
|
|
setDialogVisible(true);
|
|
|
setDialogTitle(type === 'create' ? '新增部门' : '编辑部门');
|
|
|
setFormType(type as 'create' | 'update');
|
|
|
- resetForm();
|
|
|
+ setCurrentId(id);
|
|
|
+ form.resetFields();
|
|
|
|
|
|
// 加载用户列表
|
|
|
try {
|
|
|
@@ -55,7 +46,9 @@ const DeptForm = forwardRef<DeptFormRef, DeptFormProps>(({ onSuccess }, ref) =>
|
|
|
// 加载部门树
|
|
|
try {
|
|
|
const deptList = await deptApi.getSimpleDeptList();
|
|
|
- const treeData = handleTree(deptList);
|
|
|
+ // 过滤掉没有 id 的项,并确保 id 存在
|
|
|
+ const validDeptList = deptList.filter((dept): dept is DeptVO & { id: number } => !!dept.id);
|
|
|
+ const treeData = handleTree(validDeptList);
|
|
|
// 添加顶级部门选项
|
|
|
const rootDept: TreeNode = { id: 0, name: '顶级部门', children: treeData };
|
|
|
setDeptTree([rootDept]);
|
|
|
@@ -68,12 +61,30 @@ const DeptForm = forwardRef<DeptFormRef, DeptFormProps>(({ onSuccess }, ref) =>
|
|
|
setFormLoading(true);
|
|
|
try {
|
|
|
const deptData = await deptApi.getDept(id);
|
|
|
- setFormData(deptData);
|
|
|
+ form.setFieldsValue({
|
|
|
+ name: deptData.name,
|
|
|
+ parentId: deptData.parentId ?? 0,
|
|
|
+ sort: deptData.sort ?? 0,
|
|
|
+ leaderUserId: deptData.leaderUserId,
|
|
|
+ phone: deptData.phone || '',
|
|
|
+ email: deptData.email || '',
|
|
|
+ status: deptData.status ?? CommonStatusEnum.ENABLE,
|
|
|
+ });
|
|
|
} catch (error) {
|
|
|
toast.error('获取部门信息失败');
|
|
|
} finally {
|
|
|
setFormLoading(false);
|
|
|
}
|
|
|
+ } else {
|
|
|
+ form.setFieldsValue({
|
|
|
+ name: '',
|
|
|
+ parentId: 0,
|
|
|
+ sort: 0,
|
|
|
+ leaderUserId: undefined,
|
|
|
+ phone: '',
|
|
|
+ email: '',
|
|
|
+ status: CommonStatusEnum.ENABLE,
|
|
|
+ });
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -81,243 +92,168 @@ const DeptForm = forwardRef<DeptFormRef, DeptFormProps>(({ onSuccess }, ref) =>
|
|
|
open,
|
|
|
}));
|
|
|
|
|
|
- // 重置表单
|
|
|
- const resetForm = () => {
|
|
|
- setFormData({
|
|
|
- name: '',
|
|
|
- parentId: 0,
|
|
|
- sort: 0,
|
|
|
- leaderUserId: undefined,
|
|
|
- phone: '',
|
|
|
- email: '',
|
|
|
- status: CommonStatusEnum.ENABLE,
|
|
|
- });
|
|
|
- };
|
|
|
-
|
|
|
// 提交表单
|
|
|
const submitForm = async () => {
|
|
|
- // 基本验证
|
|
|
- if (!formData.name?.trim()) {
|
|
|
- toast.error('部门名称不能为空');
|
|
|
- return;
|
|
|
- }
|
|
|
- if (formData.parentId === undefined || formData.parentId === null) {
|
|
|
- toast.error('上级部门不能为空');
|
|
|
- return;
|
|
|
- }
|
|
|
- if (formData.sort === undefined || formData.sort === null) {
|
|
|
- toast.error('显示排序不能为空');
|
|
|
- return;
|
|
|
- }
|
|
|
- if (formData.status === undefined || formData.status === null) {
|
|
|
- toast.error('状态不能为空');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 邮箱验证
|
|
|
- if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
|
|
- toast.error('请输入正确的邮箱地址');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 手机号验证
|
|
|
- if (formData.phone && !/^1[3|4|5|6|7|8|9][0-9]\d{8}$/.test(formData.phone)) {
|
|
|
- toast.error('请输入正确的手机号码');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- setFormLoading(true);
|
|
|
try {
|
|
|
- const data = formData as DeptVO;
|
|
|
+ const values = await form.validateFields();
|
|
|
+ setFormLoading(true);
|
|
|
+
|
|
|
+ const data: DeptVO = {
|
|
|
+ ...values,
|
|
|
+ parentId: values.parentId ?? 0,
|
|
|
+ sort: values.sort ?? 0,
|
|
|
+ status: values.status ?? CommonStatusEnum.ENABLE,
|
|
|
+ };
|
|
|
+
|
|
|
if (formType === 'create') {
|
|
|
await deptApi.createDept(data);
|
|
|
toast.success('创建成功');
|
|
|
} else {
|
|
|
+ if (currentId) {
|
|
|
+ data.id = currentId;
|
|
|
+ }
|
|
|
await deptApi.updateDept(data);
|
|
|
toast.success('更新成功');
|
|
|
}
|
|
|
setDialogVisible(false);
|
|
|
onSuccess?.();
|
|
|
} catch (error: any) {
|
|
|
+ if (error.errorFields) {
|
|
|
+ // 表单验证错误
|
|
|
+ return;
|
|
|
+ }
|
|
|
toast.error(error.message || '操作失败');
|
|
|
} finally {
|
|
|
setFormLoading(false);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- // 递归渲染部门树选项
|
|
|
- const renderDeptOptions = (nodes: TreeNode[], level: number = 0): React.ReactNode[] => {
|
|
|
- return nodes.map((node) => (
|
|
|
- <React.Fragment key={node.id}>
|
|
|
- <SelectItem value={String(node.id)}>
|
|
|
- {' '.repeat(level)}
|
|
|
- {node.name}
|
|
|
- </SelectItem>
|
|
|
- {node.children && node.children.length > 0 && renderDeptOptions(node.children, level + 1)}
|
|
|
- </React.Fragment>
|
|
|
- ));
|
|
|
+ // 递归构建部门树选项
|
|
|
+ const buildDeptOptions = (nodes: TreeNode[], level: number = 0): { label: string; value: number }[] => {
|
|
|
+ const options: { label: string; value: number }[] = [];
|
|
|
+ nodes.forEach((node) => {
|
|
|
+ options.push({
|
|
|
+ label: ' '.repeat(level) + node.name,
|
|
|
+ value: node.id,
|
|
|
+ });
|
|
|
+ if (node.children && node.children.length > 0) {
|
|
|
+ options.push(...buildDeptOptions(node.children, level + 1));
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return options;
|
|
|
};
|
|
|
|
|
|
- if (!dialogVisible) return null;
|
|
|
-
|
|
|
return (
|
|
|
- <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-[9999] animate-in fade-in duration-200 p-4">
|
|
|
- <div className="bg-white rounded-xl shadow-2xl w-full max-w-2xl animate-in zoom-in duration-200 relative z-[10000] max-h-[85vh] flex flex-col overflow-hidden">
|
|
|
- {/* 弹窗标题 */}
|
|
|
- <div className="px-5 py-3 border-b border-gray-200 flex items-center justify-between shrink-0">
|
|
|
- <h3 className="text-base text-gray-900">{dialogTitle}</h3>
|
|
|
- <div className="flex items-center gap-2">
|
|
|
- <button
|
|
|
- onClick={() => setDialogVisible(false)}
|
|
|
- className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
|
- >
|
|
|
- <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
|
- </svg>
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* 弹窗内容 */}
|
|
|
- <div className="px-5 py-3 overflow-y-auto overflow-x-hidden flex-1 min-h-0">
|
|
|
- {formLoading ? (
|
|
|
- <div className="py-6 text-center text-gray-500">加载中...</div>
|
|
|
- ) : (
|
|
|
- <div className="space-y-4">
|
|
|
- {/* 上级部门 */}
|
|
|
- <div className="grid grid-cols-[100px_1fr] items-center gap-3 min-h-[40px]">
|
|
|
- <label className="text-sm text-gray-700 whitespace-nowrap">上级部门 *</label>
|
|
|
- <Select
|
|
|
- value={String(formData.parentId ?? 0)}
|
|
|
- onValueChange={(value) => setFormData({ ...formData, parentId: Number(value) })}
|
|
|
- >
|
|
|
- <SelectTrigger className="h-[36px]">
|
|
|
- <SelectValue placeholder="请选择上级部门" />
|
|
|
- </SelectTrigger>
|
|
|
- <SelectContent>
|
|
|
- {renderDeptOptions(deptTree)}
|
|
|
- </SelectContent>
|
|
|
- </Select>
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* 部门名称 */}
|
|
|
- <div className="grid grid-cols-[100px_1fr] items-center gap-3 min-h-[40px]">
|
|
|
- <label className="text-sm text-gray-700 whitespace-nowrap">部门名称 *</label>
|
|
|
- <Input
|
|
|
- id="name"
|
|
|
- value={formData.name || ''}
|
|
|
- onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
|
- placeholder="请输入部门名称"
|
|
|
- className="h-[36px]"
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* 显示排序 */}
|
|
|
- <div className="grid grid-cols-[100px_1fr] items-center gap-3 min-h-[40px]">
|
|
|
- <label className="text-sm text-gray-700 whitespace-nowrap">显示排序 *</label>
|
|
|
- <Input
|
|
|
- id="sort"
|
|
|
- type="number"
|
|
|
- value={formData.sort ?? 0}
|
|
|
- onChange={(e) => setFormData({ ...formData, sort: Number(e.target.value) })}
|
|
|
- placeholder="请输入显示排序"
|
|
|
- min={0}
|
|
|
- className="h-[36px]"
|
|
|
- />
|
|
|
- </div>
|
|
|
+ <Modal
|
|
|
+ title={dialogTitle}
|
|
|
+ open={dialogVisible}
|
|
|
+ onCancel={() => setDialogVisible(false)}
|
|
|
+ onOk={submitForm}
|
|
|
+ confirmLoading={formLoading}
|
|
|
+ width={600}
|
|
|
+ destroyOnClose
|
|
|
+ >
|
|
|
+ <Spin spinning={formLoading}>
|
|
|
+ <Form
|
|
|
+ form={form}
|
|
|
+ layout="horizontal"
|
|
|
+ labelCol={{ span: 4 }}
|
|
|
+ wrapperCol={{ span: 20 }}
|
|
|
+ initialValues={{
|
|
|
+ parentId: 0,
|
|
|
+ sort: 0,
|
|
|
+ status: CommonStatusEnum.ENABLE,
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Form.Item
|
|
|
+ label="上级部门"
|
|
|
+ name="parentId"
|
|
|
+ rules={[{ required: true, message: '请选择上级部门' }]}
|
|
|
+ >
|
|
|
+ <Select
|
|
|
+ placeholder="请选择上级部门"
|
|
|
+ options={buildDeptOptions(deptTree)}
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
|
|
|
- {/* 负责人 */}
|
|
|
- <div className="grid grid-cols-[100px_1fr] items-center gap-3 min-h-[40px]">
|
|
|
- <label className="text-sm text-gray-700 whitespace-nowrap">负责人</label>
|
|
|
- <Select
|
|
|
- value={formData.leaderUserId ? String(formData.leaderUserId) : 'none'}
|
|
|
- onValueChange={(value) =>
|
|
|
- setFormData({ ...formData, leaderUserId: value === 'none' ? undefined : Number(value) })
|
|
|
- }
|
|
|
- >
|
|
|
- <SelectTrigger className="h-[36px]">
|
|
|
- <SelectValue placeholder="请选择负责人" />
|
|
|
- </SelectTrigger>
|
|
|
- <SelectContent>
|
|
|
- <SelectItem value="none">无</SelectItem>
|
|
|
- {userList.map((user) => (
|
|
|
- <SelectItem key={user.id} value={String(user.id)}>
|
|
|
- {user.nickname}
|
|
|
- </SelectItem>
|
|
|
- ))}
|
|
|
- </SelectContent>
|
|
|
- </Select>
|
|
|
- </div>
|
|
|
+ <Form.Item
|
|
|
+ label="部门名称"
|
|
|
+ name="name"
|
|
|
+ rules={[
|
|
|
+ { required: true, message: '请输入部门名称' },
|
|
|
+ { max: 50, message: '部门名称不能超过50个字符' },
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <Input placeholder="请输入部门名称" />
|
|
|
+ </Form.Item>
|
|
|
|
|
|
- {/* 联系电话 */}
|
|
|
- <div className="grid grid-cols-[100px_1fr] items-center gap-3 min-h-[40px]">
|
|
|
- <label className="text-sm text-gray-700 whitespace-nowrap">联系电话</label>
|
|
|
- <Input
|
|
|
- id="phone"
|
|
|
- value={formData.phone || ''}
|
|
|
- onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
|
|
- placeholder="请输入联系电话"
|
|
|
- maxLength={11}
|
|
|
- className="h-[36px]"
|
|
|
- />
|
|
|
- </div>
|
|
|
+ <Form.Item
|
|
|
+ label="显示排序"
|
|
|
+ name="sort"
|
|
|
+ rules={[{ required: true, message: '请输入显示排序' }]}
|
|
|
+ >
|
|
|
+ <Input type="number" placeholder="请输入显示排序" min={0} />
|
|
|
+ </Form.Item>
|
|
|
|
|
|
- {/* 邮箱 */}
|
|
|
- <div className="grid grid-cols-[100px_1fr] items-center gap-3 min-h-[40px]">
|
|
|
- <label className="text-sm text-gray-700 whitespace-nowrap">邮箱</label>
|
|
|
- <Input
|
|
|
- id="email"
|
|
|
- type="email"
|
|
|
- value={formData.email || ''}
|
|
|
- onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
|
- placeholder="请输入邮箱"
|
|
|
- maxLength={50}
|
|
|
- className="h-[36px]"
|
|
|
- />
|
|
|
- </div>
|
|
|
+ <Form.Item
|
|
|
+ label="负责人"
|
|
|
+ name="leaderUserId"
|
|
|
+ >
|
|
|
+ <Select
|
|
|
+ placeholder="请选择负责人"
|
|
|
+ allowClear
|
|
|
+ options={[
|
|
|
+ ...userList.map((user) => ({
|
|
|
+ label: user.nickname,
|
|
|
+ value: user.id,
|
|
|
+ })),
|
|
|
+ ]}
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
|
|
|
- {/* 状态 */}
|
|
|
- <div className="grid grid-cols-[100px_1fr] items-center gap-3 min-h-[40px]">
|
|
|
- <label className="text-sm text-gray-700 whitespace-nowrap">状态 *</label>
|
|
|
- <Select
|
|
|
- value={String(formData.status ?? CommonStatusEnum.ENABLE)}
|
|
|
- onValueChange={(value) => setFormData({ ...formData, status: Number(value) })}
|
|
|
- >
|
|
|
- <SelectTrigger className="h-[36px]">
|
|
|
- <SelectValue placeholder="请选择状态" />
|
|
|
- </SelectTrigger>
|
|
|
- <SelectContent>
|
|
|
- {statusOptions.map((dict) => (
|
|
|
- <SelectItem key={dict.value} value={String(dict.value)}>
|
|
|
- {dict.label}
|
|
|
- </SelectItem>
|
|
|
- ))}
|
|
|
- </SelectContent>
|
|
|
- </Select>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </div>
|
|
|
+ <Form.Item
|
|
|
+ label="联系电话"
|
|
|
+ name="phone"
|
|
|
+ rules={[
|
|
|
+ {
|
|
|
+ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
|
|
|
+ message: '请输入正确的手机号码',
|
|
|
+ },
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <Input placeholder="请输入联系电话" maxLength={11} />
|
|
|
+ </Form.Item>
|
|
|
|
|
|
- {/* 弹窗底部 */}
|
|
|
- <div className="px-5 py-3 bg-gray-50/50 border-t border-gray-200 flex justify-end gap-2 rounded-b-xl shrink-0">
|
|
|
- <button
|
|
|
- onClick={() => setDialogVisible(false)}
|
|
|
- disabled={formLoading}
|
|
|
- className="px-4 py-2 text-sm text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
+ <Form.Item
|
|
|
+ label="邮箱"
|
|
|
+ name="email"
|
|
|
+ rules={[
|
|
|
+ {
|
|
|
+ type: 'email',
|
|
|
+ message: '请输入正确的邮箱地址',
|
|
|
+ },
|
|
|
+ { max: 50, message: '邮箱不能超过50个字符' },
|
|
|
+ ]}
|
|
|
>
|
|
|
- 取消
|
|
|
- </button>
|
|
|
- <button
|
|
|
- onClick={submitForm}
|
|
|
- disabled={formLoading}
|
|
|
- className="px-4 py-2 text-sm text-white bg-gradient-to-r from-blue-500 to-blue-600 rounded-lg hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
+ <Input type="email" placeholder="请输入邮箱" maxLength={50} />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="状态"
|
|
|
+ name="status"
|
|
|
+ rules={[{ required: true, message: '请选择状态' }]}
|
|
|
>
|
|
|
- 确定
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <Select
|
|
|
+ placeholder="请选择状态"
|
|
|
+ options={statusOptions.map((dict) => ({
|
|
|
+ label: dict.label,
|
|
|
+ value: dict.value,
|
|
|
+ }))}
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+ </Form>
|
|
|
+ </Spin>
|
|
|
+ </Modal>
|
|
|
);
|
|
|
});
|
|
|
|