生产管理系统 部门树查询、新增一级部门
This commit is contained in:
@@ -7,13 +7,20 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Department } from '../types';
|
import { Department, CreateDepartmentForm } from '../types';
|
||||||
|
import {
|
||||||
|
createDepartment,
|
||||||
|
transformCreateDepartmentData,
|
||||||
|
debounce,
|
||||||
|
generateRandomOrderIndex,
|
||||||
|
} from './departmentCreateApi';
|
||||||
|
|
||||||
interface DepartmentFormDialogProps {
|
interface DepartmentFormDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -21,6 +28,7 @@ interface DepartmentFormDialogProps {
|
|||||||
editingDepartment: Department | null;
|
editingDepartment: Department | null;
|
||||||
parentDepartment: Department | null;
|
parentDepartment: Department | null;
|
||||||
onSave: (formData: Partial<Department>) => void;
|
onSave: (formData: Partial<Department>) => void;
|
||||||
|
refreshDepartmentTree?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DepartmentFormDialog({
|
export function DepartmentFormDialog({
|
||||||
@@ -28,13 +36,20 @@ export function DepartmentFormDialog({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
editingDepartment,
|
editingDepartment,
|
||||||
parentDepartment,
|
parentDepartment,
|
||||||
onSave
|
onSave,
|
||||||
|
refreshDepartmentTree,
|
||||||
}: DepartmentFormDialogProps) {
|
}: DepartmentFormDialogProps) {
|
||||||
const [formData, setFormData] = useState<Partial<Department>>({
|
const [formData, setFormData] = useState<CreateDepartmentForm>({
|
||||||
|
name: '',
|
||||||
|
code: '',
|
||||||
|
manager: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
description: '',
|
||||||
status: 'active',
|
status: 'active',
|
||||||
sort: 0,
|
sort: generateRandomOrderIndex(),
|
||||||
|
parentId: parentDepartment?.id || '',
|
||||||
level: parentDepartment ? (parentDepartment.level || 1) + 1 : 1,
|
level: parentDepartment ? (parentDepartment.level || 1) + 1 : 1,
|
||||||
parentId: parentDepartment?.id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -43,43 +58,126 @@ export function DepartmentFormDialog({
|
|||||||
useState(() => {
|
useState(() => {
|
||||||
if (editingDepartment) {
|
if (editingDepartment) {
|
||||||
setFormData({
|
setFormData({
|
||||||
...editingDepartment,
|
name: editingDepartment.name,
|
||||||
children: undefined, // 排除children字段
|
code: editingDepartment.code,
|
||||||
|
manager: editingDepartment.manager || '',
|
||||||
|
phone: editingDepartment.phone || '',
|
||||||
|
email: editingDepartment.email || '',
|
||||||
|
description: editingDepartment.description || '',
|
||||||
|
status: editingDepartment.status,
|
||||||
|
sort: editingDepartment.sort,
|
||||||
|
parentId: editingDepartment.parentId || '',
|
||||||
|
level: editingDepartment.level,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setFormData({
|
setFormData({
|
||||||
parentId: parentDepartment?.id,
|
name: '',
|
||||||
level: parentDepartment ? (parentDepartment.level || 1) + 1 : 1,
|
code: '',
|
||||||
|
manager: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
description: '',
|
||||||
status: 'active',
|
status: 'active',
|
||||||
sort: 0,
|
sort: generateRandomOrderIndex(),
|
||||||
|
parentId: parentDepartment?.id || '',
|
||||||
|
level: parentDepartment ? (parentDepartment.level || 1) + 1 : 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleInputChange = (field: keyof Department, value: string | number) => {
|
const handleInputChange = (field: keyof CreateDepartmentForm, value: string | number) => {
|
||||||
setFormData(prev => ({ ...prev, [field]: value }));
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 表单验证
|
||||||
|
const validateForm = (): boolean => {
|
||||||
|
if (!formData.name?.trim()) {
|
||||||
|
toast.error('请输入部门名称');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!formData.code?.trim()) {
|
||||||
|
toast.error('请输入部门编码');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||||
|
toast.error('邮箱格式不正确');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建部门的API调用函数(带防抖)
|
||||||
|
const createDepartmentWithDebounce = useCallback(
|
||||||
|
debounce(async (form: CreateDepartmentForm) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// 调用API创建部门
|
||||||
|
await createDepartment(form);
|
||||||
|
|
||||||
|
// 成功处理
|
||||||
|
toast.success('创建一级部门成功');
|
||||||
|
|
||||||
|
// 关闭对话框
|
||||||
|
onOpenChange(false);
|
||||||
|
|
||||||
|
// 刷新部门树
|
||||||
|
if (refreshDepartmentTree) {
|
||||||
|
refreshDepartmentTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
setFormData({
|
||||||
|
name: '',
|
||||||
|
code: '',
|
||||||
|
manager: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
description: '',
|
||||||
|
status: 'active',
|
||||||
|
sort: generateRandomOrderIndex(),
|
||||||
|
parentId: parentDepartment?.id || '',
|
||||||
|
level: parentDepartment ? (parentDepartment.level || 1) + 1 : 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// 失败处理 - 不关闭页面
|
||||||
|
console.error('创建部门失败:', error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '创建一级部门失败';
|
||||||
|
toast.error(errorMessage);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, 1000), // 1秒防抖
|
||||||
|
[onOpenChange, refreshDepartmentTree, parentDepartment]
|
||||||
|
);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!formData.name || !formData.code) {
|
// 表单验证
|
||||||
|
if (!validateForm()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果是编辑模式,使用原有的onSave逻辑
|
||||||
|
if (editingDepartment) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await onSave(formData);
|
await onSave(formData);
|
||||||
// 重置表单
|
toast.success('部门更新成功');
|
||||||
setFormData({
|
onOpenChange(false);
|
||||||
status: 'active',
|
if (refreshDepartmentTree) {
|
||||||
sort: 0,
|
refreshDepartmentTree();
|
||||||
level: parentDepartment ? (parentDepartment.level || 1) + 1 : 1,
|
}
|
||||||
parentId: parentDepartment?.id,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save department:', error);
|
console.error('Failed to update department:', error);
|
||||||
|
toast.error('部门更新失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 创建模式,使用API调用(带防抖)
|
||||||
|
createDepartmentWithDebounce(formData);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
@@ -87,10 +185,16 @@ export function DepartmentFormDialog({
|
|||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
// 重置表单
|
// 重置表单
|
||||||
setFormData({
|
setFormData({
|
||||||
|
name: '',
|
||||||
|
code: '',
|
||||||
|
manager: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
description: '',
|
||||||
status: 'active',
|
status: 'active',
|
||||||
sort: 0,
|
sort: generateRandomOrderIndex(),
|
||||||
|
parentId: parentDepartment?.id || '',
|
||||||
level: parentDepartment ? (parentDepartment.level || 1) + 1 : 1,
|
level: parentDepartment ? (parentDepartment.level || 1) + 1 : 1,
|
||||||
parentId: parentDepartment?.id,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,10 +9,12 @@
|
|||||||
|
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Building2, Plus } from 'lucide-react';
|
import { Building2, Plus, RefreshCw } from 'lucide-react';
|
||||||
|
|
||||||
interface DepartmentHeaderProps {
|
interface DepartmentHeaderProps {
|
||||||
onAdd: () => void;
|
onAdd: () => void;
|
||||||
|
onRefresh?: () => void;
|
||||||
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DepartmentHeader({ onAdd }: DepartmentHeaderProps) {
|
export function DepartmentHeader({ onAdd }: DepartmentHeaderProps) {
|
||||||
|
|||||||
@@ -0,0 +1,323 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 部门管理API接口 - 部门树形数据查询接口服务
|
||||||
|
* 功能:API请求封装、数据转换、错误处理、树形结构数据处理
|
||||||
|
* 路径:/central-config/user/department/components/departmentApi
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用SDK API调用,TypeScript类型安全
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getAuthToken } from "@/utils/token";
|
||||||
|
import {
|
||||||
|
getDepartmentTreeApiV1DepartmentsTreeGet,
|
||||||
|
getUsersApiV1UsersGet
|
||||||
|
} from "@/lib/api/sdk.gen";
|
||||||
|
|
||||||
|
// API返回的部门数据类型
|
||||||
|
export interface DepartmentApiData {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
parent_id: string | null;
|
||||||
|
manager_name: string | null;
|
||||||
|
manager_phone: string | null;
|
||||||
|
manager_email: string | null;
|
||||||
|
description: string | null;
|
||||||
|
status: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
tenant_id: string;
|
||||||
|
order_index: number;
|
||||||
|
created_by: string | null;
|
||||||
|
updated_by: string | null;
|
||||||
|
children?: DepartmentApiData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// API返回的用户数据类型(用于部门成员)
|
||||||
|
export interface UserApiData {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
full_name: string | null;
|
||||||
|
display_name: string | null;
|
||||||
|
phone: string | null;
|
||||||
|
is_active: boolean;
|
||||||
|
department_id: string | null;
|
||||||
|
department_name: string | null;
|
||||||
|
created_at: string;
|
||||||
|
last_login_at: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API响应接口
|
||||||
|
export interface DepartmentTreeApiResponse {
|
||||||
|
data: DepartmentApiData[];
|
||||||
|
total: number;
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面使用的部门数据类型(转换后的)
|
||||||
|
export interface Department {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
parentId: string | null;
|
||||||
|
managerId: string | null;
|
||||||
|
description: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
tenantId: string;
|
||||||
|
sortOrder: number;
|
||||||
|
memberCount?: number;
|
||||||
|
children: Department[];
|
||||||
|
expanded: boolean;
|
||||||
|
level: number;
|
||||||
|
// 兼容现有页面的字段
|
||||||
|
manager?: string;
|
||||||
|
employeeCount?: number;
|
||||||
|
description?: string;
|
||||||
|
status?: 'active' | 'inactive';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面使用的用户数据类型(转换后的)
|
||||||
|
export interface DepartmentUser {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
fullName: string | null;
|
||||||
|
displayName: string | null;
|
||||||
|
phone: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
departmentId: string | null;
|
||||||
|
departmentName: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
lastLoginAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询参数接口
|
||||||
|
export interface DepartmentQueryParams {
|
||||||
|
include_inactive?: boolean;
|
||||||
|
include_members?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取部门树形结构数据
|
||||||
|
*/
|
||||||
|
export async function fetchDepartmentTree(params: DepartmentQueryParams = {}): Promise<DepartmentTreeApiResponse> {
|
||||||
|
try {
|
||||||
|
// 构建查询参数对象
|
||||||
|
const queryParams: any = {};
|
||||||
|
|
||||||
|
if (params.include_inactive !== undefined) queryParams.include_inactive = params.include_inactive;
|
||||||
|
if (params.include_members !== undefined) queryParams.include_members = params.include_members;
|
||||||
|
|
||||||
|
// 获取认证token
|
||||||
|
const token = getAuthToken();
|
||||||
|
console.log('部门管理API调用参数:', queryParams);
|
||||||
|
|
||||||
|
// 使用SDK API调用部门树形结构查询接口
|
||||||
|
const response = await getDepartmentTreeApiV1DepartmentsTreeGet({
|
||||||
|
query: {
|
||||||
|
...queryParams,
|
||||||
|
// 添加时间戳防止缓存
|
||||||
|
_t: Date.now(),
|
||||||
|
},
|
||||||
|
headers: token ? {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
} : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(`API error: ${response.error.message || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = response.data as any;
|
||||||
|
console.log('部门管理API响应:', data);
|
||||||
|
|
||||||
|
// 根据实际API响应格式处理数据
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
// 如果API直接返回数组
|
||||||
|
return {
|
||||||
|
data: data,
|
||||||
|
total: data.length,
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
} else if (data && typeof data === 'object') {
|
||||||
|
// 如果API返回对象格式
|
||||||
|
return {
|
||||||
|
data: data.data || data.items || [],
|
||||||
|
total: data.total || data.count || 0,
|
||||||
|
success: data.success !== false,
|
||||||
|
message: data.message,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 其他情况,返回空结果
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid response format',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch department tree:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取部门成员列表
|
||||||
|
*/
|
||||||
|
export async function fetchDepartmentUsers(departmentId: string): Promise<DepartmentUser[]> {
|
||||||
|
try {
|
||||||
|
// 获取认证token
|
||||||
|
const token = getAuthToken();
|
||||||
|
|
||||||
|
// 使用SDK API调用用户查询接口,按部门筛选
|
||||||
|
const response = await getUsersApiV1UsersGet({
|
||||||
|
query: {
|
||||||
|
department_id: departmentId,
|
||||||
|
_t: Date.now(),
|
||||||
|
},
|
||||||
|
headers: token ? {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
} : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(`API error: ${response.error.message || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = response.data as any;
|
||||||
|
|
||||||
|
let users: any[] = [];
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
users = data;
|
||||||
|
} else if (data && typeof data === 'object' && data.data) {
|
||||||
|
users = data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return users.map(transformUserData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch department users:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将API数据转换为页面所需的部门数据格式
|
||||||
|
*/
|
||||||
|
export function transformDepartmentData(department: DepartmentApiData, level: number = 0): Department {
|
||||||
|
return {
|
||||||
|
id: department.id,
|
||||||
|
name: department.name,
|
||||||
|
code: department.code,
|
||||||
|
parentId: department.parent_id,
|
||||||
|
managerId: null, // API中没有manager_id字段
|
||||||
|
description: department.description,
|
||||||
|
isActive: department.status === 'active',
|
||||||
|
createdAt: formatDate(department.created_at),
|
||||||
|
updatedAt: formatDate(department.updated_at),
|
||||||
|
tenantId: department.tenant_id,
|
||||||
|
sortOrder: department.order_index,
|
||||||
|
memberCount: 0, // API中没有member_count字段
|
||||||
|
children: (department.children || []).map(child => transformDepartmentData(child, level + 1)),
|
||||||
|
expanded: level === 0, // 默认展开顶级部门
|
||||||
|
level,
|
||||||
|
// 兼容现有页面的字段
|
||||||
|
manager: department.manager_name || undefined,
|
||||||
|
phone: department.manager_phone || undefined,
|
||||||
|
email: department.manager_email || undefined,
|
||||||
|
employeeCount: 0,
|
||||||
|
status: department.status as 'active' | 'inactive',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将API数据转换为页面所需的用户数据格式
|
||||||
|
*/
|
||||||
|
export function transformUserData(user: UserApiData): DepartmentUser {
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
fullName: user.full_name,
|
||||||
|
displayName: user.display_name || user.full_name || user.username,
|
||||||
|
phone: user.phone,
|
||||||
|
isActive: user.is_active,
|
||||||
|
departmentId: user.department_id,
|
||||||
|
departmentName: user.department_name,
|
||||||
|
createdAt: formatDate(user.created_at),
|
||||||
|
lastLoginAt: user.last_login_at ? formatDate(user.last_login_at) : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量转换部门数据
|
||||||
|
*/
|
||||||
|
export function transformDepartmentList(departments: DepartmentApiData[]): Department[] {
|
||||||
|
return departments.map(dept => transformDepartmentData(dept));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扁平化部门树形结构(用于搜索和筛选)
|
||||||
|
*/
|
||||||
|
export function flattenDepartments(departments: Department[]): Department[] {
|
||||||
|
const result: Department[] = [];
|
||||||
|
|
||||||
|
function flatten(depts: Department[]) {
|
||||||
|
depts.forEach(dept => {
|
||||||
|
result.push(dept);
|
||||||
|
if (dept.children && dept.children.length > 0) {
|
||||||
|
flatten(dept.children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
flatten(departments);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在部门树中查找部门
|
||||||
|
*/
|
||||||
|
export function findDepartmentInTree(departments: Department[], departmentId: string): Department | null {
|
||||||
|
for (const dept of departments) {
|
||||||
|
if (dept.id === departmentId) {
|
||||||
|
return dept;
|
||||||
|
}
|
||||||
|
if (dept.children && dept.children.length > 0) {
|
||||||
|
const found = findDepartmentInTree(dept.children, departmentId);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期
|
||||||
|
*/
|
||||||
|
function formatDate(dateString: string): string {
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
}).replace(/\//g, '-');
|
||||||
|
} catch (error) {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Department tree state interface for page components
|
||||||
|
export interface DepartmentTreeState {
|
||||||
|
departments: Department[];
|
||||||
|
flattenedDepartments: Department[];
|
||||||
|
selectedDepartment: Department | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
searchKeyword: string;
|
||||||
|
expandedDepartments: Set<string>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 部门管理API接口 - 部门数据CRUD操作接口服务
|
||||||
|
* 功能:API请求封装、数据转换、错误处理、部门树形管理
|
||||||
|
* 路径:/central-config/user/department/components/departmentApi
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用SDK API调用,TypeScript类型安全
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getAuthToken } from "@/utils/token";
|
||||||
|
import {
|
||||||
|
createDepartmentApiV1DepartmentsPost,
|
||||||
|
} from "@/lib/api/sdk.gen";
|
||||||
|
import {
|
||||||
|
Department,
|
||||||
|
CreateDepartmentForm,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API请求创建部门的数据结构(对应Python字段)
|
||||||
|
*/
|
||||||
|
export interface CreateDepartmentApiRequest {
|
||||||
|
code: string; // 部门编码
|
||||||
|
name: string; // 部门名称
|
||||||
|
description?: string; // 部门描述
|
||||||
|
manager_name?: string; // 管理者名称
|
||||||
|
manager_phone?: string; // 管理者电话
|
||||||
|
manager_email?: string; // 管理者邮箱(必须符合邮箱规则)
|
||||||
|
parent_id: string; // 父级部门ID,一级部门为空字符串
|
||||||
|
order_index: number; // 排序索引,0-10000的整数
|
||||||
|
status: string; // 状态,默认"active"表示有效
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API响应数据结构
|
||||||
|
*/
|
||||||
|
export interface CreateDepartmentApiResponse {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
manager_name?: string;
|
||||||
|
manager_phone?: string;
|
||||||
|
manager_email?: string;
|
||||||
|
parent_id: string;
|
||||||
|
order_index: number;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将React表单数据(驼峰命名法)转换为API请求数据(Python蛇形命名法)
|
||||||
|
*/
|
||||||
|
export function transformCreateDepartmentData(formData: CreateDepartmentForm): CreateDepartmentApiRequest {
|
||||||
|
return {
|
||||||
|
code: formData.code,
|
||||||
|
name: formData.name,
|
||||||
|
description: formData.description || null,
|
||||||
|
manager_name: formData.manager || undefined,
|
||||||
|
manager_phone: formData.phone || undefined,
|
||||||
|
manager_email: formData.email || undefined,
|
||||||
|
parent_id: formData.parentId || "",
|
||||||
|
order_index: formData.sort || 0,
|
||||||
|
status: formData.status || "active",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮箱格式验证
|
||||||
|
*/
|
||||||
|
export function isValidEmail(email: string): boolean {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
return emailRegex.test(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建部门API调用
|
||||||
|
*/
|
||||||
|
export async function createDepartment(formData: CreateDepartmentForm): Promise<CreateDepartmentApiResponse> {
|
||||||
|
try {
|
||||||
|
// 获取认证token
|
||||||
|
const token = getAuthToken();
|
||||||
|
console.log('创建部门API调用参数:', formData);
|
||||||
|
|
||||||
|
// 转换表单数据为API请求格式
|
||||||
|
const apiRequestData = transformCreateDepartmentData(formData);
|
||||||
|
|
||||||
|
// 邮箱格式验证
|
||||||
|
if (apiRequestData.manager_email && !isValidEmail(apiRequestData.manager_email)) {
|
||||||
|
throw new Error('邮箱格式不正确');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用真正的SDK API调用
|
||||||
|
const response = await createDepartmentApiV1DepartmentsPost({
|
||||||
|
body: apiRequestData,
|
||||||
|
headers: token ? {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
} : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(`API error: ${response.error.message || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = response.data as CreateDepartmentApiResponse;
|
||||||
|
console.log('创建部门API响应:', data);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create department:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机排序索引(0-10000)
|
||||||
|
*/
|
||||||
|
export function generateRandomOrderIndex(): number {
|
||||||
|
return Math.floor(Math.random() * 10001);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防抖函数
|
||||||
|
*/
|
||||||
|
export function debounce<T extends (...args: any[]) => any>(
|
||||||
|
func: T,
|
||||||
|
delay: number
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
let timeoutId: NodeJS.Timeout;
|
||||||
|
return (...args: Parameters<T>) => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(() => func(...args), delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -16,6 +16,12 @@ import { DepartmentTree } from './components/DepartmentTree';
|
|||||||
import { DepartmentFormDialog } from './components/DepartmentFormDialog';
|
import { DepartmentFormDialog } from './components/DepartmentFormDialog';
|
||||||
import { DepartmentDeleteDialog } from './components/DepartmentDeleteDialog';
|
import { DepartmentDeleteDialog } from './components/DepartmentDeleteDialog';
|
||||||
import { DepartmentInstructions } from './components/DepartmentInstructions';
|
import { DepartmentInstructions } from './components/DepartmentInstructions';
|
||||||
|
import {
|
||||||
|
fetchDepartmentTree,
|
||||||
|
transformDepartmentList,
|
||||||
|
flattenDepartments,
|
||||||
|
type DepartmentTreeState
|
||||||
|
} from './components/departmentApi';
|
||||||
|
|
||||||
// 部门管理状态管理
|
// 部门管理状态管理
|
||||||
interface DepartmentManagementState {
|
interface DepartmentManagementState {
|
||||||
@@ -116,151 +122,63 @@ export default function DepartmentManagementPage() {
|
|||||||
try {
|
try {
|
||||||
dispatch({ type: 'SET_LOADING', payload: true });
|
dispatch({ type: 'SET_LOADING', payload: true });
|
||||||
|
|
||||||
// 暂时使用mock数据,后续可以替换为API调用
|
// 使用API调用获取部门树形数据
|
||||||
const mockDepartments: Department[] = [
|
const response = await fetchDepartmentTree({
|
||||||
{
|
include_inactive: false,
|
||||||
id: 'dept-1',
|
include_members: true,
|
||||||
name: '技术部',
|
});
|
||||||
code: 'TECH',
|
|
||||||
level: 1,
|
|
||||||
manager: '王技术',
|
|
||||||
phone: '13800138001',
|
|
||||||
email: 'tech@example.com',
|
|
||||||
description: '负责技术研发和系统维护',
|
|
||||||
sort: 1,
|
|
||||||
status: 'active',
|
|
||||||
createdAt: '2024-01-01T00:00:00',
|
|
||||||
updatedAt: '2024-01-01T00:00:00',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 'dept-1-1',
|
|
||||||
parentId: 'dept-1',
|
|
||||||
name: '研发组',
|
|
||||||
code: 'TECH-RD',
|
|
||||||
level: 2,
|
|
||||||
manager: '李研发',
|
|
||||||
phone: '13800138011',
|
|
||||||
description: '负责系统研发',
|
|
||||||
sort: 1,
|
|
||||||
status: 'active',
|
|
||||||
createdAt: '2024-01-01T00:00:00',
|
|
||||||
updatedAt: '2024-01-01T00:00:00',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dept-1-2',
|
|
||||||
parentId: 'dept-1',
|
|
||||||
name: '运维组',
|
|
||||||
code: 'TECH-OPS',
|
|
||||||
level: 2,
|
|
||||||
manager: '张运维',
|
|
||||||
phone: '13800138012',
|
|
||||||
description: '负责系统运维',
|
|
||||||
sort: 2,
|
|
||||||
status: 'active',
|
|
||||||
createdAt: '2024-01-01T00:00:00',
|
|
||||||
updatedAt: '2024-01-01T00:00:00',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dept-2',
|
|
||||||
name: '管理部',
|
|
||||||
code: 'ADMIN',
|
|
||||||
level: 1,
|
|
||||||
manager: '赵管理',
|
|
||||||
phone: '13800138002',
|
|
||||||
email: 'admin@example.com',
|
|
||||||
description: '负责行政管理',
|
|
||||||
sort: 2,
|
|
||||||
status: 'active',
|
|
||||||
createdAt: '2024-01-01T00:00:00',
|
|
||||||
updatedAt: '2024-01-01T00:00:00',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 'dept-2-1',
|
|
||||||
parentId: 'dept-2',
|
|
||||||
name: '人事组',
|
|
||||||
code: 'ADMIN-HR',
|
|
||||||
level: 2,
|
|
||||||
manager: '孙人事',
|
|
||||||
phone: '13800138021',
|
|
||||||
description: '负责人力资源管理',
|
|
||||||
sort: 1,
|
|
||||||
status: 'active',
|
|
||||||
createdAt: '2024-01-01T00:00:00',
|
|
||||||
updatedAt: '2024-01-01T00:00:00',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dept-2-2',
|
|
||||||
parentId: 'dept-2',
|
|
||||||
name: '财务组',
|
|
||||||
code: 'ADMIN-FIN',
|
|
||||||
level: 2,
|
|
||||||
manager: '周财务',
|
|
||||||
phone: '13800138022',
|
|
||||||
description: '负责财务管理',
|
|
||||||
sort: 2,
|
|
||||||
status: 'active',
|
|
||||||
createdAt: '2024-01-01T00:00:00',
|
|
||||||
updatedAt: '2024-01-01T00:00:00',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dept-3',
|
|
||||||
name: '作业部',
|
|
||||||
code: 'OPS',
|
|
||||||
level: 1,
|
|
||||||
manager: '吴作业',
|
|
||||||
phone: '13800138003',
|
|
||||||
email: 'ops@example.com',
|
|
||||||
description: '负责农机作业管理',
|
|
||||||
sort: 3,
|
|
||||||
status: 'active',
|
|
||||||
createdAt: '2024-01-01T00:00:00',
|
|
||||||
updatedAt: '2024-01-01T00:00:00',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 'dept-3-1',
|
|
||||||
parentId: 'dept-3',
|
|
||||||
name: '第一作业组',
|
|
||||||
code: 'OPS-T1',
|
|
||||||
level: 2,
|
|
||||||
manager: '郑组长',
|
|
||||||
phone: '13800138031',
|
|
||||||
description: '负责区域A作业',
|
|
||||||
sort: 1,
|
|
||||||
status: 'active',
|
|
||||||
createdAt: '2024-01-01T00:00:00',
|
|
||||||
updatedAt: '2024-01-01T00:00:00',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dept-3-2',
|
|
||||||
parentId: 'dept-3',
|
|
||||||
name: '第二作业组',
|
|
||||||
code: 'OPS-T2',
|
|
||||||
level: 2,
|
|
||||||
manager: '钱组长',
|
|
||||||
phone: '13800138032',
|
|
||||||
description: '负责区域B作业',
|
|
||||||
sort: 2,
|
|
||||||
status: 'active',
|
|
||||||
createdAt: '2024-01-01T00:00:00',
|
|
||||||
updatedAt: '2024-01-01T00:00:00',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
dispatch({ type: 'SET_DEPARTMENTS', payload: mockDepartments });
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || '获取部门数据失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换API数据为页面所需的格式
|
||||||
|
const departments = transformDepartmentList(response.data);
|
||||||
|
|
||||||
|
// 转换为与现有页面兼容的数据格式
|
||||||
|
const compatibleDepartments: Department[] = departments.map(dept => ({
|
||||||
|
id: dept.id,
|
||||||
|
name: dept.name,
|
||||||
|
code: dept.code,
|
||||||
|
level: dept.level + 1, // API的level从0开始,页面从1开始
|
||||||
|
manager: dept.manager, // 从API的manager_name字段获取
|
||||||
|
phone: dept.phone, // 从API的manager_phone字段获取
|
||||||
|
email: dept.email, // 从API的manager_email字段获取
|
||||||
|
description: dept.description,
|
||||||
|
sort: dept.sortOrder,
|
||||||
|
status: dept.status as 'active' | 'inactive',
|
||||||
|
parentId: dept.parentId || undefined,
|
||||||
|
createdAt: dept.createdAt,
|
||||||
|
updatedAt: dept.updatedAt,
|
||||||
|
children: dept.children.map(child => ({
|
||||||
|
id: child.id,
|
||||||
|
name: child.name,
|
||||||
|
code: child.code,
|
||||||
|
level: child.level + 1,
|
||||||
|
manager: child.manager, // 从API的manager_name字段获取
|
||||||
|
phone: child.phone, // 从API的manager_phone字段获取
|
||||||
|
email: child.email, // 从API的manager_email字段获取
|
||||||
|
description: child.description,
|
||||||
|
sort: child.sortOrder,
|
||||||
|
status: child.status as 'active' | 'inactive',
|
||||||
|
parentId: child.parentId || undefined,
|
||||||
|
createdAt: child.createdAt,
|
||||||
|
updatedAt: child.updatedAt,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
dispatch({ type: 'SET_DEPARTMENTS', payload: compatibleDepartments });
|
||||||
// 默认展开所有一级部门
|
// 默认展开所有一级部门
|
||||||
dispatch({ type: 'SET_EXPANDED_IDS', payload: new Set(mockDepartments.map(d => d.id)) });
|
dispatch({ type: 'SET_EXPANDED_IDS', payload: new Set(compatibleDepartments.map(d => d.id)) });
|
||||||
|
|
||||||
|
toast.success(`成功加载 ${compatibleDepartments.length} 个部门`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load departments:', error);
|
console.error('Failed to load departments:', error);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SET_ERROR',
|
type: 'SET_ERROR',
|
||||||
payload: error instanceof Error ? error.message : '加载部门数据失败'
|
payload: error instanceof Error ? error.message : '加载部门数据失败'
|
||||||
});
|
});
|
||||||
|
toast.error('加载部门数据失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -303,6 +221,11 @@ export default function DepartmentManagementPage() {
|
|||||||
dispatch({ type: 'SET_EXPANDED_IDS', payload: allIds });
|
dispatch({ type: 'SET_EXPANDED_IDS', payload: allIds });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
const refreshData = () => {
|
||||||
|
loadDepartments();
|
||||||
|
};
|
||||||
|
|
||||||
// 收起全部
|
// 收起全部
|
||||||
const collapseAll = () => {
|
const collapseAll = () => {
|
||||||
dispatch({ type: 'SET_EXPANDED_IDS', payload: new Set() });
|
dispatch({ type: 'SET_EXPANDED_IDS', payload: new Set() });
|
||||||
@@ -533,7 +456,7 @@ export default function DepartmentManagementPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 页面标题 */}
|
{/* 页面标题 */}
|
||||||
<DepartmentHeader onAdd={() => handleAdd()} />
|
<DepartmentHeader onAdd={() => handleAdd()} onRefresh={refreshData} loading={state.loading} />
|
||||||
|
|
||||||
{/* 统计卡片 */}
|
{/* 统计卡片 */}
|
||||||
<DepartmentStatsCards stats={stats} />
|
<DepartmentStatsCards stats={stats} />
|
||||||
@@ -565,6 +488,7 @@ export default function DepartmentManagementPage() {
|
|||||||
editingDepartment={state.editingDepartment}
|
editingDepartment={state.editingDepartment}
|
||||||
parentDepartment={state.parentDepartment}
|
parentDepartment={state.parentDepartment}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
|
refreshDepartmentTree={refreshData}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 删除确认对话框 */}
|
{/* 删除确认对话框 */}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function RoleList({
|
|||||||
return status === 'active' ? (
|
return status === 'active' ? (
|
||||||
<Badge className="bg-green-100 text-green-700">启用</Badge>
|
<Badge className="bg-green-100 text-green-700">启用</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge className="bg-gray-100 text-gray-700">禁用</Badge>
|
<Badge className="bg-gray-100 text-gray-700">停用</Badge>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user