Files
smart-crop-ui/crop-x/src/app/(app)/central-config/user/employee/page.tsx

742 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* filekorolheader: 员工管理页面 - 企业员工账户管理页面
* 功能:员工列表查询、添加编辑、状态管理、角色分配
* 路径:/central-config/user/employee
* 规范遵循crop-x/docs/开发项目规范.md使用事件驱动模式SearchFormPagination重构
*/
'use client';
import { useState, useEffect, useCallback, useRef } from 'react';
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 { EmployeeManagementStatsCards } from './components/EmployeeManagementStatsCards';
import { EmployeeFormDialog } from './components/EmployeeFormDialog';
import { EmployeeDetailDialog } from './components/EmployeeDetailDialog';
import { SearchFormPagination, type SearchFieldConfig, type TableColumnConfig } from '@/components/common/searchFormPagination';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Employee, Role, EmployeeFormData } from './types';
import {
fetchEmployees,
transformEmployeesList,
createEmployee,
updateEmployee,
activateUser,
deactivateUser,
deleteUser,
CreateEmployeeRequest,
UpdateEmployeeRequest,
PaginationState,
EmployeesQueryParams
} from './components/employeeApi';
import {
fetchRoles,
transformRolesList
} from '../role/components/roleApi';
export default function EmployeeManagementPage() {
const [employees, setEmployees] = useState<Employee[]>([]);
const [roles, setRoles] = useState<Role[]>([]);
const [loading, setLoading] = useState(false);
const [creating, setCreating] = useState(false);
const [updating, setUpdating] = useState(false);
const [toggling, setToggling] = useState<string | null>(null); // 记录正在操作的用户ID
const isFirstLoad = useRef(true);
// 确认对话框状态
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [userToDelete, setUserToDelete] = useState<Employee | null>(null);
const [deactivateConfirmOpen, setDeactivateConfirmOpen] = useState(false);
const [userToDeactivate, setUserToDeactivate] = useState<Employee | null>(null);
const [pagination, setPagination] = useState<PaginationState>({
page: 1,
size: 10,
total: 0,
totalPages: 0,
hasNext: false,
hasPrev: false,
});
const [searchFilters, setSearchFilters] = useState<Record<string, string>>({
search: '',
status: 'all'
});
const [showForm, setShowForm] = useState(false);
const [showDetailDialog, setShowDetailDialog] = useState(false);
const [formKey, setFormKey] = useState(0); // 添加key来强制重新渲染表单
const [editingEmployee, setEditingEmployee] = useState<Employee | null>(null);
const [selectedEmployee, setSelectedEmployee] = useState<Employee | null>(null);
const [formData, setFormData] = useState<EmployeeFormData>({
enterpriseId: 'ent-2',
enterpriseName: '丰收现代农业集团',
status: 'active',
auditStatus: 'pending',
roleIds: [],
idCard: '',
address: '',
});
// 搜索字段配置
const searchFields: SearchFieldConfig[] = [
{
key: 'search',
label: '搜索',
type: 'text',
placeholder: '搜索用户名、姓名、手机号...',
},
{
key: 'status',
label: '账户状态',
type: 'select',
defaultValue: 'all',
options: [
{ value: 'all', label: '全部状态' },
{ value: 'active', label: '正常' },
{ value: 'frozen', label: '停用' },
],
},
];
// 表格列配置
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 {
const response = await fetchRoles({
page: 1,
size: 100,
sort_order: 'desc'
});
const transformedRoles = transformRolesList(response.data);
setRoles(transformedRoles);
} catch (error) {
console.error('Failed to load roles:', error);
setRoles([]);
}
}, []);
// 加载员工数据 - 事件驱动,移除依赖项
const loadEmployees = useCallback(async (params?: {
filters?: Record<string, string>;
pagination?: { page: number; size: number };
resetPage?: boolean;
}) => {
try {
setLoading(true);
const queryParams: EmployeesQueryParams = {
page: params?.resetPage ? 1 : (params?.pagination?.page || pagination.page),
size: params?.pagination?.size || pagination.size,
sort_order: 'desc'
};
// 搜索关键词
const searchKeyword = params?.filters?.search ?? searchFilters.search;
if (searchKeyword) {
queryParams.search = searchKeyword;
}
const response = await fetchEmployees(queryParams);
const transformedEmployees = transformEmployeesList(response.data);
// 状态筛选(客户端过滤)
const statusFilter = params?.filters?.status ?? searchFilters.status;
const filteredEmployees = statusFilter === 'all'
? transformedEmployees
: transformedEmployees.filter(emp => {
const status = emp.isActive ? 'active' : 'frozen';
return status === 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('加载员工数据失败');
setEmployees([]);
} finally {
setLoading(false);
}
}, [pagination.page, pagination.size, searchFilters]);
// 初始化数据 - 只在组件挂载时执行一次
useEffect(() => {
if (isFirstLoad.current) {
isFirstLoad.current = false;
loadRoles();
loadEmployees({ resetPage: true });
}
}, [loadRoles, loadEmployees]);
// 事件处理器 - 事件驱动模式
const handleSearch = useCallback((filters: Record<string, string>) => {
setSearchFilters(filters);
loadEmployees({
filters,
pagination: { page: 1, size: pagination.size }
});
}, [loadEmployees, pagination.size]);
const handlePageChange = useCallback((page: number) => {
setPagination(prev => ({ ...prev, page }));
loadEmployees({
filters: searchFilters,
pagination: { page, size: pagination.size }
});
}, [loadEmployees, searchFilters, pagination.size]);
const handleSizeChange = useCallback((size: number) => {
setPagination(prev => ({ ...prev, size, page: 1 }));
loadEmployees({
filters: searchFilters,
pagination: { page: 1, size }
});
}, [loadEmployees, searchFilters]);
const handleAddEmployee = () => {
setEditingEmployee(null);
clearForm();
setFormKey(prev => prev + 1); // 增加key强制重新渲染
setShowForm(true);
};
const clearForm = () => {
// 直接设置空表单无需setTimeout
const emptyForm = {
enterpriseId: 'ent-2',
enterpriseName: '丰收现代农业集团',
status: 'active' as const,
auditStatus: 'pending' as const,
roleIds: [],
username: '',
name: '',
phone: '',
email: '',
department: '',
position: '',
};
setFormData(emptyForm);
};
const handleEdit = (employee: Employee) => {
setEditingEmployee(employee);
setFormData(employee);
setShowForm(true);
};
const handleSave = async () => {
if (!formData.username || !formData.name || !formData.phone) {
toast.error('请填写必填项');
return;
}
// 验证角色选择
if (!formData.roleIds || formData.roleIds.length === 0) {
toast.error('请至少选择一个角色');
return;
}
// 根据角色ID设置角色名称
const selectedRoles = roles.filter(r => formData.roleIds?.includes(r.id));
const roleNames = selectedRoles.map(r => r.name);
if (editingEmployee) {
// 更新 - 调用API
setUpdating(true);
try {
// 构建API请求参数
const updateRequest: UpdateEmployeeRequest = {
email: formData.email || '',
username: formData.username,
full_name: formData.name,
phone: formData.phone,
password: '', // 编辑时不传密码
tenant_id: formData.enterpriseId,
scope: 'tenant',
department_id: formData.departmentId || '',
is_superuser: formData.isSuperuser || false,
};
// 调用API更新用户
const updatedEmployee = await updateEmployee(editingEmployee.id, updateRequest);
// 更新本地列表中的员工数据
const updated = employees.map(emp =>
emp.id === editingEmployee.id
? {
...emp,
...updatedEmployee,
roles: roleNames,
updatedAt: new Date().toISOString(),
}
: emp
);
setEmployees(updated);
toast.success('员工信息更新成功');
setShowForm(false);
clearForm();
// 立即刷新员工列表数据,无需延迟
loadEmployees({ resetPage: true });
} catch (error) {
console.error('更新员工失败:', error);
// 处理错误,显示具体的错误消息
const errorMessage = error instanceof Error ? error.message : '员工信息更新失败';
toast.error(errorMessage);
} finally {
setUpdating(false);
}
} else {
// 新增 - 调用API
setCreating(true);
try {
// 构建API请求参数
const createRequest: CreateEmployeeRequest = {
email: formData.email || '', // 没有邮箱就传空字符串
username: formData.username,
full_name: formData.name,
phone: formData.phone,
password: '', // 传递空字符串给后端
tenant_id: formData.enterpriseId,
scope: 'tenant',
department_id: formData.departmentId || '',
is_superuser: formData.isSuperuser || false,
};
// 调用API创建用户
const newEmployee = await createEmployee(createRequest);
// 将新员工添加到列表
const updated = [newEmployee, ...employees];
setEmployees(updated);
toast.success('员工添加成功');
setShowForm(false);
clearForm();
// 立即刷新员工列表数据,无需延迟
loadEmployees({ resetPage: true });
} catch (error) {
console.error('创建员工失败:', error);
// 处理错误,显示具体的错误消息
const errorMessage = error instanceof Error ? error.message : '员工添加失败';
toast.error(errorMessage);
} finally {
setCreating(false);
}
}
};
const handleDelete = (userId: string) => {
// 防止重复操作
if (toggling) {
toast.warning('操作进行中,请稍候...');
return;
}
// 查找要删除的员工信息
const employeeToDelete = employees.find(emp => emp.id === userId);
if (!employeeToDelete) {
toast.error('未找到要删除的用户');
return;
}
// 设置要删除的用户并显示确认对话框
setUserToDelete(employeeToDelete);
setDeleteConfirmOpen(true);
};
const executeDelete = async (userId: string) => {
setToggling(userId);
try {
// 调用API删除用户
await deleteUser(userId);
// 成功后从本地列表中移除
const updated = employees.filter(emp => emp.id !== userId);
setEmployees(updated);
toast.success('用户删除成功');
// 立即刷新列表确保数据同步
loadEmployees({ resetPage: true });
// 关闭确认对话框
setDeleteConfirmOpen(false);
setUserToDelete(null);
} catch (error) {
console.error('删除用户失败:', error);
// 处理错误,显示具体的错误消息
const errorMessage = error instanceof Error ? error.message : '删除失败,请稍后重试';
toast.error(errorMessage);
// 失败时不关闭确认对话框,用户可以重试
} finally {
setToggling(null);
}
};
const handleToggleStatus = (employee: Employee) => {
if (toggling) {
toast.warning('操作进行中,请稍候...');
return;
}
if (employee.isActive) {
// 当前是激活状态,进行停用操作,需要二次确认
setUserToDeactivate(employee);
setDeactivateConfirmOpen(true);
} else {
// 当前是停用状态,进行激活操作(不需要确认)
executeToggleStatus(employee);
}
};
const executeToggleStatus = async (employee: Employee) => {
setToggling(employee.id);
try {
if (employee.isActive) {
// 当前是激活状态,进行停用操作
await deactivateUser(employee.id);
toast.success('账户已停用');
} else {
// 当前是停用状态,进行激活操作
await activateUser(employee.id);
toast.success('账户已激活');
}
// 成功后立即刷新列表
loadEmployees({ resetPage: true });
// 关闭停用确认对话框
if (deactivateConfirmOpen) {
setDeactivateConfirmOpen(false);
setUserToDeactivate(null);
}
} catch (error) {
console.error('切换用户状态失败:', error);
// 处理错误,显示具体的错误消息
const errorMessage = error instanceof Error ? error.message : '操作失败,请稍后重试';
toast.error(errorMessage);
// 失败时不关闭确认对话框,用户可以重试
} finally {
setToggling(null);
}
};
const handleViewDetail = (employee: Employee) => {
setSelectedEmployee(employee);
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);
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);
toast.success('已驳回');
}
}
};
return (
<div className="space-y-6">
<EmployeeManagementHeader
onAddEmployee={handleAddEmployee}
/>
{/* 统计卡片 - 保留原有功能 */}
<EmployeeManagementStatsCards employees={employees} />
{/* 搜索、表格和分页 - 使用重构后的组件 */}
<SearchFormPagination
formTitle="员工列表"
formRightContent={
<Button onClick={handleAddEmployee} disabled={loading}>
<Plus className="w-4 h-4 mr-2" />
</Button>
}
searchFields={searchFields}
columns={columns}
data={employees}
loading={loading}
error={null}
pagination={pagination}
onPageChange={handlePageChange}
onSizeChange={handleSizeChange}
onSearch={handleSearch}
emptyIcon={<div className="w-12 h-12 mx-auto mb-4 opacity-20" />}
emptyText="暂无员工数据"
showSizeSelector={true}
showPageInfo={true}
sizeOptions={[10, 20, 50, 100]}
/>
{/* 添加/编辑表单 - 保留原有功能 */}
<EmployeeFormDialog
key={formKey} // 使用key强制重新渲染清除浏览器缓存
open={showForm}
onOpenChange={setShowForm}
editingEmployee={editingEmployee}
formData={formData}
onFormDataChange={setFormData}
onSave={handleSave}
roles={roles}
creating={creating}
updating={updating}
onClearForm={clearForm}
/>
{/* 详情对话框 - 保留原有功能 */}
<EmployeeDetailDialog
open={showDetailDialog}
onOpenChange={setShowDetailDialog}
selectedEmployee={selectedEmployee}
/>
{/* 删除确认对话框 - 保留原有功能 */}
<AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
"
{userToDelete?.displayName || userToDelete?.fullName || userToDelete?.username || ''}
"
<br /><br />
<span className="text-red-600 font-semibold">
</span>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={toggling !== null}>
</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
if (userToDelete) {
executeDelete(userToDelete.id);
}
}}
disabled={toggling !== null}
className="bg-red-600 hover:bg-red-700"
>
{toggling ? '删除中...' : '确认删除'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* 停用确认对话框 - 保留原有功能 */}
<AlertDialog open={deactivateConfirmOpen} onOpenChange={setDeactivateConfirmOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
"
{userToDeactivate?.displayName || userToDeactivate?.fullName || userToDeactivate?.username || ''}
"
<br /><br />
<span className="text-orange-600 font-semibold">
</span>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={toggling !== null}>
</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
if (userToDeactivate) {
executeToggleStatus(userToDeactivate);
}
}}
disabled={toggling !== null}
className="bg-orange-600 hover:bg-orange-700"
>
{toggling ? '停用中...' : '确认停用'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}