生茶难管理系统 员工管理和角色管理 表单组件重构
This commit is contained in:
@@ -1,305 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { Card } from '@/components/ui/card';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
||||||
import {
|
|
||||||
Pagination,
|
|
||||||
PaginationContent,
|
|
||||||
PaginationItem,
|
|
||||||
PaginationLink,
|
|
||||||
PaginationNext,
|
|
||||||
PaginationPrevious,
|
|
||||||
PaginationEllipsis
|
|
||||||
} from '@/components/ui/pagination';
|
|
||||||
import { Eye, Edit, Trash2, UserX, UserCheck, CheckCircle, XCircle, Loader2 } from 'lucide-react';
|
|
||||||
import { Employee, UserStatus } from '../types';
|
|
||||||
import { PaginationState } from './employeeApi';
|
|
||||||
|
|
||||||
interface EmployeeListProps {
|
|
||||||
employees: Employee[];
|
|
||||||
loading?: boolean;
|
|
||||||
pagination?: PaginationState;
|
|
||||||
onPageChange?: (page: number) => void;
|
|
||||||
onPageSizeChange?: (size: number) => void;
|
|
||||||
onViewDetail: (employee: Employee) => void;
|
|
||||||
onEdit: (employee: Employee) => void;
|
|
||||||
onToggleStatus: (employee: Employee) => void;
|
|
||||||
onDelete: (id: string) => void;
|
|
||||||
onAudit?: (employee: Employee, action: 'approve' | 'reject') => void;
|
|
||||||
togglingId?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EmployeeList({
|
|
||||||
employees,
|
|
||||||
loading = false,
|
|
||||||
pagination,
|
|
||||||
onPageChange,
|
|
||||||
onPageSizeChange,
|
|
||||||
onViewDetail,
|
|
||||||
onEdit,
|
|
||||||
onToggleStatus,
|
|
||||||
onDelete,
|
|
||||||
onAudit,
|
|
||||||
togglingId
|
|
||||||
}: EmployeeListProps) {
|
|
||||||
const getStatusBadge = (isActive: boolean, status?: UserStatus) => {
|
|
||||||
// 优先使用isActive字段(来自API),其次使用status字段(兼容旧数据)
|
|
||||||
const finalStatus = isActive !== undefined ? (isActive ? 'active' : 'frozen') : status;
|
|
||||||
|
|
||||||
switch (finalStatus) {
|
|
||||||
case 'active':
|
|
||||||
return <Badge className="bg-green-100 text-green-700">正常</Badge>;
|
|
||||||
case 'frozen':
|
|
||||||
return <Badge className="bg-gray-100 text-gray-700">已冻结</Badge>;
|
|
||||||
case 'inactive':
|
|
||||||
return <Badge className="bg-red-100 text-red-700">停用</Badge>;
|
|
||||||
default:
|
|
||||||
return <Badge>{finalStatus}</Badge>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAuditStatusBadge = (auditStatus?: string) => {
|
|
||||||
switch (auditStatus) {
|
|
||||||
case 'pending':
|
|
||||||
return <Badge className="bg-yellow-100 text-yellow-700">待审核</Badge>;
|
|
||||||
case 'approved':
|
|
||||||
return <Badge className="bg-green-100 text-green-700">审核通过</Badge>;
|
|
||||||
case 'rejected':
|
|
||||||
return <Badge className="bg-red-100 text-red-700">已驳回</Badge>;
|
|
||||||
default:
|
|
||||||
return <Badge className="bg-gray-100 text-gray-700">未知</Badge>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Card>
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>姓名</TableHead>
|
|
||||||
<TableHead>用户名</TableHead>
|
|
||||||
<TableHead>电话</TableHead>
|
|
||||||
<TableHead>部门</TableHead>
|
|
||||||
<TableHead>角色</TableHead>
|
|
||||||
<TableHead>账号状态</TableHead>
|
|
||||||
<TableHead>审核状态</TableHead>
|
|
||||||
<TableHead>操作</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{loading && employees.length === 0 ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={8} className="text-center py-8">
|
|
||||||
<div className="flex items-center justify-center gap-2">
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
<span>加载中...</span>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : employees.length === 0 ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={8} className="text-center text-muted-foreground py-8">
|
|
||||||
暂无数据
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : (
|
|
||||||
employees.map((employee) => (
|
|
||||||
<TableRow key={employee.id} className={loading ? 'opacity-50' : ''}>
|
|
||||||
<TableCell>{employee.displayName || employee.name || employee.username}</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground">{employee.username}</TableCell>
|
|
||||||
<TableCell>{employee.phone || '-'}</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground">{employee.departmentName || employee.department || '-'}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{employee.roles && employee.roles.length > 0
|
|
||||||
? employee.roles.join(', ')
|
|
||||||
: '-'}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{getStatusBadge(employee.isActive, employee.status)}</TableCell>
|
|
||||||
<TableCell>{getAuditStatusBadge(employee.auditStatus)}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
{employee.auditStatus === 'pending' && onAudit && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onAudit(employee, 'approve')}
|
|
||||||
title="审核通过"
|
|
||||||
>
|
|
||||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onAudit(employee, 'reject')}
|
|
||||||
title="驳回"
|
|
||||||
>
|
|
||||||
<XCircle className="w-4 h-4 text-red-600" />
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onViewDetail(employee)}
|
|
||||||
>
|
|
||||||
<Eye className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>查看详情</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onEdit(employee)}
|
|
||||||
>
|
|
||||||
<Edit className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>编辑员工</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onToggleStatus(employee)}
|
|
||||||
disabled={togglingId === employee.id}
|
|
||||||
>
|
|
||||||
{togglingId === employee.id ? (
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
) : (employee.isActive || employee.status === 'active') ? (
|
|
||||||
<UserX className="w-4 h-4 text-orange-600" />
|
|
||||||
) : (
|
|
||||||
<UserCheck className="w-4 h-4 text-green-600" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{(employee.isActive || employee.status === 'active') ? '停用员工' : '激活员工'}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onDelete(employee.id)}
|
|
||||||
disabled={togglingId === employee.id}
|
|
||||||
>
|
|
||||||
{togglingId === employee.id ? (
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Trash2 className="w-4 h-4 text-destructive" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>删除员工</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
{/* 分页组件 */}
|
|
||||||
{pagination && (
|
|
||||||
<div className="flex items-center justify-between px-2 py-4">
|
|
||||||
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
|
||||||
<span>显示第 {((pagination.page - 1) * pagination.size) + 1} - {Math.min(pagination.page * pagination.size, pagination.total)} 条,共 {pagination.total} 条</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<div className="flex items-center space-x-2 text-sm">
|
|
||||||
<span className="whitespace-nowrap">每页显示</span>
|
|
||||||
<Select
|
|
||||||
value={pagination.size.toString()}
|
|
||||||
onValueChange={(value) => onPageSizeChange?.(parseInt(value))}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 w-[70px]">
|
|
||||||
<SelectValue placeholder={pagination.size.toString()} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent side="top">
|
|
||||||
{[10, 20, 30, 40, 50].map((size) => (
|
|
||||||
<SelectItem key={size} value={size.toString()}>
|
|
||||||
{size}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<span>条</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Pagination>
|
|
||||||
<PaginationContent>
|
|
||||||
<PaginationItem>
|
|
||||||
<PaginationPrevious
|
|
||||||
className={pagination.hasPrev ? "cursor-pointer" : "pointer-events-none opacity-50"}
|
|
||||||
onClick={() => pagination.hasPrev && onPageChange?.(pagination.page - 1)}
|
|
||||||
/>
|
|
||||||
</PaginationItem>
|
|
||||||
|
|
||||||
{/* 生成页码 */}
|
|
||||||
{Array.from({ length: Math.min(pagination.totalPages, 5) }, (_, i) => {
|
|
||||||
let pageNum;
|
|
||||||
if (pagination.totalPages <= 5) {
|
|
||||||
pageNum = i + 1;
|
|
||||||
} else if (pagination.page <= 3) {
|
|
||||||
pageNum = i + 1;
|
|
||||||
} else if (pagination.page >= pagination.totalPages - 2) {
|
|
||||||
pageNum = pagination.totalPages - 4 + i;
|
|
||||||
} else {
|
|
||||||
pageNum = pagination.page - 2 + i;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PaginationItem key={pageNum}>
|
|
||||||
<PaginationLink
|
|
||||||
isActive={pageNum === pagination.page}
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => onPageChange?.(pageNum)}
|
|
||||||
>
|
|
||||||
{pageNum}
|
|
||||||
</PaginationLink>
|
|
||||||
</PaginationItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{pagination.totalPages > 5 && pagination.page < pagination.totalPages - 2 && (
|
|
||||||
<PaginationItem>
|
|
||||||
<PaginationEllipsis />
|
|
||||||
</PaginationItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<PaginationItem>
|
|
||||||
<PaginationNext
|
|
||||||
className={pagination.hasNext ? "cursor-pointer" : "pointer-events-none opacity-50"}
|
|
||||||
onClick={() => pagination.hasNext && onPageChange?.(pagination.page + 1)}
|
|
||||||
/>
|
|
||||||
</PaginationItem>
|
|
||||||
</PaginationContent>
|
|
||||||
</Pagination>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { Card } from '@/components/ui/card';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
||||||
import { Search } from 'lucide-react';
|
|
||||||
import { EmployeeFilters } from '../types';
|
|
||||||
|
|
||||||
interface EmployeeManagementFiltersProps {
|
|
||||||
filters: EmployeeFilters;
|
|
||||||
onFiltersChange?: (filters: EmployeeFilters) => void;
|
|
||||||
onSearchChange?: (searchKeyword: string) => void;
|
|
||||||
onStatusFilterChange?: (statusFilter: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EmployeeManagementFilters({
|
|
||||||
filters,
|
|
||||||
onFiltersChange,
|
|
||||||
onSearchChange,
|
|
||||||
onStatusFilterChange
|
|
||||||
}: EmployeeManagementFiltersProps) {
|
|
||||||
const updateFilter = (key: keyof EmployeeFilters, value: string) => {
|
|
||||||
// 优先使用新的回调函数
|
|
||||||
if (key === 'searchKeyword' && onSearchChange) {
|
|
||||||
onSearchChange(value);
|
|
||||||
} else if (key === 'statusFilter' && onStatusFilterChange) {
|
|
||||||
onStatusFilterChange(value);
|
|
||||||
} else if (onFiltersChange) {
|
|
||||||
onFiltersChange({
|
|
||||||
...filters,
|
|
||||||
[key]: value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="p-4">
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="relative">
|
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
placeholder="搜索员工姓名、用户名、电话、部门..."
|
|
||||||
value={filters.searchKeyword}
|
|
||||||
onChange={(e) => updateFilter('searchKeyword', e.target.value)}
|
|
||||||
className="pl-10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Select value={filters.statusFilter} onValueChange={(value) => updateFilter('statusFilter', value)}>
|
|
||||||
<SelectTrigger className="w-40">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">全部状态</SelectItem>
|
|
||||||
<SelectItem value="active">正常</SelectItem>
|
|
||||||
<SelectItem value="frozen">已冻结</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 员工管理页面 - 企业员工账户管理页面
|
||||||
|
* 功能:员工列表查询、添加编辑、状态管理、角色分配
|
||||||
|
* 路径:/central-config/user/employee
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用事件驱动模式,SearchFormPagination重构
|
||||||
|
*/
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Plus, Eye, Edit, Trash2, Power, PowerOff } from 'lucide-react';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
|
||||||
import { EmployeeManagementHeader } from './components/EmployeeManagementHeader';
|
import { EmployeeManagementHeader } from './components/EmployeeManagementHeader';
|
||||||
import { EmployeeManagementStatsCards } from './components/EmployeeManagementStatsCards';
|
import { EmployeeManagementStatsCards } from './components/EmployeeManagementStatsCards';
|
||||||
import { EmployeeManagementFilters } from './components/EmployeeManagementFilters';
|
|
||||||
import { EmployeeList } from './components/EmployeeList';
|
|
||||||
import { EmployeeFormDialog } from './components/EmployeeFormDialog';
|
import { EmployeeFormDialog } from './components/EmployeeFormDialog';
|
||||||
import { EmployeeDetailDialog } from './components/EmployeeDetailDialog';
|
import { EmployeeDetailDialog } from './components/EmployeeDetailDialog';
|
||||||
|
import { SearchFormPagination, type SearchFieldConfig, type TableColumnConfig } from '@/components/common/searchFormPagination';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -18,9 +26,8 @@ import {
|
|||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
|
||||||
} from '@/components/ui/alert-dialog';
|
} from '@/components/ui/alert-dialog';
|
||||||
import { Employee, Role, EmployeeFilters, EmployeeFormData } from './types';
|
import { Employee, Role, EmployeeFormData } from './types';
|
||||||
import {
|
import {
|
||||||
fetchEmployees,
|
fetchEmployees,
|
||||||
transformEmployeesList,
|
transformEmployeesList,
|
||||||
@@ -46,6 +53,7 @@ export default function EmployeeManagementPage() {
|
|||||||
const [creating, setCreating] = useState(false);
|
const [creating, setCreating] = useState(false);
|
||||||
const [updating, setUpdating] = useState(false);
|
const [updating, setUpdating] = useState(false);
|
||||||
const [toggling, setToggling] = useState<string | null>(null); // 记录正在操作的用户ID
|
const [toggling, setToggling] = useState<string | null>(null); // 记录正在操作的用户ID
|
||||||
|
const isFirstLoad = useRef(true);
|
||||||
|
|
||||||
// 确认对话框状态
|
// 确认对话框状态
|
||||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||||
@@ -60,9 +68,9 @@ export default function EmployeeManagementPage() {
|
|||||||
hasNext: false,
|
hasNext: false,
|
||||||
hasPrev: false,
|
hasPrev: false,
|
||||||
});
|
});
|
||||||
const [filters, setFilters] = useState<EmployeeFilters>({
|
const [searchFilters, setSearchFilters] = useState<Record<string, string>>({
|
||||||
searchKeyword: '',
|
search: '',
|
||||||
statusFilter: 'all'
|
status: 'all'
|
||||||
});
|
});
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
const [showDetailDialog, setShowDetailDialog] = useState(false);
|
const [showDetailDialog, setShowDetailDialog] = useState(false);
|
||||||
@@ -79,60 +87,198 @@ export default function EmployeeManagementPage() {
|
|||||||
address: '',
|
address: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
// 搜索字段配置
|
||||||
loadEmployees();
|
const searchFields: SearchFieldConfig[] = [
|
||||||
loadRoles();
|
{
|
||||||
}, [pagination.page, pagination.size,filters.searchKeyword, filters.statusFilter]);
|
key: 'search',
|
||||||
|
label: '搜索',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: '搜索用户名、姓名、手机号...',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
label: '账户状态',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 'all',
|
||||||
|
options: [
|
||||||
|
{ value: 'all', label: '全部状态' },
|
||||||
|
{ value: 'active', label: '正常' },
|
||||||
|
{ value: 'frozen', label: '停用' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const loadRoles = async () => {
|
// 表格列配置
|
||||||
|
const columns: TableColumnConfig[] = [
|
||||||
|
{
|
||||||
|
key: 'username',
|
||||||
|
label: '用户名',
|
||||||
|
render: (value: string) => (
|
||||||
|
<div className="font-medium text-foreground">{value}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'fullName',
|
||||||
|
label: '姓名',
|
||||||
|
render: (value: string) => (
|
||||||
|
<div className="text-foreground">{value || '-'}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'phone',
|
||||||
|
label: '手机号',
|
||||||
|
render: (value: string) => (
|
||||||
|
<div className="font-mono text-sm">{value || '-'}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'roles',
|
||||||
|
label: '角色',
|
||||||
|
render: (value: string[]) => (
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{value && value.length > 0 ? (
|
||||||
|
value.map((role, index) => (
|
||||||
|
<Badge key={index} variant="secondary" className="font-light">
|
||||||
|
{role}
|
||||||
|
</Badge>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">-</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'department',
|
||||||
|
label: '部门',
|
||||||
|
render: (value: string) => (
|
||||||
|
<div className="text-sm text-muted-foreground">{value || '-'}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'isActive',
|
||||||
|
label: '状态',
|
||||||
|
render: (value: boolean) => (
|
||||||
|
<Badge className={`font-light ${
|
||||||
|
value
|
||||||
|
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
||||||
|
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
|
||||||
|
}`}>
|
||||||
|
{value ? '正常' : '停用'}
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'createdAt',
|
||||||
|
label: '创建时间',
|
||||||
|
render: (value: string) => (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{value ? new Date(value).toLocaleDateString('zh-CN') : '-'}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'actions',
|
||||||
|
label: '操作',
|
||||||
|
render: (_: any, row: Employee) => (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleViewDetail(row)}
|
||||||
|
className="h-8 px-2"
|
||||||
|
>
|
||||||
|
<Eye className="w-3 h-3 mr-1" />
|
||||||
|
查看
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleEdit(row)}
|
||||||
|
className="h-8 px-2"
|
||||||
|
>
|
||||||
|
<Edit className="w-3 h-3 mr-1" />
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleToggleStatus(row)}
|
||||||
|
disabled={toggling === row.id}
|
||||||
|
className={`h-8 px-2 ${
|
||||||
|
row.isActive
|
||||||
|
? 'text-orange-600 border-orange-300 hover:bg-orange-50'
|
||||||
|
: 'text-green-600 border-green-300 hover:bg-green-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{row.isActive ? (
|
||||||
|
<PowerOff className="w-3 h-3 mr-1" />
|
||||||
|
) : (
|
||||||
|
<Power className="w-3 h-3 mr-1" />
|
||||||
|
)}
|
||||||
|
{row.isActive ? '停用' : '启用'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleDelete(row.id)}
|
||||||
|
disabled={toggling === row.id}
|
||||||
|
className="h-8 px-2 text-red-600 border-red-300 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3 h-3 mr-1" />
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 加载角色数据 - 事件驱动
|
||||||
|
const loadRoles = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
// 调用角色API获取角色数据
|
|
||||||
const response = await fetchRoles({
|
const response = await fetchRoles({
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 100, // 获取所有角色
|
size: 100,
|
||||||
sort_order: 'desc'
|
sort_order: 'desc'
|
||||||
});
|
});
|
||||||
|
|
||||||
// 转换数据格式
|
|
||||||
const transformedRoles = transformRolesList(response.data);
|
const transformedRoles = transformRolesList(response.data);
|
||||||
setRoles(transformedRoles);
|
setRoles(transformedRoles);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load roles:', error);
|
console.error('Failed to load roles:', error);
|
||||||
// API失败时设置为空数组
|
|
||||||
setRoles([]);
|
setRoles([]);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const loadEmployees = async () => {
|
// 加载员工数据 - 事件驱动,移除依赖项
|
||||||
setLoading(true);
|
const loadEmployees = useCallback(async (params?: {
|
||||||
|
filters?: Record<string, string>;
|
||||||
|
pagination?: { page: number; size: number };
|
||||||
|
resetPage?: boolean;
|
||||||
|
}) => {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true);
|
||||||
const queryParams: EmployeesQueryParams = {
|
const queryParams: EmployeesQueryParams = {
|
||||||
page: pagination.page,
|
page: params?.resetPage ? 1 : (params?.pagination?.page || pagination.page),
|
||||||
size: pagination.size,
|
size: params?.pagination?.size || pagination.size,
|
||||||
sort_order: 'desc'
|
sort_order: 'desc'
|
||||||
};
|
};
|
||||||
|
|
||||||
// 如果有搜索关键词,添加到查询参数
|
// 搜索关键词
|
||||||
if (filters.searchKeyword) {
|
const searchKeyword = params?.filters?.search ?? searchFilters.search;
|
||||||
queryParams.search = filters.searchKeyword;
|
if (searchKeyword) {
|
||||||
}
|
queryParams.search = searchKeyword;
|
||||||
|
|
||||||
// 如果有状态筛选,添加到查询参数
|
|
||||||
if (filters.statusFilter !== 'all') {
|
|
||||||
// 注意:API可能不支持直接的状态筛选,这里暂时在客户端过滤
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetchEmployees(queryParams);
|
const response = await fetchEmployees(queryParams);
|
||||||
|
|
||||||
// 转换数据格式
|
|
||||||
const transformedEmployees = transformEmployeesList(response.data);
|
const transformedEmployees = transformEmployeesList(response.data);
|
||||||
|
|
||||||
// 应用状态筛选(如果API不支持)
|
// 状态筛选(客户端过滤)
|
||||||
const filteredEmployees = filters.statusFilter === 'all'
|
const statusFilter = params?.filters?.status ?? searchFilters.status;
|
||||||
|
const filteredEmployees = statusFilter === 'all'
|
||||||
? transformedEmployees
|
? transformedEmployees
|
||||||
: transformedEmployees.filter(emp => {
|
: transformedEmployees.filter(emp => {
|
||||||
const status = emp.isActive ? 'active' : 'frozen';
|
const status = emp.isActive ? 'active' : 'frozen';
|
||||||
return status === filters.statusFilter;
|
return status === statusFilter;
|
||||||
});
|
});
|
||||||
|
|
||||||
setEmployees(filteredEmployees);
|
setEmployees(filteredEmployees);
|
||||||
@@ -147,35 +293,45 @@ export default function EmployeeManagementPage() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load employees:', error);
|
console.error('Failed to load employees:', error);
|
||||||
toast.error('加载员工数据失败');
|
toast.error('加载员工数据失败');
|
||||||
// 如果API失败,使用localStorage中的数据
|
|
||||||
setEmployees([]);
|
setEmployees([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [pagination.page, pagination.size, searchFilters]);
|
||||||
|
|
||||||
// 搜索处理函数
|
// 初始化数据 - 只在组件挂载时执行一次
|
||||||
const handleSearch = (searchKeyword: string) => {
|
useEffect(() => {
|
||||||
setFilters(prev => ({ ...prev, searchKeyword }));
|
if (isFirstLoad.current) {
|
||||||
// 重置到第一页
|
isFirstLoad.current = false;
|
||||||
setPagination(prev => ({ ...prev, page: 1 }));
|
loadRoles();
|
||||||
};
|
loadEmployees({ resetPage: true });
|
||||||
|
}
|
||||||
|
}, [loadRoles, loadEmployees]);
|
||||||
|
|
||||||
// 状态筛选处理函数
|
// 事件处理器 - 事件驱动模式
|
||||||
const handleStatusFilter = (statusFilter: string) => {
|
const handleSearch = useCallback((filters: Record<string, string>) => {
|
||||||
setFilters(prev => ({ ...prev, statusFilter }));
|
setSearchFilters(filters);
|
||||||
// 重置到第一页
|
loadEmployees({
|
||||||
setPagination(prev => ({ ...prev, page: 1 }));
|
filters,
|
||||||
};
|
pagination: { page: 1, size: pagination.size }
|
||||||
|
});
|
||||||
|
}, [loadEmployees, pagination.size]);
|
||||||
|
|
||||||
// 分页处理函数
|
const handlePageChange = useCallback((page: number) => {
|
||||||
const handlePageChange = (page: number) => {
|
|
||||||
setPagination(prev => ({ ...prev, page }));
|
setPagination(prev => ({ ...prev, page }));
|
||||||
};
|
loadEmployees({
|
||||||
|
filters: searchFilters,
|
||||||
|
pagination: { page, size: pagination.size }
|
||||||
|
});
|
||||||
|
}, [loadEmployees, searchFilters, pagination.size]);
|
||||||
|
|
||||||
const handlePageSizeChange = (size: number) => {
|
const handleSizeChange = useCallback((size: number) => {
|
||||||
setPagination(prev => ({ ...prev, size, page: 1 }));
|
setPagination(prev => ({ ...prev, size, page: 1 }));
|
||||||
};
|
loadEmployees({
|
||||||
|
filters: searchFilters,
|
||||||
|
pagination: { page: 1, size }
|
||||||
|
});
|
||||||
|
}, [loadEmployees, searchFilters]);
|
||||||
|
|
||||||
const handleAddEmployee = () => {
|
const handleAddEmployee = () => {
|
||||||
setEditingEmployee(null);
|
setEditingEmployee(null);
|
||||||
@@ -185,7 +341,7 @@ export default function EmployeeManagementPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const clearForm = () => {
|
const clearForm = () => {
|
||||||
// 先设置一个空的表单对象
|
// 直接设置空表单,无需setTimeout
|
||||||
const emptyForm = {
|
const emptyForm = {
|
||||||
enterpriseId: 'ent-2',
|
enterpriseId: 'ent-2',
|
||||||
enterpriseName: '丰收现代农业集团',
|
enterpriseName: '丰收现代农业集团',
|
||||||
@@ -199,14 +355,7 @@ export default function EmployeeManagementPage() {
|
|||||||
department: '',
|
department: '',
|
||||||
position: '',
|
position: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 强制清空表单
|
|
||||||
setFormData(emptyForm);
|
setFormData(emptyForm);
|
||||||
|
|
||||||
// 使用setTimeout确保状态更新完成
|
|
||||||
setTimeout(() => {
|
|
||||||
setFormData({...emptyForm});
|
|
||||||
}, 0);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (employee: Employee) => {
|
const handleEdit = (employee: Employee) => {
|
||||||
@@ -268,8 +417,8 @@ export default function EmployeeManagementPage() {
|
|||||||
setShowForm(false);
|
setShowForm(false);
|
||||||
clearForm();
|
clearForm();
|
||||||
|
|
||||||
// 刷新员工列表数据
|
// 立即刷新员工列表数据,无需延迟
|
||||||
await loadEmployees();
|
loadEmployees({ resetPage: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新员工失败:', error);
|
console.error('更新员工失败:', error);
|
||||||
|
|
||||||
@@ -307,8 +456,8 @@ export default function EmployeeManagementPage() {
|
|||||||
setShowForm(false);
|
setShowForm(false);
|
||||||
clearForm();
|
clearForm();
|
||||||
|
|
||||||
// 刷新员工列表数据
|
// 立即刷新员工列表数据,无需延迟
|
||||||
await loadEmployees();
|
loadEmployees({ resetPage: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('创建员工失败:', error);
|
console.error('创建员工失败:', error);
|
||||||
|
|
||||||
@@ -353,8 +502,8 @@ export default function EmployeeManagementPage() {
|
|||||||
|
|
||||||
toast.success('用户删除成功');
|
toast.success('用户删除成功');
|
||||||
|
|
||||||
// 刷新列表确保数据同步
|
// 立即刷新列表确保数据同步
|
||||||
await loadEmployees();
|
loadEmployees({ resetPage: true });
|
||||||
|
|
||||||
// 关闭确认对话框
|
// 关闭确认对话框
|
||||||
setDeleteConfirmOpen(false);
|
setDeleteConfirmOpen(false);
|
||||||
@@ -402,8 +551,8 @@ export default function EmployeeManagementPage() {
|
|||||||
toast.success('账户已激活');
|
toast.success('账户已激活');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 成功后刷新列表
|
// 成功后立即刷新列表
|
||||||
await loadEmployees();
|
loadEmployees({ resetPage: true });
|
||||||
|
|
||||||
// 关闭停用确认对话框
|
// 关闭停用确认对话框
|
||||||
if (deactivateConfirmOpen) {
|
if (deactivateConfirmOpen) {
|
||||||
@@ -471,32 +620,35 @@ export default function EmployeeManagementPage() {
|
|||||||
onAddEmployee={handleAddEmployee}
|
onAddEmployee={handleAddEmployee}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 统计卡片 */}
|
{/* 统计卡片 - 保留原有功能 */}
|
||||||
<EmployeeManagementStatsCards employees={employees} />
|
<EmployeeManagementStatsCards employees={employees} />
|
||||||
|
|
||||||
{/* 搜索和筛选 */}
|
{/* 搜索、表格和分页 - 使用重构后的组件 */}
|
||||||
<EmployeeManagementFilters
|
<SearchFormPagination
|
||||||
filters={filters}
|
formTitle="员工列表"
|
||||||
onSearchChange={handleSearch}
|
formRightContent={
|
||||||
onStatusFilterChange={handleStatusFilter}
|
<Button onClick={handleAddEmployee} disabled={loading}>
|
||||||
/>
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
添加员工
|
||||||
{/* 员工列表 */}
|
</Button>
|
||||||
<EmployeeList
|
}
|
||||||
employees={employees}
|
searchFields={searchFields}
|
||||||
|
columns={columns}
|
||||||
|
data={employees}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
error={null}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
onPageSizeChange={handlePageSizeChange}
|
onSizeChange={handleSizeChange}
|
||||||
onViewDetail={handleViewDetail}
|
onSearch={handleSearch}
|
||||||
onEdit={handleEdit}
|
emptyIcon={<div className="w-12 h-12 mx-auto mb-4 opacity-20" />}
|
||||||
onToggleStatus={handleToggleStatus}
|
emptyText="暂无员工数据"
|
||||||
onDelete={handleDelete}
|
showSizeSelector={true}
|
||||||
onAudit={handleAudit}
|
showPageInfo={true}
|
||||||
togglingId={toggling}
|
sizeOptions={[10, 20, 50, 100]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 添加/编辑表单 */}
|
{/* 添加/编辑表单 - 保留原有功能 */}
|
||||||
<EmployeeFormDialog
|
<EmployeeFormDialog
|
||||||
key={formKey} // 使用key强制重新渲染,清除浏览器缓存
|
key={formKey} // 使用key强制重新渲染,清除浏览器缓存
|
||||||
open={showForm}
|
open={showForm}
|
||||||
@@ -511,14 +663,14 @@ export default function EmployeeManagementPage() {
|
|||||||
onClearForm={clearForm}
|
onClearForm={clearForm}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 详情对话框 */}
|
{/* 详情对话框 - 保留原有功能 */}
|
||||||
<EmployeeDetailDialog
|
<EmployeeDetailDialog
|
||||||
open={showDetailDialog}
|
open={showDetailDialog}
|
||||||
onOpenChange={setShowDetailDialog}
|
onOpenChange={setShowDetailDialog}
|
||||||
selectedEmployee={selectedEmployee}
|
selectedEmployee={selectedEmployee}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 删除确认对话框 */}
|
{/* 删除确认对话框 - 保留原有功能 */}
|
||||||
<AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
<AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -552,7 +704,7 @@ export default function EmployeeManagementPage() {
|
|||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
{/* 停用确认对话框 */}
|
{/* 停用确认对话框 - 保留原有功能 */}
|
||||||
<AlertDialog open={deactivateConfirmOpen} onOpenChange={setDeactivateConfirmOpen}>
|
<AlertDialog open={deactivateConfirmOpen} onOpenChange={setDeactivateConfirmOpen}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { Card } from '@/components/ui/card';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
||||||
import { DataPagination } from '@/components/ui/data-pagination';
|
|
||||||
import { Eye, Edit, Trash2, Shield } from 'lucide-react';
|
|
||||||
import { Role, RoleType } from '../types';
|
|
||||||
import { PaginationState } from './roleApi';
|
|
||||||
|
|
||||||
interface RoleListProps {
|
|
||||||
roles: Role[];
|
|
||||||
loading?: boolean;
|
|
||||||
pagination?: PaginationState;
|
|
||||||
onPageChange?: (page: number) => void;
|
|
||||||
onPageSizeChange?: (size: number) => void;
|
|
||||||
onViewDetail: (role: Role) => void;
|
|
||||||
onEdit: (role: Role) => void;
|
|
||||||
onDelete: (id: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RoleList({
|
|
||||||
roles,
|
|
||||||
loading = false,
|
|
||||||
pagination,
|
|
||||||
onPageChange,
|
|
||||||
onPageSizeChange,
|
|
||||||
onViewDetail,
|
|
||||||
onEdit,
|
|
||||||
onDelete
|
|
||||||
}: RoleListProps) {
|
|
||||||
const getRoleTypeBadge = (type: RoleType) => {
|
|
||||||
return type === 'system' ? (
|
|
||||||
<Badge className="bg-blue-100 text-blue-700">系统角色</Badge>
|
|
||||||
) : (
|
|
||||||
<Badge className="bg-green-100 text-green-700">自定义</Badge>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusBadge = (status: string) => {
|
|
||||||
return status === 'active' ? (
|
|
||||||
<Badge className="bg-green-100 text-green-700">启用</Badge>
|
|
||||||
) : (
|
|
||||||
<Badge className="bg-gray-100 text-gray-700">停用</Badge>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>角色名称</TableHead>
|
|
||||||
<TableHead>角色代码</TableHead>
|
|
||||||
<TableHead>角色描述</TableHead>
|
|
||||||
<TableHead>类型</TableHead>
|
|
||||||
<TableHead>状态</TableHead>
|
|
||||||
<TableHead>创建时间</TableHead>
|
|
||||||
<TableHead>操作</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{loading ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={7} className="text-center text-muted-foreground py-8">
|
|
||||||
加载中...
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : roles.length === 0 ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={7} className="text-center text-muted-foreground py-8">
|
|
||||||
暂无数据
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : (
|
|
||||||
roles.map((role) => (
|
|
||||||
<TableRow key={role.id}>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Shield className="w-4 h-4 text-muted-foreground" />
|
|
||||||
{role.name}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground">{role.code}</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground max-w-xs truncate">
|
|
||||||
{role.description}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{getRoleTypeBadge(role.type)}</TableCell>
|
|
||||||
<TableCell>{getStatusBadge(role.status)}</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground">
|
|
||||||
{new Date(role.createdAt).toLocaleDateString('zh-CN')}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onViewDetail(role)}
|
|
||||||
>
|
|
||||||
<Eye className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onEdit(role)}
|
|
||||||
>
|
|
||||||
<Edit className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
{role.type === 'custom' && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onDelete(role.id)}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4 text-destructive" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
{/* 分页控制 */}
|
|
||||||
{!loading && pagination && pagination.total > 0 && (
|
|
||||||
<DataPagination
|
|
||||||
currentPage={pagination.page}
|
|
||||||
totalPages={pagination.totalPages}
|
|
||||||
pageSize={pagination.size}
|
|
||||||
totalItems={pagination.total}
|
|
||||||
startIndex={(pagination.page - 1) * pagination.size + 1}
|
|
||||||
endIndex={Math.min(pagination.page * pagination.size, pagination.total)}
|
|
||||||
onPageChange={(page) => onPageChange?.(page)}
|
|
||||||
onPageSizeChange={(size) => onPageSizeChange?.(size)}
|
|
||||||
canPreviousPage={pagination.hasPrev}
|
|
||||||
canNextPage={pagination.hasNext}
|
|
||||||
pageSizeOptions={[10, 20, 50, 100]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { Card } from '@/components/ui/card';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Search } from 'lucide-react';
|
|
||||||
|
|
||||||
interface RoleSearchProps {
|
|
||||||
searchKeyword: string;
|
|
||||||
onSearchChange: (keyword: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RoleSearch({ searchKeyword, onSearchChange }: RoleSearchProps) {
|
|
||||||
return (
|
|
||||||
<Card className="p-4">
|
|
||||||
<div className="relative">
|
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
placeholder="搜索角色名称、代码、描述..."
|
|
||||||
value={searchKeyword}
|
|
||||||
onChange={(e) => onSearchChange(e.target.value)}
|
|
||||||
className="pl-10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,13 +2,14 @@
|
|||||||
* filekorolheader: 角色管理页面 - 系统角色访问控制管理
|
* filekorolheader: 角色管理页面 - 系统角色访问控制管理
|
||||||
* 功能:角色列表管理、API数据加载、分页查询、角色搜索、详情查看
|
* 功能:角色列表管理、API数据加载、分页查询、角色搜索、详情查看
|
||||||
* 路径:/central-config/user/role
|
* 路径:/central-config/user/role
|
||||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用API调用,shadcn语义化样式,支持翻页
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用SearchFormPagination公共组件,shadcn语义化样式,支持翻页
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -22,12 +23,12 @@ import {
|
|||||||
|
|
||||||
import { RoleManagementHeader } from './components/RoleManagementHeader';
|
import { RoleManagementHeader } from './components/RoleManagementHeader';
|
||||||
import { RoleManagementStatsCards } from './components/RoleManagementStatsCards';
|
import { RoleManagementStatsCards } from './components/RoleManagementStatsCards';
|
||||||
import { RoleSearch } from './components/RoleSearch';
|
|
||||||
import { RoleList } from './components/RoleList';
|
|
||||||
import { RoleFormDialog } from './components/RoleFormDialog';
|
import { RoleFormDialog } from './components/RoleFormDialog';
|
||||||
import { RoleDetailDialog } from './components/RoleDetailDialog';
|
import { RoleDetailDialog } from './components/RoleDetailDialog';
|
||||||
import { RoleManagementInstructions } from './components/RoleManagementInstructions';
|
import { RoleManagementInstructions } from './components/RoleManagementInstructions';
|
||||||
import { Role, RoleFormData, RoleFilters } from './types';
|
import { SearchFormPagination, SearchFieldConfig, TableColumnConfig } from '@/components/common/searchFormPagination';
|
||||||
|
import { Role, RoleFormData } from './types';
|
||||||
|
import { Eye, Edit, Trash2, Shield } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
fetchRoles,
|
fetchRoles,
|
||||||
createRole,
|
createRole,
|
||||||
@@ -51,9 +52,116 @@ export default function RoleManagementPage() {
|
|||||||
hasNext: false,
|
hasNext: false,
|
||||||
hasPrev: false,
|
hasPrev: false,
|
||||||
});
|
});
|
||||||
const [filters, setFilters] = useState<RoleFilters>({
|
const [searchFilters, setSearchFilters] = useState<Record<string, string>>({});
|
||||||
searchKeyword: ''
|
|
||||||
});
|
// 搜索字段配置
|
||||||
|
const searchFields: SearchFieldConfig[] = useMemo(() => [
|
||||||
|
{
|
||||||
|
key: 'search',
|
||||||
|
label: '搜索',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: '搜索角色名称、代码、描述...',
|
||||||
|
},
|
||||||
|
], []);
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
const columns: TableColumnConfig[] = useMemo(() => [
|
||||||
|
{
|
||||||
|
key: 'name',
|
||||||
|
label: '角色名称',
|
||||||
|
sortable: true,
|
||||||
|
render: (value: string, role: Role) => (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Shield className="w-4 h-4 text-muted-foreground" />
|
||||||
|
<span className="font-medium">{value}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'code',
|
||||||
|
label: '角色代码',
|
||||||
|
sortable: true,
|
||||||
|
render: (value: string) => (
|
||||||
|
<span className="text-muted-foreground">{value}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'description',
|
||||||
|
label: '角色描述',
|
||||||
|
render: (value: string) => (
|
||||||
|
<span className="text-muted-foreground max-w-xs truncate block">{value || '-'}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'type',
|
||||||
|
label: '类型',
|
||||||
|
sortable: true,
|
||||||
|
render: (value: string) => (
|
||||||
|
<div className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${
|
||||||
|
value === 'system'
|
||||||
|
? 'bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400'
|
||||||
|
: 'bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-400'
|
||||||
|
}`}>
|
||||||
|
{value === 'system' ? '系统角色' : '自定义'}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
label: '状态',
|
||||||
|
sortable: true,
|
||||||
|
render: (value: string) => (
|
||||||
|
<div className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${
|
||||||
|
value === 'active'
|
||||||
|
? 'bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-400'
|
||||||
|
: 'bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400'
|
||||||
|
}`}>
|
||||||
|
{value === 'active' ? '启用' : '停用'}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'createdAt',
|
||||||
|
label: '创建时间',
|
||||||
|
sortable: true,
|
||||||
|
render: (value: string) => (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{new Date(value).toLocaleDateString('zh-CN')}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'actions',
|
||||||
|
label: '操作',
|
||||||
|
render: (_, role: Role) => (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleViewDetail(role)}
|
||||||
|
>
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleEdit(role)}
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
{role.type === 'custom' && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDelete(role.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
], []);
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
const [showDetailDialog, setShowDetailDialog] = useState(false);
|
const [showDetailDialog, setShowDetailDialog] = useState(false);
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
@@ -69,11 +177,8 @@ export default function RoleManagementPage() {
|
|||||||
permissionIds: [],
|
permissionIds: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
// 加载角色数据
|
||||||
loadRoles();
|
const loadRoles = useCallback(async () => {
|
||||||
}, [pagination.page, pagination.size, filters.searchKeyword]);
|
|
||||||
|
|
||||||
const loadRoles = async () => {
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const queryParams: RolesQueryParams = {
|
const queryParams: RolesQueryParams = {
|
||||||
@@ -82,6 +187,11 @@ export default function RoleManagementPage() {
|
|||||||
sort_order: 'desc'
|
sort_order: 'desc'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 添加搜索条件
|
||||||
|
if (searchFilters.search) {
|
||||||
|
queryParams.search = searchFilters.search;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetchRoles(queryParams);
|
const response = await fetchRoles(queryParams);
|
||||||
|
|
||||||
// 转换数据格式
|
// 转换数据格式
|
||||||
@@ -99,67 +209,37 @@ export default function RoleManagementPage() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load roles:', error);
|
console.error('Failed to load roles:', error);
|
||||||
toast.error('加载角色数据失败');
|
toast.error('加载角色数据失败');
|
||||||
|
|
||||||
// API失败时设置为空数组
|
|
||||||
setRoles([]);
|
setRoles([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [pagination.page, pagination.size, searchFilters]);
|
||||||
|
|
||||||
// 搜索处理函数
|
// 加载数据
|
||||||
const handleSearch = (searchKeyword: string) => {
|
useEffect(() => {
|
||||||
setFilters(prev => ({ ...prev, searchKeyword }));
|
loadRoles();
|
||||||
// 重置到第一页
|
}, [loadRoles]);
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
|
const handleSearch = useCallback((filters: Record<string, string>) => {
|
||||||
|
setSearchFilters(filters);
|
||||||
setPagination(prev => ({ ...prev, page: 1 }));
|
setPagination(prev => ({ ...prev, page: 1 }));
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// 分页处理函数
|
// 分页处理
|
||||||
const handlePageChange = (page: number) => {
|
const handlePageChange = useCallback((page: number) => {
|
||||||
// 确保页码不超出范围
|
if (page < 1) {
|
||||||
const filteredCount = roles.filter(role => {
|
page = 1;
|
||||||
const matchKeyword = !filters.searchKeyword ||
|
} else if (page > pagination.totalPages && pagination.totalPages > 0) {
|
||||||
role.name.includes(filters.searchKeyword) ||
|
page = pagination.totalPages;
|
||||||
role.code.includes(filters.searchKeyword) ||
|
}
|
||||||
(role.description && role.description.includes(filters.searchKeyword));
|
setPagination(prev => ({ ...prev, page }));
|
||||||
return matchKeyword;
|
}, [pagination.totalPages]);
|
||||||
}).length;
|
|
||||||
|
|
||||||
const maxPage = Math.ceil(filteredCount / pagination.size);
|
// 每页条数变化处理
|
||||||
const validPage = Math.min(Math.max(1, page), maxPage || 1);
|
const handleSizeChange = useCallback((size: number) => {
|
||||||
|
|
||||||
setPagination(prev => ({ ...prev, page: validPage }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePageSizeChange = (size: number) => {
|
|
||||||
// 当分页大小改变时,重置到第一页
|
|
||||||
setPagination(prev => ({ ...prev, size, page: 1 }));
|
setPagination(prev => ({ ...prev, size, page: 1 }));
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// 过滤角色数据(客户端过滤,用于搜索)
|
|
||||||
const filteredRoles = roles.filter(role => {
|
|
||||||
const matchKeyword = !filters.searchKeyword ||
|
|
||||||
role.name.includes(filters.searchKeyword) ||
|
|
||||||
role.code.includes(filters.searchKeyword) ||
|
|
||||||
(role.description && role.description.includes(filters.searchKeyword));
|
|
||||||
return matchKeyword;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 客户端分页逻辑
|
|
||||||
const totalFilteredItems = filteredRoles.length;
|
|
||||||
const totalPages = Math.ceil(totalFilteredItems / pagination.size);
|
|
||||||
const startIndex = (pagination.page - 1) * pagination.size;
|
|
||||||
const endIndex = Math.min(startIndex + pagination.size, totalFilteredItems);
|
|
||||||
const paginatedRoles = totalFilteredItems > 0 ? filteredRoles.slice(startIndex, endIndex) : [];
|
|
||||||
|
|
||||||
// 计算分页状态
|
|
||||||
const clientPagination = {
|
|
||||||
...pagination,
|
|
||||||
total: totalFilteredItems,
|
|
||||||
totalPages,
|
|
||||||
hasNext: pagination.page < totalPages,
|
|
||||||
hasPrev: pagination.page > 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddRole = () => {
|
const handleAddRole = () => {
|
||||||
setEditingRole(null);
|
setEditingRole(null);
|
||||||
@@ -472,22 +552,25 @@ export default function RoleManagementPage() {
|
|||||||
{/* 统计卡片 */}
|
{/* 统计卡片 */}
|
||||||
<RoleManagementStatsCards roles={roles} />
|
<RoleManagementStatsCards roles={roles} />
|
||||||
|
|
||||||
{/* 搜索 */}
|
{/* 搜索表单、数据表格和分页 */}
|
||||||
<RoleSearch
|
<SearchFormPagination
|
||||||
searchKeyword={filters.searchKeyword}
|
formTitle="角色列表"
|
||||||
onSearchChange={handleSearch}
|
formRightContent={
|
||||||
/>
|
<Button onClick={handleAddRole}>
|
||||||
|
新建角色
|
||||||
{/* 角色列表 */}
|
</Button>
|
||||||
<RoleList
|
}
|
||||||
roles={paginatedRoles}
|
searchFields={searchFields}
|
||||||
|
columns={columns}
|
||||||
|
data={roles}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={clientPagination}
|
error={null}
|
||||||
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
onPageSizeChange={handlePageSizeChange}
|
onSizeChange={handleSizeChange}
|
||||||
onViewDetail={handleViewDetail}
|
onSearch={handleSearch}
|
||||||
onEdit={handleEdit}
|
emptyText="暂无角色数据"
|
||||||
onDelete={handleDelete}
|
sizeOptions={[10, 20, 50, 100]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 添加/编辑表单 */}
|
{/* 添加/编辑表单 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user