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

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

View File

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

View File

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

View File

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

View File

@@ -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 (

View File

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

View File

@@ -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}
/>
{/* 添加/编辑表单 */}

View File

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