生产管理系统前端 - 地块档案管理页面开发
This commit is contained in:
@@ -1,18 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Card } from '@/components/ui/card';
|
|
||||||
|
|
||||||
export default function EntryPage() {
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<Card className="p-6">
|
|
||||||
<h2 className="text-xl font-semibold">地块录入维护</h2>
|
|
||||||
<div className="p-3 bg-muted rounded-lg mt-3">
|
|
||||||
<p className="text-sm">
|
|
||||||
<strong>页面路径:</strong> /land-information/archive/entry
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
|
||||||
|
export function LandAttachments() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">地块照片</label>
|
||||||
|
<div className="mt-2 border-2 border-dashed rounded-lg p-6 text-center hover:border-green-500 transition-colors cursor-pointer">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center mb-3">
|
||||||
|
<span className="text-gray-400">📷</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
点击上传或拖拽照片到此处
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
支持 JPG、PNG 格式,最大 5MB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t pt-4">
|
||||||
|
<label className="block text-sm font-medium mb-2">合同文档</label>
|
||||||
|
<div className="mt-2 space-y-2">
|
||||||
|
<div className="border-2 border-dashed rounded-lg p-4 text-center hover:border-green-500 transition-colors cursor-pointer">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center mb-2">
|
||||||
|
<span className="text-gray-400">📄</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
上传合同扫描件
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
支持 PDF、图片格式
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||||
|
<p className="text-xs text-blue-800">
|
||||||
|
💡 建议上传:承包合同、确权证书、地块测绘报告等相关文档
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Plus, X } from 'lucide-react';
|
||||||
|
import { Land } from '@/app/(app)/land-information/archive/manage/page';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface LandAttributesProps {
|
||||||
|
formData: Partial<Land>;
|
||||||
|
onChange: (data: Partial<Land>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LandAttributes({ formData, onChange }: LandAttributesProps) {
|
||||||
|
const [tagInput, setTagInput] = useState('');
|
||||||
|
|
||||||
|
const handleFieldChange = (field: keyof Land, value: any) => {
|
||||||
|
onChange({ ...formData, [field]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加标签
|
||||||
|
const handleAddTag = () => {
|
||||||
|
if (tagInput.trim() && !formData.tags?.includes(tagInput.trim())) {
|
||||||
|
handleFieldChange('tags', [...(formData.tags || []), tagInput.trim()]);
|
||||||
|
setTagInput('');
|
||||||
|
toast.success('标签已添加');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除标签
|
||||||
|
const handleRemoveTag = (tag: string) => {
|
||||||
|
handleFieldChange('tags', formData.tags?.filter(t => t !== tag) || []);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Enter键添加标签
|
||||||
|
const handleTagKeyPress = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleAddTag();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">土壤类型</label>
|
||||||
|
<Select
|
||||||
|
value={formData.soilType}
|
||||||
|
onValueChange={(value) => handleFieldChange('soilType', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="loam">壤土</SelectItem>
|
||||||
|
<SelectItem value="sand">沙土</SelectItem>
|
||||||
|
<SelectItem value="clay">黏土</SelectItem>
|
||||||
|
<SelectItem value="silt">粉土</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">土地利用类型</label>
|
||||||
|
<Select
|
||||||
|
value={formData.landUseType || ''}
|
||||||
|
onValueChange={(value) => handleFieldChange('landUseType', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="farmland">耕地</SelectItem>
|
||||||
|
<SelectItem value="garden">园地</SelectItem>
|
||||||
|
<SelectItem value="forestland">林地</SelectItem>
|
||||||
|
<SelectItem value="grassland">草地</SelectItem>
|
||||||
|
<SelectItem value="other">其他</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">种植模式</label>
|
||||||
|
<Select
|
||||||
|
value={formData.plantingMode || ''}
|
||||||
|
onValueChange={(value) => handleFieldChange('plantingMode', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="open-field">🌾 露地</SelectItem>
|
||||||
|
<SelectItem value="greenhouse">🏠 大棚</SelectItem>
|
||||||
|
<SelectItem value="orchard">🍎 果园</SelectItem>
|
||||||
|
<SelectItem value="paddy">🌊 水田</SelectItem>
|
||||||
|
<SelectItem value="dryland">🌵 旱地</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t pt-4">
|
||||||
|
<label className="block text-sm font-medium mb-2">地块标签</label>
|
||||||
|
<div className="flex gap-2 mb-3">
|
||||||
|
<Input
|
||||||
|
value={tagInput}
|
||||||
|
onChange={(e) => setTagInput(e.target.value)}
|
||||||
|
placeholder="输入标签"
|
||||||
|
onKeyPress={handleTagKeyPress}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button size="sm" onClick={handleAddTag}>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{formData.tags?.map((tag) => (
|
||||||
|
<Badge key={tag} variant="secondary" className="gap-1">
|
||||||
|
{tag}
|
||||||
|
<X
|
||||||
|
className="w-3 h-3 cursor-pointer hover:text-red-500"
|
||||||
|
onClick={() => handleRemoveTag(tag)}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{(!formData.tags || formData.tags.length === 0) && (
|
||||||
|
<span className="text-sm text-muted-foreground">暂无标签,点击上方添加</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Land } from '@/app/(app)/land-information/archive/manage/page';
|
||||||
|
|
||||||
|
interface LandBasicInfoProps {
|
||||||
|
formData: Partial<Land>;
|
||||||
|
onChange: (data: Partial<Land>) => void;
|
||||||
|
isEdit?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LandBasicInfo({ formData, onChange, isEdit = false }: LandBasicInfoProps) {
|
||||||
|
const handleFieldChange = (field: keyof Land, value: any) => {
|
||||||
|
onChange({ ...formData, [field]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">地块编号</label>
|
||||||
|
<Input
|
||||||
|
value={formData.code || ''}
|
||||||
|
onChange={(e) => handleFieldChange('code', e.target.value)}
|
||||||
|
placeholder="系统自动生成"
|
||||||
|
disabled={isEdit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">地块名称 *</label>
|
||||||
|
<Input
|
||||||
|
value={formData.name || ''}
|
||||||
|
onChange={(e) => handleFieldChange('name', e.target.value)}
|
||||||
|
placeholder="如:东大田1号地"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">所在位置 *</label>
|
||||||
|
<Input
|
||||||
|
value={formData.location || ''}
|
||||||
|
onChange={(e) => handleFieldChange('location', e.target.value)}
|
||||||
|
placeholder="如:江苏省南京市浦口区"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">面积(亩)</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
value={formData.area || ''}
|
||||||
|
onChange={(e) => handleFieldChange('area', parseFloat(e.target.value) || 0)}
|
||||||
|
placeholder="请输入面积"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">周长(米)</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
value={formData.perimeter || ''}
|
||||||
|
onChange={(e) => handleFieldChange('perimeter', parseFloat(e.target.value) || 0)}
|
||||||
|
placeholder="请输入周长"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">地块状态</label>
|
||||||
|
<Select
|
||||||
|
value={formData.status}
|
||||||
|
onValueChange={(value) => handleFieldChange('status', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="active">使用中</SelectItem>
|
||||||
|
<SelectItem value="maintenance">维护中</SelectItem>
|
||||||
|
<SelectItem value="inactive">停用</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">备注说明</label>
|
||||||
|
<Textarea
|
||||||
|
value={formData.remarks || ''}
|
||||||
|
onChange={(e) => handleFieldChange('remarks', e.target.value)}
|
||||||
|
placeholder="其他说明信息"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import {
|
||||||
|
Map,
|
||||||
|
Upload,
|
||||||
|
Pen,
|
||||||
|
Square,
|
||||||
|
Layers,
|
||||||
|
CheckCircle2,
|
||||||
|
FileType,
|
||||||
|
MapPin
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { GeoCoordinate } from '@/app/(app)/land-information/archive/manage/page';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface LandMapContainerProps {
|
||||||
|
coordinates: GeoCoordinate[];
|
||||||
|
area: number;
|
||||||
|
perimeter: number;
|
||||||
|
onCoordinatesChange: (coordinates: GeoCoordinate[], area: number, perimeter: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LandMapContainer({
|
||||||
|
coordinates,
|
||||||
|
area,
|
||||||
|
perimeter,
|
||||||
|
onCoordinatesChange
|
||||||
|
}: LandMapContainerProps) {
|
||||||
|
const [isMapLoaded, setIsMapLoaded] = useState(false);
|
||||||
|
const [mapLayerType, setMapLayerType] = useState<'satellite' | 'street'>('satellite');
|
||||||
|
const [drawMode, setDrawMode] = useState<'none' | 'polygon' | 'rectangle'>('none');
|
||||||
|
const mapContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// 初始化地图
|
||||||
|
useEffect(() => {
|
||||||
|
// 模拟地图加载
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsMapLoaded(true);
|
||||||
|
toast.info('演示地图模式:可通过文件导入或手动输入坐标数据', { duration: 3000 });
|
||||||
|
}, 1000);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 切换地图图层
|
||||||
|
const toggleMapLayer = () => {
|
||||||
|
const newLayerType = mapLayerType === 'satellite' ? 'street' : 'satellite';
|
||||||
|
setMapLayerType(newLayerType);
|
||||||
|
toast.success(`已切换到${newLayerType === 'satellite' ? '卫星图' : '电子地图'}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开始绘制
|
||||||
|
const startDrawing = (mode: 'polygon' | 'rectangle') => {
|
||||||
|
setDrawMode(mode);
|
||||||
|
toast.info(`开始绘制${mode === 'polygon' ? '多边形' : '矩形'},点击地图描点,双击结束`);
|
||||||
|
|
||||||
|
// 模拟绘制完成
|
||||||
|
setTimeout(() => {
|
||||||
|
const mockCoordinates = [
|
||||||
|
{ lat: 39.9042 + 0.01, lng: 116.4074 - 0.01 },
|
||||||
|
{ lat: 39.9042 + 0.01, lng: 116.4074 + 0.01 },
|
||||||
|
{ lat: 39.9042 - 0.01, lng: 116.4074 + 0.01 },
|
||||||
|
{ lat: 39.9042 - 0.01, lng: 116.4074 - 0.01 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockArea = 120.5;
|
||||||
|
const mockPerimeter = 1380;
|
||||||
|
|
||||||
|
onCoordinatesChange(mockCoordinates, mockArea, mockPerimeter);
|
||||||
|
setDrawMode('none');
|
||||||
|
|
||||||
|
toast.success(`绘制完成!面积:${mockArea.toFixed(2)}亩,周长:${mockPerimeter.toFixed(0)}米`);
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 文件导入
|
||||||
|
const handleFileImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const fileExtension = file.name.split('.').pop()?.toLowerCase();
|
||||||
|
|
||||||
|
if (!['kml', 'geojson', 'json', 'shp'].includes(fileExtension || '')) {
|
||||||
|
toast.error('不支持的文件格式。请上传 KML、GeoJSON 或 SHP 文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 模拟文件解析
|
||||||
|
const mockCoordinates = [
|
||||||
|
{ lat: 39.9042 + 0.008, lng: 116.4074 - 0.008 },
|
||||||
|
{ lat: 39.9042 + 0.008, lng: 116.4074 + 0.008 },
|
||||||
|
{ lat: 39.9042 - 0.008, lng: 116.4074 + 0.008 },
|
||||||
|
{ lat: 39.9042 - 0.008, lng: 116.4074 - 0.008 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockArea = 85.3;
|
||||||
|
const mockPerimeter = 1120;
|
||||||
|
|
||||||
|
onCoordinatesChange(mockCoordinates, mockArea, mockPerimeter);
|
||||||
|
|
||||||
|
toast.success(`成功导入${fileExtension.toUpperCase()}文件!面积:${mockArea.toFixed(2)}亩,${mockCoordinates.length}个点`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('文件导入错误:', error);
|
||||||
|
toast.error('文件导入失败,请检查文件格式');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空input
|
||||||
|
event.target.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 地图工具栏 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="flex items-center gap-2">
|
||||||
|
<Map className="w-5 h-5 text-green-600" />
|
||||||
|
地块边界绘制
|
||||||
|
</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={toggleMapLayer}
|
||||||
|
>
|
||||||
|
<Layers className="w-4 h-4 mr-2" />
|
||||||
|
{mapLayerType === 'satellite' ? '卫星图' : '电子地图'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={drawMode === 'polygon' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => startDrawing('polygon')}
|
||||||
|
>
|
||||||
|
<Pen className="w-4 h-4 mr-2" />
|
||||||
|
多边形
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={drawMode === 'rectangle' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => startDrawing('rectangle')}
|
||||||
|
>
|
||||||
|
<Square className="w-4 h-4 mr-2" />
|
||||||
|
矩形
|
||||||
|
</Button>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
accept=".kml,.geojson,.json,.shp"
|
||||||
|
onChange={handleFileImport}
|
||||||
|
className="absolute inset-0 opacity-0 cursor-pointer"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="bg-green-50 hover:bg-green-100 border-green-300"
|
||||||
|
>
|
||||||
|
<Upload className="w-4 h-4 mr-2 text-green-600" />
|
||||||
|
<span className="text-green-700">导入文件</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 地图容器 */}
|
||||||
|
<div
|
||||||
|
ref={mapContainerRef}
|
||||||
|
className="w-full h-[500px] rounded-lg border-2 border-dashed relative bg-gradient-to-br from-green-50 to-blue-50"
|
||||||
|
>
|
||||||
|
{isMapLoaded && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center p-8">
|
||||||
|
<div className="text-center max-w-md">
|
||||||
|
<Map className="w-16 h-16 mx-auto mb-4 text-green-600 opacity-50" />
|
||||||
|
<h3 className="text-green-800 mb-2">演示地图模式</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
系统提供完整功能,您可以使用:
|
||||||
|
</p>
|
||||||
|
<ul className="text-sm text-left text-muted-foreground space-y-2">
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-green-600 mt-0.5 flex-shrink-0" />
|
||||||
|
<span>导入 KML/GeoJSON/SHP 文件</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-green-600 mt-0.5 flex-shrink-0" />
|
||||||
|
<span>手动绘制多边形或矩形边界</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-green-600 mt-0.5 flex-shrink-0" />
|
||||||
|
<span>自动计算面积、周长和中心点</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-green-600 mt-0.5 flex-shrink-0" />
|
||||||
|
<span>卫星图和电子地图切换</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div className="mt-4 p-3 bg-blue-50 rounded-md">
|
||||||
|
<p className="text-xs text-blue-800">
|
||||||
|
💡 如需启用真实地图,请配置地图SDK
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isMapLoaded && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<MapPin className="w-12 h-12 mx-auto mb-3 text-green-600 animate-pulse" />
|
||||||
|
<p className="text-sm text-muted-foreground">初始化中...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 地图信息 */}
|
||||||
|
{coordinates && coordinates.length > 0 && (
|
||||||
|
<Alert>
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-green-600" />
|
||||||
|
<AlertDescription>
|
||||||
|
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">边界点数:</span>
|
||||||
|
<span className="ml-2 font-medium">{coordinates.length} 个</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">面积:</span>
|
||||||
|
<span className="ml-2 font-medium text-green-600">{area.toFixed(2)} 亩</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">周长:</span>
|
||||||
|
<span className="ml-2 font-medium">{perimeter.toFixed(0)} 米</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 文件格式说明 */}
|
||||||
|
<Alert className="bg-blue-50 border-blue-200">
|
||||||
|
<FileType className="w-4 h-4 text-blue-600" />
|
||||||
|
<AlertDescription className="text-sm text-blue-800">
|
||||||
|
<strong>支持的文件格式:</strong>
|
||||||
|
<ul className="mt-2 space-y-1 ml-4 list-disc">
|
||||||
|
<li>KML - Google Earth 标准格式</li>
|
||||||
|
<li>GeoJSON - Web GIS 标准格式</li>
|
||||||
|
<li>SHP - ArcGIS Shapefile 格式</li>
|
||||||
|
</ul>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Land } from '@/app/(app)/land-information/archive/manage/page';
|
||||||
|
|
||||||
|
interface LandOwnershipInfoProps {
|
||||||
|
formData: Partial<Land>;
|
||||||
|
onChange: (data: Partial<Land>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LandOwnershipInfo({ formData, onChange }: LandOwnershipInfoProps) {
|
||||||
|
const handleFieldChange = (field: keyof Land, value: any) => {
|
||||||
|
onChange({ ...formData, [field]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">权属人 *</label>
|
||||||
|
<Input
|
||||||
|
value={formData.owner || ''}
|
||||||
|
onChange={(e) => handleFieldChange('owner', e.target.value)}
|
||||||
|
placeholder="姓名"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">联系电话</label>
|
||||||
|
<Input
|
||||||
|
type="tel"
|
||||||
|
value={formData.ownerPhone || ''}
|
||||||
|
onChange={(e) => handleFieldChange('ownerPhone', e.target.value)}
|
||||||
|
placeholder="手机号码"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">确权证号</label>
|
||||||
|
<Input
|
||||||
|
value={formData.certificateNumber || ''}
|
||||||
|
onChange={(e) => handleFieldChange('certificateNumber', e.target.value)}
|
||||||
|
placeholder="土地确权证书编号"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">承包期限(年)</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={formData.contractPeriod || ''}
|
||||||
|
onChange={(e) => handleFieldChange('contractPeriod', Number(e.target.value))}
|
||||||
|
placeholder="如:30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">承包开始</label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={formData.contractStartDate || ''}
|
||||||
|
onChange={(e) => handleFieldChange('contractStartDate', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">承包结束</label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={formData.contractEndDate || ''}
|
||||||
|
onChange={(e) => handleFieldChange('contractEndDate', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,513 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import {
|
||||||
|
History,
|
||||||
|
MapPin,
|
||||||
|
FileText,
|
||||||
|
Clock,
|
||||||
|
User,
|
||||||
|
RotateCcw,
|
||||||
|
Eye,
|
||||||
|
GitBranch,
|
||||||
|
ChevronRight,
|
||||||
|
CheckCircle2
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Land } from '../page';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
// 地块版本历史接口
|
||||||
|
interface LandVersion {
|
||||||
|
id: string;
|
||||||
|
landId: string;
|
||||||
|
version: number;
|
||||||
|
changeType: 'create' | 'update-boundary' | 'update-attributes' | 'merge' | 'split';
|
||||||
|
changes: LandVersionChange[];
|
||||||
|
coordinates?: { lat: number; lng: number }[]; // 历史边界
|
||||||
|
attributes?: Partial<Land>; // 历史属性
|
||||||
|
changedBy: string;
|
||||||
|
changedAt: string;
|
||||||
|
remarks?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 版本变更详情
|
||||||
|
interface LandVersionChange {
|
||||||
|
field: string; // 字段名
|
||||||
|
oldValue: any;
|
||||||
|
newValue: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LandVersionHistoryProps {
|
||||||
|
land: Land;
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onRestore?: (version: LandVersion) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LandVersionHistory({ land, open, onOpenChange, onRestore }: LandVersionHistoryProps) {
|
||||||
|
const [versions, setVersions] = useState<LandVersion[]>([]);
|
||||||
|
const [selectedVersion, setSelectedVersion] = useState<LandVersion | null>(null);
|
||||||
|
const [showDetailDialog, setShowDetailDialog] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && land) {
|
||||||
|
loadVersions();
|
||||||
|
}
|
||||||
|
}, [open, land]);
|
||||||
|
|
||||||
|
const loadVersions = () => {
|
||||||
|
try {
|
||||||
|
const key = `land_versions_${land.id}`;
|
||||||
|
const data = localStorage.getItem(key);
|
||||||
|
if (data) {
|
||||||
|
const versionList = JSON.parse(data) as LandVersion[];
|
||||||
|
// 按版本号降序排列
|
||||||
|
setVersions(versionList.sort((a, b) => b.version - a.version));
|
||||||
|
} else {
|
||||||
|
// 生成示例版本数据
|
||||||
|
const sampleVersions: LandVersion[] = [
|
||||||
|
{
|
||||||
|
id: `version_${land.id}_1`,
|
||||||
|
landId: land.id,
|
||||||
|
version: 3,
|
||||||
|
changeType: 'update-attributes',
|
||||||
|
changes: [
|
||||||
|
{ field: 'tags', oldValue: ['有机种植'], newValue: ['有机种植', '高产示范'] },
|
||||||
|
{ field: 'soilType', oldValue: 'clay', newValue: 'loam' }
|
||||||
|
],
|
||||||
|
coordinates: land.coordinates,
|
||||||
|
attributes: land,
|
||||||
|
changedBy: '张管理员',
|
||||||
|
changedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
remarks: '更新标签和土壤类型信息'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `version_${land.id}_2`,
|
||||||
|
landId: land.id,
|
||||||
|
version: 2,
|
||||||
|
changeType: 'update-boundary',
|
||||||
|
changes: [
|
||||||
|
{ field: 'coordinates', oldValue: '4个边界点', newValue: '5个边界点' },
|
||||||
|
{ field: 'area', oldValue: 118.5, newValue: 120.5 }
|
||||||
|
],
|
||||||
|
coordinates: land.coordinates,
|
||||||
|
attributes: land,
|
||||||
|
changedBy: '李测量员',
|
||||||
|
changedAt: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
remarks: '重新测量地块边界'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `version_${land.id}_3`,
|
||||||
|
landId: land.id,
|
||||||
|
version: 1,
|
||||||
|
changeType: 'create',
|
||||||
|
changes: [
|
||||||
|
{ field: 'name', oldValue: null, newValue: land.name },
|
||||||
|
{ field: 'code', oldValue: null, newValue: land.code },
|
||||||
|
{ field: 'area', oldValue: null, newValue: land.area },
|
||||||
|
{ field: 'location', oldValue: null, newValue: land.location }
|
||||||
|
],
|
||||||
|
coordinates: land.coordinates,
|
||||||
|
attributes: land,
|
||||||
|
changedBy: '王录入员',
|
||||||
|
changedAt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
remarks: '创建地块基础信息'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
setVersions(sampleVersions);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载版本历史失败:', error);
|
||||||
|
toast.error('加载版本历史失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChangeTypeLabel = (type: LandVersion['changeType']) => {
|
||||||
|
const labels = {
|
||||||
|
'create': '创建',
|
||||||
|
'update-boundary': '边界更新',
|
||||||
|
'update-attributes': '属性更新',
|
||||||
|
'merge': '合并',
|
||||||
|
'split': '分割',
|
||||||
|
};
|
||||||
|
return labels[type];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChangeTypeBadge = (type: LandVersion['changeType']) => {
|
||||||
|
const colors = {
|
||||||
|
'create': 'bg-green-100 text-green-700',
|
||||||
|
'update-boundary': 'bg-blue-100 text-blue-700',
|
||||||
|
'update-attributes': 'bg-purple-100 text-purple-700',
|
||||||
|
'merge': 'bg-orange-100 text-orange-700',
|
||||||
|
'split': 'bg-pink-100 text-pink-700',
|
||||||
|
};
|
||||||
|
return colors[type];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFieldLabel = (field: string) => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
name: '地块名称',
|
||||||
|
code: '地块编号',
|
||||||
|
area: '面积',
|
||||||
|
perimeter: '周长',
|
||||||
|
location: '位置',
|
||||||
|
soilType: '土壤类型',
|
||||||
|
landUseType: '土地利用类型',
|
||||||
|
plantingMode: '种植模式',
|
||||||
|
owner: '权属人',
|
||||||
|
ownerPhone: '联系电话',
|
||||||
|
contractPeriod: '承包期限',
|
||||||
|
contractStartDate: '承包开始',
|
||||||
|
contractEndDate: '承包结束',
|
||||||
|
certificateNumber: '确权证号',
|
||||||
|
tags: '标签',
|
||||||
|
status: '状态',
|
||||||
|
coordinates: '边界坐标',
|
||||||
|
};
|
||||||
|
return labels[field] || field;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatValue = (field: string, value: any) => {
|
||||||
|
if (value === null || value === undefined) return '-';
|
||||||
|
|
||||||
|
if (field === 'soilType') {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
loam: '壤土',
|
||||||
|
sand: '沙土',
|
||||||
|
clay: '黏土',
|
||||||
|
silt: '粉土',
|
||||||
|
other: '其他',
|
||||||
|
};
|
||||||
|
return labels[value] || value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field === 'landUseType') {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
farmland: '耕地',
|
||||||
|
garden: '园地',
|
||||||
|
forestland: '林地',
|
||||||
|
grassland: '草地',
|
||||||
|
other: '其他',
|
||||||
|
};
|
||||||
|
return labels[value] || value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field === 'plantingMode') {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
'open-field': '露地',
|
||||||
|
'greenhouse': '大棚',
|
||||||
|
'orchard': '果园',
|
||||||
|
'paddy': '水田',
|
||||||
|
'dryland': '旱地',
|
||||||
|
};
|
||||||
|
return labels[value] || value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field === 'status') {
|
||||||
|
return value === 'active' ? '正常' : '待确认';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field === 'area') {
|
||||||
|
return `${value} 亩`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field === 'perimeter') {
|
||||||
|
return `${value} 米`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field === 'contractPeriod') {
|
||||||
|
return `${value} 年`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field === 'tags' && Array.isArray(value)) {
|
||||||
|
return value.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field === 'coordinates' && Array.isArray(value)) {
|
||||||
|
return `${value.length} 个点`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewDetail = (version: LandVersion) => {
|
||||||
|
setSelectedVersion(version);
|
||||||
|
setShowDetailDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRestore = (version: LandVersion) => {
|
||||||
|
if (window.confirm(`确定要恢复到版本 ${version.version} 吗?这将创建一个新版本。`)) {
|
||||||
|
onRestore?.(version);
|
||||||
|
toast.success('版本已恢复');
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCurrentVersion = (version: LandVersion) => {
|
||||||
|
return version.version === 3; // 假设当前版本是3
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<History className="w-5 h-5 text-green-600" />
|
||||||
|
版本历史 - {land.name}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
查看地块的历史版本和变更记录
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{versions.length === 0 ? (
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
<History className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||||
|
<p>暂无版本历史</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="relative">
|
||||||
|
{/* 时间线 */}
|
||||||
|
<div className="absolute left-8 top-0 bottom-0 w-0.5 bg-gray-200" />
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{versions.map((version, index) => (
|
||||||
|
<div key={version.id} className="relative pl-16">
|
||||||
|
{/* 时间线节点 */}
|
||||||
|
<div className={`absolute left-6 w-5 h-5 rounded-full border-4 border-white ${
|
||||||
|
isCurrentVersion(version)
|
||||||
|
? 'bg-green-600'
|
||||||
|
: 'bg-gray-400'
|
||||||
|
}`} />
|
||||||
|
|
||||||
|
<Card className={`p-4 ${isCurrentVersion(version) ? 'border-green-600 border-2' : ''}`}>
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Badge className={getChangeTypeBadge(version.changeType)}>
|
||||||
|
{getChangeTypeLabel(version.changeType)}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
版本 {version.version}
|
||||||
|
</span>
|
||||||
|
{isCurrentVersion(version) && (
|
||||||
|
<Badge className="bg-green-600">
|
||||||
|
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||||
|
当前版本
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Clock className="w-4 h-4" />
|
||||||
|
{new Date(version.changedAt).toLocaleString('zh-CN')}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<User className="w-4 h-4" />
|
||||||
|
{version.changedBy}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{version.remarks && (
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
{version.remarks}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleViewDetail(version)}
|
||||||
|
>
|
||||||
|
<Eye className="w-4 h-4 mr-1" />
|
||||||
|
详情
|
||||||
|
</Button>
|
||||||
|
{!isCurrentVersion(version) && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleRestore(version)}
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-4 h-4 mr-1" />
|
||||||
|
恢复
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 变更摘要 */}
|
||||||
|
{version.changes.length > 0 && (
|
||||||
|
<div className="mt-3 pt-3 border-t">
|
||||||
|
<div className="text-sm space-y-1">
|
||||||
|
{version.changes.slice(0, 3).map((change, idx) => (
|
||||||
|
<div key={idx} className="flex items-center gap-2 text-muted-foreground">
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
<span>{getFieldLabel(change.field)}:</span>
|
||||||
|
<span className="line-through opacity-60">
|
||||||
|
{formatValue(change.field, change.oldValue)}
|
||||||
|
</span>
|
||||||
|
<span>→</span>
|
||||||
|
<span className="text-foreground">
|
||||||
|
{formatValue(change.field, change.newValue)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{version.changes.length > 3 && (
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
还有 {version.changes.length - 3} 项变更...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 版本详情对话框 */}
|
||||||
|
{selectedVersion && (
|
||||||
|
<Dialog open={showDetailDialog} onOpenChange={setShowDetailDialog}>
|
||||||
|
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<FileText className="w-5 h-5 text-green-600" />
|
||||||
|
版本详情 - 版本 {selectedVersion.version}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
查看版本的详细变更信息
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 版本基本信息 */}
|
||||||
|
<Card className="p-4 bg-gray-50">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>变更类型</Label>
|
||||||
|
<div className="mt-2">
|
||||||
|
<Badge className={getChangeTypeBadge(selectedVersion.changeType)}>
|
||||||
|
{getChangeTypeLabel(selectedVersion.changeType)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>版本号</Label>
|
||||||
|
<div className="mt-2">版本 {selectedVersion.version}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>变更时间</Label>
|
||||||
|
<div className="mt-2">
|
||||||
|
{new Date(selectedVersion.changedAt).toLocaleString('zh-CN')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>变更人</Label>
|
||||||
|
<div className="mt-2">{selectedVersion.changedBy}</div>
|
||||||
|
</div>
|
||||||
|
{selectedVersion.remarks && (
|
||||||
|
<div className="col-span-2">
|
||||||
|
<Label>备注</Label>
|
||||||
|
<div className="mt-2">{selectedVersion.remarks}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 变更详情 */}
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-3 flex items-center gap-2">
|
||||||
|
<GitBranch className="w-4 h-4" />
|
||||||
|
变更详情
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{selectedVersion.changes.map((change, idx) => (
|
||||||
|
<Card key={idx} className="p-3">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-sm mb-2">
|
||||||
|
<Badge variant="outline">{getFieldLabel(change.field)}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<div className="text-muted-foreground mb-1">原值</div>
|
||||||
|
<div className="p-2 bg-red-50 rounded border border-red-200 line-through">
|
||||||
|
{formatValue(change.field, change.oldValue)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-muted-foreground mb-1">新值</div>
|
||||||
|
<div className="p-2 bg-green-50 rounded border border-green-200">
|
||||||
|
{formatValue(change.field, change.newValue)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 边界信息 */}
|
||||||
|
{selectedVersion.coordinates && selectedVersion.coordinates.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-3 flex items-center gap-2">
|
||||||
|
<MapPin className="w-4 h-4" />
|
||||||
|
边界信息
|
||||||
|
</h4>
|
||||||
|
<Card className="p-4 bg-gray-50">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
边界点数量: {selectedVersion.coordinates.length} 个
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
{!isCurrentVersion(selectedVersion) && (
|
||||||
|
<Button onClick={() => {
|
||||||
|
handleRestore(selectedVersion);
|
||||||
|
setShowDetailDialog(false);
|
||||||
|
}}>
|
||||||
|
<RotateCcw className="w-4 h-4 mr-2" />
|
||||||
|
恢复此版本
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button variant="outline" onClick={() => setShowDetailDialog(false)}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Label = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<label className="text-sm text-muted-foreground">
|
||||||
|
{children}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
1211
crop-x/src/app/(app)/land-information/archive/manage/page.tsx
Normal file
1211
crop-x/src/app/(app)/land-information/archive/manage/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ import {SideBarOld} from '@/components/layouts/SideBar/SideBarOld'
|
|||||||
import '@/styles/globals.css'
|
import '@/styles/globals.css'
|
||||||
import { ThemeProvider } from 'next-themes'
|
import { ThemeProvider } from 'next-themes'
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
import { Building2, Users, Cog, Activity, Mail, UserCircle, Database, Map, BarChart3, Cloud, TrendingUp, GitCompare, AlertTriangle, FileText, MapPin, Settings, User, Package, Navigation, Zap, Target, PieChart, Calendar, Shield, Tractor, Clipboard, Brain, Droplets, Book, ShoppingCart } from 'lucide-react'
|
import { Building2, Users, Cog, Activity, Mail, UserCircle, Database, Map, BarChart3, Cloud, TrendingUp, GitCompare, AlertTriangle, FileText, MapPin, Settings, User, Package, Navigation, Zap, Target, PieChart, Calendar, Shield, Tractor, Clipboard, ClipboardCheck, Brain, Droplets, Book, ShoppingCart } from 'lucide-react'
|
||||||
|
|
||||||
const navbarData = {
|
const navbarData = {
|
||||||
logo: {
|
logo: {
|
||||||
@@ -226,20 +226,20 @@ const fieldMessageManagement = {
|
|||||||
{
|
{
|
||||||
title: "地块档案管理",
|
title: "地块档案管理",
|
||||||
url: "/land-information/archive",
|
url: "/land-information/archive",
|
||||||
icon: <Database className="w-4 h-4" />,
|
icon: <FileText className="w-4 h-4" />,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "地块录入维护",
|
title: "地块档案管理",
|
||||||
url: "/land-information/archive/entry",
|
url: "/land-information/archive/manage",
|
||||||
isActive: false
|
isActive: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "地块分类管理",
|
title: "地块分类与标签管理",
|
||||||
url: "/land-information/archive/classification",
|
url: "/land-information/archive/classification",
|
||||||
isActive: false
|
isActive: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "地块统计分析",
|
title: "统计分析",
|
||||||
url: "/land-information/archive/statistics",
|
url: "/land-information/archive/statistics",
|
||||||
isActive: false
|
isActive: false
|
||||||
}
|
}
|
||||||
@@ -251,22 +251,22 @@ const fieldMessageManagement = {
|
|||||||
icon: <Map className="w-4 h-4" />,
|
icon: <Map className="w-4 h-4" />,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "地块GIS地图",
|
title: "GIS地图管理",
|
||||||
url: "/land-information/map/gis",
|
url: "/land-information/map/gis",
|
||||||
isActive: false
|
isActive: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "地块绘制编辑",
|
title: "数字化绘制与编辑",
|
||||||
url: "/land-information/map/draw",
|
url: "/land-information/map/draw",
|
||||||
isActive: false
|
isActive: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "地块空间查询",
|
title: "空间数据管理",
|
||||||
url: "/land-information/map/spatial-query",
|
url: "/land-information/map/spatial-query",
|
||||||
isActive: false
|
isActive: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "地块卫星影像",
|
title: "地块影像",
|
||||||
url: "/land-information/map/satellite",
|
url: "/land-information/map/satellite",
|
||||||
isActive: false
|
isActive: false
|
||||||
}
|
}
|
||||||
@@ -275,7 +275,7 @@ const fieldMessageManagement = {
|
|||||||
{
|
{
|
||||||
title: "空间分析与决策支持",
|
title: "空间分析与决策支持",
|
||||||
url: "/land-information/analysis",
|
url: "/land-information/analysis",
|
||||||
icon: <BarChart3 className="w-4 h-4" />,
|
icon: <TrendingUp className="w-4 h-4" />,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "土壤基础数据",
|
title: "土壤基础数据",
|
||||||
@@ -314,27 +314,22 @@ const fieldMessageManagement = {
|
|||||||
{
|
{
|
||||||
title: "地块适宜性评价",
|
title: "地块适宜性评价",
|
||||||
url: "/land-information/suitability",
|
url: "/land-information/suitability",
|
||||||
icon: <TrendingUp className="w-4 h-4" />,
|
icon: <ClipboardCheck className="w-4 h-4" />,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "综合适宜性评价",
|
title: "多因子综合评价",
|
||||||
url: "/land-information/suitability/comprehensive",
|
url: "/land-information/suitability/comprehensive",
|
||||||
isActive: false
|
isActive: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "批量适宜性评价",
|
title: "自动化空间分析",
|
||||||
url: "/land-information/suitability/batch",
|
url: "/land-information/suitability/batch",
|
||||||
isActive: false
|
isActive: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "作物适宜性评价",
|
title: "作物适配推荐",
|
||||||
url: "/land-information/suitability/crop",
|
url: "/land-information/suitability/crop",
|
||||||
isActive: false
|
isActive: false
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "权重设置",
|
|
||||||
url: "/land-information/suitability/weight",
|
|
||||||
isActive: false
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -344,17 +339,17 @@ const fieldMessageManagement = {
|
|||||||
icon: <GitCompare className="w-4 h-4" />,
|
icon: <GitCompare className="w-4 h-4" />,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "指标对比",
|
title: "多维度指标看板",
|
||||||
url: "/land-information/comparison/indicator",
|
url: "/land-information/comparison/indicator",
|
||||||
isActive: false
|
isActive: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "图表对比",
|
title: "可视化图表分析",
|
||||||
url: "/land-information/comparison/chart",
|
url: "/land-information/comparison/chart",
|
||||||
isActive: false
|
isActive: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "报告对比",
|
title: "对比报告生成",
|
||||||
url: "/land-information/comparison/report",
|
url: "/land-information/comparison/report",
|
||||||
isActive: false
|
isActive: false
|
||||||
}
|
}
|
||||||
@@ -366,17 +361,17 @@ const fieldMessageManagement = {
|
|||||||
icon: <AlertTriangle className="w-4 h-4" />,
|
icon: <AlertTriangle className="w-4 h-4" />,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "风险监测",
|
title: "实时风险监测",
|
||||||
url: "/land-information/risk/monitoring",
|
url: "/land-information/risk/monitoring",
|
||||||
isActive: false
|
isActive: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "风险推送",
|
title: "预警推送管理",
|
||||||
url: "/land-information/risk/push",
|
url: "/land-information/risk/push",
|
||||||
isActive: false
|
isActive: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "风险处置",
|
title: "预警处置跟踪",
|
||||||
url: "/land-information/risk/disposal",
|
url: "/land-information/risk/disposal",
|
||||||
isActive: false
|
isActive: false
|
||||||
}
|
}
|
||||||
|
|||||||
188
crop-x/src/components/ui/data-pagination.tsx
Normal file
188
crop-x/src/components/ui/data-pagination.tsx
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, MoreHorizontal } from 'lucide-react';
|
||||||
|
import { Button } from './button';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface DataPaginationProps {
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalItems: number;
|
||||||
|
startIndex: number;
|
||||||
|
endIndex: number;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
onPageSizeChange: (size: number) => void;
|
||||||
|
canPreviousPage: boolean;
|
||||||
|
canNextPage: boolean;
|
||||||
|
pageSizeOptions?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataPagination({
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
pageSize,
|
||||||
|
totalItems,
|
||||||
|
startIndex,
|
||||||
|
endIndex,
|
||||||
|
onPageChange,
|
||||||
|
onPageSizeChange,
|
||||||
|
canPreviousPage,
|
||||||
|
canNextPage,
|
||||||
|
pageSizeOptions = [10, 30, 50, 100],
|
||||||
|
}: DataPaginationProps) {
|
||||||
|
// 生成页码数组
|
||||||
|
const generatePageNumbers = () => {
|
||||||
|
const pages: (number | 'ellipsis')[] = [];
|
||||||
|
const maxVisiblePages = 5;
|
||||||
|
|
||||||
|
if (totalPages <= maxVisiblePages) {
|
||||||
|
// 如果总页数少,显示所有页码
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 总是显示第一页
|
||||||
|
pages.push(1);
|
||||||
|
|
||||||
|
if (currentPage > 3) {
|
||||||
|
pages.push('ellipsis');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示当前页附近的页码
|
||||||
|
const startPage = Math.max(2, currentPage - 1);
|
||||||
|
const endPage = Math.min(totalPages - 1, currentPage + 1);
|
||||||
|
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPage < totalPages - 2) {
|
||||||
|
pages.push('ellipsis');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 总是显示最后一页
|
||||||
|
if (totalPages > 1) {
|
||||||
|
pages.push(totalPages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageNumbers = generatePageNumbers();
|
||||||
|
|
||||||
|
if (totalItems === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-t bg-background">
|
||||||
|
<div className="flex flex-col gap-4 px-4 py-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
{/* 左侧:每页显示数量 */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-muted-foreground hidden sm:inline">每页显示</span>
|
||||||
|
<Select
|
||||||
|
value={pageSize.toString()}
|
||||||
|
onValueChange={(value) => onPageSizeChange(Number(value))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-9 w-[70px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent side="top">
|
||||||
|
{pageSizeOptions.map((size) => (
|
||||||
|
<SelectItem key={size} value={size.toString()}>
|
||||||
|
{size}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<span className="text-sm text-muted-foreground">条</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 中间:分页器 */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{/* 第一页 */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(1)}
|
||||||
|
disabled={!canPreviousPage}
|
||||||
|
className="h-9 w-9 p-0 hidden sm:flex"
|
||||||
|
>
|
||||||
|
<ChevronsLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 上一页 */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(currentPage - 1)}
|
||||||
|
disabled={!canPreviousPage}
|
||||||
|
className="h-9 w-9 p-0"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 页码 */}
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{pageNumbers.map((page, index) => (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
{page === 'ellipsis' ? (
|
||||||
|
<div className="flex h-9 w-9 items-center justify-center">
|
||||||
|
<MoreHorizontal className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant={currentPage === page ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(page)}
|
||||||
|
className={cn(
|
||||||
|
'h-9 w-9 p-0',
|
||||||
|
currentPage === page && 'bg-green-600 hover:bg-green-700 text-white'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 下一页 */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(currentPage + 1)}
|
||||||
|
disabled={!canNextPage}
|
||||||
|
className="h-9 w-9 p-0"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 最后一页 */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(totalPages)}
|
||||||
|
disabled={!canNextPage}
|
||||||
|
className="h-9 w-9 p-0 hidden sm:flex"
|
||||||
|
>
|
||||||
|
<ChevronsRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右侧:统计信息 */}
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<span className="hidden sm:inline">
|
||||||
|
显示 <span className="font-medium text-foreground">{startIndex}</span> 至{' '}
|
||||||
|
<span className="font-medium text-foreground">{endIndex}</span> 条,
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
共 <span className="font-medium text-foreground">{totalItems}</span> 条
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
78
crop-x/src/lib/usePagination.ts
Normal file
78
crop-x/src/lib/usePagination.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
|
||||||
|
export interface PaginationConfig {
|
||||||
|
currentPage: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsePaginationReturn<T> {
|
||||||
|
currentPage: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
paginatedData: T[];
|
||||||
|
goToPage: (page: number) => void;
|
||||||
|
nextPage: () => void;
|
||||||
|
previousPage: () => void;
|
||||||
|
setPageSize: (size: number) => void;
|
||||||
|
canPreviousPage: boolean;
|
||||||
|
canNextPage: boolean;
|
||||||
|
startIndex: number;
|
||||||
|
endIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePagination<T>(
|
||||||
|
data: T[],
|
||||||
|
initialPageSize: number = 10
|
||||||
|
): UsePaginationReturn<T> {
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(initialPageSize);
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(data.length / pageSize);
|
||||||
|
|
||||||
|
const paginatedData = useMemo(() => {
|
||||||
|
const startIndex = (currentPage - 1) * pageSize;
|
||||||
|
const endIndex = startIndex + pageSize;
|
||||||
|
return data.slice(startIndex, endIndex);
|
||||||
|
}, [data, currentPage, pageSize]);
|
||||||
|
|
||||||
|
const goToPage = (page: number) => {
|
||||||
|
const pageNumber = Math.max(1, Math.min(page, totalPages));
|
||||||
|
setCurrentPage(pageNumber);
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextPage = () => {
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
setCurrentPage(currentPage + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const previousPage = () => {
|
||||||
|
if (currentPage > 1) {
|
||||||
|
setCurrentPage(currentPage - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetPageSize = (size: number) => {
|
||||||
|
setPageSize(size);
|
||||||
|
setCurrentPage(1); // 重置到第一页
|
||||||
|
};
|
||||||
|
|
||||||
|
const startIndex = (currentPage - 1) * pageSize + 1;
|
||||||
|
const endIndex = Math.min(currentPage * pageSize, data.length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentPage,
|
||||||
|
pageSize,
|
||||||
|
totalPages,
|
||||||
|
paginatedData,
|
||||||
|
goToPage,
|
||||||
|
nextPage,
|
||||||
|
previousPage,
|
||||||
|
setPageSize: handleSetPageSize,
|
||||||
|
canPreviousPage: currentPage > 1,
|
||||||
|
canNextPage: currentPage < totalPages,
|
||||||
|
startIndex,
|
||||||
|
endIndex,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user