deploy-ease-platform/frontend/src/pages/System/Department/components/EditDialog.tsx
2025-10-24 20:13:16 +08:00

305 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;