生产管理系统 - 员工管理列表联调
This commit is contained in:
@@ -31,6 +31,19 @@ export function EmployeeDetailDialog({
|
||||
}
|
||||
};
|
||||
|
||||
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>;
|
||||
}
|
||||
};
|
||||
|
||||
if (!selectedEmployee) return null;
|
||||
|
||||
return (
|
||||
@@ -42,68 +55,106 @@ export function EmployeeDetailDialog({
|
||||
查看员工的详细信息
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>姓名</Label>
|
||||
<div className="mt-1">{selectedEmployee.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>用户名</Label>
|
||||
<div className="mt-1">{selectedEmployee.username}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>电话</Label>
|
||||
<div className="mt-1">{selectedEmployee.phone}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>邮箱</Label>
|
||||
<div className="mt-1">{selectedEmployee.email || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>部门</Label>
|
||||
<div className="mt-1">{selectedEmployee.department || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>职位</Label>
|
||||
<div className="mt-1">{selectedEmployee.position || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>状态</Label>
|
||||
<div className="mt-1">{getStatusBadge(selectedEmployee.status)}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>角色</Label>
|
||||
<div className="mt-1 flex flex-wrap gap-2">
|
||||
{selectedEmployee.roles && selectedEmployee.roles.length > 0 ? (
|
||||
selectedEmployee.roles.map((role, index) => (
|
||||
<Badge key={index} className="bg-purple-100 text-purple-700">
|
||||
{role}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground">未分配角色</span>
|
||||
)}
|
||||
<div className="space-y-4 max-h-96 overflow-y-auto">
|
||||
{/* 基本信息 */}
|
||||
<div>
|
||||
<h4 className="mb-3 text-green-800">基本信息</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>姓名</Label>
|
||||
<div className="mt-1 field-value-inline">{selectedEmployee.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>用户名</Label>
|
||||
<div className="mt-1 field-value-inline">{selectedEmployee.username}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>手机号</Label>
|
||||
<div className="mt-1 field-value-inline">{selectedEmployee.phone}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>邮箱</Label>
|
||||
<div className="mt-1 field-value-inline">{selectedEmployee.email || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
{selectedEmployee.lastLoginTime && (
|
||||
</div>
|
||||
|
||||
{/* 工作信息 */}
|
||||
<div>
|
||||
<h4 className="mb-3 text-green-800">工作信息</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>最后登录</Label>
|
||||
<div className="mt-1">
|
||||
{new Date(selectedEmployee.lastLoginTime).toLocaleString('zh-CN')}
|
||||
<Label>部门</Label>
|
||||
<div className="mt-1 field-value-inline">{selectedEmployee.department || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>账号状态</Label>
|
||||
<div className="mt-1">{getStatusBadge(selectedEmployee.status)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>审核状态</Label>
|
||||
<div className="mt-1">{getAuditStatusBadge(selectedEmployee.auditStatus)}</div>
|
||||
</div>
|
||||
{selectedEmployee.auditReason && (
|
||||
<div>
|
||||
<Label>审核意见</Label>
|
||||
<div className="mt-1 field-value-inline">{selectedEmployee.auditReason}</div>
|
||||
</div>
|
||||
)}
|
||||
{selectedEmployee.auditor && (
|
||||
<div>
|
||||
<Label>审核人</Label>
|
||||
<div className="mt-1 field-value-inline">{selectedEmployee.auditor}</div>
|
||||
</div>
|
||||
)}
|
||||
{selectedEmployee.auditTime && (
|
||||
<div>
|
||||
<Label>审核时间</Label>
|
||||
<div className="mt-1 field-value-inline">{new Date(selectedEmployee.auditTime).toLocaleString('zh-CN')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 角色权限 */}
|
||||
<div>
|
||||
<h4 className="mb-3 text-green-800">角色权限</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedEmployee.roles && selectedEmployee.roles.length > 0 ? (
|
||||
selectedEmployee.roles.map((role, index) => (
|
||||
<Badge key={index} className="bg-purple-100 text-purple-700">
|
||||
{role}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground">未分配角色</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 系统信息 */}
|
||||
<div>
|
||||
<h4 className="mb-3 text-green-800">系统信息</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{selectedEmployee.lastLoginTime && (
|
||||
<div>
|
||||
<Label>最后登录</Label>
|
||||
<div className="mt-1 field-value-inline">
|
||||
{new Date(selectedEmployee.lastLoginTime).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Label>创建时间</Label>
|
||||
<div className="mt-1 field-value-inline">
|
||||
{new Date(selectedEmployee.createdAt).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Label>创建时间</Label>
|
||||
<div className="mt-1">
|
||||
{new Date(selectedEmployee.createdAt).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>更新时间</Label>
|
||||
<div className="mt-1">
|
||||
{new Date(selectedEmployee.updatedAt).toLocaleString('zh-CN')}
|
||||
<div>
|
||||
<Label>更新时间</Label>
|
||||
<div className="mt-1 field-value-inline">
|
||||
{new Date(selectedEmployee.updatedAt).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,62 +37,82 @@ export function EmployeeFormDialog({
|
||||
{editingEmployee ? '编辑员工信息' : '添加新员工'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="username">用户名 *</Label>
|
||||
<Input
|
||||
id="username"
|
||||
value={formData.username || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, username: e.target.value })}
|
||||
placeholder="登录用户名"
|
||||
/>
|
||||
<div className="space-y-4 max-h-96 overflow-y-auto">
|
||||
{/* 基本信息 */}
|
||||
<div>
|
||||
<h4 className="mb-3 text-green-800">基本信息</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="username">用户名 *</Label>
|
||||
<Input
|
||||
id="username"
|
||||
value={formData.username || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, username: e.target.value })}
|
||||
placeholder="登录用户名"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="name">姓名 *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, name: e.target.value })}
|
||||
placeholder="真实姓名"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="phone">手机号 *</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
value={formData.phone || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, phone: e.target.value })}
|
||||
placeholder="11位手机号码"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="email">邮箱</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, email: e.target.value })}
|
||||
placeholder="电子邮箱"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="idCard">身份证号</Label>
|
||||
<Input
|
||||
id="idCard"
|
||||
value={formData.idCard || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, idCard: e.target.value })}
|
||||
placeholder="18位身份证号码"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="address">住址</Label>
|
||||
<Input
|
||||
id="address"
|
||||
value={formData.address || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, address: e.target.value })}
|
||||
placeholder="详细住址"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="name">姓名 *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, name: e.target.value })}
|
||||
placeholder="真实姓名"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="phone">电话 *</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
value={formData.phone || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, phone: e.target.value })}
|
||||
placeholder="手机号码"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="email">邮箱</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, email: e.target.value })}
|
||||
placeholder="电子邮箱"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="department">部门</Label>
|
||||
<Input
|
||||
id="department"
|
||||
value={formData.department || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, department: e.target.value })}
|
||||
placeholder="所属部门"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="position">职位</Label>
|
||||
<Input
|
||||
id="position"
|
||||
value={formData.position || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, position: e.target.value })}
|
||||
placeholder="职位名称"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 工作信息 */}
|
||||
<div>
|
||||
<h4 className="mb-3 text-green-800">工作信息</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="department">部门</Label>
|
||||
<Input
|
||||
id="department"
|
||||
value={formData.department || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, department: e.target.value })}
|
||||
placeholder="所属部门"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,28 +5,52 @@ 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 { Eye, Edit, Lock, Trash2, UserX, UserCheck } from 'lucide-react';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
PaginationEllipsis
|
||||
} from '@/components/ui/pagination';
|
||||
import { Eye, Edit, Lock, 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;
|
||||
onResetPassword: (employee: Employee) => void;
|
||||
onToggleStatus: (employee: Employee) => void;
|
||||
onDelete: (id: string) => void;
|
||||
onAudit?: (employee: Employee, action: 'approve' | 'reject') => void;
|
||||
}
|
||||
|
||||
export function EmployeeList({
|
||||
employees,
|
||||
loading = false,
|
||||
pagination,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
onViewDetail,
|
||||
onEdit,
|
||||
onResetPassword,
|
||||
onToggleStatus,
|
||||
onDelete
|
||||
onDelete,
|
||||
onAudit
|
||||
}: EmployeeListProps) {
|
||||
const getStatusBadge = (status: UserStatus) => {
|
||||
switch (status) {
|
||||
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':
|
||||
@@ -34,7 +58,20 @@ export function EmployeeList({
|
||||
case 'inactive':
|
||||
return <Badge className="bg-red-100 text-red-700">停用</Badge>;
|
||||
default:
|
||||
return <Badge>{status}</Badge>;
|
||||
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>;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -47,14 +84,23 @@ export function EmployeeList({
|
||||
<TableHead>用户名</TableHead>
|
||||
<TableHead>电话</TableHead>
|
||||
<TableHead>部门</TableHead>
|
||||
<TableHead>职位</TableHead>
|
||||
<TableHead>角色</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>账号状态</TableHead>
|
||||
<TableHead>审核状态</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{employees.length === 0 ? (
|
||||
{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">
|
||||
暂无数据
|
||||
@@ -62,20 +108,40 @@ export function EmployeeList({
|
||||
</TableRow>
|
||||
) : (
|
||||
employees.map((employee) => (
|
||||
<TableRow key={employee.id}>
|
||||
<TableCell>{employee.name}</TableCell>
|
||||
<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.department || '-'}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{employee.position || '-'}</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.status)}</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>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -102,7 +168,7 @@ export function EmployeeList({
|
||||
size="sm"
|
||||
onClick={() => onToggleStatus(employee)}
|
||||
>
|
||||
{employee.status === 'active' ? (
|
||||
{(employee.isActive || employee.status === 'active') ? (
|
||||
<UserX className="w-4 h-4 text-orange-600" />
|
||||
) : (
|
||||
<UserCheck className="w-4 h-4 text-green-600" />
|
||||
@@ -122,6 +188,86 @@ export function EmployeeList({
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -9,18 +9,29 @@ import { EmployeeFilters } from '../types';
|
||||
|
||||
interface EmployeeManagementFiltersProps {
|
||||
filters: EmployeeFilters;
|
||||
onFiltersChange: (filters: EmployeeFilters) => void;
|
||||
onFiltersChange?: (filters: EmployeeFilters) => void;
|
||||
onSearchChange?: (searchKeyword: string) => void;
|
||||
onStatusFilterChange?: (statusFilter: string) => void;
|
||||
}
|
||||
|
||||
export function EmployeeManagementFilters({
|
||||
filters,
|
||||
onFiltersChange
|
||||
onFiltersChange,
|
||||
onSearchChange,
|
||||
onStatusFilterChange
|
||||
}: EmployeeManagementFiltersProps) {
|
||||
const updateFilter = (key: keyof EmployeeFilters, value: string) => {
|
||||
onFiltersChange({
|
||||
...filters,
|
||||
[key]: value
|
||||
});
|
||||
// 优先使用新的回调函数
|
||||
if (key === 'searchKeyword' && onSearchChange) {
|
||||
onSearchChange(value);
|
||||
} else if (key === 'statusFilter' && onStatusFilterChange) {
|
||||
onStatusFilterChange(value);
|
||||
} else if (onFiltersChange) {
|
||||
onFiltersChange({
|
||||
...filters,
|
||||
[key]: value
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* filekorolheader: 员工管理API接口 - 员工数据查询接口服务
|
||||
* 功能:API请求封装、数据转换、错误处理、分页查询
|
||||
* 路径:/central-config/user/employee/components/employeeApi
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用SDK API调用,TypeScript类型安全
|
||||
*/
|
||||
|
||||
import { getAuthToken } from "@/utils/token";
|
||||
import { getUsersApiV1UsersGet } from "@/lib/api/sdk.gen";
|
||||
|
||||
// API返回的员工数据类型
|
||||
export interface EmployeeApiData {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
full_name: string | null;
|
||||
phone: string | null;
|
||||
is_active: boolean;
|
||||
is_superuser: boolean;
|
||||
is_verified: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
last_login_at: string | null;
|
||||
avatar_url: string | null;
|
||||
bio: string | null;
|
||||
display_name: string | null;
|
||||
department_id: string | null;
|
||||
department_name: string | null;
|
||||
scope: string;
|
||||
company_name: string | null;
|
||||
}
|
||||
|
||||
// API响应接口
|
||||
export interface EmployeesApiResponse {
|
||||
data: EmployeeApiData[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
total_pages: number;
|
||||
has_next: boolean;
|
||||
has_prev: boolean;
|
||||
}
|
||||
|
||||
// 查询参数接口
|
||||
export interface EmployeesQueryParams {
|
||||
search?: string;
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort_order?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
// 页面使用的员工数据类型(转换后的)
|
||||
export interface Employee {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
fullName: string | null;
|
||||
phone: string | null;
|
||||
isActive: boolean;
|
||||
isSuperuser: boolean;
|
||||
isVerified: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastLoginAt: string | null;
|
||||
avatarUrl: string | null;
|
||||
bio: string | null;
|
||||
displayName: string | null;
|
||||
departmentId: string | null;
|
||||
departmentName: string | null;
|
||||
scope: string;
|
||||
companyName: string | null;
|
||||
tenantId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取员工列表数据
|
||||
*/
|
||||
export async function fetchEmployees(params: EmployeesQueryParams = {}): Promise<EmployeesApiResponse> {
|
||||
try {
|
||||
// 构建查询参数对象
|
||||
const queryParams: any = {};
|
||||
|
||||
if (params.search) queryParams.search = params.search;
|
||||
if (params.page) queryParams.page = params.page;
|
||||
if (params.size) queryParams.size = params.size;
|
||||
if (params.sort_order) queryParams.sort_order = params.sort_order;
|
||||
|
||||
// 默认参数
|
||||
if (!params.page) queryParams.page = 1;
|
||||
if (!params.size) queryParams.size = 10;
|
||||
if (!params.sort_order) queryParams.sort_order = 'desc';
|
||||
|
||||
// 获取认证token
|
||||
const token = getAuthToken();
|
||||
console.log('员工管理API调用参数:', queryParams);
|
||||
|
||||
// 使用SDK API调用用户查询接口
|
||||
const response = await getUsersApiV1UsersGet({
|
||||
query: {
|
||||
...queryParams,
|
||||
// 添加时间戳防止缓存
|
||||
_t: Date.now(),
|
||||
},
|
||||
headers: token ? {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
} : undefined,
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(`API error: ${response.error.message || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
const data = response.data as any;
|
||||
console.log('员工管理API响应:', data);
|
||||
|
||||
// 根据实际API响应格式处理数据
|
||||
// 如果API直接返回数组,我们需要模拟分页响应
|
||||
if (Array.isArray(data)) {
|
||||
// 如果API返回数组,假设是当前页的数据
|
||||
return {
|
||||
data: data,
|
||||
total: data.length, // 这种情况下无法获取总数,使用当前页数据量
|
||||
page: params.page || 1,
|
||||
size: params.size || 10,
|
||||
total_pages: 1, // 无法确定总页数
|
||||
has_next: false,
|
||||
has_prev: (params.page || 1) > 1,
|
||||
};
|
||||
} else if (data && typeof data === 'object' && data.data) {
|
||||
// 如果API返回分页格式(和你提供的响应一致)
|
||||
return {
|
||||
data: data.data || [],
|
||||
total: data.total || 0,
|
||||
page: data.page || 1,
|
||||
size: data.size || 10,
|
||||
total_pages: data.total_pages || 0,
|
||||
has_next: data.has_next || false,
|
||||
has_prev: data.has_prev || false,
|
||||
};
|
||||
} else {
|
||||
// 其他情况,返回空结果
|
||||
return {
|
||||
data: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
size: 10,
|
||||
total_pages: 0,
|
||||
has_next: false,
|
||||
has_prev: false,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch employees:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将API数据转换为页面所需的员工数据格式
|
||||
* 优先显示display_name,其次full_name,最后username
|
||||
*/
|
||||
export function transformEmployeeData(employee: EmployeeApiData): Employee {
|
||||
return {
|
||||
id: employee.id,
|
||||
username: employee.username,
|
||||
email: employee.email,
|
||||
fullName: employee.full_name,
|
||||
phone: employee.phone,
|
||||
isActive: employee.is_active,
|
||||
isSuperuser: employee.is_superuser,
|
||||
isVerified: employee.is_verified,
|
||||
createdAt: formatDate(employee.created_at),
|
||||
updatedAt: formatDate(employee.updated_at),
|
||||
lastLoginAt: employee.last_login_at ? formatDate(employee.last_login_at) : null,
|
||||
avatarUrl: employee.avatar_url,
|
||||
bio: employee.bio,
|
||||
displayName: employee.display_name || employee.full_name || employee.username,
|
||||
departmentId: employee.department_id,
|
||||
departmentName: employee.department_name,
|
||||
scope: employee.scope,
|
||||
companyName: employee.company_name,
|
||||
tenantId: employee.tenant_id,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量转换员工数据
|
||||
*/
|
||||
export function transformEmployeesList(employees: EmployeeApiData[]): Employee[] {
|
||||
return employees.map(transformEmployeeData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
function formatDate(dateString: string): string {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).replace(/\//g, '-');
|
||||
} catch (error) {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination state interface for page components
|
||||
export interface PaginationState {
|
||||
page: number;
|
||||
size: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
hasNext: boolean;
|
||||
hasPrev: boolean;
|
||||
}
|
||||
@@ -10,10 +10,25 @@ import { EmployeeList } from './components/EmployeeList';
|
||||
import { EmployeeFormDialog } from './components/EmployeeFormDialog';
|
||||
import { EmployeeDetailDialog } from './components/EmployeeDetailDialog';
|
||||
import { Employee, Role, EmployeeFilters, EmployeeFormData } from './types';
|
||||
import {
|
||||
fetchEmployees,
|
||||
transformEmployeesList,
|
||||
PaginationState,
|
||||
EmployeesQueryParams
|
||||
} from './components/employeeApi';
|
||||
|
||||
export default function EmployeeManagementPage() {
|
||||
const [employees, setEmployees] = useState<Employee[]>([]);
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
hasNext: false,
|
||||
hasPrev: false,
|
||||
});
|
||||
const [filters, setFilters] = useState<EmployeeFilters>({
|
||||
searchKeyword: '',
|
||||
statusFilter: 'all'
|
||||
@@ -26,13 +41,16 @@ export default function EmployeeManagementPage() {
|
||||
enterpriseId: 'ent-2',
|
||||
enterpriseName: '丰收现代农业集团',
|
||||
status: 'active',
|
||||
auditStatus: 'pending',
|
||||
roleIds: [],
|
||||
idCard: '',
|
||||
address: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadEmployees();
|
||||
loadRoles();
|
||||
}, []);
|
||||
}, [pagination.page, pagination.size,filters.searchKeyword, filters.statusFilter]);
|
||||
|
||||
const loadRoles = () => {
|
||||
const data = localStorage.getItem('smart_agriculture_roles');
|
||||
@@ -41,79 +59,83 @@ export default function EmployeeManagementPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadEmployees = () => {
|
||||
const data = localStorage.getItem('smart_agriculture_employees');
|
||||
if (data) {
|
||||
setEmployees(JSON.parse(data));
|
||||
} else {
|
||||
// 初始化示例数据
|
||||
const mockEmployees: Employee[] = [
|
||||
{
|
||||
id: 'emp-1',
|
||||
enterpriseId: 'ent-2',
|
||||
enterpriseName: '丰收现代农业集团',
|
||||
username: 'zhangsan',
|
||||
name: '张三',
|
||||
phone: '13800138001',
|
||||
email: 'zhangsan@example.com',
|
||||
department: '技术部',
|
||||
position: '农机操作员',
|
||||
roleIds: ['role-3'],
|
||||
roles: ['操作员'],
|
||||
status: 'active',
|
||||
createdAt: '2024-10-01T08:00:00',
|
||||
updatedAt: '2024-10-01T08:00:00',
|
||||
lastLoginTime: '2024-10-14T09:30:00',
|
||||
},
|
||||
{
|
||||
id: 'emp-2',
|
||||
enterpriseId: 'ent-2',
|
||||
enterpriseName: '丰收现代农业集团',
|
||||
username: 'lisi',
|
||||
name: '李四',
|
||||
phone: '13900139002',
|
||||
email: 'lisi@example.com',
|
||||
department: '管理部',
|
||||
position: '部门主管',
|
||||
roleIds: ['role-2'],
|
||||
roles: ['企业管理员'],
|
||||
status: 'active',
|
||||
createdAt: '2024-10-02T10:00:00',
|
||||
updatedAt: '2024-10-02T10:00:00',
|
||||
lastLoginTime: '2024-10-14T08:15:00',
|
||||
},
|
||||
{
|
||||
id: 'emp-3',
|
||||
enterpriseId: 'ent-2',
|
||||
enterpriseName: '丰收现代农业集团',
|
||||
username: 'wangwu',
|
||||
name: '王五',
|
||||
phone: '13700137003',
|
||||
department: '维修部',
|
||||
position: '维修技师',
|
||||
roleIds: ['role-3'],
|
||||
roles: ['操作员'],
|
||||
status: 'frozen',
|
||||
createdAt: '2024-09-28T14:00:00',
|
||||
updatedAt: '2024-10-10T16:00:00',
|
||||
},
|
||||
];
|
||||
localStorage.setItem('smart_agriculture_employees', JSON.stringify(mockEmployees));
|
||||
setEmployees(mockEmployees);
|
||||
const loadEmployees = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const queryParams: EmployeesQueryParams = {
|
||||
page: pagination.page,
|
||||
size: pagination.size,
|
||||
sort_order: 'desc'
|
||||
};
|
||||
|
||||
// 如果有搜索关键词,添加到查询参数
|
||||
if (filters.searchKeyword) {
|
||||
queryParams.search = filters.searchKeyword;
|
||||
}
|
||||
|
||||
// 如果有状态筛选,添加到查询参数
|
||||
if (filters.statusFilter !== 'all') {
|
||||
// 注意:API可能不支持直接的状态筛选,这里暂时在客户端过滤
|
||||
}
|
||||
|
||||
const response = await fetchEmployees(queryParams);
|
||||
|
||||
// 转换数据格式
|
||||
const transformedEmployees = transformEmployeesList(response.data);
|
||||
|
||||
// 应用状态筛选(如果API不支持)
|
||||
const filteredEmployees = filters.statusFilter === 'all'
|
||||
? transformedEmployees
|
||||
: transformedEmployees.filter(emp => {
|
||||
const status = emp.isActive ? 'active' : 'frozen';
|
||||
return status === filters.statusFilter;
|
||||
});
|
||||
|
||||
setEmployees(filteredEmployees);
|
||||
setPagination({
|
||||
page: response.page,
|
||||
size: response.size,
|
||||
total: response.total,
|
||||
totalPages: response.total_pages,
|
||||
hasNext: response.has_next,
|
||||
hasPrev: response.has_prev,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load employees:', error);
|
||||
toast.error('加载员工数据失败');
|
||||
|
||||
// 如果API失败,使用localStorage中的数据
|
||||
const data = localStorage.getItem('smart_agriculture_employees');
|
||||
if (data) {
|
||||
setEmployees(JSON.parse(data));
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredEmployees = employees.filter(emp => {
|
||||
const matchKeyword = !filters.searchKeyword ||
|
||||
emp.name.includes(filters.searchKeyword) ||
|
||||
emp.username.includes(filters.searchKeyword) ||
|
||||
emp.phone.includes(filters.searchKeyword) ||
|
||||
(emp.department && emp.department.includes(filters.searchKeyword));
|
||||
// 搜索处理函数
|
||||
const handleSearch = (searchKeyword: string) => {
|
||||
setFilters(prev => ({ ...prev, searchKeyword }));
|
||||
// 重置到第一页
|
||||
setPagination(prev => ({ ...prev, page: 1 }));
|
||||
};
|
||||
|
||||
const matchStatus = filters.statusFilter === 'all' || emp.status === filters.statusFilter;
|
||||
// 状态筛选处理函数
|
||||
const handleStatusFilter = (statusFilter: string) => {
|
||||
setFilters(prev => ({ ...prev, statusFilter }));
|
||||
// 重置到第一页
|
||||
setPagination(prev => ({ ...prev, page: 1 }));
|
||||
};
|
||||
|
||||
return matchKeyword && matchStatus;
|
||||
});
|
||||
// 分页处理函数
|
||||
const handlePageChange = (page: number) => {
|
||||
setPagination(prev => ({ ...prev, page }));
|
||||
};
|
||||
|
||||
const handlePageSizeChange = (size: number) => {
|
||||
setPagination(prev => ({ ...prev, size, page: 1 }));
|
||||
};
|
||||
|
||||
const handleAddEmployee = () => {
|
||||
setEditingEmployee(null);
|
||||
@@ -121,7 +143,10 @@ export default function EmployeeManagementPage() {
|
||||
enterpriseId: 'ent-2',
|
||||
enterpriseName: '丰收现代农业集团',
|
||||
status: 'active',
|
||||
auditStatus: 'pending',
|
||||
roleIds: [],
|
||||
idCard: '',
|
||||
address: '',
|
||||
});
|
||||
setShowForm(true);
|
||||
};
|
||||
@@ -169,6 +194,7 @@ export default function EmployeeManagementPage() {
|
||||
id: `emp-${Date.now()}`,
|
||||
...formData as Employee,
|
||||
roles: roleNames,
|
||||
auditStatus: 'pending',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
@@ -212,6 +238,44 @@ export default function EmployeeManagementPage() {
|
||||
setShowDetailDialog(true);
|
||||
};
|
||||
|
||||
const handleAudit = (employee: Employee, action: 'approve' | 'reject') => {
|
||||
if (action === 'approve') {
|
||||
const updated = employees.map(emp =>
|
||||
emp.id === employee.id
|
||||
? {
|
||||
...emp,
|
||||
auditStatus: 'approved' as const,
|
||||
auditTime: new Date().toISOString(),
|
||||
auditor: '当前用户',
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
: emp
|
||||
);
|
||||
setEmployees(updated);
|
||||
localStorage.setItem('smart_agriculture_employees', JSON.stringify(updated));
|
||||
toast.success('审核通过');
|
||||
} else {
|
||||
const reason = prompt('请输入驳回原因:');
|
||||
if (reason) {
|
||||
const updated = employees.map(emp =>
|
||||
emp.id === employee.id
|
||||
? {
|
||||
...emp,
|
||||
auditStatus: 'rejected' as const,
|
||||
auditReason: reason,
|
||||
auditTime: new Date().toISOString(),
|
||||
auditor: '当前用户',
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
: emp
|
||||
);
|
||||
setEmployees(updated);
|
||||
localStorage.setItem('smart_agriculture_employees', JSON.stringify(updated));
|
||||
toast.success('已驳回');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<EmployeeManagementHeader
|
||||
@@ -224,17 +288,23 @@ export default function EmployeeManagementPage() {
|
||||
{/* 搜索和筛选 */}
|
||||
<EmployeeManagementFilters
|
||||
filters={filters}
|
||||
onFiltersChange={setFilters}
|
||||
onSearchChange={handleSearch}
|
||||
onStatusFilterChange={handleStatusFilter}
|
||||
/>
|
||||
|
||||
{/* 员工列表 */}
|
||||
<EmployeeList
|
||||
employees={filteredEmployees}
|
||||
employees={employees}
|
||||
loading={loading}
|
||||
pagination={pagination}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
onViewDetail={handleViewDetail}
|
||||
onEdit={handleEdit}
|
||||
onResetPassword={handleResetPassword}
|
||||
onToggleStatus={handleToggleStatus}
|
||||
onDelete={handleDelete}
|
||||
onAudit={handleAudit}
|
||||
/>
|
||||
|
||||
{/* 添加/编辑表单 */}
|
||||
|
||||
@@ -2,19 +2,38 @@
|
||||
|
||||
export interface Employee {
|
||||
id: string;
|
||||
enterpriseId: string;
|
||||
enterpriseName: string;
|
||||
username: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
email?: string;
|
||||
department?: string;
|
||||
position?: string;
|
||||
roleIds: string[];
|
||||
roles?: string[];
|
||||
status: UserStatus;
|
||||
email: string;
|
||||
fullName: string | null;
|
||||
phone: string | null;
|
||||
isActive: boolean;
|
||||
isSuperuser: boolean;
|
||||
isVerified: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastLoginAt: string | null;
|
||||
avatarUrl: string | null;
|
||||
bio: string | null;
|
||||
displayName: string | null;
|
||||
departmentId: string | null;
|
||||
departmentName: string | null;
|
||||
scope: string;
|
||||
companyName: string | null;
|
||||
tenantId: string;
|
||||
|
||||
// 兼容现有表单和操作的字段
|
||||
enterpriseId?: string;
|
||||
enterpriseName?: string;
|
||||
name?: string;
|
||||
department?: string;
|
||||
position?: string;
|
||||
roleIds?: string[];
|
||||
roles?: string[];
|
||||
status?: UserStatus;
|
||||
auditStatus?: 'pending' | 'approved' | 'rejected';
|
||||
auditReason?: string;
|
||||
auditTime?: string;
|
||||
auditor?: string;
|
||||
lastLoginTime?: string;
|
||||
}
|
||||
|
||||
@@ -62,4 +81,7 @@ export interface EmployeeFormData {
|
||||
enterpriseName?: string;
|
||||
status?: UserStatus;
|
||||
roleIds?: string[];
|
||||
idCard?: string;
|
||||
address?: string;
|
||||
auditStatus?: 'pending' | 'approved' | 'rejected';
|
||||
}
|
||||
Reference in New Issue
Block a user