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 && (
-
- )}
-
- {/* 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 */}
-
@@ -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 && (
+ <>
+ {/* 初始加载状态 */}
+ {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