diff --git a/crop-x/next-env.d.ts b/crop-x/next-env.d.ts index c4b7818..9edff1c 100644 --- a/crop-x/next-env.d.ts +++ b/crop-x/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/crop-x/src/app/(app)/central-config/tenant/enterprise-management/page.tsx b/crop-x/src/app/(app)/central-config/tenant/enterprise-management/page.tsx index f917b41..96abca3 100644 --- a/crop-x/src/app/(app)/central-config/tenant/enterprise-management/page.tsx +++ b/crop-x/src/app/(app)/central-config/tenant/enterprise-management/page.tsx @@ -6,137 +6,334 @@ */ 'use client'; -import { useReducer, useEffect, useMemo } from 'react'; -import { Card } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; +import { useEffect, useMemo, useState, useCallback } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { ScrollArea } from '@/components/ui/scroll-area'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Building2, Eye, Power, PowerOff, Search, FileText, CreditCard, User, RefreshCw, AlertCircle, ChevronLeft, ChevronRight, Plus } from 'lucide-react'; +import { Label } from '@/components/ui/label'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import { Building2, Eye, Power, PowerOff, Plus, FileText, CreditCard, User, Search } from 'lucide-react'; import { toast } from 'sonner'; -import { enterpriseReducer, initialState, EnterpriseState, EnterpriseAction } from './components/enterpriseReducer'; +import SearchFormPagination, { + type SearchFieldConfig, + type TableColumnConfig +} from '@/components/common/searchFormPagination'; + import { fetchTenants, transformTenantData, enableTenant, disableTenant, createEnterprise, TenantsQueryParams, Enterprise } from './components/enterpriseApi'; import { CreateEnterpriseDialog } from './components/CreateEnterpriseDialog'; // Utility functions const getStatusBadge = (status: 'active' | 'inactive') => { if (status === 'active') { - return 启用; + return ( +
+ 启用 +
+ ); } - return 禁用; + return ( +
+ 禁用 +
+ ); }; const getAuditStatusBadge = (auditStatus: Enterprise['auditStatus']) => { switch (auditStatus) { case 'draft': - return 草稿; + return ( +
+ 草稿 +
+ ); case 'pending': - return 待审核; + return ( +
+ 待审核 +
+ ); case 'approved': - return 审核通过; + return ( +
+ 审核通过 +
+ ); case 'rejected': - return 已拒绝; + return ( +
+ 已拒绝 +
+ ); default: - return 草稿; + return ( +
+ 草稿 +
+ ); } }; export default function EnterpriseManagement() { - const [state, dispatch] = useReducer(enterpriseReducer, initialState); + // 对话框状态管理 + const [dialogs, setDialogs] = useState({ + showViewDialog: false, + showAddDialog: false, + showStatusDialog: false, + selectedEnterprise: null as Enterprise | null, + statusAction: 'enable' as 'enable' | 'disable' + }); - // 加载企业数据 - const loadEnterprises = async (resetPage = false) => { - try { - dispatch({ type: 'SET_LOADING', payload: true }); - - const params: TenantsQueryParams = { - search: state.filters.search || undefined, - audit_status: state.filters.audit_status || undefined, - page: resetPage ? 1 : state.pagination.page, - size: state.pagination.size, - order_by: state.sortBy, - sort_order: state.sortOrder, - }; - - const response = await fetchTenants(params); - const transformedData = response.data.map(transformTenantData); - - console.log('API Response:', response); - console.log('Transformed Data:', transformedData); - - dispatch({ - type: 'SET_ENTERPRISES', - payload: { - data: transformedData, - pagination: { - 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 enterprises:', error); - const errorMessage = error instanceof Error ? error.message : '加载企业数据失败'; - dispatch({ type: 'SET_ERROR', payload: errorMessage }); - toast.error(errorMessage); + const dispatch = (action: any) => { + switch (action.type) { + case 'SET_SELECTED_ENTERPRISE': + setDialogs(prev => ({ ...prev, selectedEnterprise: action.payload })); + break; + case 'TOGGLE_VIEW_DIALOG': + setDialogs(prev => ({ ...prev, showViewDialog: action.payload })); + break; + case 'TOGGLE_ADD_DIALOG': + setDialogs(prev => ({ ...prev, showAddDialog: action.payload })); + break; + case 'TOGGLE_STATUS_DIALOG': + setDialogs(prev => ({ ...prev, showStatusDialog: action.payload })); + break; + case 'SET_STATUS_ACTION': + setDialogs(prev => ({ ...prev, statusAction: action.payload })); + break; + case 'RESET_FORM_DATA': + setDialogs(prev => ({ ...prev, selectedEnterprise: null })); + break; } }; - // 初始加载 - useEffect(() => { - loadEnterprises(true); - }, [state.filters.search, state.filters.audit_status, state.sortBy, state.sortOrder]); + // 搜索字段配置 + const searchFields: SearchFieldConfig[] = [ + { + key: 'search', + label: '搜索', + type: 'text', + placeholder: '搜索企业名称、编码...', + }, + { + key: 'audit_status', + label: '审核状态', + type: 'select', + placeholder: '审核状态', + defaultValue: 'all', + options: [ + { value: 'all', label: '全部状态' }, + { value: '草稿', label: '草稿' }, + { value: '待审核', label: '待审核' }, + { value: '已通过', label: '审核通过' }, + { value: '已拒绝', label: '已拒绝' }, + ], + }, + ]; - // 分页加载 - useEffect(() => { - if (state.pagination.page > 1) { - loadEnterprises(false); - } - }, [state.pagination.page]); - - // 计算统计数据 - const stats = useMemo(() => ({ - total: state.enterprises.length, - active: state.enterprises.filter(e => e.status === 'active').length, - inactive: state.enterprises.filter(e => e.status === 'inactive').length, - }), [state.enterprises]); - - // 事件处理器 - const handleSearch = (value: string) => { - dispatch({ type: 'SET_FILTERS', payload: { search: value } }); - }; - - const handleAuditStatusFilter = (value: string) => { - dispatch({ type: 'SET_FILTERS', payload: { audit_status: value === 'all' ? '' : value } }); - }; - - const handleSort = (sortBy?: string) => { - const newSortOrder = state.sortBy === sortBy && state.sortOrder === 'desc' ? 'asc' : 'desc'; - dispatch({ type: 'SET_SORT', payload: { sortBy, sortOrder: newSortOrder } }); - }; - - const handlePageChange = (page: number) => { - // 边界检查,确保页码在有效范围内 - if (page < 1) { - page = 1; - } else if (page > state.pagination.totalPages && state.pagination.totalPages > 0) { - page = state.pagination.totalPages; - } - dispatch({ type: 'SET_PAGINATION', payload: { page } }); - }; + // 表格列配置 + const columns: TableColumnConfig[] = [ + { + key: 'code', + label: '企业编码', + width: '120px', + }, + { + key: 'name', + label: '企业名称', + render: (value: string) => ( +
+ + {value} +
+ ), + }, + { + key: 'type', + label: '企业类型', + render: (value: string) => ( +
+ {value} +
+ ), + }, + { + key: 'registrant', + label: '登记人', + render: (value?: string) => value || '-', + }, + { + key: 'contactPhone', + label: '联系电话', + render: (value?: string) => value || '-', + }, + { + key: 'createdAt', + label: '创建时间', + width: '160px', + }, + { + key: 'auditStatus', + label: '审核状态', + render: (value: Enterprise['auditStatus']) => getAuditStatusBadge(value), + }, + { + key: 'status', + label: '状态', + render: (value: Enterprise['status']) => getStatusBadge(value), + }, + { + key: 'actions', + label: '操作', + render: (_: any, row: Enterprise) => ( +
+ + {row.status === 'active' ? ( + + ) : ( + + )} +
+ ), + }, + ]; + // 简化的状态管理 - 只需要存储数据和加载状态 + const [enterprises, setEnterprises] = useState([]); + const [pagination, setPagination] = useState({ + page: 1, + size: 10, + total: 0, + totalPages: 0, + hasNext: false, + hasPrev: false, + }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [searchFilters, setSearchFilters] = useState>({ + search: '', + audit_status: 'all' + }); + + // 数据加载函数 - 移除不必要的依赖避免重复调用 + const loadEnterprises = useCallback(async (params?: { + filters?: Record; + pagination?: { page: number; size: number }; + sort?: { sortBy?: string; sortOrder?: 'asc' | 'desc' }; + }) => { + try { + console.log('调用了loadEnterprises') + setLoading(true); + setError(null); + + const finalParams: TenantsQueryParams = { + search: (params?.filters?.search ?? searchFilters.search) || undefined, + audit_status: params?.filters?.audit_status ?? searchFilters.audit_status, + page: params?.pagination?.page || pagination.page, + size: params?.pagination?.size || pagination.size, + order_by: params?.sort?.sortBy, + sort_order: params?.sort?.sortOrder, + }; + + // 处理audit_status,如果为'all'则不传该参数 + if (finalParams.audit_status === 'all') { + finalParams.audit_status = undefined; + } + const response = await fetchTenants(finalParams); + const transformedData = response.data.map(transformTenantData); + + setEnterprises(transformedData); + setPagination({ + page: response.page, + size: response.size, + total: response.total, + totalPages: response.total_pages, + hasNext: response.has_next, + hasPrev: response.has_prev, + }); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : '加载企业数据失败'; + setError(errorMessage); + toast.error(errorMessage); + } finally { + setLoading(false); + } + }, []); // 移除所有依赖,使用参数传递状态变化 + + // 事件处理器 + const handleSearch = useCallback((filters: Record) => { + setSearchFilters(filters); + // 搜索时重置到第一页 + loadEnterprises({ + filters, + pagination: { page: 1, size: pagination.size } + }); + }, [loadEnterprises, pagination.size]); + + const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => { + // 排序时重置到第一页 + loadEnterprises({ + pagination: { page: 1, size: pagination.size }, + sort: { sortBy, sortOrder } + }); + }, [loadEnterprises, pagination.size]); + + const handlePageChange = useCallback((page: number) => { + setPagination(prev => ({ ...prev, page })); + loadEnterprises({ + pagination: { page, size: pagination.size } + }); + }, [loadEnterprises, pagination.size]); + + const handleSizeChange = useCallback((size: number) => { + setPagination(prev => ({ ...prev, size, page: 1 })); + loadEnterprises({ + pagination: { page: 1, size } + }); + }, [loadEnterprises]); + + // 初始化数据加载 + // useEffect(() => { + // loadEnterprises(); + // }, []); + + // 计算统计数据 + const stats = useMemo(() => { + if (enterprises.length === 0) { + return { total: pagination.total, active: 0, inactive: 0 }; + } + const active = enterprises.filter(e => e.status === 'active').length; + const inactive = enterprises.filter(e => e.status === 'inactive').length; + return { total: pagination.total, active, inactive }; + }, [enterprises, pagination.total]); + + // 业务事件处理器 const handleView = (enterprise: Enterprise) => { dispatch({ type: 'SET_SELECTED_ENTERPRISE', payload: enterprise }); dispatch({ type: 'TOGGLE_VIEW_DIALOG', payload: true }); @@ -148,26 +345,16 @@ export default function EnterpriseManagement() { dispatch({ type: 'TOGGLE_STATUS_DIALOG', payload: true }); }; - const handleCreateNew = () => { - dispatch({ type: 'RESET_FORM_DATA' }); - dispatch({ type: 'TOGGLE_ADD_DIALOG', payload: true }); - }; - - const handleCreateSuccess = () => { - // 创建成功后刷新数据 - loadEnterprises(true); - }; - const confirmStatusChange = async () => { - if (!state.selectedEnterprise) return; + if (!dialogs.selectedEnterprise) return; try { - dispatch({ type: 'SET_LOADING', payload: true }); + setLoading(true); - const tenantId = state.selectedEnterprise.id; + const tenantId = dialogs.selectedEnterprise.id; let updatedTenant; - if (state.statusAction === 'enable') { + if (dialogs.statusAction === 'enable') { updatedTenant = await enableTenant(tenantId); toast.success('企业已启用'); } else { @@ -175,37 +362,49 @@ export default function EnterpriseManagement() { toast.success('企业已禁用'); } - // 验证返回的数据是否正确更新了状态 - console.log('API返回的更新数据:', updatedTenant); - - // 更新本地状态 - const updatedEnterprise = transformTenantData(updatedTenant); - dispatch({ - type: 'SET_ENTERPRISES', - payload: { - data: state.enterprises.map(ent => - ent.id === tenantId ? updatedEnterprise : ent - ), - pagination: state.pagination - } - }); - + // 状态更新成功后关闭对话框 dispatch({ type: 'TOGGLE_STATUS_DIALOG', payload: false }); - // 不需要立即刷新,因为本地状态已经更新 - // 如果用户需要看到最新数据,可以手动点击刷新按钮 + // 重新加载数据来反映状态变化 + const reloadParams: any = { + filters: searchFilters, + pagination: { + page: pagination.page, + size: pagination.size + } + }; + + loadEnterprises(reloadParams); } catch (error) { console.error('Status change failed:', error); const errorMessage = error instanceof Error ? error.message : '状态更新失败'; toast.error(errorMessage); } finally { - dispatch({ type: 'SET_LOADING', payload: false }); + setLoading(false); } }; + const handleCreateNew = () => { + dispatch({ type: 'RESET_FORM_DATA' }); + dispatch({ type: 'TOGGLE_ADD_DIALOG', payload: true }); + }; + + const handleCreateSuccess = () => { + // 创建成功后需要手动刷新页面数据 + window.location.reload(); + }; + + // 操作按钮配置 + const actionButtons = ( + + ); + return (
- {/* Page Header */} + {/* Page Header - 自定义页面头部 */}
@@ -216,44 +415,38 @@ export default function EnterpriseManagement() { 管理平台所有企业信息,支持查询、查看详情、启用/禁用企业

- +
智能查询 - - +
+
状态管理 - - +
+
详情查看 - +
-
- -
- {/* Statistics Cards */} + {/* Statistics Cards - 保持原有统计功能 */}
- +
企业总数
-
{state.pagination.total}
+
{pagination.total}
全部企业数量
- +
- +
启用企业
@@ -262,9 +455,9 @@ export default function EnterpriseManagement() {
正常运营中
- +
- +
禁用企业
@@ -273,208 +466,37 @@ export default function EnterpriseManagement() {
已暂停使用
- +
- {/* Enterprise List */} - -
-

企业列表

-
-
- - handleSearch(e.target.value)} - className="pl-10 w-64" - /> -
- -
-
- - {/* Error Display */} - {state.error && ( -
-
- - {state.error} -
-
- )} - - {/* Loading State */} - {state.loading && ( -
- -

加载中...

-
- )} - - {/* Data Table */} - {!state.loading && !state.error && ( - <> -
- - - - handleSort('tenant_code')} - > - 企业编码 - {state.sortBy === 'tenant_code' && ( - {state.sortOrder === 'asc' ? '↑' : '↓'} - )} - - handleSort('company_name')} - > - 企业名称 - {state.sortBy === 'company_name' && ( - {state.sortOrder === 'asc' ? '↑' : '↓'} - )} - - 企业类型 - 登记人 - 联系电话 - handleSort('created_at')} - > - 创建时间 - {state.sortBy === 'created_at' && ( - {state.sortOrder === 'asc' ? '↑' : '↓'} - )} - - 审核状态 - 状态 - 操作 - - - - {state.enterprises.map((enterprise) => ( - - {enterprise.code} - -
- - {enterprise.name} -
-
- - {enterprise.type} - - {enterprise.registrant || '-'} - {enterprise.contactPhone || '-'} - {enterprise.createdAt} - {getAuditStatusBadge(enterprise.auditStatus)} - {getStatusBadge(enterprise.status)} - -
- - {enterprise.status === 'active' ? ( - - ) : ( - - )} -
-
-
- ))} -
-
-
- - {state.enterprises.length === 0 && ( -
- -

暂无企业数据

-
- )} - - {/* Pagination */} - {state.pagination.totalPages > 1 && ( -
-
- 显示第 {state.pagination.page} 页,共 {state.pagination.totalPages} 页 - 总计 {state.pagination.total} 条记录 -
-
- - - {state.pagination.page} / {state.pagination.totalPages} - - -
-
- )} - - )} -
+ {/* 使用SearchFormPagination组件替换原有的企业列表 */} + } + emptyText="暂无企业数据" + /> {/* View Enterprise Details Dialog */} - dispatch({ type: 'TOGGLE_VIEW_DIALOG', payload: open })}> + dispatch({ type: 'TOGGLE_VIEW_DIALOG', payload: open })}>
企业详情 - {state.selectedEnterprise && ( + {dialogs.selectedEnterprise && (
- {getAuditStatusBadge(state.selectedEnterprise.auditStatus)} - {getStatusBadge(state.selectedEnterprise.status)} + {getAuditStatusBadge(dialogs.selectedEnterprise.auditStatus)} + {getStatusBadge(dialogs.selectedEnterprise.status)}
)}
@@ -482,7 +504,7 @@ export default function EnterpriseManagement() { 查看企业的详细信息
- {state.selectedEnterprise && ( + {dialogs.selectedEnterprise && ( @@ -509,33 +531,33 @@ export default function EnterpriseManagement() {
-
{state.selectedEnterprise.name}
+
{dialogs.selectedEnterprise.name}
-
{state.selectedEnterprise.code}
+
{dialogs.selectedEnterprise.code}
-
{state.selectedEnterprise.type}
+
{dialogs.selectedEnterprise.type}
- {state.selectedEnterprise.province || '-'} {state.selectedEnterprise.city || ''} {state.selectedEnterprise.district || ''} + {dialogs.selectedEnterprise.province || '-'} {dialogs.selectedEnterprise.city || ''} {dialogs.selectedEnterprise.district || ''}
-
{state.selectedEnterprise.address || '-'}
+
{dialogs.selectedEnterprise.address || '-'}
-
{state.selectedEnterprise.registrant || '-'}
+
{dialogs.selectedEnterprise.registrant || '-'}
-
{state.selectedEnterprise.contactPhone || '-'}
+
{dialogs.selectedEnterprise.contactPhone || '-'}
@@ -545,41 +567,41 @@ export default function EnterpriseManagement() {
-
{state.selectedEnterprise.companySize || '-'}
+
{dialogs.selectedEnterprise.companySize || '-'}
-
{state.selectedEnterprise.registeredCapital || '-'}
+
{dialogs.selectedEnterprise.registeredCapital || '-'}
-
{state.selectedEnterprise.establishmentDate || '-'}
+
{dialogs.selectedEnterprise.establishmentDate || '-'}
-
{state.selectedEnterprise.invoiceType || '-'}
+
{dialogs.selectedEnterprise.invoiceType || '-'}
- {state.selectedEnterprise.socialCreditCode ? ( + {dialogs.selectedEnterprise.socialCreditCode ? ( - {state.selectedEnterprise.socialCreditCode} + {dialogs.selectedEnterprise.socialCreditCode} ) : '-'}
-
{state.selectedEnterprise.businessScope || '-'}
+
{dialogs.selectedEnterprise.businessScope || '-'}
-
{state.selectedEnterprise.submitTime || '-'}
+
{dialogs.selectedEnterprise.submitTime || '-'}
-
{state.selectedEnterprise.auditTime || '-'}
+
{dialogs.selectedEnterprise.auditTime || '-'}
@@ -590,24 +612,24 @@ export default function EnterpriseManagement() {
- {state.selectedEnterprise.bankAccount ? ( + {dialogs.selectedEnterprise.bankAccount ? ( - {state.selectedEnterprise.bankAccount} + {dialogs.selectedEnterprise.bankAccount} ) : '-'}
-
{state.selectedEnterprise.bankName || '-'}
+
{dialogs.selectedEnterprise.bankName || '-'}
-
{state.selectedEnterprise.bankFullName || '-'}
+
{dialogs.selectedEnterprise.bankFullName || '-'}
-
{state.selectedEnterprise.bankAddress || '-'}
+
{dialogs.selectedEnterprise.bankAddress || '-'}
@@ -617,19 +639,19 @@ export default function EnterpriseManagement() {
-
{state.selectedEnterprise.legalPerson || '-'}
+
{dialogs.selectedEnterprise.legalPerson || '-'}
-
{state.selectedEnterprise.registrant || '-'}
+
{dialogs.selectedEnterprise.registrant || '-'}
-
{state.selectedEnterprise.auditor || '-'}
+
{dialogs.selectedEnterprise.auditor || '-'}
-
{state.selectedEnterprise.auditComment || '-'}
+
{dialogs.selectedEnterprise.auditComment || '-'}
@@ -645,20 +667,20 @@ export default function EnterpriseManagement() { {/* Status Change Confirmation Dialog */} - dispatch({ type: 'TOGGLE_STATUS_DIALOG', payload: open })}> + dispatch({ type: 'TOGGLE_STATUS_DIALOG', payload: open })}> - 确认{state.statusAction === 'enable' ? '启用' : '禁用'}企业 + 确认{dialogs.statusAction === 'enable' ? '启用' : '禁用'}企业 - {state.statusAction === 'enable' ? ( + {dialogs.statusAction === 'enable' ? ( <> - 启用企业 {state.selectedEnterprise?.name} 后,该企业用户将恢复正常登录和使用权限。 + 启用企业 {dialogs.selectedEnterprise?.name} 后,该企业用户将恢复正常登录和使用权限。 ) : ( <> - 禁用企业 {state.selectedEnterprise?.name} 后,该企业所有用户将无法登录系统。此操作不会删除企业数据,可随时重新启用。 + 禁用企业 {dialogs.selectedEnterprise?.name} 后,该企业所有用户将无法登录系统。此操作不会删除企业数据,可随时重新启用。 )} @@ -667,9 +689,9 @@ export default function EnterpriseManagement() { 取消 - 确认{state.statusAction === 'enable' ? '启用' : '禁用'} + 确认{dialogs.statusAction === 'enable' ? '启用' : '禁用'} @@ -677,7 +699,7 @@ export default function EnterpriseManagement() { {/* Create Enterprise Dialog */} dispatch({ type: 'TOGGLE_ADD_DIALOG', payload: open })} onSuccess={handleCreateSuccess} /> diff --git a/crop-x/src/components/common/searchFormPagination/components/PaginationComponent.tsx b/crop-x/src/components/common/searchFormPagination/components/PaginationComponent.tsx new file mode 100644 index 0000000..d6a0a39 --- /dev/null +++ b/crop-x/src/components/common/searchFormPagination/components/PaginationComponent.tsx @@ -0,0 +1,221 @@ +/** + * filekorolheader: 分页组件 - 可配置的分页导航组件 + * 功能:分页导航、页码跳转、每页条数设置、分页信息显示 + * 路径:/components/common/searchFormPagination/components/PaginationComponent + * 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式,支持完全自定义配置 + */ +'use client'; + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Input } from '@/components/ui/input'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; + +// 分页配置接口 +export interface PaginationConfig { + page: number; + size: number; + total: number; + totalPages: number; + hasNext: boolean; + hasPrev: boolean; +} + +// 组件Props接口 +export interface PaginationComponentProps { + pagination: PaginationConfig; + onPageChange: (page: number) => void; + onSizeChange?: (size: number) => void; + loading?: boolean; + showSizeSelector?: boolean; + showPageInfo?: boolean; + showQuickJumper?: boolean; + sizeOptions?: number[]; + maxVisiblePages?: number; + className?: string; +} + +export function PaginationComponent({ + pagination, + onPageChange, + onSizeChange, + loading = false, + showSizeSelector = true, + showPageInfo = true, + showQuickJumper = false, + sizeOptions = [10, 30, 50, 100], + maxVisiblePages = 7, + className = '', +}: PaginationComponentProps) { + const [jumpPage, setJumpPage] = useState(''); + + // 处理页码变化 + const handlePageChange = (page: number) => { + // 边界检查 + if (page < 1) page = 1; + if (page > pagination.totalPages && pagination.totalPages > 0) { + page = pagination.totalPages; + } + onPageChange(page); + }; + + // 处理每页条数变化 + const handleSizeChange = (size: string) => { + const newSize = parseInt(size, 10); + onSizeChange?.(newSize); + }; + + // 处理快速跳转 + const handleJumpPage = () => { + const page = parseInt(jumpPage, 10); + if (!isNaN(page) && page >= 1 && page <= pagination.totalPages) { + handlePageChange(page); + setJumpPage(''); + } + }; + + // 处理跳转输入框回车 + const handleJumpKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleJumpPage(); + } + }; + + // 生成可见页码数组 + const generateVisiblePages = () => { + const { page, totalPages } = pagination; + const visiblePages: number[] = []; + + if (totalPages <= maxVisiblePages) { + // 如果总页数少于最大可见页数,显示所有页码 + for (let i = 1; i <= totalPages; i++) { + visiblePages.push(i); + } + } else { + // 否则生成智能的页码显示范围 + const half = Math.floor(maxVisiblePages / 2); + let start = Math.max(1, page - half); + let end = Math.min(totalPages, start + maxVisiblePages - 1); + + // 调整开始位置,确保显示足够数量的页码 + if (end - start < maxVisiblePages - 1) { + start = Math.max(1, end - maxVisiblePages + 1); + } + + for (let i = start; i <= end; i++) { + visiblePages.push(i); + } + } + + return visiblePages; + }; + + const visiblePages = generateVisiblePages(); + const { page, total, totalPages, hasPrev, hasNext } = pagination; + + return ( +
+ {/* 左侧信息 */} +
+ {showPageInfo && ( +
+ 显示第 {page} 页,共 {totalPages} 页 + 总计 {total} 条记录 +
+ )} + + {showSizeSelector && onSizeChange && ( +
+ 每页显示 + + +
+ )} +
+ + {/* 右侧分页导航 - 只有超过一页时才显示分页按钮 */} + {totalPages > 1 && ( +
+ {/* 上一页按钮 */} + + + {/* 页码按钮 */} +
+ {visiblePages.map((pageNum) => ( + + ))} +
+ + {/* 下一页按钮 */} + + + {/* 快速跳转 */} + {showQuickJumper && totalPages > 5 && ( +
+ 跳至 + setJumpPage(e.target.value)} + onKeyPress={handleJumpKeyPress} + placeholder="页码" + className="w-16 h-8" + disabled={loading} + /> + + +
+ )} +
+ )} +
+ ); +} + +export default PaginationComponent; \ No newline at end of file diff --git a/crop-x/src/components/common/searchFormPagination/components/SearchFormComponent.tsx b/crop-x/src/components/common/searchFormPagination/components/SearchFormComponent.tsx new file mode 100644 index 0000000..fcd66db --- /dev/null +++ b/crop-x/src/components/common/searchFormPagination/components/SearchFormComponent.tsx @@ -0,0 +1,166 @@ +/** + * filekorolheader: 搜索表单组件 - 可配置的搜索条件表单 + * 功能:搜索条件输入、下拉选择、实时搜索、重置功能 + * 路径:/components/common/searchFormPagination/components/SearchFormComponent + * 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式,支持完全自定义 + */ +'use client'; + +import { useState, useEffect, useRef, memo } from 'react'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Search } from 'lucide-react'; + +// 搜索字段配置接口 +export interface SearchFieldConfig { + key: string; + type: 'text' | 'select'; + placeholder?: string; + options?: Array<{ value: string; label: string }>; + defaultValue?: string; +} + +// 组件Props接口 +export interface SearchFormComponentProps { + fields: SearchFieldConfig[]; + filters: Record; + onFiltersChange: (filters: Record) => void; + placeholder?: string; + loading?: boolean; + layout?: 'horizontal' | 'vertical'; + maxVisibleFields?: number; +} + +export function SearchFormComponent({ + fields, + filters, + onFiltersChange, + placeholder = '请输入搜索关键词...', + loading = false, + layout = 'horizontal', + maxVisibleFields = 3, +}: SearchFormComponentProps) { + const [localFilters, setLocalFilters] = useState>(filters); + const [showAllFields, setShowAllFields] = useState(false); + + // 使用ref保持最新的onFiltersChange引用,避免useEffect重复触发 + const onFiltersChangeRef = useRef(onFiltersChange); + onFiltersChangeRef.current = onFiltersChange; + + // 同步外部filters到本地state + useEffect(() => { + setLocalFilters(filters); + }, [filters]); + + // 处理输入变化 - 防抖搜索避免频繁刷新导致失焦 + const handleInputChange = (key: string, value: string) => { + const newFilters = { + ...localFilters, + [key]: value, + }; + setLocalFilters(newFilters); + }; + + // 使用防抖来减少搜索频率,避免频繁刷新导致失焦 + useEffect(() => { + const timer = setTimeout(() => { + // 使用ref引用最新的onFiltersChange函数,避免依赖变化导致重复触发 + onFiltersChangeRef.current(localFilters); + }, 300); // 300ms 防抖延迟 + + return () => clearTimeout(timer); + }, [localFilters]); // 只依赖localFilters,使用ref避免函数依赖问题 + + // 计算显示的字段 + const visibleFields = showAllFields + ? fields + : fields.slice(0, maxVisibleFields); + + const hasMoreFields = fields.length > maxVisibleFields; + + // 渲染单个搜索字段 + const renderSearchField = (field: SearchFieldConfig) => { + const value = localFilters[field.key] || field.defaultValue || ''; + + switch (field.type) { + case 'select': + return ( +
+ +
+ ); + + case 'text': + default: + return ( +
+ + handleInputChange(field.key, e.target.value)} + disabled={false} // 始终允许输入,不因加载而禁用 + className="pl-10 w-64" + /> +
+ ); + } + }; + + // 主搜索框(当没有配置字段时使用默认搜索) + const renderMainSearch = () => ( +
+ + handleInputChange('search', e.target.value)} + disabled={false} // 始终允许输入,不因加载而禁用 + className="pl-10" + /> +
+ ); + + // 如果没有配置字段,使用简单搜索 + if (fields.length === 0) { + return renderMainSearch(); + } + + return ( +
+ {/* 渲染搜索字段 */} + {visibleFields.map(renderSearchField)} + + {/* 展开/收起按钮 */} + {hasMoreFields && ( + + )} +
+ ); +} + +const MemoizedSearchFormComponent = memo(SearchFormComponent); +export default MemoizedSearchFormComponent; \ No newline at end of file diff --git a/crop-x/src/components/common/searchFormPagination/components/example.tsx b/crop-x/src/components/common/searchFormPagination/components/example.tsx new file mode 100644 index 0000000..1e852b9 --- /dev/null +++ b/crop-x/src/components/common/searchFormPagination/components/example.tsx @@ -0,0 +1,248 @@ +/** + * filekorolheader: 搜索表单分页组件使用示例 - 展示如何使用该组件 + * 功能:使用示例、配置示例、最佳实践展示 + * 路径:/components/common/searchFormPagination/components/example + * 规范:遵循crop-x/docs/开发项目规范.md,提供完整的使用示例 + */ +'use client'; + +import { SearchFormPagination, SearchFieldConfig, TableColumnConfig } from '../index'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Building2, Eye, Power, PowerOff, Plus } from 'lucide-react'; +import { toast } from 'sonner'; + +// 模拟数据类型 +interface MockEnterprise { + id: string; + code: string; + name: string; + type: string; + registrant?: string; + contactPhone?: string; + createdAt: string; + auditStatus: 'draft' | 'pending' | 'approved' | 'rejected'; + status: 'active' | 'inactive'; +} + +// 示例使用 +export function EnterpriseManagementExample() { + // 搜索字段配置 + const searchFields: SearchFieldConfig[] = [ + { + key: 'search', + label: '企业搜索', + type: 'text', + placeholder: '搜索企业名称、编码...', + }, + { + key: 'audit_status', + label: '审核状态', + type: 'select', + placeholder: '选择审核状态', + options: [ + { value: '', label: '全部状态' }, + { value: '草稿', label: '草稿' }, + { value: '待审核', label: '待审核' }, + { value: '已通过', label: '审核通过' }, + { value: '已拒绝', label: '已拒绝' }, + ], + }, + ]; + + // 表格列配置 + const columns: TableColumnConfig[] = [ + { + key: 'code', + label: '企业编码', + sortable: true, + width: '120px', + }, + { + key: 'name', + label: '企业名称', + sortable: true, + render: (value: string, row: MockEnterprise) => ( +
+ + {value} +
+ ), + }, + { + key: 'type', + label: '企业类型', + render: (value: string) => ( + {value} + ), + }, + { + key: 'registrant', + label: '登记人', + render: (value?: string) => value || '-', + }, + { + key: 'contactPhone', + label: '联系电话', + render: (value?: string) => value || '-', + }, + { + key: 'createdAt', + label: '创建时间', + sortable: true, + width: '160px', + }, + { + key: 'auditStatus', + label: '审核状态', + render: (value: MockEnterprise['auditStatus']) => { + const getAuditStatusBadge = (status: MockEnterprise['auditStatus']) => { + switch (status) { + case 'draft': + return 草稿; + case 'pending': + return 待审核; + case 'approved': + return 审核通过; + case 'rejected': + return 已拒绝; + default: + return 草稿; + } + }; + return getAuditStatusBadge(value); + }, + }, + { + key: 'status', + label: '状态', + render: (value: MockEnterprise['status']) => { + const getStatusBadge = (status: MockEnterprise['status']) => { + if (status === 'active') { + return 启用; + } + return 禁用; + }; + return getStatusBadge(value); + }, + }, + { + key: 'actions', + label: '操作', + render: (_: any, row: MockEnterprise) => ( +
+ + {row.status === 'active' ? ( + + ) : ( + + )} +
+ ), + }, + ]; + + // 模拟数据 + const mockData: MockEnterprise[] = [ + { + id: '1', + code: 'ENT001', + name: '示例科技有限公司', + type: '科技有限公司', + registrant: '张三', + contactPhone: '13800138000', + createdAt: '2024-01-15 10:30:00', + auditStatus: 'approved', + status: 'active', + }, + { + id: '2', + code: 'ENT002', + name: '测试农业发展有限公司', + type: '农业发展有限公司', + registrant: '李四', + contactPhone: '13900139000', + createdAt: '2024-01-16 14:20:00', + auditStatus: 'pending', + status: 'active', + }, + ]; + + // 模拟分页配置 + const mockPagination = { + page: 1, + size: 10, + total: 2, + totalPages: 1, + hasNext: false, + hasPrev: false, + }; + + // 处理搜索 + const handleSearch = (filters: Record) => { + console.log('搜索条件:', filters); + toast.success('搜索条件已更新'); + }; + + // 处理排序 + const handleSort = (sortBy: string, sortOrder: 'asc' | 'desc') => { + console.log('排序:', { sortBy, sortOrder }); + toast.success(`排序: ${sortBy} ${sortOrder}`); + }; + + // 处理分页 + const handlePageChange = (page: number) => { + console.log('切换到页面:', page); + toast.success(`切换到第 ${page} 页`); + }; + + // 操作按钮 + const actionButtons = ( + + ); + + return ( + } + emptyText="暂无企业数据" + /> + ); +} + +export default EnterpriseManagementExample; \ No newline at end of file diff --git a/crop-x/src/components/common/searchFormPagination/components/searchFormPaginationReducer.tsx b/crop-x/src/components/common/searchFormPagination/components/searchFormPaginationReducer.tsx new file mode 100644 index 0000000..0f93f3e --- /dev/null +++ b/crop-x/src/components/common/searchFormPagination/components/searchFormPaginationReducer.tsx @@ -0,0 +1,196 @@ +/** + * filekorolheader: 搜索表单分页状态管理 - 管理组件的状态和actions + * 功能:状态管理、数据更新、分页控制、搜索过滤 + * 路径:/components/common/searchFormPagination/components/searchFormPaginationReducer + * 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer模式管理复杂状态 + */ +'use client'; + +// 状态接口定义 +export interface SearchFormPaginationState { + // 数据相关 + data: any[]; + loading: boolean; + error: string | null; + + // 搜索过滤 + filters: Record; + + // 分页相关 + pagination: { + page: number; + size: number; + total: number; + totalPages: number; + hasNext: boolean; + hasPrev: boolean; + }; + + // 排序相关 + sortBy?: string; + sortOrder: 'asc' | 'desc'; +} + +// Action类型定义 +export type SearchFormPaginationAction = + | { type: 'SET_DATA'; payload: any[] } + | { type: 'SET_LOADING'; payload: boolean } + | { type: 'SET_ERROR'; payload: string | null } + | { type: 'SET_FILTERS'; payload: Record } + | { type: 'UPDATE_FILTER'; payload: { key: string; value: string } } + | { type: 'CLEAR_FILTERS' } + | { type: 'SET_PAGINATION'; payload: SearchFormPaginationState['pagination'] } + | { type: 'SET_PAGINATION_PAGE'; payload: number } + | { type: 'SET_PAGINATION_SIZE'; payload: number } + | { type: 'SET_SORT_BY'; payload: string } + | { type: 'SET_SORT_ORDER'; payload: 'asc' | 'desc' } + | { type: 'SET_SORT'; payload: { sortBy?: string; sortOrder: 'asc' | 'desc' } } + | { type: 'TOGGLE_SORT'; payload: string } + | { type: 'SET_DATA_AND_PAGINATION'; payload: { data: any[]; pagination: SearchFormPaginationState['pagination'] } } + | { type: 'RESET_STATE' }; + +// 初始状态 +export const initialState: SearchFormPaginationState = { + data: [], + loading: false, + error: null, + filters: {}, + pagination: { + page: 1, + size: 10, + total: 0, + totalPages: 0, + hasNext: false, + hasPrev: false, + }, + sortBy: undefined, + sortOrder: 'asc', +}; + +// Reducer函数 +export function SearchFormPaginationReducer( + state: SearchFormPaginationState, + action: SearchFormPaginationAction +): SearchFormPaginationState { + switch (action.type) { + case 'SET_DATA': + return { + ...state, + data: action.payload, + }; + + case 'SET_LOADING': + return { + ...state, + loading: action.payload, + }; + + case 'SET_ERROR': + return { + ...state, + error: action.payload, + loading: false, + }; + + case 'SET_FILTERS': + return { + ...state, + filters: action.payload, + }; + + case 'UPDATE_FILTER': + return { + ...state, + filters: { + ...state.filters, + [action.payload.key]: action.payload.value, + }, + }; + + case 'CLEAR_FILTERS': + return { + ...state, + filters: {}, + }; + + case 'SET_PAGINATION': + return { + ...state, + pagination: action.payload, + }; + + case 'SET_PAGINATION_PAGE': + return { + ...state, + pagination: { + ...state.pagination, + page: action.payload, + }, + }; + + case 'SET_PAGINATION_SIZE': + return { + ...state, + pagination: { + ...state.pagination, + size: action.payload, + }, + }; + + case 'SET_SORT_BY': + return { + ...state, + sortBy: action.payload, + }; + + case 'SET_SORT_ORDER': + return { + ...state, + sortOrder: action.payload, + }; + + case 'SET_SORT': + return { + ...state, + sortBy: action.payload.sortBy, + sortOrder: action.payload.sortOrder, + }; + + case 'TOGGLE_SORT': + const columnKey = action.payload; + let newSortOrder: 'asc' | 'desc'; + + if (state.sortBy === columnKey) { + // 如果点击的是当前排序列,切换排序方向 + newSortOrder = state.sortOrder === 'desc' ? 'asc' : 'desc'; + } else { + // 如果点击的是新列,设置为升序 + newSortOrder = 'asc'; + } + + return { + ...state, + sortBy: columnKey, + sortOrder: newSortOrder, + }; + + case 'SET_DATA_AND_PAGINATION': + return { + ...state, + data: action.payload.data, + pagination: action.payload.pagination, + loading: false, + error: null, + }; + + case 'RESET_STATE': + return { + ...initialState, + filters: state.filters, // 保留搜索过滤条件 + }; + + default: + console.warn('Unknown action type:', (action as any).type); + return state; + } +} \ No newline at end of file diff --git a/crop-x/src/components/common/searchFormPagination/index.ts b/crop-x/src/components/common/searchFormPagination/index.ts new file mode 100644 index 0000000..625600f --- /dev/null +++ b/crop-x/src/components/common/searchFormPagination/index.ts @@ -0,0 +1,38 @@ +/** + * filekorolheader: 搜索表单分页组件导出 - 统一导出所有相关组件和类型 + * 功能:组件导出、类型导出、便捷导入 + * 路径:/components/common/searchFormPagination + * 规范:遵循crop-x/docs/开发项目规范.md,提供统一的导出入口 + */ + +// 主组件 +export { SearchFormPagination } from './page'; +export { default } from './page'; + +// 子组件 +export { default as SearchFormComponent } from './components/SearchFormComponent'; +export { default as PaginationComponent } from './components/PaginationComponent'; + +// 状态管理 +export { SearchFormPaginationReducer, initialState } from './components/searchFormPaginationReducer'; + +// 类型定义 +export type { + SearchFieldConfig, + TableColumnConfig, + PaginationConfig, + SearchFormPaginationProps, +} from './page'; + +export type { + SearchFormPaginationState, + SearchFormPaginationAction, +} from './components/searchFormPaginationReducer'; + +export type { + SearchFormComponentProps, +} from './components/SearchFormComponent'; + +export type { + PaginationComponentProps, +} from './components/PaginationComponent'; \ No newline at end of file diff --git a/crop-x/src/components/common/searchFormPagination/page.tsx b/crop-x/src/components/common/searchFormPagination/page.tsx new file mode 100644 index 0000000..43b7c40 --- /dev/null +++ b/crop-x/src/components/common/searchFormPagination/page.tsx @@ -0,0 +1,364 @@ +/** + * filekorolheader: 搜索表单分页公共组件 - 提供可复用的搜索、表单和分页功能 + * 功能:搜索条件管理、表头渲染、分页控制、加载状态处理 + * 路径:/components/common/searchFormPagination + * 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式,支持完全自定义配置 + */ +'use client'; + +import { useState, useEffect, useMemo, useCallback } from 'react'; +import { Card } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { AlertCircle, ChevronLeft, ChevronRight, RefreshCw } from 'lucide-react'; +import { toast } from 'sonner'; + +import { SearchFormComponent } from './components/SearchFormComponent'; +import { PaginationComponent } from './components/PaginationComponent'; + +// 搜索条件配置接口 +export interface SearchFieldConfig { + key: string; + label: string; + type: 'text' | 'select'; + placeholder?: string; + options?: Array<{ value: string; label: string }>; + defaultValue?: string; +} + +// 表头配置接口 +export interface TableColumnConfig { + key: string; + label: string; + sortable?: boolean; + width?: string; + render?: (value: any, row: any, index: number) => React.ReactNode; +} + +// 分页配置接口 +export interface PaginationConfig { + page: number; + size: number; + total: number; + totalPages: number; + hasNext: boolean; + hasPrev: boolean; +} + +// 组件Props接口 - 简化版本 +export interface SearchFormPaginationProps { + // 搜索表单配置 + formTitle?: string; + formRightContent?: React.ReactNode; + searchFields: SearchFieldConfig[]; + searchPlaceholder?: string; + onSearch?: (filters: Record) => void; + + // 表格配置 + columns: TableColumnConfig[]; + data?: T[]; + loading?: boolean; + error?: string | null; + + // 分页配置 + pagination?: PaginationConfig; + onPageChange?: (page: number) => void; + onSizeChange?: (size: number) => void; + + // 排序配置 + sortBy?: string; + sortOrder?: 'asc' | 'desc'; + onSort?: (sortBy: string, sortOrder: 'asc' | 'desc') => void; + + // 空状态配置 + emptyIcon?: React.ReactNode; + emptyText?: string; + + // 分页器配置 + showSizeSelector?: boolean; + showPageInfo?: boolean; + showQuickJumper?: boolean; + sizeOptions?: number[]; + maxVisiblePages?: number; + + // 自定义样式 + className?: string; + + // 数据更新回调 - 用于父组件获取搜索条件变化 + onDataUpdate?: (data: { + items: T[]; + pagination: PaginationConfig; + loading: boolean; + error: string | null; + }) => void; +} + +export function SearchFormPagination({ + formTitle, + formRightContent, + searchFields, + searchPlaceholder = '请输入搜索关键词...', + onSearch, + columns, + data = [], + loading = false, + error = null, + pagination, + onPageChange, + onSizeChange, + sortBy, + sortOrder, + onSort, + emptyIcon, + emptyText = '暂无数据', + showSizeSelector = true, + showPageInfo = true, + showQuickJumper = false, + sizeOptions = [10, 30, 50, 100], + maxVisiblePages = 7, + className = '', + onDataUpdate, +}: SearchFormPaginationProps) { + // 简化的内部状态 - 只管理搜索条件 + const [filters, setFilters] = useState>( + searchFields.reduce((acc, field) => { + acc[field.key] = field.defaultValue || ''; + return acc; + }, {} as Record) + ); + + // 同步外部排序状态 + const [currentSort, setCurrentSort] = useState<{ sortBy?: string; sortOrder: 'asc' | 'desc' }>({ + sortBy, + sortOrder: sortOrder || 'asc' + }); + + // 数据更新回调 - 通知父组件数据变化 + useEffect(() => { + onDataUpdate?.({ + items: data, + pagination: pagination || { + page: 1, + size: 10, + total: 0, + totalPages: 0, + hasNext: false, + hasPrev: false, + }, + loading, + error, + }); + }, [data, pagination, loading, error, onDataUpdate]); + + // 简化的事件处理器 - 纯粹的状态通知 + const handleSearch = useCallback((newFilters: Record) => { + setFilters(newFilters); + onSearch?.(newFilters); + }, [onSearch]); + + const handleSort = useCallback((columnKey: string) => { + const column = columns.find(col => col.key === columnKey); + if (!column?.sortable) return; + + // 计算新的排序状态 + let newSortOrder: 'asc' | 'desc'; + if (currentSort.sortBy === columnKey) { + newSortOrder = currentSort.sortOrder === 'desc' ? 'asc' : 'desc'; + } else { + newSortOrder = 'asc'; + } + + const newSort = { sortBy: columnKey, sortOrder: newSortOrder }; + setCurrentSort(newSort); + onSort?.(columnKey, newSortOrder); + }, [columns, currentSort, onSort]); + + const handlePageChange = useCallback((page: number) => { + onPageChange?.(page); + }, [onPageChange]); + + const handleSizeChange = useCallback((size: number) => { + onSizeChange?.(size); + }, [onSizeChange]); + + // 稳定的filters引用 + const stableFilters = useMemo(() => filters, [filters]); + + // 渲染表头 + const renderTableHeader = () => { + // 计算列宽:对于自定义渲染的列,使用最小宽度;对于简单列,根据内容计算宽度 + const getColumnWidth = (column: TableColumnConfig) => { + if (column.width) { + return column.width; // 如果明确指定了宽度,使用指定宽度 + } + + // 对于简单文本列,计算内容长度并设置合理的最小宽度 + if (!column.render) { + return 'min-w-[100px] max-w-[200px]'; // 普通文本列的宽度范围 + } + + // 对于自定义渲染的列,给一个合理的最小宽度 + return 'min-w-[120px] max-w-[300px]'; // 自定义列的宽度范围 + }; + + return ( + + + {columns.map((column) => ( + column.sortable && handleSort(column.key)} + > +
+ {column.label} +
+ {column.sortable && currentSort.sortBy === column.key && ( + {currentSort.sortOrder === 'asc' ? '↑' : '↓'} + )} +
+ ))} +
+
+ ); + }; + + // 渲染表格行 + const renderTableRow = (row: T, index: number) => ( + + {columns.map((column) => ( + +
+ {column.render + ? column.render(row[column.key as keyof T], row, index) + : String(row[column.key as keyof T] ?? '-')} +
+
+ ))} +
+ ); + + return ( +
+ {/* 搜索表单和数据表格在同一个Card里面 */} + + {/* 搜索表单 - 左右两部分布局 */} + {(formTitle || formRightContent || searchFields.length > 0) && ( +
+ {/* 左侧 - 表单名称 */} + {formTitle && ( +

{formTitle}

+ )} + + {/* 右侧 - 搜索控件和自定义内容 */} +
+ + {formRightContent} +
+
+ )} + + {/* 错误状态 */} + {error && ( +
+
+ + {error} +
+
+ )} + + {/* 数据表格 */} + {!error && ( + <> + {/* 初始加载状态 */} + {loading && data.length === 0 ? ( +
+ +

加载中...

+
+ ) : ( + <> + {/* 表格加载遮罩 */} +
+ {loading && ( +
+
+ +

加载中...

+
+
+ )} + +
+ + {renderTableHeader()} + + {data.map((row, index) => renderTableRow(row, index))} + +
+
+
+ + {/* 空状态 */} + {data.length === 0 && !loading && ( +
+ {emptyIcon ||
} +

{emptyText}

+
+ )} + + {/* 分页组件 */} + {pagination && ( + + )} + + )} + + )} + +
+ ); +} + +export default SearchFormPagination; \ No newline at end of file