生产管理系统 部门树查询、新增一级部门

This commit is contained in:
2025-11-04 17:55:37 +08:00
parent 1058767515
commit 8974ea802a
6 changed files with 652 additions and 169 deletions

View File

@@ -7,13 +7,20 @@
'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 { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
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 {
open: boolean;
@@ -21,6 +28,7 @@ interface DepartmentFormDialogProps {
editingDepartment: Department | null;
parentDepartment: Department | null;
onSave: (formData: Partial<Department>) => void;
refreshDepartmentTree?: () => void;
}
export function DepartmentFormDialog({
@@ -28,13 +36,20 @@ export function DepartmentFormDialog({
onOpenChange,
editingDepartment,
parentDepartment,
onSave
onSave,
refreshDepartmentTree,
}: DepartmentFormDialogProps) {
const [formData, setFormData] = useState<Partial<Department>>({
const [formData, setFormData] = useState<CreateDepartmentForm>({
name: '',
code: '',
manager: '',
phone: '',
email: '',
description: '',
status: 'active',
sort: 0,
sort: generateRandomOrderIndex(),
parentId: parentDepartment?.id || '',
level: parentDepartment ? (parentDepartment.level || 1) + 1 : 1,
parentId: parentDepartment?.id,
});
const [loading, setLoading] = useState(false);
@@ -43,42 +58,125 @@ export function DepartmentFormDialog({
useState(() => {
if (editingDepartment) {
setFormData({
...editingDepartment,
children: undefined, // 排除children字段
name: editingDepartment.name,
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 {
setFormData({
parentId: parentDepartment?.id,
level: parentDepartment ? (parentDepartment.level || 1) + 1 : 1,
name: '',
code: '',
manager: '',
phone: '',
email: '',
description: '',
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 }));
};
// 表单验证
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 () => {
if (!formData.name || !formData.code) {
// 表单验证
if (!validateForm()) {
return;
}
setLoading(true);
try {
await onSave(formData);
// 重置表单
setFormData({
status: 'active',
sort: 0,
level: parentDepartment ? (parentDepartment.level || 1) + 1 : 1,
parentId: parentDepartment?.id,
});
} catch (error) {
console.error('Failed to save department:', error);
} finally {
setLoading(false);
// 如果是编辑模式使用原有的onSave逻辑
if (editingDepartment) {
setLoading(true);
try {
await onSave(formData);
toast.success('部门更新成功');
onOpenChange(false);
if (refreshDepartmentTree) {
refreshDepartmentTree();
}
} catch (error) {
console.error('Failed to update department:', error);
toast.error('部门更新失败');
} finally {
setLoading(false);
}
} else {
// 创建模式使用API调用带防抖
createDepartmentWithDebounce(formData);
}
};
@@ -87,10 +185,16 @@ export function DepartmentFormDialog({
onOpenChange(false);
// 重置表单
setFormData({
name: '',
code: '',
manager: '',
phone: '',
email: '',
description: '',
status: 'active',
sort: 0,
sort: generateRandomOrderIndex(),
parentId: parentDepartment?.id || '',
level: parentDepartment ? (parentDepartment.level || 1) + 1 : 1,
parentId: parentDepartment?.id,
});
}
};

View File

@@ -9,10 +9,12 @@
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Building2, Plus } from 'lucide-react';
import { Building2, Plus, RefreshCw } from 'lucide-react';
interface DepartmentHeaderProps {
onAdd: () => void;
onRefresh?: () => void;
loading?: boolean;
}
export function DepartmentHeader({ onAdd }: DepartmentHeaderProps) {

View File

@@ -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>;
}

View File

@@ -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);
};
}

View File

@@ -16,6 +16,12 @@ import { DepartmentTree } from './components/DepartmentTree';
import { DepartmentFormDialog } from './components/DepartmentFormDialog';
import { DepartmentDeleteDialog } from './components/DepartmentDeleteDialog';
import { DepartmentInstructions } from './components/DepartmentInstructions';
import {
fetchDepartmentTree,
transformDepartmentList,
flattenDepartments,
type DepartmentTreeState
} from './components/departmentApi';
// 部门管理状态管理
interface DepartmentManagementState {
@@ -116,151 +122,63 @@ export default function DepartmentManagementPage() {
try {
dispatch({ type: 'SET_LOADING', payload: true });
// 暂时使用mock数据后续可以替换为API调用
const mockDepartments: Department[] = [
{
id: 'dept-1',
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',
},
],
},
];
// 使用API调用获取部门树形数据
const response = await fetchDepartmentTree({
include_inactive: false,
include_members: true,
});
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) {
console.error('Failed to load departments:', error);
dispatch({
type: 'SET_ERROR',
payload: error instanceof Error ? error.message : '加载部门数据失败'
});
toast.error('加载部门数据失败');
}
};
@@ -303,6 +221,11 @@ export default function DepartmentManagementPage() {
dispatch({ type: 'SET_EXPANDED_IDS', payload: allIds });
};
// 刷新数据
const refreshData = () => {
loadDepartments();
};
// 收起全部
const collapseAll = () => {
dispatch({ type: 'SET_EXPANDED_IDS', payload: new Set() });
@@ -533,7 +456,7 @@ export default function DepartmentManagementPage() {
return (
<div className="space-y-6">
{/* 页面标题 */}
<DepartmentHeader onAdd={() => handleAdd()} />
<DepartmentHeader onAdd={() => handleAdd()} onRefresh={refreshData} loading={state.loading} />
{/* 统计卡片 */}
<DepartmentStatsCards stats={stats} />
@@ -565,6 +488,7 @@ export default function DepartmentManagementPage() {
editingDepartment={state.editingDepartment}
parentDepartment={state.parentDepartment}
onSave={handleSave}
refreshDepartmentTree={refreshData}
/>
{/* 删除确认对话框 */}

View File

@@ -42,7 +42,7 @@ export function RoleList({
return status === 'active' ? (
<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>
);
};