305 lines
9.7 KiB
TypeScript
305 lines
9.7 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
DialogFooter,
|
||
} from '@/components/ui/dialog';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Label } from '@/components/ui/label';
|
||
import { Textarea } from '@/components/ui/textarea';
|
||
import { Switch } from '@/components/ui/switch';
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||
import { useToast } from '@/components/ui/use-toast';
|
||
import request from '@/utils/request';
|
||
import type { DepartmentResponse, DepartmentRequest } from '../types';
|
||
import type { UserResponse } from '../../User/types';
|
||
|
||
interface EditDialogProps {
|
||
open: boolean;
|
||
record?: DepartmentResponse;
|
||
departmentTree: DepartmentResponse[];
|
||
users: UserResponse[];
|
||
onOpenChange: (open: boolean) => void;
|
||
onSuccess: () => void;
|
||
}
|
||
|
||
/**
|
||
* 部门编辑对话框
|
||
*/
|
||
const EditDialog: React.FC<EditDialogProps> = ({
|
||
open,
|
||
record,
|
||
departmentTree,
|
||
users,
|
||
onOpenChange,
|
||
onSuccess,
|
||
}) => {
|
||
const { toast } = useToast();
|
||
const [formData, setFormData] = useState<Partial<DepartmentRequest>>({
|
||
enabled: true,
|
||
sort: 1,
|
||
});
|
||
|
||
useEffect(() => {
|
||
if (open) {
|
||
if (record) {
|
||
setFormData({
|
||
code: record.code,
|
||
name: record.name,
|
||
description: record.description,
|
||
parentId: record.parentId || undefined,
|
||
sort: record.sort,
|
||
enabled: record.enabled,
|
||
leaderId: record.leaderId,
|
||
leaderName: record.leaderName,
|
||
});
|
||
} else {
|
||
// 新增时,计算下一个排序值
|
||
const allDepts = flattenDepartments(departmentTree);
|
||
const maxSort = Math.max(0, ...allDepts.map(d => d.sort));
|
||
setFormData({ enabled: true, sort: maxSort + 1 });
|
||
}
|
||
}
|
||
}, [open, record, departmentTree]);
|
||
|
||
// 扁平化部门列表
|
||
const flattenDepartments = (depts: DepartmentResponse[], level: number = 0): Array<DepartmentResponse & { level: number }> => {
|
||
const result: Array<DepartmentResponse & { level: number }> = [];
|
||
depts.forEach(dept => {
|
||
// 过滤掉自己和子部门,避免循环引用
|
||
if (!record || (dept.id !== record.id && !isChildOf(dept.id, record, depts))) {
|
||
result.push({ ...dept, level });
|
||
if (dept.children) {
|
||
result.push(...flattenDepartments(dept.children, level + 1));
|
||
}
|
||
}
|
||
});
|
||
return result;
|
||
};
|
||
|
||
// 检查是否是子部门
|
||
const isChildOf = (deptId: number, parent: DepartmentResponse, allDepts: DepartmentResponse[]): boolean => {
|
||
const findDept = (depts: DepartmentResponse[], id: number): DepartmentResponse | null => {
|
||
for (const dept of depts) {
|
||
if (dept.id === id) return dept;
|
||
if (dept.children) {
|
||
const found = findDept(dept.children, id);
|
||
if (found) return found;
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
|
||
const dept = findDept(allDepts, deptId);
|
||
if (!dept || !dept.children) return false;
|
||
|
||
const checkChildren = (children: DepartmentResponse[]): boolean => {
|
||
return children.some(child => {
|
||
if (child.id === parent.id) return true;
|
||
if (child.children) return checkChildren(child.children);
|
||
return false;
|
||
});
|
||
};
|
||
|
||
return checkChildren(dept.children);
|
||
};
|
||
|
||
const handleSubmit = async () => {
|
||
try {
|
||
// 验证
|
||
if (!formData.code?.trim()) {
|
||
toast({
|
||
title: '提示',
|
||
description: '请输入部门编码',
|
||
variant: 'destructive'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 验证部门编码格式
|
||
if (!/^[A-Z_]+$/.test(formData.code)) {
|
||
toast({
|
||
title: '提示',
|
||
description: '部门编码只能包含大写字母和下划线',
|
||
variant: 'destructive'
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!formData.name?.trim()) {
|
||
toast({
|
||
title: '提示',
|
||
description: '请输入部门名称',
|
||
variant: 'destructive'
|
||
});
|
||
return;
|
||
}
|
||
|
||
const data = {
|
||
...formData as DepartmentRequest,
|
||
parentId: formData.parentId || 0
|
||
};
|
||
|
||
if (record) {
|
||
await request.put(`/api/v1/department/${record.id}`, {
|
||
...data,
|
||
version: record.version
|
||
});
|
||
toast({
|
||
title: '更新成功',
|
||
description: `部门 "${formData.name}" 已更新`,
|
||
});
|
||
} else {
|
||
await request.post('/api/v1/department', data);
|
||
toast({
|
||
title: '创建成功',
|
||
description: `部门 "${formData.name}" 已创建`,
|
||
});
|
||
}
|
||
|
||
onSuccess();
|
||
onOpenChange(false);
|
||
} catch (error) {
|
||
console.error('保存失败:', error);
|
||
toast({
|
||
title: '保存失败',
|
||
description: error instanceof Error ? error.message : '未知错误',
|
||
variant: 'destructive'
|
||
});
|
||
}
|
||
};
|
||
|
||
const flatDepartments = flattenDepartments(departmentTree);
|
||
const hasChildren = record?.children && record.children.length > 0;
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
<DialogContent className="sm:max-w-[600px]">
|
||
<DialogHeader>
|
||
<DialogTitle>{record ? '编辑部门' : '新增部门'}</DialogTitle>
|
||
</DialogHeader>
|
||
<div className="grid gap-4 py-4">
|
||
<div className="grid gap-2">
|
||
<Label htmlFor="parentId">上级部门</Label>
|
||
<Select
|
||
value={formData.parentId?.toString() || 'none'}
|
||
onValueChange={(value) => setFormData({ ...formData, parentId: value === 'none' ? undefined : Number(value) })}
|
||
disabled={hasChildren}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="不选择则为顶级部门" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="none">顶级部门</SelectItem>
|
||
{flatDepartments.map(dept => (
|
||
<SelectItem key={dept.id} value={dept.id.toString()}>
|
||
{' '.repeat(dept.level)}{dept.name}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
{hasChildren && (
|
||
<p className="text-xs text-muted-foreground">该部门有子部门,不能修改上级部门</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="grid gap-2">
|
||
<Label htmlFor="code">部门编码 *</Label>
|
||
<Input
|
||
id="code"
|
||
value={formData.code || ''}
|
||
onChange={(e) => setFormData({ ...formData, code: e.target.value.toUpperCase() })}
|
||
placeholder="请输入部门编码(大写字母和下划线)"
|
||
/>
|
||
<p className="text-xs text-muted-foreground">只能包含大写字母和下划线</p>
|
||
</div>
|
||
|
||
<div className="grid gap-2">
|
||
<Label htmlFor="name">部门名称 *</Label>
|
||
<Input
|
||
id="name"
|
||
value={formData.name || ''}
|
||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||
placeholder="请输入部门名称"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid gap-2">
|
||
<Label htmlFor="description">部门描述</Label>
|
||
<Textarea
|
||
id="description"
|
||
value={formData.description || ''}
|
||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||
placeholder="请输入部门描述"
|
||
rows={3}
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid gap-2">
|
||
<Label htmlFor="sort">显示排序 *</Label>
|
||
<Input
|
||
id="sort"
|
||
type="number"
|
||
value={formData.sort || 1}
|
||
onChange={(e) => setFormData({ ...formData, sort: Number(e.target.value) })}
|
||
placeholder="请输入显示排序"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid gap-2">
|
||
<Label htmlFor="leaderId">负责人</Label>
|
||
<Select
|
||
value={formData.leaderId?.toString() || 'none'}
|
||
onValueChange={(value) => {
|
||
if (value === 'none') {
|
||
setFormData({ ...formData, leaderId: undefined, leaderName: undefined });
|
||
} else {
|
||
const user = users.find(u => u.id === Number(value));
|
||
setFormData({
|
||
...formData,
|
||
leaderId: Number(value),
|
||
leaderName: user?.nickname || user?.username
|
||
});
|
||
}
|
||
}}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="请选择负责人" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="none">无</SelectItem>
|
||
{users.map(user => (
|
||
<SelectItem key={user.id} value={user.id.toString()}>
|
||
{user.nickname || user.username}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between">
|
||
<Label htmlFor="enabled">启用状态</Label>
|
||
<Switch
|
||
id="enabled"
|
||
checked={formData.enabled}
|
||
onCheckedChange={(checked) => setFormData({ ...formData, enabled: checked })}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||
取消
|
||
</Button>
|
||
<Button onClick={handleSubmit}>确定</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
};
|
||
|
||
export default EditDialog;
|
||
|