子仓库提交

This commit is contained in:
2025-11-10 09:19:56 +08:00
parent 5b93b6ff7d
commit caae0492ee
733 changed files with 141413 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function CustomersPage() {
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> /agricultural-asset/basic/customers
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function MaterialsPage() {
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> /agricultural-asset/basic/materials
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function BasicPage() {
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> /agricultural-asset/basic
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function SuppliersPage() {
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> /agricultural-asset/basic/suppliers
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function ToolsPage() {
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> /agricultural-asset/basic/tools
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function ArchivePage() {
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> /agricultural-asset/equipment/archive
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function DepreciationPage() {
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> /agricultural-asset/equipment/depreciation
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function DispatchPage() {
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> /agricultural-asset/equipment/dispatch
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function DisposalPage() {
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> /agricultural-asset/equipment/disposal
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function MaintenancePage() {
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> /agricultural-asset/equipment/maintenance
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function EquipmentPage() {
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> /agricultural-asset/equipment
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function CheckPage() {
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> /agricultural-asset/inventory/check
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function DetailPage() {
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> /agricultural-asset/inventory/detail
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function InPage() {
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> /agricultural-asset/inventory/in
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function LocationPage() {
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> /agricultural-asset/inventory/location
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function InventoryPage() {
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> /agricultural-asset/inventory
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function SuggestPage() {
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> /agricultural-asset/inventory/suggest
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function WarningPage() {
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> /agricultural-asset/inventory/warning
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,7 @@
export default function AgriculturalAssetLayout({
children,
}: {
children: React.ReactNode
}) {
return <>{children}</>
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function AgriculturalAssetPage() {
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> /agricultural-asset
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function OrderPage() {
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> /agricultural-asset/purchase/order
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function PurchasePage() {
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> /agricultural-asset/purchase
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function PlanPage() {
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> /agricultural-asset/purchase/plan
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function ConsumptionPage() {
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> /agricultural-asset/report/consumption
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function InventoryReportPage() {
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> /agricultural-asset/report/inventory
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function OverviewPage() {
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> /agricultural-asset/report/overview
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function ReportPage() {
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> /agricultural-asset/report
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function ApplyPage() {
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> /agricultural-asset/requisition/apply
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function ApprovalPage() {
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> /agricultural-asset/requisition/approval
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function CheckoutPage() {
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> /agricultural-asset/requisition/checkout
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function RequisitionPage() {
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> /agricultural-asset/requisition
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function RecordPage() {
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> /agricultural-asset/requisition/record
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function HistoryPage() {
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> /agricultural-asset/return/history
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function ReturnPage() {
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> /agricultural-asset/return
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function ProcessPage() {
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> /agricultural-asset/return/process
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function RegisterPage() {
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> /agricultural-asset/return/register
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function SettlementPage() {
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> /agricultural-asset/return/settlement
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function ClassificationPage() {
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> /agricultural-machinery/archive/classification
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card } from '@/components/ui/card';
import { FileText } from 'lucide-react';
export default function AgriculturalMachineryEntryPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<FileText className="w-6 h-6 text-blue-600" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3">
<p className="text-muted-foreground">
</p>
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm">
<strong></strong> /agricultural-machinery/archive/entry
</p>
<p className="text-sm mt-1">
<strong></strong>
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card } from '@/components/ui/card';
import { Package } from 'lucide-react';
export default function ArchivePage() {
return (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<Package className="w-6 h-6 text-blue-600" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3">
<p className="text-muted-foreground">
</p>
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm">
<strong></strong> /agricultural-machinery/archive
</p>
<p className="text-sm mt-1">
<strong></strong>
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card } from '@/components/ui/card';
import { QrCode } from 'lucide-react';
export default function AgriculturalMachineryQrCodePage() {
return (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<QrCode className="w-6 h-6 text-purple-600" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3">
<p className="text-muted-foreground">
</p>
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm">
<strong></strong> /agricultural-machinery/archive/qrcode
</p>
<p className="text-sm mt-1">
<strong></strong>
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function AnalysisPage() {
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> /agricultural-machinery/data-analysis/analysis
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function ComparisonPage() {
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> /agricultural-machinery/data-analysis/comparison
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card } from '@/components/ui/card';
import { PieChart } from 'lucide-react';
export default function DataAnalysisPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<PieChart className="w-6 h-6 text-cyan-600" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3">
<p className="text-muted-foreground">
</p>
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm">
<strong></strong> /agricultural-machinery/data-analysis
</p>
<p className="text-sm mt-1">
<strong></strong>
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card } from '@/components/ui/card';
import { User } from 'lucide-react';
export default function DriverInfoManagementPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<User className="w-6 h-6 text-orange-600" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3">
<p className="text-muted-foreground">
</p>
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm">
<strong></strong> /agricultural-machinery/driver-archive/info
</p>
<p className="text-sm mt-1">
<strong></strong>
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card } from '@/components/ui/card';
import { User } from 'lucide-react';
export default function DriverArchivePage() {
return (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<User className="w-6 h-6 text-green-600" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3">
<p className="text-muted-foreground">
</p>
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm">
<strong></strong> /agricultural-machinery/driver-archive
</p>
<p className="text-sm mt-1">
<strong></strong>
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card } from '@/components/ui/card';
import { Calendar } from 'lucide-react';
export default function DriverTaskManagementPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<Calendar className="w-6 h-6 text-indigo-600" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3">
<p className="text-muted-foreground">
</p>
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm">
<strong></strong> /agricultural-machinery/driver-archive/task
</p>
<p className="text-sm mt-1">
<strong></strong>
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,35 @@
'use client'
import { useEffect } from 'react'
export default function AgriculturalMachineryError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error('农机管理系统错误:', error)
}, [error])
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="text-6xl mb-4">🚙</div>
<h2 className="text-2xl font-bold text-red-800 mb-4">
</h2>
<p className="text-red-600 mb-6">
{error.message || '未知系统错误'}
</p>
<button
onClick={reset}
className="px-6 py-3 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
>
</button>
</div>
</div>
)
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function AlertRulesPage() {
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> /agricultural-machinery/fault-diagnosis/alert-rules
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function HealthPage() {
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> /agricultural-machinery/fault-diagnosis/health
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card } from '@/components/ui/card';
import { Zap } from 'lucide-react';
export default function FaultDiagnosisPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<Zap className="w-6 h-6 text-red-600" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3">
<p className="text-muted-foreground">
</p>
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm">
<strong></strong> /agricultural-machinery/fault-diagnosis
</p>
<p className="text-sm mt-1">
<strong></strong>
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function ParameterPage() {
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> /agricultural-machinery/fault-diagnosis/parameter
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function WarningPage() {
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> /agricultural-machinery/fault-diagnosis/warning
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,7 @@
export default function AgriculturalMachineryLayout({
children,
}: {
children: React.ReactNode
}) {
return <>{children}</>
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card } from '@/components/ui/card';
import { Cog } from 'lucide-react';
export default function LoadDeviceManagementPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<Cog className="w-6 h-6 text-red-600" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3">
<p className="text-muted-foreground">
</p>
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm">
<strong></strong> /agricultural-machinery/load-management/device
</p>
<p className="text-sm mt-1">
<strong></strong>
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card } from '@/components/ui/card';
import { Database } from 'lucide-react';
export default function LoadDeviceLibraryPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<Database className="w-6 h-6 text-emerald-600" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3">
<p className="text-muted-foreground">
</p>
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm">
<strong></strong> /agricultural-machinery/load-management/library
</p>
<p className="text-sm mt-1">
<strong></strong>
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card } from '@/components/ui/card';
import { Settings } from 'lucide-react';
export default function LoadManagementPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<Settings className="w-6 h-6 text-orange-600" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3">
<p className="text-muted-foreground">
</p>
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm">
<strong></strong> /agricultural-machinery/load-management
</p>
<p className="text-sm mt-1">
<strong></strong>
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card } from '@/components/ui/card';
import { Settings } from 'lucide-react';
export default function LoadParameterManagementPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<Settings className="w-6 h-6 text-amber-600" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3">
<p className="text-muted-foreground">
</p>
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm">
<strong></strong> /agricultural-machinery/load-management/parameter
</p>
<p className="text-sm mt-1">
<strong></strong>
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card } from '@/components/ui/card';
import { Package } from 'lucide-react';
export default function LoadTypeManagementPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<Package className="w-6 h-6 text-cyan-600" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3">
<p className="text-muted-foreground">
</p>
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm">
<strong></strong> /agricultural-machinery/load-management/type
</p>
<p className="text-sm mt-1">
<strong></strong>
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,10 @@
export default function AgriculturalMachineryLoading() {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600 mx-auto mb-4"></div>
<p className="text-gray-600">...</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card } from '@/components/ui/card';
import { MapPin } from 'lucide-react';
export default function RealTimeLocationMonitoringPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<MapPin className="w-6 h-6 text-blue-600" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3">
<p className="text-muted-foreground">
GPS定位和轨迹监控系统
</p>
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm">
<strong></strong> /agricultural-machinery/monitoring/location
</p>
<p className="text-sm mt-1">
<strong></strong> GPS定位
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card } from '@/components/ui/card';
import { BarChart3 } from 'lucide-react';
export default function OperationDataMonitoringPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<BarChart3 className="w-6 h-6 text-orange-600" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3">
<p className="text-muted-foreground">
</p>
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm">
<strong></strong> /agricultural-machinery/monitoring/operation
</p>
<p className="text-sm mt-1">
<strong></strong>
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card } from '@/components/ui/card';
import { Navigation } from 'lucide-react';
export default function MonitoringPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<Navigation className="w-6 h-6 text-purple-600" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3">
<p className="text-muted-foreground">
</p>
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm">
<strong></strong> /agricultural-machinery/monitoring
</p>
<p className="text-sm mt-1">
<strong></strong>
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card } from '@/components/ui/card';
import { Activity } from 'lucide-react';
export default function WorkStatusMonitoringPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<Activity className="w-6 h-6 text-green-600" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3">
<p className="text-muted-foreground">
</p>
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm">
<strong></strong> /agricultural-machinery/monitoring/status
</p>
<p className="text-sm mt-1">
<strong></strong>
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,93 @@
import Link from 'next/link'
export default function AgriculturalMachineryPage() {
return (
<div className="space-y-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold text-gray-800 mb-4">
</h2>
<p className="text-gray-600 mb-6">
</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<Link
href="/agricultural-machinery/archive/machinery-entry"
className="block p-4 bg-green-50 rounded-lg hover:bg-green-100 transition-colors"
>
<h3 className="font-semibold text-green-900 mb-2">
📋
</h3>
<p className="text-green-700 text-sm">
</p>
</Link>
<Link
href="/agricultural-machinery/monitoring/real-time-location-tracking"
className="block p-4 bg-blue-50 rounded-lg hover:bg-blue-100 transition-colors"
>
<h3 className="font-semibold text-blue-900 mb-2">
📍
</h3>
<p className="text-blue-700 text-sm">
</p>
</Link>
<Link
href="/agricultural-machinery/scheduling/task-assignment"
className="block p-4 bg-purple-50 rounded-lg hover:bg-purple-100 transition-colors"
>
<h3 className="font-semibold text-purple-900 mb-2">
📅
</h3>
<p className="text-purple-700 text-sm">
</p>
</Link>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
📊
</h3>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-gray-600"></span>
<span className="text-green-600 font-semibold">12 </span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600"></span>
<span className="text-gray-600 font-semibold">8 </span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600"></span>
<span className="text-yellow-600 font-semibold">3 </span>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
🔧
</h3>
<div className="space-y-2">
<button className="w-full px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 transition-colors">
</button>
<button className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors">
</button>
<button className="w-full px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700 transition-colors">
</button>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function CockpitPage() {
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> /agricultural-machinery/precision-operation/cockpit
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function DispatchPage() {
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> /agricultural-machinery/precision-operation/dispatch
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card } from '@/components/ui/card';
import { Target } from 'lucide-react';
export default function PrecisionOperationPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<Target className="w-6 h-6 text-indigo-600" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3">
<p className="text-muted-foreground">
</p>
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm">
<strong></strong> /agricultural-machinery/precision-operation
</p>
<p className="text-sm mt-1">
<strong></strong>
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function RecordPage() {
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> /agricultural-machinery/precision-operation/record
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function RoutePage() {
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> /agricultural-machinery/precision-operation/route
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function AssignmentPage() {
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> /agricultural-machinery/scheduling/assignment
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function DispatchPage() {
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> /agricultural-machinery/scheduling/dispatch
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card } from '@/components/ui/card';
import { Calendar } from 'lucide-react';
export default function SchedulingPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<Calendar className="w-6 h-6 text-amber-600" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3">
<p className="text-muted-foreground">
</p>
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm">
<strong></strong> /agricultural-machinery/scheduling
</p>
<p className="text-sm mt-1">
<strong></strong>
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function PlaybackPage() {
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> /agricultural-machinery/scheduling/playback
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function FencePage() {
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> /agricultural-machinery/security/fence
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card } from '@/components/ui/card';
import { Shield } from 'lucide-react';
export default function SecurityPage() {
return (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<Shield className="w-6 h-6 text-emerald-600" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3">
<p className="text-muted-foreground">
</p>
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm">
<strong></strong> /agricultural-machinery/security
</p>
<p className="text-sm mt-1">
<strong></strong>
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function DeviceControlPage() {
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> /ai-crop-model/application/device-control
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function ExternalSystemPage() {
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> /ai-crop-model/application/external-system
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use client';
import { Card } from '@/components/ui/card';
export default function ApplicationPage() {
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> /ai-crop-model/application
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,465 @@
/**
* filekorolheader: 添加/编辑参数对话框 - 参数模板配置组件
* 功能:新增参数、编辑参数信息、表单验证、不同类型参数配置
* 路径:/ai-crop-model/data-sense-center/device-parameter/components
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
import { useState, useEffect } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { ParameterDefinition, DeviceType, DeviceParameterAction } from './deviceParameterReducer';
import { Save, X, Plus } from 'lucide-react';
import { toast } from 'sonner';
interface AddParameterDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
editingParam: ParameterDefinition | null;
selectedType: DeviceType | null;
dispatch: React.Dispatch<DeviceParameterAction>;
}
interface ParamForm {
key: string;
label: string;
type: 'string' | 'number' | 'boolean' | 'select';
required: boolean;
defaultValue: string;
unit: string;
min: string;
max: string;
description: string;
options: { label: string; value: string }[];
}
export function AddParameterDialog({ open, onOpenChange, editingParam, selectedType, dispatch }: AddParameterDialogProps) {
const [paramForm, setParamForm] = useState<ParamForm>({
key: '',
label: '',
type: 'string',
required: false,
defaultValue: '',
unit: '',
min: '',
max: '',
description: '',
options: []
});
const [optionLabel, setOptionLabel] = useState('');
const [optionValue, setOptionValue] = useState('');
const [errors, setErrors] = useState<Partial<Record<keyof ParamForm, string>>>({});
useEffect(() => {
if (editingParam) {
setParamForm({
key: editingParam.key,
label: editingParam.label,
type: editingParam.type,
required: editingParam.required || false,
defaultValue: editingParam.defaultValue?.toString() || '',
unit: editingParam.unit || '',
min: editingParam.min?.toString() || '',
max: editingParam.max?.toString() || '',
description: editingParam.description || '',
options: editingParam.options || []
});
} else {
setParamForm({
key: '',
label: '',
type: 'string',
required: false,
defaultValue: '',
unit: '',
min: '',
max: '',
description: '',
options: []
});
}
setErrors({});
setOptionLabel('');
setOptionValue('');
}, [editingParam, open]);
const validateForm = (): boolean => {
const newErrors: Partial<Record<keyof ParamForm, string>> = {};
if (!paramForm.key.trim()) {
newErrors.key = '请输入参数标识';
}
if (!paramForm.label.trim()) {
newErrors.label = '请输入参数名称';
}
// 检查参数标识是否重复(编辑时除外)
if (!editingParam || editingParam.key !== paramForm.key) {
const exists = selectedType?.parameterDefinitions?.some(p => p.key === paramForm.key.trim());
if (exists) {
newErrors.key = '参数标识已存在';
}
}
// 验证数字类型的范围
if (paramForm.type === 'number') {
if (paramForm.min && paramForm.max) {
const min = parseFloat(paramForm.min);
const max = parseFloat(paramForm.max);
if (min >= max) {
newErrors.max = '最大值必须大于最小值';
}
}
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSaveParam = () => {
if (!selectedType) return;
if (!validateForm()) {
return;
}
try {
// 构建新参数
const newParam: ParameterDefinition = {
key: paramForm.key.trim(),
label: paramForm.label.trim(),
type: paramForm.type,
required: paramForm.required,
description: paramForm.description.trim()
};
// 根据类型设置默认值和属性
if (paramForm.type === 'number') {
newParam.defaultValue = paramForm.defaultValue ? parseFloat(paramForm.defaultValue) : 0;
if (paramForm.unit) newParam.unit = paramForm.unit;
if (paramForm.min) newParam.min = parseFloat(paramForm.min);
if (paramForm.max) newParam.max = parseFloat(paramForm.max);
} else if (paramForm.type === 'boolean') {
newParam.defaultValue = paramForm.defaultValue === 'true' || paramForm.defaultValue === true;
} else if (paramForm.type === 'select') {
newParam.options = paramForm.options;
newParam.defaultValue = paramForm.defaultValue || (paramForm.options[0]?.value || '');
} else {
newParam.defaultValue = paramForm.defaultValue;
}
if (editingParam) {
dispatch({ type: 'UPDATE_PARAMETER', payload: newParam });
} else {
dispatch({ type: 'ADD_PARAMETER', payload: newParam });
}
onOpenChange(false);
} catch (error) {
console.error('保存失败:', error);
toast.error('保存失败,请重试');
}
};
const handleAddOption = () => {
if (!optionLabel.trim() || !optionValue.trim()) {
toast.error('请填写选项标签和值');
return;
}
const newOption = { label: optionLabel.trim(), value: optionValue.trim() };
setParamForm({
...paramForm,
options: [...paramForm.options, newOption]
});
setOptionLabel('');
setOptionValue('');
};
const handleRemoveOption = (index: number) => {
setParamForm({
...paramForm,
options: paramForm.options.filter((_, i) => i !== index)
});
};
const handleInputChange = (field: keyof ParamForm, value: any) => {
setParamForm(prev => ({ ...prev, [field]: value }));
// 清除该字段的错误
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-xl">
{editingParam ? '编辑参数' : '新增参数'}
</DialogTitle>
<DialogDescription>
{selectedType?.name}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 基本信息 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="key" className="text-sm font-medium">
<span className="text-red-500">*</span>
</Label>
<Input
id="key"
value={paramForm.key}
onChange={(e) => handleInputChange('key', e.target.value)}
placeholder="例如temperature"
disabled={!!editingParam}
className={errors.key ? 'border-red-500' : ''}
/>
{errors.key && (
<p className="text-sm text-red-500">{errors.key}</p>
)}
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="space-y-2">
<Label htmlFor="label" className="text-sm font-medium">
<span className="text-red-500">*</span>
</Label>
<Input
id="label"
value={paramForm.label}
onChange={(e) => handleInputChange('label', e.target.value)}
placeholder="例如:温度"
className={errors.label ? 'border-red-500' : ''}
/>
{errors.label && (
<p className="text-sm text-red-500">{errors.label}</p>
)}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="type" className="text-sm font-medium">
<span className="text-red-500">*</span>
</Label>
<Select
value={paramForm.type}
onValueChange={(value: any) => handleInputChange('type', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="string"></SelectItem>
<SelectItem value="number"></SelectItem>
<SelectItem value="boolean"></SelectItem>
<SelectItem value="select"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="required" className="text-sm font-medium">
</Label>
<div className="flex items-center space-x-2 h-10">
<Switch
id="required"
checked={paramForm.required}
onCheckedChange={(checked) => handleInputChange('required', checked)}
/>
<Label htmlFor="required" className="cursor-pointer text-sm">
{paramForm.required ? '必填' : '选填'}
</Label>
</div>
</div>
</div>
{/* 根据类型显示不同字段 */}
{paramForm.type === 'number' && (
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="unit" className="text-sm font-medium">
</Label>
<Input
id="unit"
value={paramForm.unit}
onChange={(e) => handleInputChange('unit', e.target.value)}
placeholder="例如°C"
/>
</div>
<div className="space-y-2">
<Label htmlFor="min" className="text-sm font-medium">
</Label>
<Input
id="min"
type="number"
value={paramForm.min}
onChange={(e) => handleInputChange('min', e.target.value)}
placeholder="最小值"
/>
</div>
<div className="space-y-2">
<Label htmlFor="max" className="text-sm font-medium">
</Label>
<Input
id="max"
type="number"
value={paramForm.max}
onChange={(e) => handleInputChange('max', e.target.value)}
placeholder="最大值"
className={errors.max ? 'border-red-500' : ''}
/>
{errors.max && (
<p className="text-sm text-red-500">{errors.max}</p>
)}
</div>
</div>
)}
{/* 选择类型的选项配置 */}
{paramForm.type === 'select' && (
<div className="space-y-2">
<Label className="text-sm font-medium"></Label>
<div className="border rounded-lg p-4 space-y-3 bg-muted/30">
<div className="flex gap-2">
<Input
value={optionLabel}
onChange={(e) => setOptionLabel(e.target.value)}
placeholder="选项标签(显示文本)"
className="flex-1"
/>
<Input
value={optionValue}
onChange={(e) => setOptionValue(e.target.value)}
placeholder="选项值"
className="flex-1"
/>
<Button onClick={handleAddOption} size="sm">
<Plus className="w-4 h-4" />
</Button>
</div>
{paramForm.options.length > 0 && (
<div className="space-y-2">
<p className="text-sm text-muted-foreground"></p>
{paramForm.options.map((option, index) => (
<div key={index} className="flex items-center justify-between p-2 bg-muted rounded">
<span className="text-sm">
{option.label} <span className="text-muted-foreground">({option.value})</span>
</span>
<Button
variant="ghost"
size="sm"
onClick={() => handleRemoveOption(index)}
className="h-6 w-6 p-0"
>
<X className="w-3 h-3" />
</Button>
</div>
))}
</div>
)}
</div>
</div>
)}
{/* 默认值 */}
<div className="space-y-2">
<Label htmlFor="defaultValue" className="text-sm font-medium">
</Label>
{paramForm.type === 'boolean' ? (
<Select
value={paramForm.defaultValue?.toString() || 'false'}
onValueChange={(value) => handleInputChange('defaultValue', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="true"></SelectItem>
<SelectItem value="false"></SelectItem>
</SelectContent>
</Select>
) : paramForm.type === 'select' && paramForm.options.length > 0 ? (
<Select
value={paramForm.defaultValue}
onValueChange={(value) => handleInputChange('defaultValue', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{paramForm.options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
id="defaultValue"
type={paramForm.type === 'number' ? 'number' : 'text'}
value={paramForm.defaultValue}
onChange={(e) => handleInputChange('defaultValue', e.target.value)}
placeholder="参数默认值"
/>
)}
</div>
{/* 描述 */}
<div className="space-y-2">
<Label htmlFor="description" className="text-sm font-medium">
</Label>
<Textarea
id="description"
value={paramForm.description}
onChange={(e) => handleInputChange('description', e.target.value)}
placeholder="参数说明..."
rows={3}
/>
</div>
</div>
<DialogFooter className="flex justify-between sm:justify-between">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
className="px-6"
>
<X className="w-4 h-4 mr-2" />
</Button>
<Button
type="button"
onClick={handleSaveParam}
className="px-6"
>
<Save className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,55 @@
/**
* filekorolheader: 删除参数确认对话框 - 参数删除确认组件
* 功能:确认删除参数、防止误操作、影响提示
* 路径:/ai-crop-model/data-sense-center/device-parameter/components
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
import { DeviceParameterAction } from './deviceParameterReducer';
import { AlertTriangle } from 'lucide-react';
interface DeleteParameterConfirmDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
pendingDeleteParam: string | null;
dispatch: React.Dispatch<DeviceParameterAction>;
}
export function DeleteParameterConfirmDialog({ open, onOpenChange, pendingDeleteParam, dispatch }: DeleteParameterConfirmDialogProps) {
const confirmDelete = () => {
if (pendingDeleteParam) {
dispatch({ type: 'DELETE_PARAMETER', payload: pendingDeleteParam });
}
};
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2 text-destructive">
<AlertTriangle className="w-5 h-5" />
</AlertDialogTitle>
<AlertDialogDescription className="space-y-2">
<p>
</p>
<p className="text-sm text-muted-foreground">
</p>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => onOpenChange(false)}></AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
className="bg-destructive hover:bg-destructive/90"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View File

@@ -0,0 +1,103 @@
/**
* filekorolheader: 设备参数统计组件 - 参数模板统计展示组件
* 功能:显示设备类型统计数据、参数配置情况、参数类型分布
* 路径:/ai-crop-model/data-sense-center/device-parameter/components
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { DeviceParameterState } from './deviceParameterReducer';
import { BarChart3, Settings, Database, TrendingUp } from 'lucide-react';
interface DeviceParameterStatsProps {
state: DeviceParameterState;
}
export function DeviceParameterStats({ state }: DeviceParameterStatsProps) {
const { deviceTypes, selectedType } = state;
// 计算统计数据
const totalParams = deviceTypes.reduce((sum, type) => sum + (type.parameterDefinitions?.length || 0), 0);
const typesWithParams = deviceTypes.filter(type => (type.parameterDefinitions?.length || 0) > 0).length;
const currentTypeParams = selectedType?.parameterDefinitions?.length || 0;
// 参数类型统计
const paramTypeStats = deviceTypes.reduce((acc, type) => {
type.parameterDefinitions?.forEach(param => {
acc[param.type] = (acc[param.type] || 0) + 1;
});
return acc;
}, {} as Record<string, number>);
const getParamTypeLabel = (type: string) => {
const labels: Record<string, string> = {
'number': '数字',
'string': '文本',
'boolean': '布尔',
'select': '选择'
};
return labels[type] || type;
};
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* 设备类型总数 */}
<Card className="p-4 bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-green-600 dark:text-green-400 font-medium">
</div>
<div className="text-2xl font-bold text-green-700 dark:text-green-300 mt-1">
{deviceTypes.length}
</div>
</div>
<div className="text-green-500 dark:text-green-400">
<Settings className="w-8 h-8" />
</div>
</div>
</Card>
{/* 已配置模板 */}
<Card className="p-4 bg-orange-50 dark:bg-orange-950 border-orange-200 dark:border-orange-800">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-orange-600 dark:text-orange-400 font-medium">
</div>
<div className="text-2xl font-bold text-orange-700 dark:text-orange-300 mt-1">
{typesWithParams} / {deviceTypes.length}
</div>
<div className="text-xs text-orange-600 dark:text-orange-400 mt-1">
{deviceTypes.length > 0 ? Math.round((typesWithParams / deviceTypes.length) * 100) : 0}%
</div>
</div>
<div className="text-orange-500 dark:text-orange-400">
<TrendingUp className="w-8 h-8" />
</div>
</div>
</Card>
{/* 当前类型参数 */}
<Card className="p-4 bg-purple-50 dark:bg-purple-950 border-purple-200 dark:border-purple-800">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-purple-600 dark:text-purple-400 font-medium">
</div>
<div className="text-2xl font-bold text-purple-700 dark:text-purple-300 mt-1">
{currentTypeParams}
</div>
<div className="text-xs text-purple-600 dark:text-purple-400 mt-1">
{selectedType?.name || '未选择'}
</div>
</div>
<div className="text-purple-500 dark:text-purple-400">
<BarChart3 className="w-8 h-8" />
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,221 @@
/**
* filekorolheader: 设备参数表格组件 - 参数模板列表展示组件
* 功能:参数列表展示、操作按钮、参数类型显示、取值范围显示
* 路径:/ai-crop-model/data-sense-center/device-parameter/components
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Eye, Edit, Trash2 } from 'lucide-react';
import { DeviceParameterState } from './deviceParameterReducer';
interface DeviceParameterTableProps {
state: DeviceParameterState;
onEdit: (param: any) => void;
onView: (param: any) => void;
onDelete: (paramKey: string) => void;
}
export function DeviceParameterTable({ state, onEdit, onView, onDelete }: DeviceParameterTableProps) {
const { selectedType } = state;
const getParamTypeLabel = (type: string) => {
const labels: Record<string, string> = {
'number': '数字',
'string': '文本',
'boolean': '布尔',
'select': '选择'
};
return labels[type] || type;
};
const getParamTypeColor = (type: string) => {
const colors: Record<string, string> = {
'number': 'border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300',
'string': 'border-green-200 dark:border-green-800 text-green-700 dark:text-green-300',
'boolean': 'border-purple-200 dark:border-purple-800 text-purple-700 dark:text-purple-300',
'select': 'border-orange-200 dark:border-orange-800 text-orange-700 dark:text-orange-300'
};
return colors[type] || 'border-gray-200 dark:border-gray-800 text-gray-700 dark:text-gray-300';
};
return (
<div className="p-4">
{selectedType ? (
<>
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-primary">{selectedType.name} - </h3>
<p className="text-sm text-muted-foreground mt-1">
{selectedType.description || '暂无描述'}
</p>
</div>
<Badge variant="outline" className="font-light">
{selectedType.parameterDefinitions?.length || 0}
</Badge>
</div>
{selectedType.parameterDefinitions && selectedType.parameterDefinitions.length > 0 ? (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[150px]"></TableHead>
<TableHead className="w-[150px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[120px]"></TableHead>
<TableHead className="w-[150px]">/</TableHead>
<TableHead className="min-w-[200px]"></TableHead>
<TableHead className="text-right w-[120px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{selectedType.parameterDefinitions.map((param) => (
<TableRow key={param.key} className="hover:bg-muted/30">
<TableCell className="font-mono text-sm font-medium">
{param.key}
</TableCell>
<TableCell>
<div className="font-medium">{param.label}</div>
</TableCell>
<TableCell>
<Badge
variant="outline"
className={`font-light ${getParamTypeColor(param.type)}`}
>
{getParamTypeLabel(param.type)}
</Badge>
</TableCell>
<TableCell>
{param.required ? (
<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>
) : (
<span className="text-muted-foreground text-sm font-light"></span>
)}
</TableCell>
<TableCell>
{param.type === 'boolean' ? (
<Badge
variant={param.defaultValue ? 'default' : 'secondary'}
className="font-light"
>
{param.defaultValue ? '是' : '否'}
</Badge>
) : param.type === 'select' && param.options ? (
<div className="text-sm">
<span className="font-medium">
{param.options.find(opt => opt.value === param.defaultValue)?.label || param.defaultValue}
</span>
<div className="text-xs text-muted-foreground">
({param.options.length} )
</div>
</div>
) : (
<span className="text-sm font-medium">
{param.defaultValue?.toString() || '-'}
</span>
)}
</TableCell>
<TableCell>
<div className="text-sm">
{param.unit && (
<span className="font-medium">{param.unit}</span>
)}
{param.type === 'number' && (param.min !== undefined || param.max !== undefined) && (
<div className="text-muted-foreground">
{param.min !== undefined && param.max !== undefined
? `(${param.min}~${param.max})`
: param.min !== undefined
? `(≥${param.min})`
: param.max !== undefined
? `(≤${param.max})`
: ''
}
</div>
)}
{param.type === 'select' && param.options && (
<div className="text-muted-foreground">
{param.options.length}
</div>
)}
</div>
</TableCell>
<TableCell>
<div className="max-w-xs">
<div
className="text-sm text-muted-foreground"
title={param.description}
>
{param.description || '-'}
</div>
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => onView(param)}
title="查看详情"
className="p-1 h-auto hover:bg-blue-50 dark:hover:bg-blue-950"
>
<Eye className="w-4 h-4 text-blue-600 dark:text-blue-400" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onEdit(param)}
title="编辑"
className="p-1 h-auto hover:bg-amber-50 dark:hover:bg-amber-950"
>
<Edit className="w-4 h-4 text-amber-600 dark:text-amber-400" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onDelete(param.key)}
title="删除"
className="p-1 h-auto hover:bg-red-50 dark:hover:bg-red-950"
>
<Trash2 className="w-4 h-4 text-red-600 dark:text-red-400" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="text-center py-12">
<div className="space-y-3">
<div className="text-lg font-medium text-muted-foreground">
</div>
<div className="text-sm text-muted-foreground">
"新增参数"
</div>
</div>
</div>
)}
</>
) : (
<div className="text-center py-12">
<div className="space-y-3">
<div className="text-lg font-medium text-muted-foreground">
</div>
<div className="text-sm text-muted-foreground">
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,71 @@
/**
* filekorolheader: 设备类型选择器组件 - 设备类型选择列表组件
* 功能:设备类型列表展示、选中状态管理、参数数量显示
* 路径:/ai-crop-model/data-sense-center/device-parameter/components
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { DeviceParameterState, DeviceParameterAction } from './deviceParameterReducer';
interface DeviceTypeSelectorProps {
state: DeviceParameterState;
dispatch: React.Dispatch<DeviceParameterAction>;
}
export function DeviceTypeSelector({ state, dispatch }: DeviceTypeSelectorProps) {
const { deviceTypes, selectedType } = state;
const handleTypeSelect = (typeId: string) => {
const type = deviceTypes.find(t => t.id === typeId);
if (type) {
dispatch({ type: 'SET_SELECTED_TYPE', payload: type });
}
};
return (
<Card className="p-4 bg-card">
<h3 className="mb-4 text-primary"></h3>
{deviceTypes.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
<div className="space-y-2">
<div></div>
<div className="text-sm">"设备类型管理"</div>
</div>
</div>
) : (
<div className="space-y-2">
{deviceTypes.map(type => (
<div
key={type.id}
onClick={() => handleTypeSelect(type.id)}
className={`p-3 rounded-lg cursor-pointer transition-all ${
selectedType?.id === type.id
? 'bg-primary-muted border-2 border-primary'
: 'bg-muted hover:bg-accent border-2 border-transparent'
}`}
>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<h4 className="truncate font-medium">{type.name}</h4>
<p className="text-sm text-muted-foreground mt-1">
{type.manufacturer || '未知品牌'} {type.model ? `· ${type.model}` : ''}
</p>
{type.description && (
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">
{type.description}
</p>
)}
</div>
<Badge variant="outline" className="ml-2 font-light">
{type.parameterDefinitions?.length || 0}
</Badge>
</div>
</div>
))}
</div>
)}
</Card>
);
}

View File

@@ -0,0 +1,215 @@
/**
* filekorolheader: 查看参数对话框 - 参数详情展示组件
* 功能:展示参数详细信息、参数配置、选项列表
* 路径:/ai-crop-model/data-sense-center/device-parameter/components
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
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 { ParameterDefinition, DeviceType } from './deviceParameterReducer';
import { Eye, Settings, Hash } from 'lucide-react';
interface ViewParameterDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
viewingParam: ParameterDefinition | null;
selectedType: DeviceType | null;
}
export function ViewParameterDialog({ open, onOpenChange, viewingParam, selectedType }: ViewParameterDialogProps) {
const getParamTypeLabel = (type: string) => {
const labels: Record<string, string> = {
'number': '数字',
'string': '文本',
'boolean': '布尔',
'select': '选择'
};
return labels[type] || type;
};
const getParamTypeColor = (type: string) => {
const colors: Record<string, string> = {
'number': 'border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950',
'string': 'border-green-200 dark:border-green-800 text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950',
'boolean': 'border-purple-200 dark:border-purple-800 text-purple-700 dark:text-purple-300 bg-purple-50 dark:bg-purple-950',
'select': 'border-orange-200 dark:border-orange-800 text-orange-700 dark:text-orange-300 bg-orange-50 dark:bg-orange-950'
};
return colors[type] || 'border-gray-200 dark:border-gray-800 text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-950';
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-xl">
<Eye className="w-5 h-5" />
</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
{viewingParam && (
<div className="space-y-6">
{/* 参数基本信息 */}
<Card className="p-4">
<div className="flex items-center gap-2 mb-4">
<Hash className="w-5 h-5 text-primary" />
<h3 className="text-lg font-medium"></h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="font-mono text-sm font-medium bg-muted p-2 rounded">
{viewingParam.key}
</div>
</div>
<div>
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="font-medium text-base">{viewingParam.label}</div>
</div>
<div>
<div className="text-sm text-muted-foreground mb-1"></div>
<Badge
variant="outline"
className={`font-light ${getParamTypeColor(viewingParam.type)}`}
>
{getParamTypeLabel(viewingParam.type)}
</Badge>
</div>
<div>
<div className="text-sm text-muted-foreground mb-1"></div>
{viewingParam.required ? (
<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>
) : (
<span className="text-muted-foreground text-sm font-light"></span>
)}
</div>
<div className="md:col-span-2">
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="text-sm bg-muted/30 p-3 rounded">
{viewingParam.description || '暂无描述'}
</div>
</div>
</div>
</Card>
{/* 参数配置 */}
<Card className="p-4">
<div className="flex items-center gap-2 mb-4">
<Settings className="w-5 h-5 text-primary" />
<h3 className="text-lg font-medium"></h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="bg-muted/30 p-3 rounded">
{viewingParam.type === 'boolean' ? (
<Badge
variant={viewingParam.defaultValue ? 'default' : 'secondary'}
className="font-light"
>
{viewingParam.defaultValue ? '是' : '否'}
</Badge>
) : viewingParam.type === 'select' && viewingParam.options ? (
<div>
<span className="font-medium">
{viewingParam.options.find(opt => opt.value === viewingParam.defaultValue)?.label || viewingParam.defaultValue}
</span>
<div className="text-xs text-muted-foreground mt-1">
: {viewingParam.defaultValue}
</div>
</div>
) : (
<span className="font-medium">
{viewingParam.defaultValue?.toString() || '-'}
</span>
)}
</div>
</div>
{viewingParam.unit && (
<div>
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="bg-muted/30 p-3 rounded font-medium">
{viewingParam.unit}
</div>
</div>
)}
{viewingParam.type === 'number' && (viewingParam.min !== undefined || viewingParam.max !== undefined) && (
<div className="md:col-span-2">
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="bg-muted/30 p-3 rounded font-medium">
{viewingParam.min !== undefined && viewingParam.max !== undefined
? `${viewingParam.min} ~ ${viewingParam.max}`
: viewingParam.min !== undefined
? `${viewingParam.min}`
: viewingParam.max !== undefined
? `${viewingParam.max}`
: '-'}
{viewingParam.unit && ` ${viewingParam.unit}`}
</div>
</div>
)}
</div>
</Card>
{/* 选择类型选项 */}
{viewingParam.type === 'select' && viewingParam.options && (
<Card className="p-4">
<div className="flex items-center gap-2 mb-4">
<Settings className="w-5 h-5 text-primary" />
<h3 className="text-lg font-medium"></h3>
<Badge variant="outline" className="font-light">
{viewingParam.options.length}
</Badge>
</div>
<div className="space-y-2">
{viewingParam.options.map((option, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-muted/30 rounded">
<div>
<span className="font-medium">{option.label}</span>
<span className="text-muted-foreground text-sm ml-2">
({option.value})
</span>
{viewingParam.defaultValue === option.value && (
<Badge variant="default" className="ml-2 text-xs">
</Badge>
)}
</div>
</div>
))}
</div>
</Card>
)}
{/* 设备信息 */}
<Card className="p-4">
<div className="text-sm text-muted-foreground mb-2"></div>
<div className="font-medium">{selectedType?.name}</div>
{selectedType?.manufacturer && (
<div className="text-sm text-muted-foreground mt-1">
{selectedType.manufacturer} {selectedType.model ? `· ${selectedType.model}` : ''}
</div>
)}
</Card>
</div>
)}
<DialogFooter>
<Button onClick={() => onOpenChange(false)}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,502 @@
/**
* filekorolheader: 设备参数管理状态管理器 - 参数模板配置中心
* 功能:设备类型数据管理、参数模板配置、对话框状态控制、操作处理
* 路径:/ai-crop-model/data-sense-center/device-parameter/components
* 规范遵循crop-x/docs/开发项目规范.mduseReducer状态管理模式
*/
import { toast } from 'sonner';
// 参数定义接口
export interface ParameterDefinition {
key: string;
label: string;
type: 'string' | 'number' | 'boolean' | 'select';
required?: boolean;
defaultValue?: any;
options?: { label: string; value: string }[];
unit?: string;
min?: number;
max?: number;
description?: string;
}
// 设备类型接口
export interface DeviceType {
id: string;
name: string;
manufacturer?: string;
model?: string;
description?: string;
parameterDefinitions: ParameterDefinition[];
createdAt: string;
updatedAt: string;
}
// 状态接口
export interface DeviceParameterState {
deviceTypes: DeviceType[];
selectedType: DeviceType | null;
loading: boolean;
error: string | null;
// 对话框状态
showAddParamDialog: boolean;
showViewParamDialog: boolean;
showDeleteConfirm: boolean;
// 编辑/查看数据
editingParam: ParameterDefinition | null;
viewingParam: ParameterDefinition | null;
pendingDeleteParam: string | null;
}
// Action类型定义
export type DeviceParameterAction =
| { type: 'LOAD_DATA' }
| { type: 'SET_DEVICE_TYPES'; payload: DeviceType[] }
| { type: 'SET_SELECTED_TYPE'; payload: DeviceType | null }
| { type: 'SHOW_ADD_PARAM_DIALOG' }
| { type: 'SET_ADD_PARAM_DIALOG'; payload: boolean }
| { type: 'SHOW_EDIT_PARAM_DIALOG'; payload: ParameterDefinition }
| { type: 'SHOW_VIEW_PARAM_DIALOG'; payload: ParameterDefinition }
| { type: 'SET_VIEW_PARAM_DIALOG'; payload: boolean }
| { type: 'SHOW_DELETE_CONFIRM'; payload: string }
| { type: 'SET_DELETE_CONFIRM'; payload: boolean }
| { type: 'ADD_PARAMETER'; payload: ParameterDefinition }
| { type: 'UPDATE_PARAMETER'; payload: ParameterDefinition }
| { type: 'DELETE_PARAMETER'; payload: string }
| { type: 'CLEAR_EDITING_PARAM' };
// 初始状态
export const initialState: DeviceParameterState = {
deviceTypes: [],
selectedType: null,
loading: false,
error: null,
showAddParamDialog: false,
showViewParamDialog: false,
showDeleteConfirm: false,
editingParam: null,
viewingParam: null,
pendingDeleteParam: null,
};
// Reducer函数
export function deviceParameterReducer(state: DeviceParameterState, action: DeviceParameterAction): DeviceParameterState {
switch (action.type) {
case 'LOAD_DATA':
return loadData(state);
case 'SET_DEVICE_TYPES':
return { ...state, deviceTypes: action.payload };
case 'SET_SELECTED_TYPE':
return { ...state, selectedType: action.payload };
case 'SHOW_ADD_PARAM_DIALOG':
return {
...state,
showAddParamDialog: true,
editingParam: null
};
case 'SET_ADD_PARAM_DIALOG':
return {
...state,
showAddParamDialog: action.payload,
editingParam: action.payload ? null : state.editingParam
};
case 'SHOW_EDIT_PARAM_DIALOG':
return {
...state,
showAddParamDialog: true,
editingParam: action.payload
};
case 'SHOW_VIEW_PARAM_DIALOG':
return {
...state,
showViewParamDialog: true,
viewingParam: action.payload
};
case 'SET_VIEW_PARAM_DIALOG':
return {
...state,
showViewParamDialog: action.payload,
viewingParam: action.payload ? null : state.viewingParam
};
case 'SHOW_DELETE_CONFIRM':
return {
...state,
showDeleteConfirm: true,
pendingDeleteParam: action.payload
};
case 'SET_DELETE_CONFIRM':
return {
...state,
showDeleteConfirm: action.payload,
pendingDeleteParam: action.payload ? null : state.pendingDeleteParam
};
case 'ADD_PARAMETER':
return addParameter(state, action.payload);
case 'UPDATE_PARAMETER':
return updateParameter(state, action.payload);
case 'DELETE_PARAMETER':
return deleteParameter(state, action.payload);
case 'CLEAR_EDITING_PARAM':
return { ...state, editingParam: null };
default:
return state;
}
}
// 加载数据
function loadData(state: DeviceParameterState): DeviceParameterState {
try {
const data = localStorage.getItem('smart_agriculture_ai_device_types');
if (data) {
const types = JSON.parse(data);
const normalizedTypes = types.map((type: any) => ({
...type,
parameterDefinitions: type.parameterDefinitions || []
}));
return {
...state,
deviceTypes: normalizedTypes,
selectedType: normalizedTypes.length > 0 ? normalizedTypes[0] : null
};
} else {
// 创建模拟数据
const mockTypes: DeviceType[] = [
{
id: 'device-type-1',
name: '土壤传感器',
manufacturer: '施耐德',
model: 'SOIL-100',
description: '高精度土壤温湿度、养分监测传感器',
parameterDefinitions: [
{
key: 'measurementInterval',
label: '测量间隔',
type: 'number',
unit: '分钟',
required: true,
defaultValue: 30,
min: 5,
max: 120,
description: '数据测量时间间隔'
},
{
key: 'uploadMode',
label: '上传模式',
type: 'select',
options: [
{ label: '实时上传', value: 'realtime' },
{ label: '定时上传', value: 'scheduled' }
],
defaultValue: 'scheduled',
description: '数据上传方式'
},
{
key: 'depth',
label: '测量深度',
type: 'number',
unit: 'cm',
required: true,
defaultValue: 10,
min: 5,
max: 100,
description: '传感器在土壤中的测量深度'
}
],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: 'device-type-2',
name: '气象站',
manufacturer: '华为',
model: 'WEATHER-200',
description: '多参数气象监测站,支持温度、湿度、风速、降雨量等监测',
parameterDefinitions: [
{
key: 'sampleRate',
label: '采样频率',
type: 'number',
unit: '次/小时',
required: true,
defaultValue: 12,
min: 1,
max: 60,
description: '每小时采集次数'
},
{
key: 'windSpeedUnit',
label: '风速单位',
type: 'select',
options: [
{ label: '米/秒', value: 'm/s' },
{ label: '公里/小时', value: 'km/h' }
],
defaultValue: 'm/s'
},
{
key: 'autoCalibration',
label: '自动校准',
type: 'boolean',
defaultValue: true,
description: '是否启用设备自动校准功能'
}
],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: 'device-type-3',
name: '虫情监测仪',
manufacturer: '托普云农',
model: 'PEST-300',
description: 'AI智能虫情监测仪自动拍照识别害虫种类和数量',
parameterDefinitions: [
{
key: 'photoInterval',
label: '拍照间隔',
type: 'number',
unit: '小时',
required: true,
defaultValue: 2,
min: 1,
max: 24,
description: '自动拍照时间间隔'
},
{
key: 'aiRecognition',
label: 'AI识别',
type: 'boolean',
defaultValue: true,
description: '是否启用AI自动识别'
},
{
key: 'detectionSensitivity',
label: '检测灵敏度',
type: 'select',
options: [
{ label: '高灵敏度', value: 'high' },
{ label: '中灵敏度', value: 'medium' },
{ label: '低灵敏度', value: 'low' }
],
defaultValue: 'medium',
description: '害虫检测的灵敏度设置'
}
],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: 'device-type-4',
name: '水质监测仪',
manufacturer: '海康威视',
model: 'WATER-400',
description: '多参数水质在线监测仪支持pH、溶解氧、浊度等监测',
parameterDefinitions: [
{
key: 'measurementCycle',
label: '测量周期',
type: 'number',
unit: '分钟',
required: true,
defaultValue: 15,
min: 5,
max: 60
},
{
key: 'alarmEnabled',
label: '启用预警',
type: 'boolean',
defaultValue: true
},
{
key: 'phThreshold',
label: 'pH阈值',
type: 'number',
unit: 'pH',
defaultValue: 7.0,
min: 6.0,
max: 8.5,
description: 'pH值异常预警阈值'
}
],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: 'device-type-5',
name: '灌溉控制器',
manufacturer: '大疆',
model: 'IRRI-500',
description: '智能灌溉控制器,支持远程控制和定时灌溉',
parameterDefinitions: [
{
key: 'controlMode',
label: '控制模式',
type: 'select',
options: [
{ label: '手动控制', value: 'manual' },
{ label: '自动控制', value: 'auto' },
{ label: '定时控制', value: 'scheduled' }
],
required: true,
defaultValue: 'auto'
},
{
key: 'flowRateLimit',
label: '流量限制',
type: 'number',
unit: 'L/min',
defaultValue: 100,
min: 10,
max: 500
},
{
key: 'moistureThreshold',
label: '湿度阈值',
type: 'number',
unit: '%',
defaultValue: 30,
min: 10,
max: 80,
description: '土壤湿度触发灌溉的阈值'
}
],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
localStorage.setItem('smart_agriculture_ai_device_types', JSON.stringify(mockTypes));
localStorage.setItem('smart_agriculture_device_types', JSON.stringify(mockTypes));
return {
...state,
deviceTypes: mockTypes,
selectedType: mockTypes[0]
};
}
} catch (error) {
console.error('加载数据失败:', error);
toast.error('加载数据失败');
return state;
}
}
// 添加参数
function addParameter(state: DeviceParameterState, newParam: ParameterDefinition): DeviceParameterState {
try {
if (!state.selectedType) return state;
const updatedParams = [...state.selectedType.parameterDefinitions, newParam];
const updatedType: DeviceType = {
...state.selectedType,
parameterDefinitions: updatedParams,
updatedAt: new Date().toISOString()
};
const updatedTypes = state.deviceTypes.map(t =>
t.id === state.selectedType?.id ? updatedType : t
);
localStorage.setItem('smart_agriculture_ai_device_types', JSON.stringify(updatedTypes));
localStorage.setItem('smart_agriculture_device_types', JSON.stringify(updatedTypes));
toast.success('参数添加成功');
return {
...state,
deviceTypes: updatedTypes,
selectedType: updatedType,
showAddParamDialog: false,
editingParam: null
};
} catch (error) {
console.error('添加失败:', error);
toast.error('添加失败');
return state;
}
}
// 更新参数
function updateParameter(state: DeviceParameterState, updatedParam: ParameterDefinition): DeviceParameterState {
try {
if (!state.selectedType) return state;
const updatedParams = state.selectedType.parameterDefinitions.map(p =>
p.key === updatedParam.key ? updatedParam : p
);
const updatedType: DeviceType = {
...state.selectedType,
parameterDefinitions: updatedParams,
updatedAt: new Date().toISOString()
};
const updatedTypes = state.deviceTypes.map(t =>
t.id === state.selectedType?.id ? updatedType : t
);
localStorage.setItem('smart_agriculture_ai_device_types', JSON.stringify(updatedTypes));
localStorage.setItem('smart_agriculture_device_types', JSON.stringify(updatedTypes));
toast.success('参数更新成功');
return {
...state,
deviceTypes: updatedTypes,
selectedType: updatedType,
showAddParamDialog: false,
editingParam: null
};
} catch (error) {
console.error('更新失败:', error);
toast.error('更新失败');
return state;
}
}
// 删除参数
function deleteParameter(state: DeviceParameterState, paramKey: string): DeviceParameterState {
try {
if (!state.selectedType) return state;
const updatedParams = state.selectedType.parameterDefinitions.filter(p => p.key !== paramKey);
const updatedType: DeviceType = {
...state.selectedType,
parameterDefinitions: updatedParams,
updatedAt: new Date().toISOString()
};
const updatedTypes = state.deviceTypes.map(t =>
t.id === state.selectedType?.id ? updatedType : t
);
localStorage.setItem('smart_agriculture_ai_device_types', JSON.stringify(updatedTypes));
localStorage.setItem('smart_agriculture_device_types', JSON.stringify(updatedTypes));
toast.success('参数删除成功');
return {
...state,
deviceTypes: updatedTypes,
selectedType: updatedType,
showDeleteConfirm: false,
pendingDeleteParam: null
};
} catch (error) {
console.error('删除失败:', error);
toast.error('删除失败');
return state;
}
}

View File

@@ -0,0 +1,116 @@
/**
* filekorolheader: 设备参数管理页面 - IoT设备参数模板配置中心
* 功能:设备类型选择、参数模板配置、参数对比分析、报告生成
* 路径:/ai-crop-model/data-sense-center/device-parameter
* 规范遵循crop-x/docs/开发项目规范.md使用useReducer状态管理shadcn语义化样式
*/
'use client';
import { useReducer, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Plus, Edit, Trash2, Eye } from 'lucide-react';
import { DeviceParameterStats } from './components/DeviceParameterStats';
import { DeviceParameterTable } from './components/DeviceParameterTable';
import { DeviceTypeSelector } from './components/DeviceTypeSelector';
import { deviceParameterReducer, initialState, DeviceParameterState } from './components/deviceParameterReducer';
import { AddParameterDialog } from './components/AddParameterDialog';
import { ViewParameterDialog } from './components/ViewParameterDialog';
import { DeleteParameterConfirmDialog } from './components/DeleteParameterConfirmDialog';
import { toast } from 'sonner';
export default function DeviceParameterPage() {
const [state, dispatch] = useReducer(deviceParameterReducer, initialState);
useEffect(() => {
dispatch({ type: 'LOAD_DATA' });
}, []);
const handleAddParam = () => {
if (!state.selectedType) {
toast.error('请先选择一个设备类型');
return;
}
dispatch({ type: 'SHOW_ADD_PARAM_DIALOG' });
};
const handleEditParam = (param: any) => {
dispatch({ type: 'SHOW_EDIT_PARAM_DIALOG', payload: param });
};
const handleViewParam = (param: any) => {
dispatch({ type: 'SHOW_VIEW_PARAM_DIALOG', payload: param });
};
const handleDeleteParam = (paramKey: string) => {
dispatch({ type: 'SHOW_DELETE_CONFIRM', payload: paramKey });
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-primary"></h2>
<p className="text-muted-foreground"></p>
</div>
<Button onClick={handleAddParam}>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 统计卡片 */}
<DeviceParameterStats state={state} />
<div className="grid grid-cols-12 gap-6">
{/* 左侧:设备类型选择器 */}
<div className="col-span-4">
<DeviceTypeSelector
state={state}
dispatch={dispatch}
/>
</div>
{/* 右侧:参数列表 */}
<div className="col-span-8">
<Card className="bg-card">
<DeviceParameterTable
state={state}
onEdit={handleEditParam}
onView={handleViewParam}
onDelete={handleDeleteParam}
/>
</Card>
</div>
</div>
{/* 添加/编辑参数对话框 */}
<AddParameterDialog
open={state.showAddParamDialog}
onOpenChange={(open) => dispatch({ type: 'SET_ADD_PARAM_DIALOG', payload: open })}
editingParam={state.editingParam}
selectedType={state.selectedType}
dispatch={dispatch}
/>
{/* 查看参数对话框 */}
<ViewParameterDialog
open={state.showViewParamDialog}
onOpenChange={(open) => dispatch({ type: 'SET_VIEW_PARAM_DIALOG', payload: open })}
viewingParam={state.viewingParam}
selectedType={state.selectedType}
/>
{/* 删除确认对话框 */}
<DeleteParameterConfirmDialog
open={state.showDeleteConfirm}
onOpenChange={(open) => dispatch({ type: 'SET_DELETE_CONFIRM', payload: open })}
pendingDeleteParam={state.pendingDeleteParam}
dispatch={dispatch}
/>
</div>
);
}

View File

@@ -0,0 +1,133 @@
/**
* filekorolheader: 添加/编辑设备类型对话框 - 设备类型信息录入组件
* 功能:新增设备类型、编辑设备类型信息、表单验证
* 路径:/ai-crop-model/data-sense-center/device-type/components
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
import React from 'react';
import { useForm } from 'react-hook-form';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { DeviceType, DeviceTypeAction } from './deviceTypeReducer';
interface AddDeviceTypeDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
editingType: DeviceType | null;
dispatch: React.Dispatch<DeviceTypeAction>;
}
export function AddDeviceTypeDialog({ open, onOpenChange, editingType, dispatch }: AddDeviceTypeDialogProps) {
const { register, handleSubmit, setValue, reset, formState: { errors } } = useForm();
// 当编辑类型变化时,填充表单
React.useEffect(() => {
if (editingType) {
setValue('name', editingType.name);
setValue('manufacturer', editingType.manufacturer || '');
setValue('model', editingType.model || '');
setValue('description', editingType.description || '');
} else {
reset();
}
}, [editingType, setValue, reset]);
const onSubmit = (data: any) => {
if (editingType) {
const updated: DeviceType = {
...editingType,
...data,
updatedAt: new Date().toISOString(),
};
dispatch({ type: 'UPDATE_DEVICE_TYPE', payload: updated });
} else {
const newType: DeviceType = {
id: `device-type-${Date.now()}`,
...data,
parameterDefinitions: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
dispatch({ type: 'ADD_DEVICE_TYPE', payload: newType });
}
reset();
};
const handleOpenChange = (open: boolean) => {
if (!open) {
reset();
}
onOpenChange(open);
};
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{editingType ? '编辑设备类型' : '新增设备类型'}</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name"> *</Label>
<Input
id="name"
{...register('name', { required: '请输入类型名称' })}
placeholder="例如:土壤传感器"
/>
{errors.name && (
<p className="text-sm text-destructive">{errors.name.message as string}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="manufacturer"></Label>
<Input
id="manufacturer"
{...register('manufacturer')}
placeholder="例如:施耐德"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="model"></Label>
<Input
id="model"
{...register('model')}
placeholder="例如SOIL-100"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="description"></Label>
<Textarea
id="description"
{...register('description')}
placeholder="设备类型的详细描述..."
rows={3}
/>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => handleOpenChange(false)}>
</Button>
<Button type="submit">
{editingType ? '保存' : '添加'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,41 @@
/**
* filekorolheader: 删除确认对话框 - 删除操作确认组件
* 功能:确认删除设备类型、防止误操作、影响提示
* 路径:/ai-crop-model/data-sense-center/device-type/components
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
import { DeviceTypeAction } from './deviceTypeReducer';
interface DeleteConfirmDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
pendingDeleteId: string | null;
dispatch: React.Dispatch<DeviceTypeAction>;
}
export function DeleteConfirmDialog({ open, onOpenChange, pendingDeleteId, dispatch }: DeleteConfirmDialogProps) {
const confirmDelete = () => {
if (pendingDeleteId) {
dispatch({ type: 'DELETE_DEVICE_TYPE', payload: pendingDeleteId });
}
};
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => onOpenChange(false)}></AlertDialogCancel>
<AlertDialogAction onClick={confirmDelete}></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View File

@@ -0,0 +1,56 @@
/**
* filekorolheader: 设备类型统计组件 - 统计卡片展示组件
* 功能:显示设备类型统计数据、参数配置情况、品牌型号覆盖率、分类统计
* 路径:/ai-crop-model/data-sense-center/device-type/components
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { DeviceTypeState } from './deviceTypeReducer';
interface DeviceTypeStatsProps {
state: DeviceTypeState;
}
export function DeviceTypeStats({ state }: DeviceTypeStatsProps) {
const totalParams = state.deviceTypes.reduce((sum, type) => sum + (type.parameterDefinitions?.length || 0), 0);
const typesWithParams = state.deviceTypes.filter(type => (type.parameterDefinitions?.length || 0) > 0).length;
const typesWithManufacturer = state.deviceTypes.filter(t => t.manufacturer).length;
const typesWithModel = state.deviceTypes.filter(t => t.model).length;
return (
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<Card className="p-4 bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800">
<div className="text-sm text-muted-foreground"></div>
<div className="mt-1 text-2xl font-semibold text-green-600 dark:text-green-400">
{state.deviceTypes.length}
</div>
</Card>
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
<div className="text-sm text-muted-foreground"></div>
<div className="mt-1 text-2xl font-semibold text-blue-600 dark:text-blue-400">
{totalParams}
</div>
</Card>
<Card className="p-4 bg-orange-50 dark:bg-orange-950 border-orange-200 dark:border-orange-800">
<div className="text-sm text-muted-foreground"></div>
<div className="mt-1 text-2xl font-semibold text-orange-600 dark:text-orange-400">
{typesWithParams} / {state.deviceTypes.length}
</div>
</Card>
<Card className="p-4 bg-purple-50 dark:bg-purple-950 border-purple-200 dark:border-purple-800">
<div className="text-sm text-muted-foreground"></div>
<div className="mt-1 text-2xl font-semibold text-purple-600 dark:text-purple-400">
{typesWithManufacturer}
</div>
</Card>
<Card className="p-4 bg-pink-50 dark:bg-pink-950 border-pink-200 dark:border-pink-800">
<div className="text-sm text-muted-foreground"></div>
<div className="mt-1 text-2xl font-semibold text-pink-600 dark:text-pink-400">
{typesWithModel}
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,125 @@
/**
* filekorolheader: 设备类型表格组件 - 数据列表展示组件
* 功能:设备类型列表展示、操作按钮、参数模板链接、分类标签
* 路径:/ai-crop-model/data-sense-center/device-type/components
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Eye, Edit, Trash2, Settings } from 'lucide-react';
import { DeviceTypeState, DeviceType } from './deviceTypeReducer';
interface DeviceTypeTableProps {
state: DeviceTypeState;
onEdit: (deviceType: DeviceType) => void;
onView: (deviceType: DeviceType) => void;
onViewParams: (deviceType: DeviceType) => void;
onDelete: (id: string) => void;
}
export function DeviceTypeTable({ state, onEdit, onView, onViewParams, onDelete }: DeviceTypeTableProps) {
const getProtocolColor = (protocol?: string) => {
const colors: Record<string, string> = {
'LoRaWAN': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
'MQTT': 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
'Modbus RTU': 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
'HTTP': 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
};
return colors[protocol || ''] || 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
};
const getCategoryColor = (category?: string) => {
const colors: Record<string, string> = {
'环境监测': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
'气象监测': 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
'灌溉控制': 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
'土壤监测': 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200',
};
return colors[category || ''] || 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
};
return (
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{state.deviceTypes.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
</TableCell>
</TableRow>
) : (
state.deviceTypes.map(type => (
<TableRow key={type.id}>
<TableCell className="font-medium">{type.name}</TableCell>
<TableCell>{type.manufacturer || '-'}</TableCell>
<TableCell>{type.model || '-'}</TableCell>
<TableCell className="max-w-md">
<div className="truncate">
{type.description || '-'}
</div>
</TableCell>
<TableCell>
{(type.parameterDefinitions?.length || 0) > 0 ? (
<Button
variant="ghost"
size="sm"
onClick={() => onViewParams(type)}
className="text-primary hover:text-primary/80 p-1 h-auto"
>
<Eye className="w-4 h-4 mr-1" />
<span className="text-sm">{type.parameterDefinitions.length} </span>
</Button>
) : (
<span className="text-muted-foreground text-sm"></span>
)}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => onView(type)}
title="查看"
className="p-1 h-auto"
>
<Eye className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onEdit(type)}
title="编辑"
className="p-1 h-auto"
>
<Edit className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onDelete(type.id)}
title="删除"
className="p-1 h-auto"
>
<Trash2 className="w-4 h-4 text-destructive" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
);
}

View File

@@ -0,0 +1,75 @@
/**
* filekorolheader: 查看设备类型详情对话框 - 设备类型详情展示组件
* 功能:展示设备类型完整信息、参数配置、技术规格
* 路径:/ai-crop-model/data-sense-center/device-type/components
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { DeviceType } from './deviceTypeReducer';
interface ViewDeviceTypeDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
viewingType: DeviceType | null;
}
export function ViewDeviceTypeDialog({ open, onOpenChange, viewingType }: ViewDeviceTypeDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
{viewingType && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-muted-foreground"></label>
<div className="field-value mt-1 p-2 bg-muted rounded">{viewingType.name}</div>
</div>
<div>
<label className="text-muted-foreground"></label>
<div className="field-value mt-1 p-2 bg-muted rounded">{viewingType.manufacturer || '-'}</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-muted-foreground"></label>
<div className="field-value mt-1 p-2 bg-muted rounded">{viewingType.model || '-'}</div>
</div>
<div>
<label className="text-muted-foreground"></label>
<div className="field-value mt-1 p-2 bg-muted rounded">
{viewingType.parameterDefinitions?.length || 0}
</div>
</div>
</div>
<div>
<label className="text-muted-foreground"></label>
<div className="field-value mt-1 p-2 bg-muted rounded">{viewingType.description || '-'}</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-muted-foreground"></label>
<div className="field-value mt-1 p-2 bg-muted rounded">
{new Date(viewingType.createdAt).toLocaleString('zh-CN')}
</div>
</div>
<div>
<label className="text-muted-foreground"></label>
<div className="field-value mt-1 p-2 bg-muted rounded">
{new Date(viewingType.updatedAt).toLocaleString('zh-CN')}
</div>
</div>
</div>
</div>
)}
<DialogFooter>
<Button onClick={() => onOpenChange(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,102 @@
/**
* filekorolheader: 查看参数模板对话框 - 参数配置详情展示组件
* 功能:展示设备类型参数定义、参数类型和配置详情
* 路径:/ai-crop-model/data-sense-center/device-type/components
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式
*/
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { DeviceType } from './deviceTypeReducer';
interface ViewParamsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
viewingParams: DeviceType | null;
}
export function ViewParamsDialog({ open, onOpenChange, viewingParams }: ViewParamsDialogProps) {
const getParamTypeLabel = (type: string) => {
const labels: Record<string, string> = {
'number': '数字',
'string': '文本',
'boolean': '布尔',
'select': '选择'
};
return labels[type] || type;
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle> - {viewingParams?.name}</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
{viewingParams && viewingParams.parameterDefinitions.length > 0 ? (
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{viewingParams.parameterDefinitions.map((param, index) => (
<TableRow key={index}>
<TableCell className="font-mono text-sm">{param.key}</TableCell>
<TableCell>{param.label}</TableCell>
<TableCell>
<Badge variant="outline">{getParamTypeLabel(param.type)}</Badge>
</TableCell>
<TableCell>
{param.required ? (
<Badge className="bg-red-50 dark:bg-red-950 text-red-600 dark:text-red-400 border-red-200 dark:border-red-800"></Badge>
) : (
<span className="text-muted-foreground"></span>
)}
</TableCell>
<TableCell>
{param.type === 'boolean'
? (param.defaultValue ? '是' : '否')
: (param.defaultValue?.toString() || '-')
}
</TableCell>
<TableCell>{param.unit || '-'}</TableCell>
<TableCell>
{param.min !== undefined && param.max !== undefined
? `${param.min} ~ ${param.max}`
: '-'
}
</TableCell>
<TableCell className="max-w-xs">
<div className="truncate" title={param.description}>
{param.description || '-'}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<div className="text-center text-muted-foreground py-8">
</div>
)}
<DialogFooter>
<Button onClick={() => onOpenChange(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,411 @@
/**
* filekorolheader: 设备类型状态管理 - 集中化状态管理核心
* 功能:设备类型数据管理、弹窗状态控制、筛选条件管理
* 路径:/ai-crop-model/data-sense-center/device-type/components
* 规范遵循crop-x/docs/开发项目规范.md使用useReducer模式
*/
import { toast } from 'sonner';
// 参数定义接口
export interface ParameterDefinition {
key: string;
label: string;
type: 'string' | 'number' | 'boolean' | 'select';
required?: boolean;
defaultValue?: any;
options?: { label: string; value: any }[];
unit?: string;
min?: number;
max?: number;
description?: string;
}
// 设备类型接口
export interface DeviceType {
id: string;
name: string;
manufacturer?: string;
model?: string;
category?: string;
description?: string;
protocol?: string;
connectivity?: string;
powerSupply?: string;
operatingTemperature?: string;
protectionRating?: string;
parameterDefinitions: ParameterDefinition[];
createdAt: string;
updatedAt: string;
}
// 状态接口
export interface DeviceTypeState {
deviceTypes: DeviceType[];
loading: boolean;
error: string | null;
// 对话框状态
showAddDialog: boolean;
showEditDialog: boolean;
showViewDialog: boolean;
showParamsDialog: boolean;
showDeleteDialog: boolean;
// 编辑/查看数据
editingType: DeviceType | null;
viewingType: DeviceType | null;
viewingParams: DeviceType | null;
pendingDeleteId: string | null;
}
// Action类型定义
export type DeviceTypeAction =
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string | null }
| { type: 'SET_DEVICE_TYPES'; payload: DeviceType[] }
| { type: 'ADD_DEVICE_TYPE'; payload: DeviceType }
| { type: 'UPDATE_DEVICE_TYPE'; payload: DeviceType }
| { type: 'DELETE_DEVICE_TYPE'; payload: string }
| { type: 'SHOW_ADD_DIALOG' }
| { type: 'SHOW_EDIT_DIALOG'; payload: DeviceType }
| { type: 'SHOW_VIEW_DIALOG'; payload: DeviceType }
| { type: 'SHOW_PARAMS_DIALOG'; payload: DeviceType }
| { type: 'SHOW_DELETE_DIALOG'; payload: string }
| { type: 'SET_ADD_DIALOG'; payload: boolean }
| { type: 'SET_EDIT_DIALOG'; payload: boolean }
| { type: 'SET_VIEW_DIALOG'; payload: boolean }
| { type: 'SET_PARAMS_DIALOG'; payload: boolean }
| { type: 'SET_DELETE_DIALOG'; payload: boolean }
| { type: 'LOAD_DATA' };
// 初始状态
export const initialState: DeviceTypeState = {
deviceTypes: [],
loading: false,
error: null,
showAddDialog: false,
showEditDialog: false,
showViewDialog: false,
showParamsDialog: false,
showDeleteDialog: false,
editingType: null,
viewingType: null,
viewingParams: null,
pendingDeleteId: null,
};
// Reducer函数
export function deviceTypeReducer(state: DeviceTypeState, action: DeviceTypeAction): DeviceTypeState {
switch (action.type) {
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload, loading: false };
case 'SET_DEVICE_TYPES':
return { ...state, deviceTypes: action.payload, loading: false };
case 'ADD_DEVICE_TYPE':
return {
...state,
deviceTypes: [...state.deviceTypes, action.payload],
showAddDialog: false,
editingType: null,
};
case 'UPDATE_DEVICE_TYPE':
return {
...state,
deviceTypes: state.deviceTypes.map(type =>
type.id === action.payload.id ? action.payload : type
),
showEditDialog: false,
showAddDialog: false,
editingType: null,
};
case 'DELETE_DEVICE_TYPE':
return {
...state,
deviceTypes: state.deviceTypes.filter(type => type.id !== action.payload),
showDeleteDialog: false,
pendingDeleteId: null,
};
case 'SHOW_ADD_DIALOG':
return {
...state,
showAddDialog: true,
editingType: null,
};
case 'SHOW_EDIT_DIALOG':
return {
...state,
showEditDialog: true,
editingType: action.payload,
};
case 'SHOW_VIEW_DIALOG':
return {
...state,
showViewDialog: true,
viewingType: action.payload,
};
case 'SHOW_PARAMS_DIALOG':
return {
...state,
showParamsDialog: true,
viewingParams: action.payload,
};
case 'SHOW_DELETE_DIALOG':
return {
...state,
showDeleteDialog: true,
pendingDeleteId: action.payload,
};
case 'SET_ADD_DIALOG':
return {
...state,
showAddDialog: action.payload,
editingType: action.payload ? null : state.editingType,
};
case 'SET_EDIT_DIALOG':
return {
...state,
showEditDialog: action.payload,
editingType: action.payload ? null : state.editingType,
};
case 'SET_VIEW_DIALOG':
return {
...state,
showViewDialog: action.payload,
viewingType: action.payload ? null : state.viewingType,
};
case 'SET_PARAMS_DIALOG':
return {
...state,
showParamsDialog: action.payload,
viewingParams: action.payload ? null : state.viewingParams,
};
case 'SET_DELETE_DIALOG':
return {
...state,
showDeleteDialog: action.payload,
pendingDeleteId: action.payload ? null : state.pendingDeleteId,
};
case 'LOAD_DATA':
return loadData(state);
default:
return state;
}
}
// 加载数据
function loadData(state: DeviceTypeState): DeviceTypeState {
const savedData = localStorage.getItem('smart_agriculture_ai_device_types');
if (savedData) {
try {
const deviceTypes = JSON.parse(savedData);
return {
...state,
deviceTypes,
loading: false,
};
} catch (error) {
console.error('Failed to load device types:', error);
// 如果加载失败,初始化测试数据
return {
...state,
deviceTypes: initializeTestData(),
loading: false,
};
}
} else {
// 初始化测试数据
return {
...state,
deviceTypes: initializeTestData(),
loading: false,
};
}
}
// 初始化测试数据
function initializeTestData(): DeviceType[] {
const testDeviceTypes: DeviceType[] = [
{
id: '1',
name: '智能土壤监测传感器',
manufacturer: 'GreenTech',
model: 'GT-SS-100',
category: '环境监测',
description: '用于实时监测土壤温度、湿度、pH值等环境参数的多功能传感器',
protocol: 'LoRaWAN',
connectivity: '无线',
powerSupply: '太阳能+电池',
operatingTemperature: '-30°C ~ 70°C',
protectionRating: 'IP67',
parameterDefinitions: [
{
key: 'temperature',
label: '土壤温度',
type: 'number',
required: true,
unit: '°C',
min: -30,
max: 70,
description: '土壤温度监测,用于判断作物生长环境',
},
{
key: 'humidity',
label: '土壤湿度',
type: 'number',
required: true,
unit: '%',
min: 0,
max: 100,
description: '土壤相对湿度百分比',
},
{
key: 'ph',
label: 'pH值',
type: 'number',
required: true,
unit: 'pH',
min: 0,
max: 14,
defaultValue: 7,
description: '土壤酸碱度,影响作物养分吸收',
},
],
createdAt: '2024-01-15T08:00:00Z',
updatedAt: '2024-01-15T08:00:00Z',
},
{
id: '2',
name: '智能气象站',
manufacturer: 'WeatherPro',
model: 'WP-WS-200',
category: '气象监测',
description: '综合气象监测设备,可监测温度、湿度、风速、风向、降雨量等',
protocol: 'MQTT',
connectivity: '4G/WiFi',
powerSupply: '市电+电池',
operatingTemperature: '-40°C ~ 85°C',
protectionRating: 'IP66',
parameterDefinitions: [
{
key: 'air_temp',
label: '空气温度',
type: 'number',
required: true,
unit: '°C',
min: -40,
max: 85,
description: '环境空气温度',
},
{
key: 'air_humidity',
label: '空气湿度',
type: 'number',
required: true,
unit: '%',
min: 0,
max: 100,
description: '相对空气湿度',
},
{
key: 'wind_speed',
label: '风速',
type: 'number',
required: false,
unit: 'm/s',
min: 0,
max: 50,
description: '风速监测',
},
{
key: 'rainfall',
label: '降雨量',
type: 'number',
required: false,
unit: 'mm',
min: 0,
max: 500,
defaultValue: 0,
description: '累计降雨量',
},
],
createdAt: '2024-01-16T09:30:00Z',
updatedAt: '2024-01-16T09:30:00Z',
},
{
id: '3',
name: '智能灌溉控制器',
manufacturer: 'IrrigationTech',
model: 'IT-IC-300',
category: '灌溉控制',
description: '自动化灌溉控制系统,支持定时灌溉、土壤湿度阈值控制等多种模式',
protocol: 'Modbus RTU',
connectivity: 'RS485',
powerSupply: 'DC 12V',
operatingTemperature: '-20°C ~ 60°C',
protectionRating: 'IP65',
parameterDefinitions: [
{
key: 'valve_status',
label: '阀门状态',
type: 'boolean',
required: true,
defaultValue: false,
description: '灌溉阀门开启/关闭状态',
},
{
key: 'flow_rate',
label: '流量',
type: 'number',
required: true,
unit: 'L/min',
min: 0,
max: 100,
defaultValue: 0,
description: '实时流量监测',
},
{
key: 'irrigation_mode',
label: '灌溉模式',
type: 'select',
required: true,
options: [
{ label: '手动控制', value: 'manual' },
{ label: '定时灌溉', value: 'scheduled' },
{ label: '湿度控制', value: 'humidity' },
{ label: '智能控制', value: 'smart' },
],
defaultValue: 'manual',
description: '灌溉控制模式选择',
},
],
createdAt: '2024-01-17T10:15:00Z',
updatedAt: '2024-01-17T10:15:00Z',
},
];
// 保存到 localStorage
localStorage.setItem('smart_agriculture_ai_device_types', JSON.stringify(testDeviceTypes));
return testDeviceTypes;
}

View File

@@ -0,0 +1,114 @@
/**
* filekorolheader: 设备类型管理页面 - IoT设备类型定义与参数管理中心
* 功能:设备类型列表管理、参数模板配置、设备类型统计
* 路径:/ai-crop-model/data-sense-center/device-type
* 规范遵循crop-x/docs/开发项目规范.md使用useReducer状态管理shadcn语义化样式
*/
'use client';
import { useReducer, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Plus } from 'lucide-react';
import { DeviceTypeTable } from './components/DeviceTypeTable';
import { DeviceTypeStats } from './components/DeviceTypeStats';
import { deviceTypeReducer, initialState, DeviceTypeState } from './components/deviceTypeReducer';
import { AddDeviceTypeDialog } from './components/AddDeviceTypeDialog';
import { ViewDeviceTypeDialog } from './components/ViewDeviceTypeDialog';
import { ViewParamsDialog } from './components/ViewParamsDialog';
import { DeleteConfirmDialog } from './components/DeleteConfirmDialog';
export default function DeviceTypePage() {
const [state, dispatch] = useReducer(deviceTypeReducer, initialState);
useEffect(() => {
dispatch({ type: 'LOAD_DATA' });
}, []);
const handleAdd = () => {
dispatch({ type: 'SHOW_ADD_DIALOG' });
};
const handleEdit = (deviceType: any) => {
dispatch({ type: 'SHOW_EDIT_DIALOG', payload: deviceType });
};
const handleView = (deviceType: any) => {
dispatch({ type: 'SHOW_VIEW_DIALOG', payload: deviceType });
};
const handleViewParams = (deviceType: any) => {
dispatch({ type: 'SHOW_PARAMS_DIALOG', payload: deviceType });
};
const handleDelete = (id: string) => {
dispatch({ type: 'SHOW_DELETE_DIALOG', payload: id });
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-primary"></h2>
<p className="text-muted-foreground"></p>
</div>
<Button onClick={handleAdd}>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 统计卡片 */}
<DeviceTypeStats state={state} />
{/* 设备类型列表 */}
<Card>
<DeviceTypeTable
state={state}
onEdit={handleEdit}
onView={handleView}
onViewParams={handleViewParams}
onDelete={handleDelete}
/>
</Card>
{/* 添加/编辑对话框 */}
<AddDeviceTypeDialog
open={state.showAddDialog || state.showEditDialog}
onOpenChange={(open) => {
if (open) {
dispatch({ type: 'SET_ADD_DIALOG', payload: true });
} else {
dispatch({ type: 'SET_ADD_DIALOG', payload: false });
dispatch({ type: 'SET_EDIT_DIALOG', payload: false });
}
}}
editingType={state.editingType}
dispatch={dispatch}
/>
{/* 查看详情对话框 */}
<ViewDeviceTypeDialog
open={state.showViewDialog}
onOpenChange={(open) => dispatch({ type: 'SET_VIEW_DIALOG', payload: open })}
viewingType={state.viewingType}
/>
{/* 查看参数模板对话框 */}
<ViewParamsDialog
open={state.showParamsDialog}
onOpenChange={(open) => dispatch({ type: 'SET_PARAMS_DIALOG', payload: open })}
viewingParams={state.viewingParams}
/>
{/* 删除确认对话框 */}
<DeleteConfirmDialog
open={state.showDeleteDialog}
onOpenChange={(open) => dispatch({ type: 'SET_DELETE_DIALOG', payload: open })}
pendingDeleteId={state.pendingDeleteId}
dispatch={dispatch}
/>
</div>
);
}

View File

@@ -0,0 +1,238 @@
'use client';
import { useState } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog';
import { ExternalDataState, ExternalDataAction } from './externalDataReducer';
import { DataSourceForm, DataSourceType, AccessMethod, accessMethods } from '../types';
import { Database, Plus, Upload, Code, Wifi } from 'lucide-react';
import { toast } from 'sonner';
interface AddDataSourceDialogProps {
state: ExternalDataState;
dispatch: React.Dispatch<ExternalDataAction>;
}
export function AddDataSourceDialog({ state, dispatch }: AddDataSourceDialogProps) {
const [formData, setFormData] = useState<DataSourceForm>({
name: '',
type: '气象数据',
provider: '',
accessMethod: 'API对接',
apiEndpoint: '',
updateFrequency: '',
description: '',
});
const resetForm = () => {
setFormData({
name: '',
type: '气象数据',
provider: '',
accessMethod: 'API对接',
apiEndpoint: '',
updateFrequency: '',
description: '',
});
};
const handleSubmit = () => {
if (!formData.name || !formData.provider || !formData.updateFrequency) {
toast.error('请填写必要字段');
return;
}
const newDataSource = {
id: `ext-${Date.now()}`,
...formData,
lastUpdateTime: new Date().toLocaleString('zh-CN'),
dataPoints: 0,
status: '待配置' as const,
dataFields: [],
};
dispatch({ type: 'ADD_DATA_SOURCE', payload: newDataSource });
dispatch({ type: 'SHOW_ADD_DIALOG', payload: false });
resetForm();
toast.success('数据源添加成功');
};
const handleClose = () => {
dispatch({ type: 'SHOW_ADD_DIALOG', payload: false });
resetForm();
};
const getAccessMethodIcon = (method: AccessMethod) => {
switch (method) {
case 'API对接':
return <Code className="w-4 h-4" />;
case 'FTP传输':
return <Upload className="w-4 h-4" />;
case 'WebSocket':
return <Wifi className="w-4 h-4" />;
case '手动上传':
return <Upload className="w-4 h-4" />;
default:
return <Database className="w-4 h-4" />;
}
};
return (
<Dialog open={state.showAddDialog} onOpenChange={handleClose}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Plus className="w-5 h-5" />
</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{/* 基本信息 */}
<Card className="p-4 bg-muted/20">
<h3 className="font-medium mb-4"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="name"> *</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="例如国家气象局API"
/>
</div>
<div>
<Label htmlFor="provider"> *</Label>
<Input
id="provider"
value={formData.provider}
onChange={(e) => setFormData({ ...formData, provider: e.target.value })}
placeholder="例如:中国气象局"
/>
</div>
</div>
</Card>
{/* 数据配置 */}
<Card className="p-4 bg-muted/20">
<h3 className="font-medium mb-4"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="type"></Label>
<Select
value={formData.type}
onValueChange={(value: DataSourceType) =>
setFormData({ ...formData, type: value })
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="气象数据"></SelectItem>
<SelectItem value="卫星遥感"></SelectItem>
<SelectItem value="土壤数据"></SelectItem>
<SelectItem value="作物生长"></SelectItem>
<SelectItem value="其他"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="updateFrequency"> *</Label>
<Input
id="updateFrequency"
value={formData.updateFrequency}
onChange={(e) => setFormData({ ...formData, updateFrequency: e.target.value })}
placeholder="例如每小时、每5天"
/>
</div>
</div>
</Card>
{/* 接入方式 */}
<Card className="p-4 bg-muted/20">
<h3 className="font-medium mb-4"></h3>
<div className="space-y-4">
<div>
<Label></Label>
<div className="grid grid-cols-2 gap-3 mt-2">
{accessMethods.map((method) => (
<Button
key={method}
type="button"
variant={formData.accessMethod === method ? 'default' : 'outline'}
className="justify-start h-auto p-3"
onClick={() => setFormData({ ...formData, accessMethod: method })}
>
<div className="flex items-center gap-2">
{getAccessMethodIcon(method)}
<div className="text-left">
<div className="font-medium text-sm">{method}</div>
</div>
</div>
</Button>
))}
</div>
</div>
{formData.accessMethod === 'API对接' && (
<div>
<Label htmlFor="apiEndpoint">API端点</Label>
<Input
id="apiEndpoint"
value={formData.apiEndpoint}
onChange={(e) => setFormData({ ...formData, apiEndpoint: e.target.value })}
placeholder="https://api.example.com/v1/data"
/>
</div>
)}
</div>
</Card>
{/* 描述 */}
<Card className="p-4 bg-muted/20">
<Label htmlFor="description"></Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="描述数据源的用途、数据内容等信息..."
rows={3}
/>
</Card>
</div>
<DialogFooter>
<Button variant="outline" onClick={handleClose}>
</Button>
<Button onClick={handleSubmit}>
<Plus className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,197 @@
'use client';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ExternalDataSource, dataSourceTypes, dataSourceStatuses } from '../types';
import { ExternalDataAction } from './externalDataReducer';
import {
Database,
Eye,
Edit,
Trash2,
Cloud,
Code,
Upload,
Wifi,
CheckCircle,
XCircle,
AlertTriangle,
Clock,
Link,
} from 'lucide-react';
interface DataSourceCardProps {
dataSource: ExternalDataSource;
onView: (dataSource: ExternalDataSource) => void;
onEdit: (dataSource: ExternalDataSource) => void;
onDelete: (id: string) => void;
}
export function DataSourceCard({ dataSource, onView, onEdit, onDelete }: DataSourceCardProps) {
const getStatusIcon = (status: string) => {
switch (status) {
case '正常':
return <CheckCircle className="w-4 h-4 text-success" />;
case '异常':
return <XCircle className="w-4 h-4 text-destructive" />;
case '离线':
return <Clock className="w-4 h-4 text-muted-foreground" />;
case '待配置':
return <AlertTriangle className="w-4 h-4 text-warning" />;
default:
return <Clock className="w-4 h-4 text-muted-foreground" />;
}
};
const getAccessMethodIcon = (method: string) => {
switch (method) {
case 'API对接':
return <Link className="w-4 h-4" />;
case 'FTP传输':
return <Upload className="w-4 h-4" />;
case 'WebSocket':
return <Wifi className="w-4 h-4" />;
case '手动上传':
return <Upload className="w-4 h-4" />;
default:
return <Database className="w-4 h-4" />;
}
};
const getStatusColor = (status: string) => {
const statusConfig = dataSourceStatuses.find(s => s.key === status);
return statusConfig?.color || '#6b7280';
};
const getTypeColor = (type: string) => {
const typeConfig = dataSourceTypes.find(t => t.key === type);
return typeConfig?.color || '#6b7280';
};
return (
<Card className="p-6 bg-card hover:bg-muted/50 transition-colors">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-lg flex items-center justify-center"
style={{ backgroundColor: `${getTypeColor(dataSource.type)}20` }}
>
<Cloud
className="w-5 h-5"
style={{ color: getTypeColor(dataSource.type) }}
/>
</div>
<div>
<h3 className="font-semibold text-foreground">{dataSource.name}</h3>
<p className="text-sm text-muted-foreground">{dataSource.provider}</p>
</div>
</div>
<div className="flex items-center gap-2">
{getStatusIcon(dataSource.status)}
<Badge
variant="outline"
className="font-light"
style={{
borderColor: getStatusColor(dataSource.status),
color: getStatusColor(dataSource.status),
}}
>
{dataSource.status}
</Badge>
</div>
</div>
<div className="space-y-3 mb-4">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground"></span>
<Badge
variant="outline"
className="font-light"
style={{
borderColor: getTypeColor(dataSource.type),
color: getTypeColor(dataSource.type),
}}
>
{dataSource.type}
</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground"></span>
<div className="flex items-center gap-2">
{getAccessMethodIcon(dataSource.accessMethod)}
<span className="text-sm">{dataSource.accessMethod}</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm">{dataSource.updateFrequency}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm font-medium">{dataSource.dataPoints.toLocaleString()}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm">{dataSource.lastUpdateTime}</span>
</div>
</div>
<div className="mb-4">
<p className="text-sm text-muted-foreground line-clamp-2">
{dataSource.description}
</p>
</div>
<div className="flex flex-wrap gap-1 mb-4">
{dataSource.dataFields.slice(0, 3).map((field, index) => (
<Badge
key={index}
variant="secondary"
className="text-xs font-light"
>
{field}
</Badge>
))}
{dataSource.dataFields.length > 3 && (
<Badge variant="secondary" className="text-xs font-light">
+{dataSource.dataFields.length - 3}
</Badge>
)}
</div>
<div className="flex gap-2 pt-4 border-t border-border">
<Button
variant="outline"
size="sm"
onClick={() => onView(dataSource)}
className="flex-1"
>
<Eye className="w-4 h-4 mr-1" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onEdit(dataSource)}
className="flex-1"
>
<Edit className="w-4 h-4 mr-1" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onDelete(dataSource.id)}
className="text-destructive hover:text-destructive hover:bg-destructive/10"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</Card>
);
}

View File

@@ -0,0 +1,164 @@
'use client';
import { useState } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Label } from '@/components/ui/label';
import { ExternalDataState, ExternalDataAction } from './externalDataReducer';
import { dataSourceTypes, dataSourceStatuses } from '../types';
import { Search, Filter, X } from 'lucide-react';
interface FilterPanelProps {
state: ExternalDataState;
dispatch: React.Dispatch<ExternalDataAction>;
}
export function FilterPanel({ state, dispatch }: FilterPanelProps) {
const [isExpanded, setIsExpanded] = useState(false);
const uniqueProviders = Array.from(new Set(state.dataSources.map(ds => ds.provider)));
const handleFilterChange = (key: keyof ExternalDataState['filters'], value: any) => {
dispatch({ type: 'UPDATE_FILTER', payload: { key, value } });
};
const handleToggleFilter = (filterType: 'type' | 'status' | 'provider', value: string) => {
dispatch({ type: 'TOGGLE_ARRAY_FILTER', payload: { key: filterType, value } });
};
const clearAllFilters = () => {
dispatch({ type: 'CLEAR_FILTERS' });
};
const hasActiveFilters = Object.values(state.filters).some(
value => Array.isArray(value) ? value.length > 0 : value !== ''
);
const activeFilterCount = Object.values(state.filters).reduce(
(count, value) => count + (Array.isArray(value) ? value.length : (value ? 1 : 0)),
0
);
return (
<Card className="bg-card border-border">
<div className="p-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Filter className="w-4 h-4 text-muted-foreground" />
<h3 className="font-medium"></h3>
{hasActiveFilters && (
<Badge variant="secondary" className="ml-2">
{activeFilterCount}
</Badge>
)}
</div>
<div className="flex items-center gap-2">
{hasActiveFilters && (
<Button
variant="ghost"
size="sm"
onClick={clearAllFilters}
className="text-muted-foreground hover:text-foreground"
>
<X className="w-4 h-4 mr-1" />
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? '收起' : '展开'}
</Button>
</div>
</div>
{/* 搜索框 */}
<div className="relative mb-4">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="搜索数据源名称、提供商..."
value={state.filters.searchTerm}
onChange={(e) => handleFilterChange('searchTerm', e.target.value)}
className="pl-10"
/>
</div>
{isExpanded && (
<div className="space-y-4">
{/* 数据类型筛选 */}
<div>
<Label className="text-sm font-medium mb-2 block"></Label>
<div className="flex flex-wrap gap-2">
{dataSourceTypes.map((type) => (
<Badge
key={type.key}
variant={state.filters.type.includes(type.key) ? 'default' : 'outline'}
className="cursor-pointer font-light"
style={{
backgroundColor: state.filters.type.includes(type.key) ? type.color : 'transparent',
borderColor: type.color,
color: state.filters.type.includes(type.key) ? 'white' : type.color,
}}
onClick={() => handleToggleFilter('type', type.key)}
>
{type.name}
</Badge>
))}
</div>
</div>
{/* 状态筛选 */}
<div>
<Label className="text-sm font-medium mb-2 block"></Label>
<div className="flex flex-wrap gap-2">
{dataSourceStatuses.map((status) => (
<Badge
key={status.key}
variant={state.filters.status.includes(status.key) ? 'default' : 'outline'}
className="cursor-pointer font-light"
style={{
backgroundColor: state.filters.status.includes(status.key) ? status.color : 'transparent',
borderColor: status.color,
color: state.filters.status.includes(status.key) ? 'white' : status.color,
}}
onClick={() => handleToggleFilter('status', status.key)}
>
{status.name}
</Badge>
))}
</div>
</div>
{/* 提供商筛选 */}
{uniqueProviders.length > 0 && (
<div>
<Label className="text-sm font-medium mb-2 block"></Label>
<div className="flex flex-wrap gap-2">
{uniqueProviders.map((provider) => (
<Badge
key={provider}
variant={state.filters.provider.includes(provider) ? 'default' : 'outline'}
className="cursor-pointer font-light"
style={{
backgroundColor: state.filters.provider.includes(provider) ? '#3b82f6' : 'transparent',
borderColor: '#3b82f6',
color: state.filters.provider.includes(provider) ? 'white' : '#3b82f6',
}}
onClick={() => handleToggleFilter('provider', provider)}
>
{provider}
</Badge>
))}
</div>
</div>
)}
</div>
)}
</div>
</Card>
);
}

View File

@@ -0,0 +1,78 @@
'use client';
import { Card } from '@/components/ui/card';
import { ExternalDataState } from './externalDataReducer';
import {
Database,
Cloud,
Activity,
Clock,
TrendingUp,
CheckCircle,
} from 'lucide-react';
interface StatisticsOverviewProps {
state: ExternalDataState;
}
export function StatisticsOverview({ state }: StatisticsOverviewProps) {
const activeRate = state.statistics.totalSources > 0
? (state.statistics.activeSources / state.statistics.totalSources * 100).toFixed(1)
: '0';
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
{/* 总数据源 */}
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-blue-600 dark:text-blue-400 font-light"></p>
<p className="text-2xl font-bold text-blue-700 dark:text-blue-300">
{state.statistics.totalSources}
</p>
</div>
<Database className="w-8 h-8 text-blue-500 dark:text-blue-400" />
</div>
</Card>
{/* 活跃数据源 */}
<Card className="p-4 bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-green-600 dark:text-green-400 font-light"></p>
<p className="text-2xl font-bold text-green-700 dark:text-green-300">
{state.statistics.activeSources}
</p>
</div>
<CheckCircle className="w-8 h-8 text-green-500 dark:text-green-400" />
</div>
</Card>
{/* 总数据点 */}
<Card className="p-4 bg-purple-50 dark:bg-purple-950 border-purple-200 dark:border-purple-800">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-purple-600 dark:text-purple-400 font-light"></p>
<p className="text-2xl font-bold text-purple-700 dark:text-purple-300">
{state.statistics.totalDataPoints.toLocaleString()}
</p>
</div>
<Cloud className="w-8 h-8 text-purple-500 dark:text-purple-400" />
</div>
</Card>
{/* 活跃率 */}
<Card className="p-4 bg-orange-50 dark:bg-orange-950 border-orange-200 dark:border-orange-800">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-orange-600 dark:text-orange-400 font-light"></p>
<p className="text-2xl font-bold text-orange-700 dark:text-orange-300">
{activeRate}%
</p>
</div>
<TrendingUp className="w-8 h-8 text-orange-500 dark:text-orange-400" />
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,223 @@
'use client';
import { ExternalDataSource, DataSourceType, DataSourceStatus, AccessMethod } from '../types';
export interface ExternalDataState {
dataSources: ExternalDataSource[];
filters: {
type: string[];
status: string[];
provider: string[];
searchTerm: string;
};
selectedDataSource: ExternalDataSource | null;
showAddDialog: boolean;
showUploadDialog: boolean;
showEditDialog: boolean;
showDataPreviewDialog: boolean;
uploadedFile: File | null;
uploadProgress: number;
selectedAccessMethod: AccessMethod;
statistics: {
totalSources: number;
activeSources: number;
totalDataPoints: number;
lastUpdateTime: string;
};
}
export type ExternalDataAction =
| { type: 'SET_DATA_SOURCES'; payload: ExternalDataSource[] }
| { type: 'SET_FILTERS'; payload: Partial<ExternalDataState['filters']> }
| { type: 'UPDATE_FILTER'; payload: { key: keyof ExternalDataState['filters']; value: any } }
| { type: 'TOGGLE_ARRAY_FILTER'; payload: { key: 'type' | 'status' | 'provider'; value: string } }
| { type: 'CLEAR_FILTERS' }
| { type: 'SET_SELECTED_DATA_SOURCE'; payload: ExternalDataSource | null }
| { type: 'SHOW_ADD_DIALOG'; payload: boolean }
| { type: 'SHOW_UPLOAD_DIALOG'; payload: boolean }
| { type: 'SHOW_EDIT_DIALOG'; payload: boolean }
| { type: 'SHOW_DATA_PREVIEW_DIALOG'; payload: boolean }
| { type: 'SET_UPLOADED_FILE'; payload: File | null }
| { type: 'SET_UPLOAD_PROGRESS'; payload: number }
| { type: 'SET_SELECTED_ACCESS_METHOD'; payload: AccessMethod }
| { type: 'ADD_DATA_SOURCE'; payload: ExternalDataSource }
| { type: 'UPDATE_DATA_SOURCE'; payload: { id: string; updates: Partial<ExternalDataSource> } }
| { type: 'DELETE_DATA_SOURCE'; payload: string }
| { type: 'SET_STATISTICS'; payload: Partial<ExternalDataState['statistics']> };
export const initialState: ExternalDataState = {
dataSources: [],
filters: {
type: [],
status: [],
provider: [],
searchTerm: '',
},
selectedDataSource: null,
showAddDialog: false,
showUploadDialog: false,
showEditDialog: false,
showDataPreviewDialog: false,
uploadedFile: null,
uploadProgress: 0,
selectedAccessMethod: 'API对接',
statistics: {
totalSources: 0,
activeSources: 0,
totalDataPoints: 0,
lastUpdateTime: '',
},
};
export function externalDataReducer(state: ExternalDataState, action: ExternalDataAction): ExternalDataState {
switch (action.type) {
case 'SET_DATA_SOURCES':
return {
...state,
dataSources: action.payload,
};
case 'SET_FILTERS':
return {
...state,
filters: {
...state.filters,
...action.payload,
},
};
case 'UPDATE_FILTER':
return {
...state,
filters: {
...state.filters,
[action.payload.key]: action.payload.value,
},
};
case 'TOGGLE_ARRAY_FILTER':
const { key, value } = action.payload;
const currentArray = state.filters[key];
const newArray = currentArray.includes(value)
? currentArray.filter(v => v !== value)
: [...currentArray, value];
return {
...state,
filters: {
...state.filters,
[key]: newArray,
},
};
case 'CLEAR_FILTERS':
return {
...state,
filters: {
type: [],
status: [],
provider: [],
searchTerm: '',
},
};
case 'SET_SELECTED_DATA_SOURCE':
return {
...state,
selectedDataSource: action.payload,
};
case 'SHOW_ADD_DIALOG':
return {
...state,
showAddDialog: action.payload,
};
case 'SHOW_UPLOAD_DIALOG':
return {
...state,
showUploadDialog: action.payload,
};
case 'SHOW_EDIT_DIALOG':
return {
...state,
showEditDialog: action.payload,
};
case 'SHOW_DATA_PREVIEW_DIALOG':
return {
...state,
showDataPreviewDialog: action.payload,
};
case 'SET_UPLOADED_FILE':
return {
...state,
uploadedFile: action.payload,
};
case 'SET_UPLOAD_PROGRESS':
return {
...state,
uploadProgress: action.payload,
};
case 'SET_SELECTED_ACCESS_METHOD':
return {
...state,
selectedAccessMethod: action.payload,
};
case 'ADD_DATA_SOURCE':
return {
...state,
dataSources: [...state.dataSources, action.payload],
};
case 'UPDATE_DATA_SOURCE':
return {
...state,
dataSources: state.dataSources.map(ds =>
ds.id === action.payload.id
? { ...ds, ...action.payload.updates }
: ds
),
};
case 'DELETE_DATA_SOURCE':
return {
...state,
dataSources: state.dataSources.filter(ds => ds.id !== action.payload),
};
case 'SET_STATISTICS':
return {
...state,
statistics: {
...state.statistics,
...action.payload,
},
};
default:
return state;
}
}
export function calculateStatistics(dataSources: ExternalDataSource[]): ExternalDataState['statistics'] {
const totalSources = dataSources.length;
const activeSources = dataSources.filter(ds => ds.status === '正常').length;
const totalDataPoints = dataSources.reduce((sum, ds) => sum + ds.dataPoints, 0);
const lastUpdateTime = dataSources
.filter(ds => ds.status === '正常')
.map(ds => ds.lastUpdateTime)
.sort()
.pop() || '';
return {
totalSources,
activeSources,
totalDataPoints,
lastUpdateTime,
};
}

Some files were not shown because too many files have changed in this diff Show More