Compare commits
2 Commits
9f1cf21042
...
191d218ec4
| Author | SHA1 | Date | |
|---|---|---|---|
| 191d218ec4 | |||
| 956494b3ed |
2
crop-x/next-env.d.ts
vendored
2
crop-x/next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/dev/types/routes.d.ts";
|
import "./.next/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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 (
|
||||||
|
<div className={`flex items-center justify-between mt-4 ${className}`}>
|
||||||
|
{/* 左侧信息 */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{showPageInfo && (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
显示第 {page} 页,共 {totalPages} 页
|
||||||
|
<span className="ml-2">总计 {total} 条记录</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showSizeSelector && onSizeChange && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-muted-foreground">每页显示</span>
|
||||||
|
<Select value={String(pagination.size)} onValueChange={handleSizeChange}>
|
||||||
|
<SelectTrigger className="w-20">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{sizeOptions.map((size) => (
|
||||||
|
<SelectItem key={size} value={String(size)}>
|
||||||
|
{size}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<span className="text-sm text-muted-foreground">条</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右侧分页导航 - 只有超过一页时才显示分页按钮 */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{/* 上一页按钮 */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(page - 1)}
|
||||||
|
disabled={!hasPrev || loading}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4" />
|
||||||
|
上一页
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 页码按钮 */}
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{visiblePages.map((pageNum) => (
|
||||||
|
<Button
|
||||||
|
key={pageNum}
|
||||||
|
variant={pageNum === page ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(pageNum)}
|
||||||
|
disabled={loading}
|
||||||
|
className="min-w-[2.5rem]"
|
||||||
|
>
|
||||||
|
{pageNum}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 下一页按钮 */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(page + 1)}
|
||||||
|
disabled={!hasNext || loading}
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 快速跳转 */}
|
||||||
|
{showQuickJumper && totalPages > 5 && (
|
||||||
|
<div className="flex items-center gap-2 ml-4">
|
||||||
|
<span className="text-sm text-muted-foreground">跳至</span>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={totalPages}
|
||||||
|
value={jumpPage}
|
||||||
|
onChange={(e) => setJumpPage(e.target.value)}
|
||||||
|
onKeyPress={handleJumpKeyPress}
|
||||||
|
placeholder="页码"
|
||||||
|
className="w-16 h-8"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-muted-foreground">页</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleJumpPage}
|
||||||
|
disabled={loading || !jumpPage}
|
||||||
|
>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PaginationComponent;
|
||||||
@@ -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<string, string>;
|
||||||
|
onFiltersChange: (filters: Record<string, string>) => 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<Record<string, string>>(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 (
|
||||||
|
<div key={field.key}>
|
||||||
|
<Select
|
||||||
|
value={value}
|
||||||
|
onValueChange={(newValue) => handleInputChange(field.key, newValue)}
|
||||||
|
disabled={false} // 始终允许选择,不因加载而禁用
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-40">
|
||||||
|
<SelectValue placeholder={field.placeholder || '请选择'} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{field.options?.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'text':
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<div key={field.key} className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder={field.placeholder || placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => handleInputChange(field.key, e.target.value)}
|
||||||
|
disabled={false} // 始终允许输入,不因加载而禁用
|
||||||
|
className="pl-10 w-64"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 主搜索框(当没有配置字段时使用默认搜索)
|
||||||
|
const renderMainSearch = () => (
|
||||||
|
<div className="relative flex-1 max-w-md">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={localFilters.search || ''}
|
||||||
|
onChange={(e) => handleInputChange('search', e.target.value)}
|
||||||
|
disabled={false} // 始终允许输入,不因加载而禁用
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 如果没有配置字段,使用简单搜索
|
||||||
|
if (fields.length === 0) {
|
||||||
|
return renderMainSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex ${layout === 'horizontal' ? 'flex-row items-end' : 'flex-col items-start'} gap-4 flex-wrap`}>
|
||||||
|
{/* 渲染搜索字段 */}
|
||||||
|
{visibleFields.map(renderSearchField)}
|
||||||
|
|
||||||
|
{/* 展开/收起按钮 */}
|
||||||
|
{hasMoreFields && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowAllFields(!showAllFields)}
|
||||||
|
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"
|
||||||
|
>
|
||||||
|
{showAllFields ? '收起' : `展开更多 (${fields.length - maxVisibleFields})`}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemoizedSearchFormComponent = memo(SearchFormComponent);
|
||||||
|
export default MemoizedSearchFormComponent;
|
||||||
@@ -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) => (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Building2 className="w-4 h-4 text-blue-500" />
|
||||||
|
<span className="font-medium">{value}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'type',
|
||||||
|
label: '企业类型',
|
||||||
|
render: (value: string) => (
|
||||||
|
<Badge variant="outline" className="font-light">{value}</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 <Badge className="bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-800 font-light">草稿</Badge>;
|
||||||
|
case 'pending':
|
||||||
|
return <Badge className="bg-yellow-50 dark:bg-yellow-950 text-yellow-600 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800 font-light">待审核</Badge>;
|
||||||
|
case 'approved':
|
||||||
|
return <Badge className="bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-400 border-green-200 dark:border-green-800 font-light">审核通过</Badge>;
|
||||||
|
case 'rejected':
|
||||||
|
return <Badge className="bg-red-50 dark:bg-red-950 text-red-600 dark:text-red-400 border-red-200 dark:border-red-800 font-light">已拒绝</Badge>;
|
||||||
|
default:
|
||||||
|
return <Badge className="bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-800 font-light">草稿</Badge>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return getAuditStatusBadge(value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
label: '状态',
|
||||||
|
render: (value: MockEnterprise['status']) => {
|
||||||
|
const getStatusBadge = (status: MockEnterprise['status']) => {
|
||||||
|
if (status === 'active') {
|
||||||
|
return <Badge className="bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-400 border-green-200 dark:border-green-800 font-light">启用</Badge>;
|
||||||
|
}
|
||||||
|
return <Badge className="bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-800 font-light">禁用</Badge>;
|
||||||
|
};
|
||||||
|
return getStatusBadge(value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'actions',
|
||||||
|
label: '操作',
|
||||||
|
render: (_: any, row: MockEnterprise) => (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => toast.success(`查看企业: ${row.name}`)}
|
||||||
|
>
|
||||||
|
<Eye className="w-3 h-3 mr-1" />
|
||||||
|
查看
|
||||||
|
</Button>
|
||||||
|
{row.status === 'active' ? (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="text-gray-600 dark:text-gray-400 border-gray-300 dark:border-gray-600"
|
||||||
|
onClick={() => toast.success(`禁用企业: ${row.name}`)}
|
||||||
|
>
|
||||||
|
<PowerOff className="w-3 h-3 mr-1" />
|
||||||
|
禁用
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="text-green-600 dark:text-green-400 border-green-300 dark:border-green-600"
|
||||||
|
onClick={() => toast.success(`启用企业: ${row.name}`)}
|
||||||
|
>
|
||||||
|
<Power className="w-3 h-3 mr-1" />
|
||||||
|
启用
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 模拟数据
|
||||||
|
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<string, string>) => {
|
||||||
|
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 = (
|
||||||
|
<Button onClick={() => toast.success('新建企业')}>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
新建企业
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SearchFormPagination
|
||||||
|
title="企业管理"
|
||||||
|
description="管理平台所有企业信息,支持查询、查看详情、启用/禁用企业"
|
||||||
|
searchFields={searchFields}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
columns={columns}
|
||||||
|
data={mockData}
|
||||||
|
pagination={mockPagination}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
onSort={handleSort}
|
||||||
|
actionButtons={actionButtons}
|
||||||
|
emptyIcon={<Building2 className="w-12 h-12 mx-auto mb-4 opacity-20" />}
|
||||||
|
emptyText="暂无企业数据"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EnterpriseManagementExample;
|
||||||
@@ -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<string, string>;
|
||||||
|
|
||||||
|
// 分页相关
|
||||||
|
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<string, string> }
|
||||||
|
| { 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
crop-x/src/components/common/searchFormPagination/index.ts
Normal file
38
crop-x/src/components/common/searchFormPagination/index.ts
Normal file
@@ -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';
|
||||||
364
crop-x/src/components/common/searchFormPagination/page.tsx
Normal file
364
crop-x/src/components/common/searchFormPagination/page.tsx
Normal file
@@ -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<T = any> {
|
||||||
|
// 搜索表单配置
|
||||||
|
formTitle?: string;
|
||||||
|
formRightContent?: React.ReactNode;
|
||||||
|
searchFields: SearchFieldConfig[];
|
||||||
|
searchPlaceholder?: string;
|
||||||
|
onSearch?: (filters: Record<string, string>) => 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<T = any>({
|
||||||
|
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<T>) {
|
||||||
|
// 简化的内部状态 - 只管理搜索条件
|
||||||
|
const [filters, setFilters] = useState<Record<string, string>>(
|
||||||
|
searchFields.reduce((acc, field) => {
|
||||||
|
acc[field.key] = field.defaultValue || '';
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, string>)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 同步外部排序状态
|
||||||
|
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<string, string>) => {
|
||||||
|
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 (
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
{columns.map((column) => (
|
||||||
|
<TableHead
|
||||||
|
key={column.key}
|
||||||
|
className={column.sortable ? 'cursor-pointer hover:bg-muted' : ''}
|
||||||
|
style={{
|
||||||
|
width: getColumnWidth(column),
|
||||||
|
minWidth: column.render ? '120px' : '100px',
|
||||||
|
maxWidth: column.render ? '300px' : '200px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}
|
||||||
|
onClick={() => column.sortable && handleSort(column.key)}
|
||||||
|
>
|
||||||
|
<div className="truncate" title={column.label}>
|
||||||
|
{column.label}
|
||||||
|
</div>
|
||||||
|
{column.sortable && currentSort.sortBy === column.key && (
|
||||||
|
<span className="ml-1 flex-shrink-0">{currentSort.sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染表格行
|
||||||
|
const renderTableRow = (row: T, index: number) => (
|
||||||
|
<TableRow key={index}>
|
||||||
|
{columns.map((column) => (
|
||||||
|
<TableCell
|
||||||
|
key={column.key}
|
||||||
|
style={{
|
||||||
|
maxWidth: column.render ? '300px' : '200px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="truncate" title={
|
||||||
|
column.render
|
||||||
|
? (column.render(row[column.key as keyof T], row, index) as any)?.toString() || ''
|
||||||
|
: String(row[column.key as keyof T] ?? '-')
|
||||||
|
}>
|
||||||
|
{column.render
|
||||||
|
? column.render(row[column.key as keyof T], row, index)
|
||||||
|
: String(row[column.key as keyof T] ?? '-')}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`space-y-6 ${className}`}>
|
||||||
|
{/* 搜索表单和数据表格在同一个Card里面 */}
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
{/* 搜索表单 - 左右两部分布局 */}
|
||||||
|
{(formTitle || formRightContent || searchFields.length > 0) && (
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
{/* 左侧 - 表单名称 */}
|
||||||
|
{formTitle && (
|
||||||
|
<h3 className="text-lg font-semibold text-foreground">{formTitle}</h3>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 右侧 - 搜索控件和自定义内容 */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<SearchFormComponent
|
||||||
|
key="search-form"
|
||||||
|
fields={searchFields}
|
||||||
|
filters={stableFilters}
|
||||||
|
onFiltersChange={handleSearch}
|
||||||
|
placeholder={searchPlaceholder}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
{formRightContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 错误状态 */}
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 p-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg">
|
||||||
|
<div className="flex items-center gap-2 text-red-600 dark:text-red-400">
|
||||||
|
<AlertCircle className="w-4 h-4" />
|
||||||
|
<span>{error}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 数据表格 */}
|
||||||
|
{!error && (
|
||||||
|
<>
|
||||||
|
{/* 初始加载状态 */}
|
||||||
|
{loading && data.length === 0 ? (
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
<RefreshCw className="w-8 h-8 mx-auto mb-2 animate-spin" />
|
||||||
|
<p>加载中...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* 表格加载遮罩 */}
|
||||||
|
<div className="relative">
|
||||||
|
{loading && (
|
||||||
|
<div className="absolute inset-0 bg-white/50 dark:bg-black/50 backdrop-blur-sm z-10 flex items-center justify-center rounded-lg">
|
||||||
|
<div className="flex flex-col items-center text-muted-foreground">
|
||||||
|
<RefreshCw className="w-6 h-6 animate-spin mb-2" />
|
||||||
|
<p className="text-sm">加载中...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="border rounded-lg overflow-hidden">
|
||||||
|
<Table>
|
||||||
|
{renderTableHeader()}
|
||||||
|
<TableBody>
|
||||||
|
{data.map((row, index) => renderTableRow(row, index))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 空状态 */}
|
||||||
|
{data.length === 0 && !loading && (
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
{emptyIcon || <div className="w-12 h-12 mx-auto mb-4 opacity-20" />}
|
||||||
|
<p>{emptyText}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 分页组件 */}
|
||||||
|
{pagination && (
|
||||||
|
<PaginationComponent
|
||||||
|
pagination={pagination}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
onSizeChange={handleSizeChange}
|
||||||
|
loading={loading}
|
||||||
|
showSizeSelector={showSizeSelector}
|
||||||
|
showPageInfo={showPageInfo}
|
||||||
|
showQuickJumper={showQuickJumper}
|
||||||
|
sizeOptions={sizeOptions}
|
||||||
|
maxVisiblePages={maxVisiblePages}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchFormPagination;
|
||||||
Reference in New Issue
Block a user