子仓库提交
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
18
crop-x-new/src/app/(app)/agricultural-asset/basic/page.tsx
Normal file
18
crop-x-new/src/app/(app)/agricultural-asset/basic/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
7
crop-x-new/src/app/(app)/agricultural-asset/layout.tsx
Normal file
7
crop-x-new/src/app/(app)/agricultural-asset/layout.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function AgriculturalAssetLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return <>{children}</>
|
||||
}
|
||||
18
crop-x-new/src/app/(app)/agricultural-asset/page.tsx
Normal file
18
crop-x-new/src/app/(app)/agricultural-asset/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
18
crop-x-new/src/app/(app)/agricultural-asset/report/page.tsx
Normal file
18
crop-x-new/src/app/(app)/agricultural-asset/report/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
18
crop-x-new/src/app/(app)/agricultural-asset/return/page.tsx
Normal file
18
crop-x-new/src/app/(app)/agricultural-asset/return/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
35
crop-x-new/src/app/(app)/agricultural-machinery/error.tsx
Normal file
35
crop-x-new/src/app/(app)/agricultural-machinery/error.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default function AgriculturalMachineryLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
10
crop-x-new/src/app/(app)/agricultural-machinery/loading.tsx
Normal file
10
crop-x-new/src/app/(app)/agricultural-machinery/loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
93
crop-x-new/src/app/(app)/agricultural-machinery/page.tsx
Normal file
93
crop-x-new/src/app/(app)/agricultural-machinery/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
18
crop-x-new/src/app/(app)/ai-crop-model/application/page.tsx
Normal file
18
crop-x-new/src/app/(app)/ai-crop-model/application/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,502 @@
|
||||
/**
|
||||
* filekorolheader: 设备参数管理状态管理器 - 参数模板配置中心
|
||||
* 功能:设备类型数据管理、参数模板配置、对话框状态控制、操作处理
|
||||
* 路径:/ai-crop-model/data-sense-center/device-parameter/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: 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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
238
crop-x-new/src/app/(app)/ai-crop-model/data-sense-center/external/components/AddDataSourceDialog.tsx
vendored
Normal file
238
crop-x-new/src/app/(app)/ai-crop-model/data-sense-center/external/components/AddDataSourceDialog.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
197
crop-x-new/src/app/(app)/ai-crop-model/data-sense-center/external/components/DataSourceCard.tsx
vendored
Normal file
197
crop-x-new/src/app/(app)/ai-crop-model/data-sense-center/external/components/DataSourceCard.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
164
crop-x-new/src/app/(app)/ai-crop-model/data-sense-center/external/components/FilterPanel.tsx
vendored
Normal file
164
crop-x-new/src/app/(app)/ai-crop-model/data-sense-center/external/components/FilterPanel.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
223
crop-x-new/src/app/(app)/ai-crop-model/data-sense-center/external/components/externalDataReducer.tsx
vendored
Normal file
223
crop-x-new/src/app/(app)/ai-crop-model/data-sense-center/external/components/externalDataReducer.tsx
vendored
Normal 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
Reference in New Issue
Block a user