357 lines
12 KiB
TypeScript
357 lines
12 KiB
TypeScript
import React, { useState, useEffect, useMemo } from 'react';
|
||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { Button } from '@/components/ui/button';
|
||
import { useToast } from '@/components/ui/use-toast';
|
||
import {
|
||
Loader2, Plus, Edit, Trash2, ChevronRight, ChevronDown,
|
||
Building2, Network, CheckCircle, XCircle
|
||
} from 'lucide-react';
|
||
import { getDepartmentTree, getUsers } from './service';
|
||
import type { DepartmentResponse } from './types';
|
||
import type { UserResponse } from '../User/types';
|
||
import request from '@/utils/request';
|
||
import EditDialog from './components/EditDialog';
|
||
import DeleteDialog from './components/DeleteDialog';
|
||
|
||
/**
|
||
* 部门管理页面
|
||
*/
|
||
const DepartmentPage: React.FC = () => {
|
||
const { toast } = useToast();
|
||
const [loading, setLoading] = useState(false);
|
||
const [departmentTree, setDepartmentTree] = useState<DepartmentResponse[]>([]);
|
||
const [users, setUsers] = useState<UserResponse[]>([]);
|
||
const [expandedKeys, setExpandedKeys] = useState<Set<number>>(new Set());
|
||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||
const [editRecord, setEditRecord] = useState<DepartmentResponse>();
|
||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||
const [deleteRecord, setDeleteRecord] = useState<DepartmentResponse | null>(null);
|
||
|
||
// 加载部门树
|
||
const loadDepartmentTree = async () => {
|
||
try {
|
||
setLoading(true);
|
||
const data = await getDepartmentTree();
|
||
setDepartmentTree(data);
|
||
// 默认展开所有节点
|
||
const allIds = new Set<number>();
|
||
const collectIds = (depts: DepartmentResponse[]) => {
|
||
depts.forEach(dept => {
|
||
if (dept.children && dept.children.length > 0) {
|
||
allIds.add(dept.id);
|
||
collectIds(dept.children);
|
||
}
|
||
});
|
||
};
|
||
collectIds(data);
|
||
setExpandedKeys(allIds);
|
||
} catch (error) {
|
||
console.error('加载部门数据失败:', error);
|
||
toast({
|
||
title: '加载失败',
|
||
description: error instanceof Error ? error.message : '未知错误',
|
||
variant: 'destructive'
|
||
});
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
// 加载用户列表
|
||
const loadUsers = async () => {
|
||
try {
|
||
const response = await getUsers();
|
||
setUsers(response.content || []);
|
||
} catch (error) {
|
||
console.error('加载用户列表失败:', error);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
loadDepartmentTree();
|
||
loadUsers();
|
||
}, []);
|
||
|
||
// 新增
|
||
const handleCreate = () => {
|
||
setEditRecord(undefined);
|
||
setEditDialogOpen(true);
|
||
};
|
||
|
||
// 编辑
|
||
const handleEdit = (record: DepartmentResponse) => {
|
||
setEditRecord(record);
|
||
setEditDialogOpen(true);
|
||
};
|
||
|
||
// 删除
|
||
const handleDelete = (record: DepartmentResponse) => {
|
||
setDeleteRecord(record);
|
||
setDeleteDialogOpen(true);
|
||
};
|
||
|
||
const confirmDelete = async () => {
|
||
if (!deleteRecord) return;
|
||
try {
|
||
await request.delete(`/api/v1/department/${deleteRecord.id}`);
|
||
toast({
|
||
title: '删除成功',
|
||
description: `部门 "${deleteRecord.name}" 已删除`,
|
||
});
|
||
loadDepartmentTree();
|
||
setDeleteDialogOpen(false);
|
||
setDeleteRecord(null);
|
||
} catch (error) {
|
||
console.error('删除失败:', error);
|
||
toast({
|
||
title: '删除失败',
|
||
description: error instanceof Error ? error.message : '未知错误',
|
||
variant: 'destructive'
|
||
});
|
||
}
|
||
};
|
||
|
||
// 切换展开/收起
|
||
const toggleExpand = (id: number) => {
|
||
setExpandedKeys(prev => {
|
||
const newSet = new Set(prev);
|
||
if (newSet.has(id)) {
|
||
newSet.delete(id);
|
||
} else {
|
||
newSet.add(id);
|
||
}
|
||
return newSet;
|
||
});
|
||
};
|
||
|
||
// 获取状态徽章
|
||
const getStatusBadge = (enabled: boolean) => {
|
||
return enabled ? (
|
||
<Badge variant="success" className="inline-flex items-center gap-1">
|
||
<CheckCircle className="h-3 w-3" />
|
||
启用
|
||
</Badge>
|
||
) : (
|
||
<Badge variant="secondary" className="inline-flex items-center gap-1">
|
||
<XCircle className="h-3 w-3" />
|
||
禁用
|
||
</Badge>
|
||
);
|
||
};
|
||
|
||
// 递归渲染部门行
|
||
const renderDepartmentRow = (dept: DepartmentResponse, level: number = 0): React.ReactNode[] => {
|
||
const hasChildren = dept.children && dept.children.length > 0;
|
||
const isExpanded = expandedKeys.has(dept.id);
|
||
const rows: React.ReactNode[] = [];
|
||
|
||
// 当前部门行
|
||
rows.push(
|
||
<TableRow key={dept.id} className="hover:bg-muted/50">
|
||
<TableCell>
|
||
<div className="flex items-center gap-2" style={{ paddingLeft: `${level * 24}px` }}>
|
||
{hasChildren ? (
|
||
<button
|
||
onClick={() => toggleExpand(dept.id)}
|
||
className="p-0.5 hover:bg-muted rounded"
|
||
>
|
||
{isExpanded ? (
|
||
<ChevronDown className="h-4 w-4" />
|
||
) : (
|
||
<ChevronRight className="h-4 w-4" />
|
||
)}
|
||
</button>
|
||
) : (
|
||
<span className="w-5" />
|
||
)}
|
||
<span className="font-medium">{dept.name}</span>
|
||
</div>
|
||
</TableCell>
|
||
<TableCell>
|
||
<code className="text-sm bg-muted px-2 py-0.5 rounded">{dept.code}</code>
|
||
</TableCell>
|
||
<TableCell className="text-sm max-w-[200px] truncate" title={dept.description}>
|
||
{dept.description || '-'}
|
||
</TableCell>
|
||
<TableCell className="text-center">{dept.sort}</TableCell>
|
||
<TableCell>{dept.leaderName || '-'}</TableCell>
|
||
<TableCell>{getStatusBadge(dept.enabled)}</TableCell>
|
||
<TableCell className="text-right">
|
||
<div className="flex justify-end gap-2">
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleEdit(dept)}
|
||
title="编辑"
|
||
>
|
||
<Edit className="h-4 w-4" />
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleDelete(dept)}
|
||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||
title="删除"
|
||
disabled={hasChildren}
|
||
>
|
||
<Trash2 className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</TableCell>
|
||
</TableRow>
|
||
);
|
||
|
||
// 递归渲染子部门
|
||
if (hasChildren && isExpanded) {
|
||
dept.children!.forEach(child => {
|
||
rows.push(...renderDepartmentRow(child, level + 1));
|
||
});
|
||
}
|
||
|
||
return rows;
|
||
};
|
||
|
||
// 统计数据
|
||
const stats = useMemo(() => {
|
||
const countDepartments = (depts: DepartmentResponse[]): { total: number; enabled: number; disabled: number } => {
|
||
let total = 0;
|
||
let enabled = 0;
|
||
let disabled = 0;
|
||
|
||
const traverse = (dept: DepartmentResponse) => {
|
||
total++;
|
||
if (dept.enabled) enabled++;
|
||
else disabled++;
|
||
|
||
if (dept.children) {
|
||
dept.children.forEach(traverse);
|
||
}
|
||
};
|
||
|
||
depts.forEach(traverse);
|
||
return { total, enabled, disabled };
|
||
};
|
||
|
||
return countDepartments(departmentTree);
|
||
}, [departmentTree]);
|
||
|
||
return (
|
||
<div className="p-6">
|
||
<div className="mb-6">
|
||
<h1 className="text-3xl font-bold text-foreground">部门管理</h1>
|
||
<p className="text-muted-foreground mt-2">
|
||
管理组织架构,设置部门层级和负责人。
|
||
</p>
|
||
</div>
|
||
|
||
{/* 统计卡片 */}
|
||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||
<Card className="bg-gradient-to-br from-emerald-500/10 to-emerald-500/5 border-emerald-500/20">
|
||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
<CardTitle className="text-sm font-medium text-emerald-700">总部门数</CardTitle>
|
||
<Building2 className="h-4 w-4 text-emerald-500" />
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="text-2xl font-bold">{stats.total}</div>
|
||
<p className="text-xs text-muted-foreground mt-1">所有部门</p>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="bg-gradient-to-br from-green-500/10 to-green-500/5 border-green-500/20">
|
||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
<CardTitle className="text-sm font-medium text-green-700">启用部门</CardTitle>
|
||
<Network className="h-4 w-4 text-green-500" />
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="text-2xl font-bold">{stats.enabled}</div>
|
||
<p className="text-xs text-muted-foreground mt-1">正常使用</p>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="bg-gradient-to-br from-gray-500/10 to-gray-500/5 border-gray-500/20">
|
||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
<CardTitle className="text-sm font-medium text-gray-700">禁用部门</CardTitle>
|
||
<XCircle className="h-4 w-4 text-gray-500" />
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="text-2xl font-bold">{stats.disabled}</div>
|
||
<p className="text-xs text-muted-foreground mt-1">已停用</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
<Card>
|
||
<CardHeader className="flex flex-row items-center justify-between">
|
||
<CardTitle>部门列表</CardTitle>
|
||
<Button onClick={handleCreate}>
|
||
<Plus className="h-4 w-4 mr-2" />
|
||
新增部门
|
||
</Button>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{/* 表格 */}
|
||
<div className="rounded-md border">
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead className="w-[250px]">部门名称</TableHead>
|
||
<TableHead className="w-[150px]">部门编码</TableHead>
|
||
<TableHead className="w-[200px]">部门描述</TableHead>
|
||
<TableHead className="w-[100px] text-center">排序</TableHead>
|
||
<TableHead className="w-[120px]">负责人</TableHead>
|
||
<TableHead className="w-[100px]">状态</TableHead>
|
||
<TableHead className="text-right">操作</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{loading ? (
|
||
<TableRow>
|
||
<TableCell colSpan={7} className="h-24 text-center">
|
||
<div className="flex items-center justify-center">
|
||
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||
<span className="text-sm text-muted-foreground">加载中...</span>
|
||
</div>
|
||
</TableCell>
|
||
</TableRow>
|
||
) : departmentTree.length > 0 ? (
|
||
departmentTree.map(dept => renderDepartmentRow(dept))
|
||
) : (
|
||
<TableRow>
|
||
<TableCell colSpan={7} className="h-24 text-center">
|
||
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||
<Building2 className="w-16 h-16 mb-4 text-muted-foreground/50" />
|
||
<div className="text-lg font-semibold mb-2">暂无部门数据</div>
|
||
<div className="text-sm">点击右上角"新增部门"开始创建部门。</div>
|
||
</div>
|
||
</TableCell>
|
||
</TableRow>
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 编辑对话框 */}
|
||
<EditDialog
|
||
open={editDialogOpen}
|
||
record={editRecord}
|
||
departmentTree={departmentTree}
|
||
users={users}
|
||
onOpenChange={setEditDialogOpen}
|
||
onSuccess={loadDepartmentTree}
|
||
/>
|
||
|
||
{/* 删除确认对话框 */}
|
||
<DeleteDialog
|
||
open={deleteDialogOpen}
|
||
record={deleteRecord}
|
||
onOpenChange={setDeleteDialogOpen}
|
||
onConfirm={confirmDelete}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default DepartmentPage;
|