From fffd37a0a91540fa2a3f8ed15ae32088471f9a16 Mon Sep 17 00:00:00 2001 From: peng Date: Tue, 4 Nov 2025 15:55:29 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=9F=E4=BA=A7=E7=AE=A1=E7=90=86=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=20-=20=E5=91=98=E5=B7=A5=E7=AE=A1=E7=90=86=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E8=81=94=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/auditHistoryApi.ts | 3 + .../tenant/audit-history/page.tsx | 46 +- .../tenant/enterprise-audit/page.tsx | 22 +- .../components/userManagementApi.ts | 9 +- .../tenant/user-management/page.tsx | 41 +- .../components/DepartmentDeleteDialog.tsx | 106 ++++ .../components/DepartmentFormDialog.tsx | 230 +++++++ .../components/DepartmentHeader.tsx | 51 ++ .../components/DepartmentInstructions.tsx | 70 +++ .../components/DepartmentStatsCards.tsx | 77 +++ .../department/components/DepartmentTree.tsx | 235 +++++++ .../central-config/user/department/page.tsx | 591 ++++++++++++++++++ .../central-config/user/department/types.ts | 60 ++ .../components/EmployeeDetailDialog.tsx | 165 +++-- .../components/EmployeeFormDialog.tsx | 130 ++-- .../user/employee/components/EmployeeList.tsx | 176 +++++- .../components/EmployeeManagementFilters.tsx | 23 +- .../user/employee/components/employeeApi.ts | 220 +++++++ .../central-config/user/employee/page.tsx | 212 ++++--- .../central-config/user/employee/types.ts | 42 +- crop-x/src/app/layout.tsx | 5 + crop-x/src/components/ui/pagination.tsx | 4 +- .../config/EnterpriseManagement.tsx | 9 +- 23 files changed, 2251 insertions(+), 276 deletions(-) create mode 100644 crop-x/src/app/(app)/central-config/user/department/components/DepartmentDeleteDialog.tsx create mode 100644 crop-x/src/app/(app)/central-config/user/department/components/DepartmentFormDialog.tsx create mode 100644 crop-x/src/app/(app)/central-config/user/department/components/DepartmentHeader.tsx create mode 100644 crop-x/src/app/(app)/central-config/user/department/components/DepartmentInstructions.tsx create mode 100644 crop-x/src/app/(app)/central-config/user/department/components/DepartmentStatsCards.tsx create mode 100644 crop-x/src/app/(app)/central-config/user/department/components/DepartmentTree.tsx create mode 100644 crop-x/src/app/(app)/central-config/user/department/page.tsx create mode 100644 crop-x/src/app/(app)/central-config/user/department/types.ts create mode 100644 crop-x/src/app/(app)/central-config/user/employee/components/employeeApi.ts diff --git a/crop-x/src/app/(app)/central-config/tenant/audit-history/components/auditHistoryApi.ts b/crop-x/src/app/(app)/central-config/tenant/audit-history/components/auditHistoryApi.ts index 5214cbf..ca6d40b 100644 --- a/crop-x/src/app/(app)/central-config/tenant/audit-history/components/auditHistoryApi.ts +++ b/crop-x/src/app/(app)/central-config/tenant/audit-history/components/auditHistoryApi.ts @@ -108,6 +108,9 @@ export interface AuditRecord { */ export async function fetchAuditLogs(params: AuditLogsQueryParams = {}): Promise { try { + // 调用计数器 + console.log(`[API] fetchAuditLogs 调用次数: ${++fetchAuditLogs.callCount || (fetchAuditLogs.callCount = 1)}`, params); + // 构建查询参数对象 const queryParams: any = {}; diff --git a/crop-x/src/app/(app)/central-config/tenant/audit-history/page.tsx b/crop-x/src/app/(app)/central-config/tenant/audit-history/page.tsx index 3fd676c..59ee3a6 100644 --- a/crop-x/src/app/(app)/central-config/tenant/audit-history/page.tsx +++ b/crop-x/src/app/(app)/central-config/tenant/audit-history/page.tsx @@ -6,7 +6,7 @@ */ 'use client'; -import { useReducer, useEffect, useMemo, useState } from 'react'; +import { useReducer, useEffect, useMemo, useState, useRef, useCallback } from 'react'; import { toast } from 'sonner'; import { Card } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -128,15 +128,16 @@ const initialState: AuditHistoryState = { export default function AuditHistoryPage() { const [state, dispatch] = useReducer(auditHistoryReducer, initialState); + const isFirstLoad = useRef(true); // 加载审核历史数据 - const loadAuditHistory = async (resetPage = false) => { + const loadAuditHistory = useCallback(async () => { try { dispatch({ type: 'SET_LOADING', payload: true }); const params: AuditLogsQueryParams = { search: state.filters.search_keyword || undefined, - page: resetPage ? 1 : state.pagination.page, + page: state.pagination.page, size: state.pagination.size }; @@ -164,7 +165,7 @@ export default function AuditHistoryPage() { payload: error instanceof Error ? error.message : '加载审核历史失败' }); } - }; + }, []); // 空依赖数组,函数内部使用最新状态 // 搜索处理 const handleSearch = (value: string) => { @@ -212,28 +213,33 @@ export default function AuditHistoryPage() { // 刷新数据 const handleRefresh = () => { dispatch({ type: 'REFRESH_DATA' }); - loadAuditHistory(true); + dispatch({ type: 'SET_PAGINATION', payload: { page: 1 } }); toast.success('数据已刷新'); }; - // 初始化和监听器 + // 合并所有状态变化,统一处理数据加载 useEffect(() => { - loadAuditHistory(); - }, []); - - useEffect(() => { - const timer = setTimeout(() => { - loadAuditHistory(); - }, 300); - - return () => clearTimeout(timer); - }, [state.filters.search_keyword, state.filters.typeFilter, state.filters.resultFilter, state.filters.dateRange, state.sortBy, state.sortOrder]); - - useEffect(() => { - if (state.pagination.page > 1) { + if (isFirstLoad.current) { + // 首次加载 + isFirstLoad.current = false; loadAuditHistory(); + } else { + // 后续状态变化,使用防抖 + const timer = setTimeout(() => { + loadAuditHistory(); + }, 300); + return () => clearTimeout(timer); } - }, [state.pagination.page]); + }, [ + state.filters.search_keyword, + state.filters.typeFilter, + state.filters.resultFilter, + state.filters.dateRange, + state.sortBy, + state.sortOrder, + state.pagination.page, + state.pagination.size + ]); // 工具函数 const getActionBadge = (action: string) => { diff --git a/crop-x/src/app/(app)/central-config/tenant/enterprise-audit/page.tsx b/crop-x/src/app/(app)/central-config/tenant/enterprise-audit/page.tsx index c25bb2b..4cacc3e 100644 --- a/crop-x/src/app/(app)/central-config/tenant/enterprise-audit/page.tsx +++ b/crop-x/src/app/(app)/central-config/tenant/enterprise-audit/page.tsx @@ -6,7 +6,7 @@ */ 'use client'; -import { useReducer, useEffect, useMemo } from 'react'; +import { useReducer, useEffect, useMemo, useRef } from 'react'; import { toast } from 'sonner'; import { Building2, RefreshCw } from 'lucide-react'; @@ -117,6 +117,7 @@ const initialState: AuditState = { export default function EnterpriseAuditPage() { const [state, dispatch] = useReducer(auditReducer, initialState); + const isFirstLoad = useRef(true); // 加载企业数据 const loadEnterprises = async (resetPage = false) => { @@ -157,14 +158,27 @@ export default function EnterpriseAuditPage() { } }; - // 初始加载 + // 首次加载数据 useEffect(() => { - loadEnterprises(true); + if (isFirstLoad.current) { + isFirstLoad.current = false; + loadEnterprises(true); + } + }, []); + + // 监听筛选和排序变化(排除首次加载) + useEffect(() => { + if (!isFirstLoad.current) { + const timer = setTimeout(() => { + loadEnterprises(true); + }, 300); + return () => clearTimeout(timer); + } }, [state.filters.search, state.filters.audit_status, state.sortBy, state.sortOrder]); // 分页加载 useEffect(() => { - if (state.pagination.page > 1) { + if (!isFirstLoad.current && state.pagination.page > 1) { loadEnterprises(false); } }, [state.pagination.page]); diff --git a/crop-x/src/app/(app)/central-config/tenant/user-management/components/userManagementApi.ts b/crop-x/src/app/(app)/central-config/tenant/user-management/components/userManagementApi.ts index 1acf497..e6fa071 100644 --- a/crop-x/src/app/(app)/central-config/tenant/user-management/components/userManagementApi.ts +++ b/crop-x/src/app/(app)/central-config/tenant/user-management/components/userManagementApi.ts @@ -7,7 +7,7 @@ import { getAuthToken } from "@/utils/token"; import { - getUsersApiV1UsersGet, + listSystemUsersApiV1UsersSystemUsersGet, } from "@/lib/api/sdk.gen"; // API返回的用户数据类型 @@ -18,7 +18,8 @@ export interface UserData { username: string; full_name: string | null; phone: string | null; - is_active: boolean; + is_active?: boolean; + status?: string; is_superuser: boolean; is_verified: boolean; created_at: string; @@ -101,7 +102,7 @@ export async function fetchUsers(params: UsersQueryParams = {}): Promise { try { + dispatch({ type: 'SET_LOADING', payload: true }); const params: UsersQueryParams = { - search: state.filters.searchKeyword || undefined, page: resetPage ? 1 : state.pagination.page, size: state.pagination.size, - order_by: state.sortBy, - sort_order: state.sortOrder, + is_active: true, }; - - // 根据状态筛选器设置 is_active 参数 - if (state.filters.statusFilter === 'active') { - params.is_active = true; - } else if (state.filters.statusFilter === 'inactive') { - params.is_active = false; - } - const response: UsersApiResponse = await fetchUsers(params); const transformedUsers = response.data.map(transformUserData); @@ -150,15 +141,15 @@ export default function TenantUserManagementPage() { }; // 加载企业数据(这里暂时使用mock数据,后续可以添加企业API) - const loadEnterprises = () => { - // 这里可以添加企业API调用,现在使用mock数据 - const mockEnterprises: Enterprise[] = [ - { id: 'ent-1', name: '丰收现代农业集团' }, - { id: 'ent-2', name: '绿色种植科技有限公司' }, - { id: 'ent-3', name: '智慧农业示范区' }, - ]; - dispatch({ type: 'SET_ENTERPRISES', payload: mockEnterprises }); - }; + // const loadEnterprises = () => { + // // 这里可以添加企业API调用,现在使用mock数据 + // const mockEnterprises: Enterprise[] = [ + // { id: 'ent-1', name: '丰收现代农业集团' }, + // { id: 'ent-2', name: '绿色种植科技有限公司' }, + // { id: 'ent-3', name: '智慧农业示范区' }, + // ]; + // dispatch({ type: 'SET_ENTERPRISES', payload: mockEnterprises }); + // }; // 搜索处理 const handleSearch = (value: string) => { @@ -262,18 +253,8 @@ export default function TenantUserManagementPage() { }, ]; - // 初始化和监听器 useEffect(() => { - loadUsers(); - loadEnterprises(); - }, []); - - useEffect(() => { - const timer = setTimeout(() => { loadUsers(); - }, 300); - - return () => clearTimeout(timer); }, [state.filters.searchKeyword, state.filters.statusFilter, state.filters.typeFilter, state.sortBy, state.sortOrder]); useEffect(() => { diff --git a/crop-x/src/app/(app)/central-config/user/department/components/DepartmentDeleteDialog.tsx b/crop-x/src/app/(app)/central-config/user/department/components/DepartmentDeleteDialog.tsx new file mode 100644 index 0000000..3d8eb26 --- /dev/null +++ b/crop-x/src/app/(app)/central-config/user/department/components/DepartmentDeleteDialog.tsx @@ -0,0 +1,106 @@ +/** + * filekorolheader: 部门删除对话框组件 - 部门删除确认界面 + * 功能:删除确认、影响说明、操作处理 + * 路径:/central-config/user/department/components/DepartmentDeleteDialog + * 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全 + */ + +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { AlertTriangle } from 'lucide-react'; +import { Department } from '../types'; + +interface DepartmentDeleteDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + deletingDepartment: Department | null; + onConfirm: () => void; +} + +export function DepartmentDeleteDialog({ + open, + onOpenChange, + deletingDepartment, + onConfirm +}: DepartmentDeleteDialogProps) { + const handleConfirm = async () => { + try { + await onConfirm(); + } catch (error) { + console.error('Failed to delete department:', error); + } + }; + + return ( + + + +
+
+ +
+
+ 确认删除 +
+
+
+ + + {deletingDepartment && ( +
+ {/* 部门信息 */} +
+ 🏢 +
+
{deletingDepartment.name}
+
部门编码:{deletingDepartment.code}
+ {deletingDepartment.manager && ( +
+ 负责人:{deletingDepartment.manager} +
+ )} +
+
+ + {/* 删除影响说明 */} +
+
+ +
+
+ 删除影响: +
+
    +
  • • 部门将永久删除,此操作不可恢复
  • +
  • • 部门下的所有相关信息将被清除
  • +
  • • 如有员工归属于此部门,需要重新分配
  • +
+
+
+
+ + {/* 确认提示 */} +
+

+ 确定要删除部门 {deletingDepartment.name} 吗? +

+
+
+ )} +
+ + + + + +
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/user/department/components/DepartmentFormDialog.tsx b/crop-x/src/app/(app)/central-config/user/department/components/DepartmentFormDialog.tsx new file mode 100644 index 0000000..58c892c --- /dev/null +++ b/crop-x/src/app/(app)/central-config/user/department/components/DepartmentFormDialog.tsx @@ -0,0 +1,230 @@ +/** + * filekorolheader: 部门表单对话框组件 - 部门添加/编辑表单界面 + * 功能:部门信息表单、输入验证、提交处理 + * 路径:/central-config/user/department/components/DepartmentFormDialog + * 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全 + */ + +'use client'; + +import { useState } from 'react'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Department } from '../types'; + +interface DepartmentFormDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + editingDepartment: Department | null; + parentDepartment: Department | null; + onSave: (formData: Partial) => void; +} + +export function DepartmentFormDialog({ + open, + onOpenChange, + editingDepartment, + parentDepartment, + onSave +}: DepartmentFormDialogProps) { + const [formData, setFormData] = useState>({ + status: 'active', + sort: 0, + level: parentDepartment ? (parentDepartment.level || 1) + 1 : 1, + parentId: parentDepartment?.id, + }); + + const [loading, setLoading] = useState(false); + + // 当编辑部门或父部门变化时,重置表单数据 + useState(() => { + if (editingDepartment) { + setFormData({ + ...editingDepartment, + children: undefined, // 排除children字段 + }); + } else { + setFormData({ + parentId: parentDepartment?.id, + level: parentDepartment ? (parentDepartment.level || 1) + 1 : 1, + status: 'active', + sort: 0, + }); + } + }); + + const handleInputChange = (field: keyof Department, value: string | number) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + + const handleSubmit = async () => { + if (!formData.name || !formData.code) { + return; + } + + setLoading(true); + try { + await onSave(formData); + // 重置表单 + setFormData({ + status: 'active', + sort: 0, + level: parentDepartment ? (parentDepartment.level || 1) + 1 : 1, + parentId: parentDepartment?.id, + }); + } catch (error) { + console.error('Failed to save department:', error); + } finally { + setLoading(false); + } + }; + + const handleClose = () => { + if (!loading) { + onOpenChange(false); + // 重置表单 + setFormData({ + status: 'active', + sort: 0, + level: parentDepartment ? (parentDepartment.level || 1) + 1 : 1, + parentId: parentDepartment?.id, + }); + } + }; + + const title = editingDepartment + ? '编辑部门' + : parentDepartment + ? `添加子部门(父级:${parentDepartment.name})` + : '添加一级部门'; + + return ( + + + + {title} + + {editingDepartment ? '编辑部门信息' : '添加新部门'} + + + +
+
+
+ + handleInputChange('name', e.target.value)} + placeholder="请输入部门名称" + disabled={loading} + /> +
+ +
+ + handleInputChange('code', e.target.value.toUpperCase())} + placeholder="请输入部门编码,如:TECH" + className="font-mono" + disabled={loading} + /> +
+ +
+ + handleInputChange('manager', e.target.value)} + placeholder="请输入负责人姓名" + disabled={loading} + /> +
+ +
+ + handleInputChange('phone', e.target.value)} + placeholder="请输入联系电话" + disabled={loading} + /> +
+ +
+ + handleInputChange('email', e.target.value)} + placeholder="请输入邮箱" + disabled={loading} + /> +
+ +
+ + handleInputChange('sort', parseInt(e.target.value) || 0)} + placeholder="数字越小越靠前" + disabled={loading} + /> +
+ +
+ + +
+
+ +
+ + handleInputChange('description', e.target.value)} + placeholder="请输入部门描述" + disabled={loading} + /> +
+
+ + + + + +
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/user/department/components/DepartmentHeader.tsx b/crop-x/src/app/(app)/central-config/user/department/components/DepartmentHeader.tsx new file mode 100644 index 0000000..62aabfd --- /dev/null +++ b/crop-x/src/app/(app)/central-config/user/department/components/DepartmentHeader.tsx @@ -0,0 +1,51 @@ +/** + * filekorolheader: 部门管理头部组件 - 页面标题和操作按钮 + * 功能:页面标题显示、添加一级部门功能 + * 路径:/central-config/user/department/components/DepartmentHeader + * 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全 + */ + +'use client'; + +import { Card } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Building2, Plus } from 'lucide-react'; + +interface DepartmentHeaderProps { + onAdd: () => void; +} + +export function DepartmentHeader({ onAdd }: DepartmentHeaderProps) { + return ( + +
+
+ +
+

部门管理

+

+ 树形结构管理企业部门信息,支持拖动排序 +

+
+ + 树形结构 + + + 拖动排序 + + + 层级管理 + +
+
+
+
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/user/department/components/DepartmentInstructions.tsx b/crop-x/src/app/(app)/central-config/user/department/components/DepartmentInstructions.tsx new file mode 100644 index 0000000..0ee54bd --- /dev/null +++ b/crop-x/src/app/(app)/central-config/user/department/components/DepartmentInstructions.tsx @@ -0,0 +1,70 @@ +/** + * filekorolheader: 部门管理说明组件 - 功能使用说明界面 + * 功能:功能说明、操作指引、注意事项 + * 路径:/central-config/user/department/components/DepartmentInstructions + * 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全 + */ + +import { Card } from '@/components/ui/card'; +import { Building2, GripVertical, AlertCircle, Users } from 'lucide-react'; + +export function DepartmentInstructions() { + return ( + +
+ +

部门管理说明

+
+ +
    +
  • + +
    + 树形结构: + 支持二级部门管理(一级部门 → 二级部门) +
    +
  • + +
  • + +
    + 拖动排序: + 按住部门左侧的 ⋮⋮ 图标拖动,可调整同级部门的顺序 +
    +
  • + +
  • + +
    + 部门编码: + 建议使用英文大写字母,如TECH、ADMIN等 +
    +
  • + +
  • + +
    + 员工关联: + 在员工管理中新增员工时,可选择此处维护的部门 +
    +
  • + +
  • + +
    + 删除限制: + 删除部门前请先删除其所有子部门 +
    +
  • + +
  • + +
    + 状态管理: + 部门状态为"停用"时,该部门仍可查看但不可用于新建员工 +
    +
  • +
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/user/department/components/DepartmentStatsCards.tsx b/crop-x/src/app/(app)/central-config/user/department/components/DepartmentStatsCards.tsx new file mode 100644 index 0000000..0cdf1e7 --- /dev/null +++ b/crop-x/src/app/(app)/central-config/user/department/components/DepartmentStatsCards.tsx @@ -0,0 +1,77 @@ +/** + * filekorolheader: 部门管理统计卡片组件 - 部门统计数据展示界面 + * 功能:一级部门、二级部门、部门总数统计展示 + * 路径:/central-config/user/department/components/DepartmentStatsCards + * 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全 + */ + +'use client'; + +import { Card } from '@/components/ui/card'; +import { Building2, Users, Layers } from 'lucide-react'; +import { DepartmentStats } from '../types'; + +interface DepartmentStatsCardsProps { + stats: DepartmentStats; + loading?: boolean; +} + +export function DepartmentStatsCards({ + stats, + loading = false +}: DepartmentStatsCardsProps) { + const statsData = [ + { + label: '一级部门', + value: stats.level1, + icon: , + color: 'text-blue-600 dark:text-blue-400', + bg: 'bg-blue-50 dark:bg-blue-950', + }, + { + label: '二级部门', + value: stats.level2, + icon: , + color: 'text-green-600 dark:text-green-400', + bg: 'bg-green-50 dark:bg-green-950', + }, + { + label: '部门总数', + value: stats.total, + icon: , + color: 'text-orange-600 dark:text-orange-400', + bg: 'bg-orange-50 dark:bg-orange-950', + }, + ]; + + if (loading) { + return ( +
+ {statsData.map((_, index) => ( + +
+
+
+
+
+ ))} +
+ ); + } + + return ( +
+ {statsData.map((stat, index) => ( + +
+
{stat.label}
+
+ {stat.icon} +
+
+
{stat.value}
+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/user/department/components/DepartmentTree.tsx b/crop-x/src/app/(app)/central-config/user/department/components/DepartmentTree.tsx new file mode 100644 index 0000000..cf15d18 --- /dev/null +++ b/crop-x/src/app/(app)/central-config/user/department/components/DepartmentTree.tsx @@ -0,0 +1,235 @@ +/** + * filekorolheader: 部门树组件 - 部门树形结构展示界面 + * 功能:部门树形展示、展开收起、拖拽排序、操作按钮 + * 路径:/central-config/user/department/components/DepartmentTree + * 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全 + */ + +import { Department } from '../types'; +import { Card } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { + ChevronRight, + ChevronDown, + Plus, + Edit, + Trash2, + Building2, + GripVertical +} from 'lucide-react'; + +interface DepartmentTreeProps { + departments: Department[]; + expandedIds: Set; + loading: boolean; + draggedItem: { + dept: Department; + index: number; + parentId?: string; + } | null; + dragOverItem: { + index: number; + parentId?: string; + } | null; + onToggleExpand: (id: string) => void; + onExpandAll: () => void; + onCollapseAll: () => void; + onAdd: (parent?: Department) => void; + onEdit: (dept: Department) => void; + onDelete: (dept: Department) => void; + onDragStart: (dept: Department, index: number, parentId?: string) => void; + onDragEnd: () => void; + onDragOver: (e: React.DragEvent, index: number, parentId?: string) => void; + onDragLeave: () => void; + onDrop: (e: React.DragEvent, hoverIndex: number, parentId?: string) => void; +} + +export function DepartmentTree({ + departments, + expandedIds, + loading, + draggedItem, + dragOverItem, + onToggleExpand, + onExpandAll, + onCollapseAll, + onAdd, + onEdit, + onDelete, + onDragStart, + onDragEnd, + onDragOver, + onDragLeave, + onDrop +}: DepartmentTreeProps) { + const getStatusBadge = (status: string) => { + return status === 'active' ? ( + 启用 + ) : ( + 停用 + ); + }; + + // 渲染部门树 + const renderDepartmentTree = (items: Department[], level: number = 0, parentId?: string) => { + return items.map((dept, index) => { + const isExpanded = expandedIds.has(dept.id); + const hasChildren = dept.children && dept.children.length > 0; + const indent = level * 24; + + const isDragOver = dragOverItem?.index === index && dragOverItem?.parentId === parentId; + const isDragging = draggedItem?.dept.id === dept.id; + + return ( +
+
onDragStart(dept, index, parentId)} + onDragEnd={onDragEnd} + onDragOver={(e) => onDragOver(e, index, parentId)} + onDragLeave={onDragLeave} + onDrop={(e) => onDrop(e, index, parentId)} + className={`flex items-center justify-between p-3 border rounded-lg mb-2 transition-all ${ + isDragging + ? 'opacity-50 bg-muted' + : isDragOver + ? 'bg-green-50 dark:bg-green-950/30 border-green-300 dark:border-green-700' + : 'hover:bg-muted/50' + }`} + style={{ marginLeft: `${indent}px`, cursor: 'move' }} + > +
+ {/* 拖动手柄 */} +
+ +
+ + {/* 展开/收起图标 */} +
+ {hasChildren ? ( + + ) : ( +
+ )} +
+ + {/* 部门图标 */} + + + {/* 部门信息 */} +
+
+ {dept.name} + ({dept.code}) +
+ {dept.manager && ( +
+ 负责人:{dept.manager} + {dept.phone && ` · ${dept.phone}`} +
+ )} +
+ + {/* 状态标签 */} +
+ {getStatusBadge(dept.status)} + 排序: {dept.sort} +
+
+ + {/* 操作按钮 */} +
+ {level < 1 && ( + + )} + + +
+
+ + {/* 递归渲染子部门 */} + {hasChildren && isExpanded && ( +
+ {renderDepartmentTree(dept.children!, level + 1, dept.id)} +
+ )} +
+ ); + }); + }; + + if (loading) { + return ( + +
+
加载中...
+
+
+ ); + } + + return ( + +
+

部门结构

+
+ + +
+
+ +
+ {departments.length > 0 ? ( + renderDepartmentTree(departments) + ) : ( +
+ 暂无部门数据,点击上方按钮添加一级部门 +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/user/department/page.tsx b/crop-x/src/app/(app)/central-config/user/department/page.tsx new file mode 100644 index 0000000..ce7b1b3 --- /dev/null +++ b/crop-x/src/app/(app)/central-config/user/department/page.tsx @@ -0,0 +1,591 @@ +/** + * filekorolheader: 部门管理页面 - 企业部门树形结构管理页面 + * 功能:部门树形管理、拖拽排序、增删改查、层级管理 + * 路径:/central-config/user/department + * 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,API集成,shadcn语义化样式 + */ +'use client'; + +import { useReducer, useEffect, useRef } from 'react'; +import { toast } from 'sonner'; + +import { Department } from './types'; +import { DepartmentHeader } from './components/DepartmentHeader'; +import { DepartmentStatsCards } from './components/DepartmentStatsCards'; +import { DepartmentTree } from './components/DepartmentTree'; +import { DepartmentFormDialog } from './components/DepartmentFormDialog'; +import { DepartmentDeleteDialog } from './components/DepartmentDeleteDialog'; +import { DepartmentInstructions } from './components/DepartmentInstructions'; + +// 部门管理状态管理 +interface DepartmentManagementState { + departments: Department[]; + expandedIds: Set; + loading: boolean; + error: string | null; + showForm: boolean; + showDeleteDialog: boolean; + editingDepartment: Department | null; + parentDepartment: Department | null; + deletingDepartment: Department | null; + draggedItem: { + dept: Department; + index: number; + parentId?: string; + } | null; + dragOverItem: { + index: number; + parentId?: string; + } | null; +} + +type DepartmentManagementAction = + | { type: 'SET_DEPARTMENTS'; payload: Department[] } + | { type: 'SET_LOADING'; payload: boolean } + | { type: 'SET_ERROR'; payload: string | null } + | { type: 'TOGGLE_EXPAND'; payload: string } + | { type: 'SET_EXPANDED_IDS'; payload: Set } + | { type: 'TOGGLE_FORM'; payload: boolean } + | { type: 'TOGGLE_DELETE_DIALOG'; payload: boolean } + | { type: 'SET_EDITING_DEPARTMENT'; payload: Department | null } + | { type: 'SET_PARENT_DEPARTMENT'; payload: Department | null } + | { type: 'SET_DELETING_DEPARTMENT'; payload: Department | null } + | { type: 'SET_DRAGGED_ITEM'; payload: { dept: Department; index: number; parentId?: string } | null } + | { type: 'SET_DRAG_OVER_ITEM'; payload: { index: number; parentId?: string } | null } + | { type: 'REFRESH_DATA' }; + +const departmentManagementReducer = (state: DepartmentManagementState, action: DepartmentManagementAction): DepartmentManagementState => { + switch (action.type) { + case 'SET_DEPARTMENTS': + return { ...state, departments: action.payload, loading: false, error: null }; + case 'SET_LOADING': + return { ...state, loading: action.payload }; + case 'SET_ERROR': + return { ...state, error: action.payload, loading: false }; + case 'TOGGLE_EXPAND': + const newExpanded = new Set(state.expandedIds); + if (newExpanded.has(action.payload)) { + newExpanded.delete(action.payload); + } else { + newExpanded.add(action.payload); + } + return { ...state, expandedIds: newExpanded }; + case 'SET_EXPANDED_IDS': + return { ...state, expandedIds: action.payload }; + case 'TOGGLE_FORM': + return { ...state, showForm: !state.showForm }; + case 'TOGGLE_DELETE_DIALOG': + return { ...state, showDeleteDialog: !state.showDeleteDialog }; + case 'SET_EDITING_DEPARTMENT': + return { ...state, editingDepartment: action.payload }; + case 'SET_PARENT_DEPARTMENT': + return { ...state, parentDepartment: action.payload }; + case 'SET_DELETING_DEPARTMENT': + return { ...state, deletingDepartment: action.payload }; + case 'SET_DRAGGED_ITEM': + return { ...state, draggedItem: action.payload }; + case 'SET_DRAG_OVER_ITEM': + return { ...state, dragOverItem: action.payload }; + case 'REFRESH_DATA': + return { ...state, error: null }; + default: + return state; + } +}; + +const initialState: DepartmentManagementState = { + departments: [], + expandedIds: new Set(), + loading: false, + error: null, + showForm: false, + showDeleteDialog: false, + editingDepartment: null, + parentDepartment: null, + deletingDepartment: null, + draggedItem: null, + dragOverItem: null, +}; + +export default function DepartmentManagementPage() { + const [state, dispatch] = useReducer(departmentManagementReducer, initialState); + const isFirstLoad = useRef(true); + + // 加载部门数据 + const loadDepartments = async () => { + try { + dispatch({ type: 'SET_LOADING', payload: true }); + + // 暂时使用mock数据,后续可以替换为API调用 + const mockDepartments: Department[] = [ + { + id: 'dept-1', + name: '技术部', + code: 'TECH', + level: 1, + manager: '王技术', + phone: '13800138001', + email: 'tech@example.com', + description: '负责技术研发和系统维护', + sort: 1, + status: 'active', + createdAt: '2024-01-01T00:00:00', + updatedAt: '2024-01-01T00:00:00', + children: [ + { + id: 'dept-1-1', + parentId: 'dept-1', + name: '研发组', + code: 'TECH-RD', + level: 2, + manager: '李研发', + phone: '13800138011', + description: '负责系统研发', + sort: 1, + status: 'active', + createdAt: '2024-01-01T00:00:00', + updatedAt: '2024-01-01T00:00:00', + }, + { + id: 'dept-1-2', + parentId: 'dept-1', + name: '运维组', + code: 'TECH-OPS', + level: 2, + manager: '张运维', + phone: '13800138012', + description: '负责系统运维', + sort: 2, + status: 'active', + createdAt: '2024-01-01T00:00:00', + updatedAt: '2024-01-01T00:00:00', + }, + ], + }, + { + id: 'dept-2', + name: '管理部', + code: 'ADMIN', + level: 1, + manager: '赵管理', + phone: '13800138002', + email: 'admin@example.com', + description: '负责行政管理', + sort: 2, + status: 'active', + createdAt: '2024-01-01T00:00:00', + updatedAt: '2024-01-01T00:00:00', + children: [ + { + id: 'dept-2-1', + parentId: 'dept-2', + name: '人事组', + code: 'ADMIN-HR', + level: 2, + manager: '孙人事', + phone: '13800138021', + description: '负责人力资源管理', + sort: 1, + status: 'active', + createdAt: '2024-01-01T00:00:00', + updatedAt: '2024-01-01T00:00:00', + }, + { + id: 'dept-2-2', + parentId: 'dept-2', + name: '财务组', + code: 'ADMIN-FIN', + level: 2, + manager: '周财务', + phone: '13800138022', + description: '负责财务管理', + sort: 2, + status: 'active', + createdAt: '2024-01-01T00:00:00', + updatedAt: '2024-01-01T00:00:00', + }, + ], + }, + { + id: 'dept-3', + name: '作业部', + code: 'OPS', + level: 1, + manager: '吴作业', + phone: '13800138003', + email: 'ops@example.com', + description: '负责农机作业管理', + sort: 3, + status: 'active', + createdAt: '2024-01-01T00:00:00', + updatedAt: '2024-01-01T00:00:00', + children: [ + { + id: 'dept-3-1', + parentId: 'dept-3', + name: '第一作业组', + code: 'OPS-T1', + level: 2, + manager: '郑组长', + phone: '13800138031', + description: '负责区域A作业', + sort: 1, + status: 'active', + createdAt: '2024-01-01T00:00:00', + updatedAt: '2024-01-01T00:00:00', + }, + { + id: 'dept-3-2', + parentId: 'dept-3', + name: '第二作业组', + code: 'OPS-T2', + level: 2, + manager: '钱组长', + phone: '13800138032', + description: '负责区域B作业', + sort: 2, + status: 'active', + createdAt: '2024-01-01T00:00:00', + updatedAt: '2024-01-01T00:00:00', + }, + ], + }, + ]; + + dispatch({ type: 'SET_DEPARTMENTS', payload: mockDepartments }); + // 默认展开所有一级部门 + dispatch({ type: 'SET_EXPANDED_IDS', payload: new Set(mockDepartments.map(d => d.id)) }); + } catch (error) { + console.error('Failed to load departments:', error); + dispatch({ + type: 'SET_ERROR', + payload: error instanceof Error ? error.message : '加载部门数据失败' + }); + } + }; + + // 统计部门数量 + const countDepartments = (depts: Department[]): { level1: number; level2: number; total: number } => { + let level1 = 0; + let level2 = 0; + + depts.forEach(dept => { + if (!dept.parentId) { + level1++; + if (dept.children) { + level2 += dept.children.length; + } + } + }); + + return { level1, level2, total: level1 + level2 }; + }; + + const stats = countDepartments(state.departments); + + // 展开/收起部门 + const toggleExpand = (id: string) => { + dispatch({ type: 'TOGGLE_EXPAND', payload: id }); + }; + + // 展开全部 + const expandAll = () => { + const allIds = new Set(); + const collectIds = (depts: Department[]) => { + depts.forEach(dept => { + allIds.add(dept.id); + if (dept.children) { + collectIds(dept.children); + } + }); + }; + collectIds(state.departments); + dispatch({ type: 'SET_EXPANDED_IDS', payload: allIds }); + }; + + // 收起全部 + const collapseAll = () => { + dispatch({ type: 'SET_EXPANDED_IDS', payload: new Set() }); + }; + + // 添加部门 + const handleAdd = (parent?: Department) => { + dispatch({ type: 'SET_EDITING_DEPARTMENT', payload: null }); + dispatch({ type: 'SET_PARENT_DEPARTMENT', payload: parent || null }); + dispatch({ type: 'TOGGLE_FORM', payload: true }); + }; + + // 编辑部门 + const handleEdit = (dept: Department) => { + dispatch({ type: 'SET_EDITING_DEPARTMENT', payload: dept }); + dispatch({ type: 'SET_PARENT_DEPARTMENT', payload: null }); + dispatch({ type: 'TOGGLE_FORM', payload: true }); + }; + + // 删除部门 + const handleDelete = (dept: Department) => { + if (dept.children && dept.children.length > 0) { + toast.error('请先删除该部门下的子部门'); + return; + } + dispatch({ type: 'SET_DELETING_DEPARTMENT', payload: dept }); + dispatch({ type: 'TOGGLE_DELETE_DIALOG', payload: true }); + }; + + // 保存部门 + const handleSave = (formData: Partial) => { + if (!formData.name || !formData.code) { + toast.error('请填写必填项'); + return; + } + + const now = new Date().toISOString(); + + if (state.editingDepartment) { + // 更新部门 + const updateInTree = (items: Department[]): Department[] => { + return items.map(item => { + if (item.id === state.editingDepartment!.id) { + return { + ...item, + ...formData, + updatedAt: now, + children: item.children, + } as Department; + } + if (item.children) { + return { + ...item, + children: updateInTree(item.children), + }; + } + return item; + }); + }; + + const updated = updateInTree(state.departments); + dispatch({ type: 'SET_DEPARTMENTS', payload: updated }); + toast.success('部门更新成功'); + } else { + // 新增部门 + const newDept: Department = { + id: `dept-${Date.now()}`, + ...formData as Department, + createdAt: now, + updatedAt: now, + }; + + if (state.parentDepartment) { + // 添加到父部门下 + const addToParent = (items: Department[]): Department[] => { + return items.map(item => { + if (item.id === state.parentDepartment!.id) { + return { + ...item, + children: [...(item.children || []), newDept], + }; + } + if (item.children) { + return { + ...item, + children: addToParent(item.children), + }; + } + return item; + }); + }; + + const updated = addToParent(state.departments); + dispatch({ type: 'SET_DEPARTMENTS', payload: updated }); + dispatch({ type: 'TOGGLE_EXPAND', payload: state.parentDepartment.id }); + } else { + // 添加为一级部门 + const updated = [...state.departments, newDept]; + dispatch({ type: 'SET_DEPARTMENTS', payload: updated }); + } + + toast.success('部门添加成功'); + } + + dispatch({ type: 'TOGGLE_FORM', payload: false }); + }; + + // 确认删除 + const confirmDelete = () => { + if (!state.deletingDepartment) return; + + const deleteFromTree = (items: Department[]): Department[] => { + return items + .filter(item => item.id !== state.deletingDepartment!.id) + .map(item => { + if (item.children) { + return { + ...item, + children: deleteFromTree(item.children), + }; + } + return item; + }); + }; + + const updated = deleteFromTree(state.departments); + dispatch({ type: 'SET_DEPARTMENTS', payload: updated }); + toast.success('部门删除成功'); + + dispatch({ type: 'TOGGLE_DELETE_DIALOG', payload: false }); + dispatch({ type: 'SET_DELETING_DEPARTMENT', payload: null }); + }; + + // 拖拽功能 + const handleDragStart = (dept: Department, index: number, parentId?: string) => { + dispatch({ type: 'SET_DRAGGED_ITEM', payload: { dept, index, parentId } }); + }; + + const handleDragEnd = () => { + dispatch({ type: 'SET_DRAGGED_ITEM', payload: null }); + dispatch({ type: 'SET_DRAG_OVER_ITEM', payload: null }); + }; + + const handleDragOver = (e: React.DragEvent, index: number, parentId?: string) => { + e.preventDefault(); + if (state.draggedItem && state.draggedItem.parentId === parentId) { + dispatch({ type: 'SET_DRAG_OVER_ITEM', payload: { index, parentId } }); + } + }; + + const handleDragLeave = () => { + dispatch({ type: 'SET_DRAG_OVER_ITEM', payload: null }); + }; + + const handleDrop = (e: React.DragEvent, hoverIndex: number, parentId?: string) => { + e.preventDefault(); + + if (!state.draggedItem) return; + + if (state.draggedItem.parentId !== parentId) { + toast.error('不能跨级别拖动部门'); + dispatch({ type: 'SET_DRAG_OVER_ITEM', payload: null }); + return; + } + + const dragIndex = state.draggedItem.index; + if (dragIndex === hoverIndex) { + dispatch({ type: 'SET_DRAG_OVER_ITEM', payload: null }); + return; + } + + let updated: Department[]; + + if (!parentId) { + // 一级部门 + const newDepts = [...state.departments]; + const [removed] = newDepts.splice(dragIndex, 1); + newDepts.splice(hoverIndex, 0, removed); + + updated = newDepts.map((item, index) => ({ + ...item, + sort: index + 1, + })); + } else { + // 二级部门 + const updateInTree = (items: Department[]): Department[] => { + return items.map(item => { + if (item.id === parentId && item.children) { + const newChildren = [...item.children]; + const [removed] = newChildren.splice(dragIndex, 1); + newChildren.splice(hoverIndex, 0, removed); + + return { + ...item, + children: newChildren.map((child, index) => ({ + ...child, + sort: index + 1, + })), + }; + } + if (item.children) { + return { + ...item, + children: updateInTree(item.children), + }; + } + return item; + }); + }; + + updated = updateInTree(state.departments); + } + + dispatch({ type: 'SET_DEPARTMENTS', payload: updated }); + toast.success('部门顺序已更新'); + dispatch({ type: 'SET_DRAG_OVER_ITEM', payload: null }); + }; + + // 合并所有状态变化,统一处理数据加载 + useEffect(() => { + if (isFirstLoad.current) { + // 首次加载 + isFirstLoad.current = false; + loadDepartments(); + } + }, []); + + return ( +
+ {/* 页面标题 */} + handleAdd()} /> + + {/* 统计卡片 */} + + + {/* 部门树 */} + + + {/* 表单对话框 */} + dispatch({ type: 'TOGGLE_FORM', payload: open })} + editingDepartment={state.editingDepartment} + parentDepartment={state.parentDepartment} + onSave={handleSave} + /> + + {/* 删除确认对话框 */} + dispatch({ type: 'TOGGLE_DELETE_DIALOG', payload: open })} + deletingDepartment={state.deletingDepartment} + onConfirm={confirmDelete} + /> + + {/* 功能说明 */} + + + {/* 错误显示 */} + {state.error && ( +
+
+ {state.error} +
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/user/department/types.ts b/crop-x/src/app/(app)/central-config/user/department/types.ts new file mode 100644 index 0000000..f9cdb78 --- /dev/null +++ b/crop-x/src/app/(app)/central-config/user/department/types.ts @@ -0,0 +1,60 @@ +/** + * filekorolheader: 部门管理类型定义 - 部门数据类型和接口定义 + * 功能:TypeScript类型定义、接口规范、数据结构 + * 路径:/central-config/user/department/types + * 规范:遵循crop-x/docs/开发项目规范.md,TypeScript类型安全 + */ + +// 部门状态枚举 +export type DepartmentStatus = 'active' | 'inactive'; + +// 部门接口定义 +export interface Department { + id: string; + name: string; + code: string; + level: number; + manager?: string; + phone?: string; + email?: string; + description?: string; + sort: number; + status: DepartmentStatus; + parentId?: string; + createdAt: string; + updatedAt: string; + children?: Department[]; +} + +// 创建部门表单数据类型 +export interface CreateDepartmentForm { + name: string; + code: string; + manager?: string; + phone?: string; + email?: string; + description?: string; + sort: number; + status: DepartmentStatus; + parentId?: string; + level: number; +} + +// 部门统计数据类型 +export interface DepartmentStats { + level1: number; + level2: number; + total: number; +} + +// 拖拽项目类型 +export interface DraggedItem { + dept: Department; + index: number; + parentId?: string; +} + +export interface DragOverItem { + index: number; + parentId?: string; +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/user/employee/components/EmployeeDetailDialog.tsx b/crop-x/src/app/(app)/central-config/user/employee/components/EmployeeDetailDialog.tsx index e4e5c1c..af1d3a6 100644 --- a/crop-x/src/app/(app)/central-config/user/employee/components/EmployeeDetailDialog.tsx +++ b/crop-x/src/app/(app)/central-config/user/employee/components/EmployeeDetailDialog.tsx @@ -31,6 +31,19 @@ export function EmployeeDetailDialog({ } }; + const getAuditStatusBadge = (auditStatus?: string) => { + switch (auditStatus) { + case 'pending': + return 待审核; + case 'approved': + return 审核通过; + case 'rejected': + return 已驳回; + default: + return 未知; + } + }; + if (!selectedEmployee) return null; return ( @@ -42,68 +55,106 @@ export function EmployeeDetailDialog({ 查看员工的详细信息 -
-
-
- -
{selectedEmployee.name}
-
-
- -
{selectedEmployee.username}
-
-
- -
{selectedEmployee.phone}
-
-
- -
{selectedEmployee.email || '-'}
-
-
- -
{selectedEmployee.department || '-'}
-
-
- -
{selectedEmployee.position || '-'}
-
-
- -
{getStatusBadge(selectedEmployee.status)}
-
-
- -
- {selectedEmployee.roles && selectedEmployee.roles.length > 0 ? ( - selectedEmployee.roles.map((role, index) => ( - - {role} - - )) - ) : ( - 未分配角色 - )} +
+ {/* 基本信息 */} +
+

基本信息

+
+
+ +
{selectedEmployee.name}
+
+
+ +
{selectedEmployee.username}
+
+
+ +
{selectedEmployee.phone}
+
+
+ +
{selectedEmployee.email || '-'}
- {selectedEmployee.lastLoginTime && ( +
+ + {/* 工作信息 */} +
+

工作信息

+
- -
- {new Date(selectedEmployee.lastLoginTime).toLocaleString('zh-CN')} + +
{selectedEmployee.department || '-'}
+
+
+ +
{getStatusBadge(selectedEmployee.status)}
+
+
+ +
{getAuditStatusBadge(selectedEmployee.auditStatus)}
+
+ {selectedEmployee.auditReason && ( +
+ +
{selectedEmployee.auditReason}
+
+ )} + {selectedEmployee.auditor && ( +
+ +
{selectedEmployee.auditor}
+
+ )} + {selectedEmployee.auditTime && ( +
+ +
{new Date(selectedEmployee.auditTime).toLocaleString('zh-CN')}
+
+ )} +
+
+ + {/* 角色权限 */} +
+

角色权限

+
+ {selectedEmployee.roles && selectedEmployee.roles.length > 0 ? ( + selectedEmployee.roles.map((role, index) => ( + + {role} + + )) + ) : ( + 未分配角色 + )} +
+
+ + {/* 系统信息 */} +
+

系统信息

+
+ {selectedEmployee.lastLoginTime && ( +
+ +
+ {new Date(selectedEmployee.lastLoginTime).toLocaleString('zh-CN')} +
+
+ )} +
+ +
+ {new Date(selectedEmployee.createdAt).toLocaleString('zh-CN')}
- )} -
- -
- {new Date(selectedEmployee.createdAt).toLocaleString('zh-CN')} -
-
-
- -
- {new Date(selectedEmployee.updatedAt).toLocaleString('zh-CN')} +
+ +
+ {new Date(selectedEmployee.updatedAt).toLocaleString('zh-CN')} +
diff --git a/crop-x/src/app/(app)/central-config/user/employee/components/EmployeeFormDialog.tsx b/crop-x/src/app/(app)/central-config/user/employee/components/EmployeeFormDialog.tsx index 5c21e41..a705eee 100644 --- a/crop-x/src/app/(app)/central-config/user/employee/components/EmployeeFormDialog.tsx +++ b/crop-x/src/app/(app)/central-config/user/employee/components/EmployeeFormDialog.tsx @@ -37,62 +37,82 @@ export function EmployeeFormDialog({ {editingEmployee ? '编辑员工信息' : '添加新员工'} -
-
-
- - onFormDataChange({ ...formData, username: e.target.value })} - placeholder="登录用户名" - /> +
+ {/* 基本信息 */} +
+

基本信息

+
+
+ + onFormDataChange({ ...formData, username: e.target.value })} + placeholder="登录用户名" + /> +
+
+ + onFormDataChange({ ...formData, name: e.target.value })} + placeholder="真实姓名" + /> +
+
+ + onFormDataChange({ ...formData, phone: e.target.value })} + placeholder="11位手机号码" + /> +
+
+ + onFormDataChange({ ...formData, email: e.target.value })} + placeholder="电子邮箱" + /> +
+
+ + onFormDataChange({ ...formData, idCard: e.target.value })} + placeholder="18位身份证号码" + /> +
+
+ + onFormDataChange({ ...formData, address: e.target.value })} + placeholder="详细住址" + /> +
-
- - onFormDataChange({ ...formData, name: e.target.value })} - placeholder="真实姓名" - /> -
-
- - onFormDataChange({ ...formData, phone: e.target.value })} - placeholder="手机号码" - /> -
-
- - onFormDataChange({ ...formData, email: e.target.value })} - placeholder="电子邮箱" - /> -
-
- - onFormDataChange({ ...formData, department: e.target.value })} - placeholder="所属部门" - /> -
-
- - onFormDataChange({ ...formData, position: e.target.value })} - placeholder="职位名称" - /> +
+ + {/* 工作信息 */} +
+

工作信息

+
+
+ + onFormDataChange({ ...formData, department: e.target.value })} + placeholder="所属部门" + /> +
diff --git a/crop-x/src/app/(app)/central-config/user/employee/components/EmployeeList.tsx b/crop-x/src/app/(app)/central-config/user/employee/components/EmployeeList.tsx index 7a061e9..45557dd 100644 --- a/crop-x/src/app/(app)/central-config/user/employee/components/EmployeeList.tsx +++ b/crop-x/src/app/(app)/central-config/user/employee/components/EmployeeList.tsx @@ -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 正常; case 'frozen': @@ -34,7 +58,20 @@ export function EmployeeList({ case 'inactive': return 停用; default: - return {status}; + return {finalStatus}; + } + }; + + const getAuditStatusBadge = (auditStatus?: string) => { + switch (auditStatus) { + case 'pending': + return 待审核; + case 'approved': + return 审核通过; + case 'rejected': + return 已驳回; + default: + return 未知; } }; @@ -47,14 +84,23 @@ export function EmployeeList({ 用户名 电话 部门 - 职位 角色 - 状态 + 账号状态 + 审核状态 操作 - {employees.length === 0 ? ( + {loading && employees.length === 0 ? ( + + +
+ + 加载中... +
+
+
+ ) : employees.length === 0 ? ( 暂无数据 @@ -62,20 +108,40 @@ export function EmployeeList({ ) : ( employees.map((employee) => ( - - {employee.name} + + {employee.displayName || employee.name || employee.username} {employee.username} - {employee.phone} - {employee.department || '-'} - {employee.position || '-'} + {employee.phone || '-'} + {employee.departmentName || employee.department || '-'} {employee.roles && employee.roles.length > 0 ? employee.roles.join(', ') : '-'} - {getStatusBadge(employee.status)} + {getStatusBadge(employee.isActive, employee.status)} + {getAuditStatusBadge(employee.auditStatus)}
+ {employee.auditStatus === 'pending' && onAudit && ( + <> + + + + )}