生产管理系统 - 员工管理列表联调

This commit is contained in:
2025-11-04 15:55:29 +08:00
parent aec67101cb
commit fffd37a0a9
23 changed files with 2251 additions and 276 deletions

View File

@@ -0,0 +1,106 @@
/**
* filekorolheader: 部门删除对话框组件 - 部门删除确认界面
* 功能:删除确认、影响说明、操作处理
* 路径:/central-config/user/department/components/DepartmentDeleteDialog
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn/ui组件TypeScript类型安全
*/
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { AlertTriangle } from 'lucide-react';
import { Department } from '../types';
interface DepartmentDeleteDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
deletingDepartment: Department | null;
onConfirm: () => void;
}
export function DepartmentDeleteDialog({
open,
onOpenChange,
deletingDepartment,
onConfirm
}: DepartmentDeleteDialogProps) {
const handleConfirm = async () => {
try {
await onConfirm();
} catch (error) {
console.error('Failed to delete department:', error);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<div className="flex items-center gap-3">
<div className="p-2 rounded-full bg-red-100 dark:bg-red-900">
<AlertTriangle className="w-5 h-5 text-red-600 dark:text-red-400" />
</div>
<div>
<DialogTitle></DialogTitle>
</div>
</div>
</DialogHeader>
<DialogDescription className="text-left">
{deletingDepartment && (
<div className="space-y-4">
{/* 部门信息 */}
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<span className="text-2xl">🏢</span>
<div>
<div className="font-medium">{deletingDepartment.name}</div>
<div className="text-sm text-muted-foreground">{deletingDepartment.code}</div>
{deletingDepartment.manager && (
<div className="text-sm text-muted-foreground">
{deletingDepartment.manager}
</div>
)}
</div>
</div>
{/* 删除影响说明 */}
<div className="p-4 bg-orange-50 dark:bg-orange-950 border border-orange-200 dark:border-orange-800 rounded-lg">
<div className="flex items-start gap-3">
<AlertTriangle className="w-5 h-5 mt-0.5 flex-shrink-0 text-orange-600 dark:text-orange-400" />
<div>
<div className="font-medium mb-2 text-orange-800 dark:text-orange-200">
</div>
<ul className="text-sm space-y-1 text-orange-700 dark:text-orange-300">
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
</div>
</div>
{/* 确认提示 */}
<div className="text-center">
<p className="text-muted-foreground">
<strong>{deletingDepartment.name}</strong>
</p>
</div>
</div>
)}
</DialogDescription>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button
variant="destructive"
onClick={handleConfirm}
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,230 @@
/**
* filekorolheader: 部门表单对话框组件 - 部门添加/编辑表单界面
* 功能:部门信息表单、输入验证、提交处理
* 路径:/central-config/user/department/components/DepartmentFormDialog
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn/ui组件TypeScript类型安全
*/
'use client';
import { useState } from 'react';
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';
interface DepartmentFormDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
editingDepartment: Department | null;
parentDepartment: Department | null;
onSave: (formData: Partial<Department>) => void;
}
export function DepartmentFormDialog({
open,
onOpenChange,
editingDepartment,
parentDepartment,
onSave
}: DepartmentFormDialogProps) {
const [formData, setFormData] = useState<Partial<Department>>({
status: 'active',
sort: 0,
level: parentDepartment ? (parentDepartment.level || 1) + 1 : 1,
parentId: parentDepartment?.id,
});
const [loading, setLoading] = useState(false);
// 当编辑部门或父部门变化时,重置表单数据
useState(() => {
if (editingDepartment) {
setFormData({
...editingDepartment,
children: undefined, // 排除children字段
});
} else {
setFormData({
parentId: parentDepartment?.id,
level: parentDepartment ? (parentDepartment.level || 1) + 1 : 1,
status: 'active',
sort: 0,
});
}
});
const handleInputChange = (field: keyof Department, value: string | number) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => {
if (!formData.name || !formData.code) {
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);
}
};
const handleClose = () => {
if (!loading) {
onOpenChange(false);
// 重置表单
setFormData({
status: 'active',
sort: 0,
level: parentDepartment ? (parentDepartment.level || 1) + 1 : 1,
parentId: parentDepartment?.id,
});
}
};
const title = editingDepartment
? '编辑部门'
: parentDepartment
? `添加子部门(父级:${parentDepartment.name}`
: '添加一级部门';
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription className="sr-only">
{editingDepartment ? '编辑部门信息' : '添加新部门'}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="name"> *</Label>
<Input
id="name"
value={formData.name || ''}
onChange={(e) => handleInputChange('name', e.target.value)}
placeholder="请输入部门名称"
disabled={loading}
/>
</div>
<div>
<Label htmlFor="code"> *</Label>
<Input
id="code"
value={formData.code || ''}
onChange={(e) => handleInputChange('code', e.target.value.toUpperCase())}
placeholder="请输入部门编码TECH"
className="font-mono"
disabled={loading}
/>
</div>
<div>
<Label htmlFor="manager"></Label>
<Input
id="manager"
value={formData.manager || ''}
onChange={(e) => handleInputChange('manager', e.target.value)}
placeholder="请输入负责人姓名"
disabled={loading}
/>
</div>
<div>
<Label htmlFor="phone"></Label>
<Input
id="phone"
value={formData.phone || ''}
onChange={(e) => handleInputChange('phone', e.target.value)}
placeholder="请输入联系电话"
disabled={loading}
/>
</div>
<div>
<Label htmlFor="email"></Label>
<Input
id="email"
type="email"
value={formData.email || ''}
onChange={(e) => handleInputChange('email', e.target.value)}
placeholder="请输入邮箱"
disabled={loading}
/>
</div>
<div>
<Label htmlFor="sort"> *</Label>
<Input
id="sort"
type="number"
value={formData.sort || 0}
onChange={(e) => handleInputChange('sort', parseInt(e.target.value) || 0)}
placeholder="数字越小越靠前"
disabled={loading}
/>
</div>
<div>
<Label htmlFor="status"> *</Label>
<Select
value={formData.status}
onValueChange={(value: 'active' | 'inactive') => handleInputChange('status', value)}
disabled={loading}
>
<SelectTrigger id="status">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="active"></SelectItem>
<SelectItem value="inactive"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div>
<Label htmlFor="description"></Label>
<Input
id="description"
value={formData.description || ''}
onChange={(e) => handleInputChange('description', e.target.value)}
placeholder="请输入部门描述"
disabled={loading}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={handleClose} disabled={loading}>
</Button>
<Button
onClick={handleSubmit}
disabled={loading || !formData.name?.trim() || !formData.code?.trim()}
className="bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700"
>
{loading ? '保存中...' : '保存'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,51 @@
/**
* filekorolheader: 部门管理头部组件 - 页面标题和操作按钮
* 功能:页面标题显示、添加一级部门功能
* 路径:/central-config/user/department/components/DepartmentHeader
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn/ui组件TypeScript类型安全
*/
'use client';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Building2, Plus } from 'lucide-react';
interface DepartmentHeaderProps {
onAdd: () => void;
}
export function DepartmentHeader({ onAdd }: DepartmentHeaderProps) {
return (
<Card className="p-6 bg-gradient-to-r from-green-50 dark:from-green-950 to-emerald-50 dark:to-emerald-950 border-green-200 dark:border-green-800">
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<Building2 className="w-6 h-6 text-green-600 dark:text-green-400 flex-shrink-0 mt-1" />
<div className="flex-1">
<h2 className="mb-2"></h2>
<p className="text-sm text-muted-foreground mb-3">
</p>
<div className="flex flex-wrap gap-2">
<span className="inline-flex items-center text-sm bg-white dark:bg-gray-800 px-3 py-1 rounded-full">
</span>
<span className="inline-flex items-center text-sm bg-white dark:bg-gray-800 px-3 py-1 rounded-full">
</span>
<span className="inline-flex items-center text-sm bg-white dark:bg-gray-800 px-3 py-1 rounded-full">
</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button onClick={onAdd} className="bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700">
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
</Card>
);
}

View File

@@ -0,0 +1,70 @@
/**
* filekorolheader: 部门管理说明组件 - 功能使用说明界面
* 功能:功能说明、操作指引、注意事项
* 路径:/central-config/user/department/components/DepartmentInstructions
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn/ui组件TypeScript类型安全
*/
import { Card } from '@/components/ui/card';
import { Building2, GripVertical, AlertCircle, Users } from 'lucide-react';
export function DepartmentInstructions() {
return (
<Card className="p-4 bg-blue-50 dark:bg-blue-950/30 border-blue-200 dark:border-blue-900">
<div className="flex items-center gap-2 mb-3">
<Building2 className="w-5 h-5 text-blue-900 dark:text-blue-400" />
<h4 className="text-blue-900 dark:text-blue-400 font-medium"></h4>
</div>
<ul className="space-y-2 text-sm text-blue-800 dark:text-blue-300">
<li className="flex items-start gap-2">
<span className="text-blue-600 dark:text-blue-400 mt-0.5"></span>
<div>
<strong className="text-blue-900 dark:text-blue-400"></strong>
</div>
</li>
<li className="flex items-start gap-2">
<GripVertical className="w-4 h-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
<div>
<strong className="text-blue-900 dark:text-blue-400"></strong>
</div>
</li>
<li className="flex items-start gap-2">
<span className="text-blue-600 dark:text-blue-400 mt-0.5"></span>
<div>
<strong className="text-blue-900 dark:text-blue-400"></strong>
使TECHADMIN等
</div>
</li>
<li className="flex items-start gap-2">
<Users className="w-4 h-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
<div>
<strong className="text-blue-900 dark:text-blue-400"></strong>
</div>
</li>
<li className="flex items-start gap-2">
<AlertCircle className="w-4 h-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
<div>
<strong className="text-blue-900 dark:text-blue-400"></strong>
</div>
</li>
<li className="flex items-start gap-2">
<span className="text-blue-600 dark:text-blue-400 mt-0.5"></span>
<div>
<strong className="text-blue-900 dark:text-blue-400"></strong>
"停用"
</div>
</li>
</ul>
</Card>
);
}

View File

@@ -0,0 +1,77 @@
/**
* filekorolheader: 部门管理统计卡片组件 - 部门统计数据展示界面
* 功能:一级部门、二级部门、部门总数统计展示
* 路径:/central-config/user/department/components/DepartmentStatsCards
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn/ui组件TypeScript类型安全
*/
'use client';
import { Card } from '@/components/ui/card';
import { Building2, Users, Layers } from 'lucide-react';
import { DepartmentStats } from '../types';
interface DepartmentStatsCardsProps {
stats: DepartmentStats;
loading?: boolean;
}
export function DepartmentStatsCards({
stats,
loading = false
}: DepartmentStatsCardsProps) {
const statsData = [
{
label: '一级部门',
value: stats.level1,
icon: <Building2 className="w-5 h-5" />,
color: 'text-blue-600 dark:text-blue-400',
bg: 'bg-blue-50 dark:bg-blue-950',
},
{
label: '二级部门',
value: stats.level2,
icon: <Users className="w-5 h-5" />,
color: 'text-green-600 dark:text-green-400',
bg: 'bg-green-50 dark:bg-green-950',
},
{
label: '部门总数',
value: stats.total,
icon: <Layers className="w-5 h-5" />,
color: 'text-orange-600 dark:text-orange-400',
bg: 'bg-orange-50 dark:bg-orange-950',
},
];
if (loading) {
return (
<div className="grid grid-cols-3 gap-4">
{statsData.map((_, index) => (
<Card key={index} className="p-4">
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded mb-2"></div>
<div className="h-8 bg-gray-200 rounded"></div>
</div>
</Card>
))}
</div>
);
}
return (
<div className="grid grid-cols-3 gap-4">
{statsData.map((stat, index) => (
<Card key={index} className={`p-4 ${stat.bg}`}>
<div className="flex items-center justify-between mb-2">
<div className="text-sm text-muted-foreground">{stat.label}</div>
<div className={stat.color}>
{stat.icon}
</div>
</div>
<div className={`text-2xl font-bold ${stat.color}`}>{stat.value}</div>
</Card>
))}
</div>
);
}

View File

@@ -0,0 +1,235 @@
/**
* filekorolheader: 部门树组件 - 部门树形结构展示界面
* 功能:部门树形展示、展开收起、拖拽排序、操作按钮
* 路径:/central-config/user/department/components/DepartmentTree
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn/ui组件TypeScript类型安全
*/
import { Department } from '../types';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
ChevronRight,
ChevronDown,
Plus,
Edit,
Trash2,
Building2,
GripVertical
} from 'lucide-react';
interface DepartmentTreeProps {
departments: Department[];
expandedIds: Set<string>;
loading: boolean;
draggedItem: {
dept: Department;
index: number;
parentId?: string;
} | null;
dragOverItem: {
index: number;
parentId?: string;
} | null;
onToggleExpand: (id: string) => void;
onExpandAll: () => void;
onCollapseAll: () => void;
onAdd: (parent?: Department) => void;
onEdit: (dept: Department) => void;
onDelete: (dept: Department) => void;
onDragStart: (dept: Department, index: number, parentId?: string) => void;
onDragEnd: () => void;
onDragOver: (e: React.DragEvent, index: number, parentId?: string) => void;
onDragLeave: () => void;
onDrop: (e: React.DragEvent, hoverIndex: number, parentId?: string) => void;
}
export function DepartmentTree({
departments,
expandedIds,
loading,
draggedItem,
dragOverItem,
onToggleExpand,
onExpandAll,
onCollapseAll,
onAdd,
onEdit,
onDelete,
onDragStart,
onDragEnd,
onDragOver,
onDragLeave,
onDrop
}: DepartmentTreeProps) {
const getStatusBadge = (status: string) => {
return status === 'active' ? (
<Badge className="bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300"></Badge>
) : (
<Badge className="bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300"></Badge>
);
};
// 渲染部门树
const renderDepartmentTree = (items: Department[], level: number = 0, parentId?: string) => {
return items.map((dept, index) => {
const isExpanded = expandedIds.has(dept.id);
const hasChildren = dept.children && dept.children.length > 0;
const indent = level * 24;
const isDragOver = dragOverItem?.index === index && dragOverItem?.parentId === parentId;
const isDragging = draggedItem?.dept.id === dept.id;
return (
<div key={dept.id}>
<div
draggable
onDragStart={(e) => onDragStart(dept, index, parentId)}
onDragEnd={onDragEnd}
onDragOver={(e) => onDragOver(e, index, parentId)}
onDragLeave={onDragLeave}
onDrop={(e) => onDrop(e, index, parentId)}
className={`flex items-center justify-between p-3 border rounded-lg mb-2 transition-all ${
isDragging
? 'opacity-50 bg-muted'
: isDragOver
? 'bg-green-50 dark:bg-green-950/30 border-green-300 dark:border-green-700'
: 'hover:bg-muted/50'
}`}
style={{ marginLeft: `${indent}px`, cursor: 'move' }}
>
<div className="flex items-center gap-3 flex-1">
{/* 拖动手柄 */}
<div className="cursor-grab active:cursor-grabbing" title="拖动调整顺序">
<GripVertical className="w-4 h-4 text-muted-foreground hover:text-green-600 dark:hover:text-green-400" />
</div>
{/* 展开/收起图标 */}
<div className="w-5 flex items-center justify-center">
{hasChildren ? (
<button
onClick={() => onToggleExpand(dept.id)}
className="hover:bg-muted rounded p-0.5"
>
{isExpanded ? (
<ChevronDown className="w-4 h-4 text-muted-foreground" />
) : (
<ChevronRight className="w-4 h-4 text-muted-foreground" />
)}
</button>
) : (
<div className="w-4" />
)}
</div>
{/* 部门图标 */}
<Building2 className="w-5 h-5 text-blue-600 dark:text-blue-400" />
{/* 部门信息 */}
<div className="flex-1">
<div className="flex items-center gap-2">
<span>{dept.name}</span>
<span className="text-xs text-muted-foreground">({dept.code})</span>
</div>
{dept.manager && (
<div className="text-xs text-muted-foreground mt-0.5">
{dept.manager}
{dept.phone && ` · ${dept.phone}`}
</div>
)}
</div>
{/* 状态标签 */}
<div className="flex items-center gap-2">
{getStatusBadge(dept.status)}
<span className="text-xs text-muted-foreground">: {dept.sort}</span>
</div>
</div>
{/* 操作按钮 */}
<div className="flex items-center gap-1 ml-4">
{level < 1 && (
<Button
variant="ghost"
size="sm"
onClick={() => onAdd(dept)}
title="添加子部门"
>
<Plus className="w-4 h-4 text-green-600 dark:text-green-400" />
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={() => onEdit(dept)}
title="编辑"
>
<Edit className="w-4 h-4 text-blue-600 dark:text-blue-400" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onDelete(dept)}
title="删除"
>
<Trash2 className="w-4 h-4 text-red-600 dark:text-red-400" />
</Button>
</div>
</div>
{/* 递归渲染子部门 */}
{hasChildren && isExpanded && (
<div className="mt-2">
{renderDepartmentTree(dept.children!, level + 1, dept.id)}
</div>
)}
</div>
);
});
};
if (loading) {
return (
<Card className="p-6">
<div className="flex items-center justify-center h-96">
<div className="text-muted-foreground">...</div>
</div>
</Card>
);
}
return (
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h3></h3>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={onExpandAll}
>
</Button>
<Button
variant="outline"
size="sm"
onClick={onCollapseAll}
>
</Button>
</div>
</div>
<div className="space-y-2">
{departments.length > 0 ? (
renderDepartmentTree(departments)
) : (
<div className="text-center py-8 text-muted-foreground">
</div>
)}
</div>
</Card>
);
}

View File

@@ -0,0 +1,591 @@
/**
* filekorolheader: 部门管理页面 - 企业部门树形结构管理页面
* 功能:部门树形管理、拖拽排序、增删改查、层级管理
* 路径:/central-config/user/department
* 规范遵循crop-x/docs/开发项目规范.md使用useReducer状态管理API集成shadcn语义化样式
*/
'use client';
import { useReducer, useEffect, useRef } from 'react';
import { toast } from 'sonner';
import { Department } from './types';
import { DepartmentHeader } from './components/DepartmentHeader';
import { DepartmentStatsCards } from './components/DepartmentStatsCards';
import { DepartmentTree } from './components/DepartmentTree';
import { DepartmentFormDialog } from './components/DepartmentFormDialog';
import { DepartmentDeleteDialog } from './components/DepartmentDeleteDialog';
import { DepartmentInstructions } from './components/DepartmentInstructions';
// 部门管理状态管理
interface DepartmentManagementState {
departments: Department[];
expandedIds: Set<string>;
loading: boolean;
error: string | null;
showForm: boolean;
showDeleteDialog: boolean;
editingDepartment: Department | null;
parentDepartment: Department | null;
deletingDepartment: Department | null;
draggedItem: {
dept: Department;
index: number;
parentId?: string;
} | null;
dragOverItem: {
index: number;
parentId?: string;
} | null;
}
type DepartmentManagementAction =
| { type: 'SET_DEPARTMENTS'; payload: Department[] }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string | null }
| { type: 'TOGGLE_EXPAND'; payload: string }
| { type: 'SET_EXPANDED_IDS'; payload: Set<string> }
| { type: 'TOGGLE_FORM'; payload: boolean }
| { type: 'TOGGLE_DELETE_DIALOG'; payload: boolean }
| { type: 'SET_EDITING_DEPARTMENT'; payload: Department | null }
| { type: 'SET_PARENT_DEPARTMENT'; payload: Department | null }
| { type: 'SET_DELETING_DEPARTMENT'; payload: Department | null }
| { type: 'SET_DRAGGED_ITEM'; payload: { dept: Department; index: number; parentId?: string } | null }
| { type: 'SET_DRAG_OVER_ITEM'; payload: { index: number; parentId?: string } | null }
| { type: 'REFRESH_DATA' };
const departmentManagementReducer = (state: DepartmentManagementState, action: DepartmentManagementAction): DepartmentManagementState => {
switch (action.type) {
case 'SET_DEPARTMENTS':
return { ...state, departments: action.payload, loading: false, error: null };
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload, loading: false };
case 'TOGGLE_EXPAND':
const newExpanded = new Set(state.expandedIds);
if (newExpanded.has(action.payload)) {
newExpanded.delete(action.payload);
} else {
newExpanded.add(action.payload);
}
return { ...state, expandedIds: newExpanded };
case 'SET_EXPANDED_IDS':
return { ...state, expandedIds: action.payload };
case 'TOGGLE_FORM':
return { ...state, showForm: !state.showForm };
case 'TOGGLE_DELETE_DIALOG':
return { ...state, showDeleteDialog: !state.showDeleteDialog };
case 'SET_EDITING_DEPARTMENT':
return { ...state, editingDepartment: action.payload };
case 'SET_PARENT_DEPARTMENT':
return { ...state, parentDepartment: action.payload };
case 'SET_DELETING_DEPARTMENT':
return { ...state, deletingDepartment: action.payload };
case 'SET_DRAGGED_ITEM':
return { ...state, draggedItem: action.payload };
case 'SET_DRAG_OVER_ITEM':
return { ...state, dragOverItem: action.payload };
case 'REFRESH_DATA':
return { ...state, error: null };
default:
return state;
}
};
const initialState: DepartmentManagementState = {
departments: [],
expandedIds: new Set(),
loading: false,
error: null,
showForm: false,
showDeleteDialog: false,
editingDepartment: null,
parentDepartment: null,
deletingDepartment: null,
draggedItem: null,
dragOverItem: null,
};
export default function DepartmentManagementPage() {
const [state, dispatch] = useReducer(departmentManagementReducer, initialState);
const isFirstLoad = useRef(true);
// 加载部门数据
const loadDepartments = async () => {
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',
},
],
},
];
dispatch({ type: 'SET_DEPARTMENTS', payload: mockDepartments });
// 默认展开所有一级部门
dispatch({ type: 'SET_EXPANDED_IDS', payload: new Set(mockDepartments.map(d => d.id)) });
} catch (error) {
console.error('Failed to load departments:', error);
dispatch({
type: 'SET_ERROR',
payload: error instanceof Error ? error.message : '加载部门数据失败'
});
}
};
// 统计部门数量
const countDepartments = (depts: Department[]): { level1: number; level2: number; total: number } => {
let level1 = 0;
let level2 = 0;
depts.forEach(dept => {
if (!dept.parentId) {
level1++;
if (dept.children) {
level2 += dept.children.length;
}
}
});
return { level1, level2, total: level1 + level2 };
};
const stats = countDepartments(state.departments);
// 展开/收起部门
const toggleExpand = (id: string) => {
dispatch({ type: 'TOGGLE_EXPAND', payload: id });
};
// 展开全部
const expandAll = () => {
const allIds = new Set<string>();
const collectIds = (depts: Department[]) => {
depts.forEach(dept => {
allIds.add(dept.id);
if (dept.children) {
collectIds(dept.children);
}
});
};
collectIds(state.departments);
dispatch({ type: 'SET_EXPANDED_IDS', payload: allIds });
};
// 收起全部
const collapseAll = () => {
dispatch({ type: 'SET_EXPANDED_IDS', payload: new Set() });
};
// 添加部门
const handleAdd = (parent?: Department) => {
dispatch({ type: 'SET_EDITING_DEPARTMENT', payload: null });
dispatch({ type: 'SET_PARENT_DEPARTMENT', payload: parent || null });
dispatch({ type: 'TOGGLE_FORM', payload: true });
};
// 编辑部门
const handleEdit = (dept: Department) => {
dispatch({ type: 'SET_EDITING_DEPARTMENT', payload: dept });
dispatch({ type: 'SET_PARENT_DEPARTMENT', payload: null });
dispatch({ type: 'TOGGLE_FORM', payload: true });
};
// 删除部门
const handleDelete = (dept: Department) => {
if (dept.children && dept.children.length > 0) {
toast.error('请先删除该部门下的子部门');
return;
}
dispatch({ type: 'SET_DELETING_DEPARTMENT', payload: dept });
dispatch({ type: 'TOGGLE_DELETE_DIALOG', payload: true });
};
// 保存部门
const handleSave = (formData: Partial<Department>) => {
if (!formData.name || !formData.code) {
toast.error('请填写必填项');
return;
}
const now = new Date().toISOString();
if (state.editingDepartment) {
// 更新部门
const updateInTree = (items: Department[]): Department[] => {
return items.map(item => {
if (item.id === state.editingDepartment!.id) {
return {
...item,
...formData,
updatedAt: now,
children: item.children,
} as Department;
}
if (item.children) {
return {
...item,
children: updateInTree(item.children),
};
}
return item;
});
};
const updated = updateInTree(state.departments);
dispatch({ type: 'SET_DEPARTMENTS', payload: updated });
toast.success('部门更新成功');
} else {
// 新增部门
const newDept: Department = {
id: `dept-${Date.now()}`,
...formData as Department,
createdAt: now,
updatedAt: now,
};
if (state.parentDepartment) {
// 添加到父部门下
const addToParent = (items: Department[]): Department[] => {
return items.map(item => {
if (item.id === state.parentDepartment!.id) {
return {
...item,
children: [...(item.children || []), newDept],
};
}
if (item.children) {
return {
...item,
children: addToParent(item.children),
};
}
return item;
});
};
const updated = addToParent(state.departments);
dispatch({ type: 'SET_DEPARTMENTS', payload: updated });
dispatch({ type: 'TOGGLE_EXPAND', payload: state.parentDepartment.id });
} else {
// 添加为一级部门
const updated = [...state.departments, newDept];
dispatch({ type: 'SET_DEPARTMENTS', payload: updated });
}
toast.success('部门添加成功');
}
dispatch({ type: 'TOGGLE_FORM', payload: false });
};
// 确认删除
const confirmDelete = () => {
if (!state.deletingDepartment) return;
const deleteFromTree = (items: Department[]): Department[] => {
return items
.filter(item => item.id !== state.deletingDepartment!.id)
.map(item => {
if (item.children) {
return {
...item,
children: deleteFromTree(item.children),
};
}
return item;
});
};
const updated = deleteFromTree(state.departments);
dispatch({ type: 'SET_DEPARTMENTS', payload: updated });
toast.success('部门删除成功');
dispatch({ type: 'TOGGLE_DELETE_DIALOG', payload: false });
dispatch({ type: 'SET_DELETING_DEPARTMENT', payload: null });
};
// 拖拽功能
const handleDragStart = (dept: Department, index: number, parentId?: string) => {
dispatch({ type: 'SET_DRAGGED_ITEM', payload: { dept, index, parentId } });
};
const handleDragEnd = () => {
dispatch({ type: 'SET_DRAGGED_ITEM', payload: null });
dispatch({ type: 'SET_DRAG_OVER_ITEM', payload: null });
};
const handleDragOver = (e: React.DragEvent, index: number, parentId?: string) => {
e.preventDefault();
if (state.draggedItem && state.draggedItem.parentId === parentId) {
dispatch({ type: 'SET_DRAG_OVER_ITEM', payload: { index, parentId } });
}
};
const handleDragLeave = () => {
dispatch({ type: 'SET_DRAG_OVER_ITEM', payload: null });
};
const handleDrop = (e: React.DragEvent, hoverIndex: number, parentId?: string) => {
e.preventDefault();
if (!state.draggedItem) return;
if (state.draggedItem.parentId !== parentId) {
toast.error('不能跨级别拖动部门');
dispatch({ type: 'SET_DRAG_OVER_ITEM', payload: null });
return;
}
const dragIndex = state.draggedItem.index;
if (dragIndex === hoverIndex) {
dispatch({ type: 'SET_DRAG_OVER_ITEM', payload: null });
return;
}
let updated: Department[];
if (!parentId) {
// 一级部门
const newDepts = [...state.departments];
const [removed] = newDepts.splice(dragIndex, 1);
newDepts.splice(hoverIndex, 0, removed);
updated = newDepts.map((item, index) => ({
...item,
sort: index + 1,
}));
} else {
// 二级部门
const updateInTree = (items: Department[]): Department[] => {
return items.map(item => {
if (item.id === parentId && item.children) {
const newChildren = [...item.children];
const [removed] = newChildren.splice(dragIndex, 1);
newChildren.splice(hoverIndex, 0, removed);
return {
...item,
children: newChildren.map((child, index) => ({
...child,
sort: index + 1,
})),
};
}
if (item.children) {
return {
...item,
children: updateInTree(item.children),
};
}
return item;
});
};
updated = updateInTree(state.departments);
}
dispatch({ type: 'SET_DEPARTMENTS', payload: updated });
toast.success('部门顺序已更新');
dispatch({ type: 'SET_DRAG_OVER_ITEM', payload: null });
};
// 合并所有状态变化,统一处理数据加载
useEffect(() => {
if (isFirstLoad.current) {
// 首次加载
isFirstLoad.current = false;
loadDepartments();
}
}, []);
return (
<div className="space-y-6">
{/* 页面标题 */}
<DepartmentHeader onAdd={() => handleAdd()} />
{/* 统计卡片 */}
<DepartmentStatsCards stats={stats} />
{/* 部门树 */}
<DepartmentTree
departments={state.departments}
expandedIds={state.expandedIds}
loading={state.loading}
draggedItem={state.draggedItem}
dragOverItem={state.dragOverItem}
onToggleExpand={toggleExpand}
onExpandAll={expandAll}
onCollapseAll={collapseAll}
onAdd={handleAdd}
onEdit={handleEdit}
onDelete={handleDelete}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
/>
{/* 表单对话框 */}
<DepartmentFormDialog
open={state.showForm}
onOpenChange={(open) => dispatch({ type: 'TOGGLE_FORM', payload: open })}
editingDepartment={state.editingDepartment}
parentDepartment={state.parentDepartment}
onSave={handleSave}
/>
{/* 删除确认对话框 */}
<DepartmentDeleteDialog
open={state.showDeleteDialog}
onOpenChange={(open) => dispatch({ type: 'TOGGLE_DELETE_DIALOG', payload: open })}
deletingDepartment={state.deletingDepartment}
onConfirm={confirmDelete}
/>
{/* 功能说明 */}
<DepartmentInstructions />
{/* 错误显示 */}
{state.error && (
<div className="p-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg">
<div className="flex items-center gap-2 text-red-600 dark:text-red-400">
<span>{state.error}</span>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,60 @@
/**
* filekorolheader: 部门管理类型定义 - 部门数据类型和接口定义
* 功能TypeScript类型定义、接口规范、数据结构
* 路径:/central-config/user/department/types
* 规范遵循crop-x/docs/开发项目规范.mdTypeScript类型安全
*/
// 部门状态枚举
export type DepartmentStatus = 'active' | 'inactive';
// 部门接口定义
export interface Department {
id: string;
name: string;
code: string;
level: number;
manager?: string;
phone?: string;
email?: string;
description?: string;
sort: number;
status: DepartmentStatus;
parentId?: string;
createdAt: string;
updatedAt: string;
children?: Department[];
}
// 创建部门表单数据类型
export interface CreateDepartmentForm {
name: string;
code: string;
manager?: string;
phone?: string;
email?: string;
description?: string;
sort: number;
status: DepartmentStatus;
parentId?: string;
level: number;
}
// 部门统计数据类型
export interface DepartmentStats {
level1: number;
level2: number;
total: number;
}
// 拖拽项目类型
export interface DraggedItem {
dept: Department;
index: number;
parentId?: string;
}
export interface DragOverItem {
index: number;
parentId?: string;
}