生产管理系统 - 全领域数据感知中心开发
This commit is contained in:
@@ -1,5 +1,41 @@
|
|||||||
# 开发项目规范
|
# 开发项目规范
|
||||||
|
|
||||||
|
## 通用开发规约
|
||||||
|
|
||||||
|
### 1. 文件头部注释规范(filekorolheader)
|
||||||
|
|
||||||
|
**规范要求:**
|
||||||
|
所有页面文件(page.tsx)必须在最上方添加filekorolheader注释,说明文件对应的页面功能、路径和用途。
|
||||||
|
|
||||||
|
**格式标准:**
|
||||||
|
```tsx
|
||||||
|
/**
|
||||||
|
* filekorolheader: [页面名称] - [功能描述]
|
||||||
|
* 功能:[主要功能列表]
|
||||||
|
* 路径:[页面路由路径]
|
||||||
|
* 规范:[遵循的特殊规范说明]
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
```tsx
|
||||||
|
/**
|
||||||
|
* filekorolheader: 物联设备数据接入页面 - IoT设备数据管理中心
|
||||||
|
* 功能:设备列表管理、实时数据监控、数据对比分析、报告生成
|
||||||
|
* 路径:/ai-crop-model/data-sense-center/iot
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,shadcn语义化样式
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
**实施要点:**
|
||||||
|
- 必须放在文件最顶部,在'use client'之前
|
||||||
|
- 页面名称要准确反映业务功能
|
||||||
|
- 功能描述要简明扼要,列出核心功能
|
||||||
|
- 路径必须是完整的路由路径
|
||||||
|
- 如有特殊规范遵循,需要在规范字段说明
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## path:land-information/archive/statistics,name:统计分析页面开发经验
|
## path:land-information/archive/statistics,name:统计分析页面开发经验
|
||||||
|
|
||||||
### 总体开发经验总结
|
### 总体开发经验总结
|
||||||
|
|||||||
5
crop-x/env/.env.dev
vendored
5
crop-x/env/.env.dev
vendored
@@ -5,7 +5,10 @@ NODE_ENV=development
|
|||||||
FRONTEND_BASE_URL=https://cavin-smart-crop-ui-app.dev.maimaiag.com
|
FRONTEND_BASE_URL=https://cavin-smart-crop-ui-app.dev.maimaiag.com
|
||||||
|
|
||||||
# 后端 API 地址
|
# 后端 API 地址
|
||||||
BACKEND_BASE_URL=http://pengcode.tech:8080
|
BACKEND_BASE_URL=https://gitea-admin-hm-smart-agri-app.dev.maimaiag.com/
|
||||||
|
|
||||||
|
# OpenAPI 生成配置
|
||||||
|
API_BASE_URL=https://gitea-admin-hm-smart-agri-app.dev.maimaiag.com
|
||||||
|
|
||||||
# API 版本
|
# API 版本
|
||||||
API_VERSION=v1
|
API_VERSION=v1
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Card } from '@/components/ui/card';
|
|
||||||
|
|
||||||
export default function ExternalPage() {
|
|
||||||
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/data-center/external
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Card } from '@/components/ui/card';
|
|
||||||
|
|
||||||
export default function IotPage() {
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<Card className="p-6">
|
|
||||||
<h2 className="text-xl font-semibold">IoT数据</h2>
|
|
||||||
<div className="p-3 bg-muted rounded-lg mt-3">
|
|
||||||
<p className="text-sm">
|
|
||||||
<strong>页面路径:</strong> /ai-crop-model/data-center/iot
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
238
crop-x/src/app/(app)/ai-crop-model/data-sense-center/external/components/AddDataSourceDialog.tsx
vendored
Normal file
238
crop-x/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/src/app/(app)/ai-crop-model/data-sense-center/external/components/DataSourceCard.tsx
vendored
Normal file
197
crop-x/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/src/app/(app)/ai-crop-model/data-sense-center/external/components/FilterPanel.tsx
vendored
Normal file
164
crop-x/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>
|
||||||
|
);
|
||||||
|
}
|
||||||
78
crop-x/src/app/(app)/ai-crop-model/data-sense-center/external/components/StatisticsOverview.tsx
vendored
Normal file
78
crop-x/src/app/(app)/ai-crop-model/data-sense-center/external/components/StatisticsOverview.tsx
vendored
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { ExternalDataState } from './externalDataReducer';
|
||||||
|
import {
|
||||||
|
Database,
|
||||||
|
Cloud,
|
||||||
|
Activity,
|
||||||
|
Clock,
|
||||||
|
TrendingUp,
|
||||||
|
CheckCircle,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface StatisticsOverviewProps {
|
||||||
|
state: ExternalDataState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatisticsOverview({ state }: StatisticsOverviewProps) {
|
||||||
|
const activeRate = state.statistics.totalSources > 0
|
||||||
|
? (state.statistics.activeSources / state.statistics.totalSources * 100).toFixed(1)
|
||||||
|
: '0';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||||
|
{/* 总数据源 */}
|
||||||
|
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-blue-600 dark:text-blue-400 font-light">总数据源</p>
|
||||||
|
<p className="text-2xl font-bold text-blue-700 dark:text-blue-300">
|
||||||
|
{state.statistics.totalSources}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Database className="w-8 h-8 text-blue-500 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 活跃数据源 */}
|
||||||
|
<Card className="p-4 bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-green-600 dark:text-green-400 font-light">活跃数据源</p>
|
||||||
|
<p className="text-2xl font-bold text-green-700 dark:text-green-300">
|
||||||
|
{state.statistics.activeSources}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<CheckCircle className="w-8 h-8 text-green-500 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 总数据点 */}
|
||||||
|
<Card className="p-4 bg-purple-50 dark:bg-purple-950 border-purple-200 dark:border-purple-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-purple-600 dark:text-purple-400 font-light">总数据点</p>
|
||||||
|
<p className="text-2xl font-bold text-purple-700 dark:text-purple-300">
|
||||||
|
{state.statistics.totalDataPoints.toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Cloud className="w-8 h-8 text-purple-500 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 活跃率 */}
|
||||||
|
<Card className="p-4 bg-orange-50 dark:bg-orange-950 border-orange-200 dark:border-orange-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-orange-600 dark:text-orange-400 font-light">活跃率</p>
|
||||||
|
<p className="text-2xl font-bold text-orange-700 dark:text-orange-300">
|
||||||
|
{activeRate}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<TrendingUp className="w-8 h-8 text-orange-500 dark:text-orange-400" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
223
crop-x/src/app/(app)/ai-crop-model/data-sense-center/external/components/externalDataReducer.tsx
vendored
Normal file
223
crop-x/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,
|
||||||
|
};
|
||||||
|
}
|
||||||
831
crop-x/src/app/(app)/ai-crop-model/data-sense-center/external/page.tsx
vendored
Normal file
831
crop-x/src/app/(app)/ai-crop-model/data-sense-center/external/page.tsx
vendored
Normal file
@@ -0,0 +1,831 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 外部数据源管理页面 - 多源数据接入管理中心
|
||||||
|
* 功能:外部数据源管理、数据质量监控、异常告警处理、数据接入配置
|
||||||
|
* 路径:/ai-crop-model/data-sense-center/external
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用useState状态管理,shadcn语义化样式
|
||||||
|
*/
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import {
|
||||||
|
Database,
|
||||||
|
Plus,
|
||||||
|
Upload,
|
||||||
|
RefreshCw,
|
||||||
|
CheckCircle,
|
||||||
|
XCircle,
|
||||||
|
AlertCircle,
|
||||||
|
Clock,
|
||||||
|
Cloud,
|
||||||
|
Satellite,
|
||||||
|
Wifi,
|
||||||
|
WifiOff,
|
||||||
|
Activity,
|
||||||
|
Thermometer,
|
||||||
|
Droplet,
|
||||||
|
Sun,
|
||||||
|
Wind,
|
||||||
|
Camera,
|
||||||
|
Gauge,
|
||||||
|
Zap,
|
||||||
|
Settings,
|
||||||
|
Eye,
|
||||||
|
Edit,
|
||||||
|
Trash2,
|
||||||
|
Link,
|
||||||
|
PlayCircle,
|
||||||
|
PauseCircle,
|
||||||
|
BarChart3,
|
||||||
|
Calendar,
|
||||||
|
MapPin,
|
||||||
|
Signal,
|
||||||
|
CheckCircle2,
|
||||||
|
AlertTriangle,
|
||||||
|
TrendingUp,
|
||||||
|
Filter,
|
||||||
|
FileText,
|
||||||
|
Search,
|
||||||
|
Download,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
type DataSourceType = '气象数据' | '卫星遥感' | '土壤数据' | '作物生长' | '其他';
|
||||||
|
type DataSourceStatus = '正常' | '异常' | '离线' | '待配置';
|
||||||
|
type AccessMethod = '手动上传' | 'API对接' | 'FTP传输' | 'WebSocket';
|
||||||
|
|
||||||
|
interface ExternalDataSource {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: DataSourceType;
|
||||||
|
provider: string;
|
||||||
|
accessMethod: AccessMethod;
|
||||||
|
apiEndpoint?: string;
|
||||||
|
updateFrequency: string;
|
||||||
|
lastUpdateTime: string;
|
||||||
|
dataPoints: number;
|
||||||
|
status: DataSourceStatus;
|
||||||
|
dataFields: string[];
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DataQuality {
|
||||||
|
completeness: number;
|
||||||
|
accuracy: number;
|
||||||
|
timeliness: number;
|
||||||
|
consistency: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExternalPage() {
|
||||||
|
const [activeTab, setActiveTab] = useState('external');
|
||||||
|
const [showDataSourceDialog, setShowDataSourceDialog] = useState(false);
|
||||||
|
const [showUploadDialog, setShowUploadDialog] = useState(false);
|
||||||
|
const [showValidationDialog, setShowValidationDialog] = useState(false);
|
||||||
|
const [showDataPreviewDialog, setShowDataPreviewDialog] = useState(false);
|
||||||
|
const [selectedDataSource, setSelectedDataSource] = useState<ExternalDataSource | null>(null);
|
||||||
|
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
|
||||||
|
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
|
||||||
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
|
const [selectedAccessMethod, setSelectedAccessMethod] = useState<AccessMethod>('API对接');
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [selectedType, setSelectedType] = useState<string>('all');
|
||||||
|
const [selectedStatus, setSelectedStatus] = useState<string>('all');
|
||||||
|
|
||||||
|
// 外部数据源模拟数据
|
||||||
|
const [externalDataSources] = useState<ExternalDataSource[]>([
|
||||||
|
{
|
||||||
|
id: 'ext-1',
|
||||||
|
name: '国家气象局API',
|
||||||
|
type: '气象数据',
|
||||||
|
provider: '中国气象局',
|
||||||
|
accessMethod: 'API对接',
|
||||||
|
apiEndpoint: 'https://api.weather.gov.cn/v1/data',
|
||||||
|
updateFrequency: '每小时',
|
||||||
|
lastUpdateTime: '2024-10-15 14:00:00',
|
||||||
|
dataPoints: 24850,
|
||||||
|
status: '正常',
|
||||||
|
dataFields: ['温度', '湿度', '气压', '降水量', '风速', '风向'],
|
||||||
|
description: '实时气象数据,包含温度、湿度、降水等多维度信息',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ext-2',
|
||||||
|
name: 'Sentinel-2卫星数据',
|
||||||
|
type: '卫星遥感',
|
||||||
|
provider: 'ESA欧空局',
|
||||||
|
accessMethod: 'API对接',
|
||||||
|
apiEndpoint: 'https://scihub.copernicus.eu/dhus',
|
||||||
|
updateFrequency: '每5天',
|
||||||
|
lastUpdateTime: '2024-10-12 08:30:00',
|
||||||
|
dataPoints: 1280,
|
||||||
|
status: '正常',
|
||||||
|
dataFields: ['NDVI', 'EVI', 'LAI', '地表温度', '土壤湿度指数'],
|
||||||
|
description: '高分辨率卫星遥感影像,用于作物长势监测',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ext-3',
|
||||||
|
name: '土壤数据库',
|
||||||
|
type: '土壤数据',
|
||||||
|
provider: '农业部土壤监测中心',
|
||||||
|
accessMethod: '手动上传',
|
||||||
|
updateFrequency: '每季度',
|
||||||
|
lastUpdateTime: '2024-09-20 10:15:00',
|
||||||
|
dataPoints: 385,
|
||||||
|
status: '正常',
|
||||||
|
dataFields: ['pH值', '有机质', '氮磷钾含量', '土壤质地', '盐分'],
|
||||||
|
description: '区域土壤理化性质数据',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ext-4',
|
||||||
|
name: '光照辐射数据',
|
||||||
|
type: '气象数据',
|
||||||
|
provider: '光伏气象站网络',
|
||||||
|
accessMethod: 'FTP传输',
|
||||||
|
updateFrequency: '每30分钟',
|
||||||
|
lastUpdateTime: '2024-10-15 13:30:00',
|
||||||
|
dataPoints: 15620,
|
||||||
|
status: '正常',
|
||||||
|
dataFields: ['总辐射', '直接辐射', '散射辐射', '光合有效辐射'],
|
||||||
|
description: '太阳辐射数据,用于光合作用分析',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ext-5',
|
||||||
|
name: '作物生长监测',
|
||||||
|
type: '作物生长',
|
||||||
|
provider: '农业科学院',
|
||||||
|
accessMethod: 'API对接',
|
||||||
|
updateFrequency: '每周',
|
||||||
|
lastUpdateTime: '2024-10-14 09:00:00',
|
||||||
|
dataPoints: 2450,
|
||||||
|
status: '异常',
|
||||||
|
dataFields: ['株高', '叶面积指数', '生物量', '产量预测'],
|
||||||
|
description: '作物生长参数动态监测数据',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 数据质量评估
|
||||||
|
const [dataQuality] = useState<DataQuality>({
|
||||||
|
completeness: 94.5,
|
||||||
|
accuracy: 96.8,
|
||||||
|
timeliness: 92.3,
|
||||||
|
consistency: 95.2,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const totalExternalSources = externalDataSources.length;
|
||||||
|
const activeExternalSources = externalDataSources.filter(s => s.status === '正常').length;
|
||||||
|
|
||||||
|
// 过滤数据源
|
||||||
|
const filteredDataSources = externalDataSources.filter(source => {
|
||||||
|
const matchesSearch = source.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
source.provider.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
source.description.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
|
|
||||||
|
const matchesType = selectedType === 'all' || source.type === selectedType;
|
||||||
|
const matchesStatus = selectedStatus === 'all' || source.status === selectedStatus;
|
||||||
|
|
||||||
|
return matchesSearch && matchesType && matchesStatus;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getStatusColor = (status: DataSourceStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case '正常':
|
||||||
|
return 'bg-green-100 text-green-800 border-green-200 dark:bg-green-900/50 dark:text-green-300';
|
||||||
|
case '异常':
|
||||||
|
return 'bg-red-100 text-red-800 border-red-200 dark:bg-red-900/50 dark:text-red-300';
|
||||||
|
case '离线':
|
||||||
|
return 'bg-gray-100 text-gray-800 border-gray-200 dark:bg-gray-900/50 dark:text-gray-300';
|
||||||
|
case '待配置':
|
||||||
|
return 'bg-yellow-100 text-yellow-800 border-yellow-200 dark:bg-yellow-900/50 dark:text-yellow-300';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800 border-gray-200 dark:bg-gray-900/50 dark:text-gray-300';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusIcon = (status: DataSourceStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case '正常':
|
||||||
|
return <CheckCircle className="w-4 h-4 text-green-600" />;
|
||||||
|
case '异常':
|
||||||
|
return <XCircle className="w-4 h-4 text-red-600" />;
|
||||||
|
case '离线':
|
||||||
|
return <WifiOff className="w-4 h-4 text-gray-500" />;
|
||||||
|
case '待配置':
|
||||||
|
return <AlertCircle className="w-4 h-4 text-yellow-600" />;
|
||||||
|
default:
|
||||||
|
return <AlertCircle className="w-4 h-4 text-gray-500" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDataTypeIcon = (type: DataSourceType) => {
|
||||||
|
switch (type) {
|
||||||
|
case '气象数据':
|
||||||
|
return <Cloud className="w-4 h-4 text-blue-600" />;
|
||||||
|
case '卫星遥感':
|
||||||
|
return <Satellite className="w-4 h-4 text-purple-600" />;
|
||||||
|
case '土壤数据':
|
||||||
|
return <Database className="w-4 h-4 text-green-600" />;
|
||||||
|
case '作物生长':
|
||||||
|
return <TrendingUp className="w-4 h-4 text-orange-600" />;
|
||||||
|
default:
|
||||||
|
return <Database className="w-4 h-4 text-gray-600" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 handleTestConnection = () => {
|
||||||
|
toast.success('连接测试成功,数据接入正常');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDataPreview = () => {
|
||||||
|
setShowDataPreviewDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = event.target.files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
const fileArray = Array.from(files);
|
||||||
|
setUploadedFiles(fileArray);
|
||||||
|
setUploadedFile(fileArray[0]);
|
||||||
|
let progress = 0;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
progress += 10;
|
||||||
|
setUploadProgress(progress);
|
||||||
|
if (progress >= 100) {
|
||||||
|
clearInterval(interval);
|
||||||
|
toast.success(`成功上传${fileArray.length}个文件`);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6 bg-background">
|
||||||
|
{/* 页面标题和说明 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Database className="w-8 h-8 text-blue-600" />
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-foreground">多源数据接入</h1>
|
||||||
|
<p className="text-muted-foreground">多源数据智能接入,构建全面精准的农业数据底座</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 统计卡片 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">外部数据源</p>
|
||||||
|
<p className="mt-2 text-3xl text-sky-600">{activeExternalSources}/{totalExternalSources}</p>
|
||||||
|
<p className="text-xs text-sky-600 mt-1">正常运行</p>
|
||||||
|
</div>
|
||||||
|
<Database className="w-12 h-12 text-sky-600 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">物联设备</p>
|
||||||
|
<p className="mt-2 text-3xl text-green-600">3/5</p>
|
||||||
|
<p className="text-xs text-green-600 mt-1">在线设备</p>
|
||||||
|
</div>
|
||||||
|
<Wifi className="w-12 h-12 text-green-600 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">数据质量</p>
|
||||||
|
<p className="mt-2 text-3xl text-green-600">{dataQuality.accuracy}%</p>
|
||||||
|
<p className="text-xs text-green-600 mt-1">准确率</p>
|
||||||
|
</div>
|
||||||
|
<CheckCircle2 className="w-12 h-12 text-green-600 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">异常告警</p>
|
||||||
|
<p className="mt-2 text-3xl text-amber-600">2</p>
|
||||||
|
<p className="text-xs text-amber-600 mt-1">待处理</p>
|
||||||
|
</div>
|
||||||
|
<AlertTriangle className="w-12 h-12 text-amber-600 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
<TabsTrigger value="external">多源数据接入</TabsTrigger>
|
||||||
|
<TabsTrigger value="iot">物联设备数据接入</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{/* 多源数据接入 */}
|
||||||
|
<TabsContent value="external" className="space-y-4">
|
||||||
|
<Card className="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-blue-200 dark:from-blue-950/50 dark:to-indigo-950/50 dark:border-blue-800">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<Database className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
<p className="mb-2 font-medium">多源数据接入功能:</p>
|
||||||
|
<ul className="space-y-1 text-xs">
|
||||||
|
<li>• <strong>灵活接入</strong>: 支持手动上传、API对接、FTP传输等多种方式</li>
|
||||||
|
<li>• <strong>数据校验</strong>: 提供格式适配与数据质量控制机制</li>
|
||||||
|
<li>• <strong>气象数据</strong>: 温度、湿度、光照、降水等实时气象信息</li>
|
||||||
|
<li>• <strong>卫星遥感</strong>: NDVI植被指数、作物长势监测影像</li>
|
||||||
|
<li>• <strong>高质量支撑</strong>: 确保数据完整性、准确性、时效性</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button onClick={() => {
|
||||||
|
setSelectedDataSource(null);
|
||||||
|
setSelectedAccessMethod('API对接');
|
||||||
|
setUploadedFile(null);
|
||||||
|
setUploadedFiles([]);
|
||||||
|
setUploadProgress(0);
|
||||||
|
setShowDataSourceDialog(true);
|
||||||
|
}}>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
添加数据源
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={() => setShowUploadDialog(true)}>
|
||||||
|
<Upload className="w-4 h-4 mr-2" />
|
||||||
|
手动上传数据
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={() => setShowValidationDialog(true)}>
|
||||||
|
<CheckCircle2 className="w-4 h-4 mr-2" />
|
||||||
|
数据校验规则
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 筛选条件 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Search className="w-4 h-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="搜索数据源..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-64"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Select value={selectedType} onValueChange={setSelectedType}>
|
||||||
|
<SelectTrigger className="w-40">
|
||||||
|
<SelectValue placeholder="数据类型" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">全部类型</SelectItem>
|
||||||
|
<SelectItem value="气象数据">气象数据</SelectItem>
|
||||||
|
<SelectItem value="卫星遥感">卫星遥感</SelectItem>
|
||||||
|
<SelectItem value="土壤数据">土壤数据</SelectItem>
|
||||||
|
<SelectItem value="作物生长">作物生长</SelectItem>
|
||||||
|
<SelectItem value="其他">其他</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
|
||||||
|
<SelectTrigger className="w-32">
|
||||||
|
<SelectValue placeholder="状态" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">全部状态</SelectItem>
|
||||||
|
<SelectItem value="正常">正常</SelectItem>
|
||||||
|
<SelectItem value="异常">异常</SelectItem>
|
||||||
|
<SelectItem value="离线">离线</SelectItem>
|
||||||
|
<SelectItem value="待配置">待配置</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 数据源列表 */}
|
||||||
|
<Card className="bg-card border-border">
|
||||||
|
<div className="p-4 border-b border-border bg-muted/30">
|
||||||
|
<h3 className="flex items-center gap-2 text-lg font-semibold">
|
||||||
|
<Database className="w-5 h-5 text-blue-600" />
|
||||||
|
外部数据源
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-muted/50">
|
||||||
|
<TableHead className="font-medium">数据源名称</TableHead>
|
||||||
|
<TableHead className="font-medium">数据类型</TableHead>
|
||||||
|
<TableHead className="font-medium">提供商</TableHead>
|
||||||
|
<TableHead className="font-medium">接入方式</TableHead>
|
||||||
|
<TableHead className="font-medium">更新频率</TableHead>
|
||||||
|
<TableHead className="font-medium">数据点数</TableHead>
|
||||||
|
<TableHead className="font-medium">最后更新</TableHead>
|
||||||
|
<TableHead className="font-medium">状态</TableHead>
|
||||||
|
<TableHead className="font-medium">操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filteredDataSources.map((source) => (
|
||||||
|
<TableRow key={source.id} className="hover:bg-muted/30">
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getDataTypeIcon(source.type)}
|
||||||
|
<span className="font-medium">{source.name}</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="outline" className="font-light">{source.type}</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs">{source.provider}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getAccessMethodIcon(source.accessMethod)}
|
||||||
|
<Badge variant="outline" className="font-light">{source.accessMethod}</Badge>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs">{source.updateFrequency}</TableCell>
|
||||||
|
<TableCell className="text-xs">{source.dataPoints.toLocaleString()}</TableCell>
|
||||||
|
<TableCell className="text-xs">{source.lastUpdateTime}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getStatusIcon(source.status)}
|
||||||
|
<Badge className={`font-light ${getStatusColor(source.status)}`}>{source.status}</Badge>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{source.accessMethod !== '手动上传' && (
|
||||||
|
<Button size="sm" variant="outline" onClick={handleTestConnection}>
|
||||||
|
<Link className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button size="sm" variant="outline" onClick={() => {
|
||||||
|
setSelectedDataSource(source);
|
||||||
|
setSelectedAccessMethod(source.accessMethod);
|
||||||
|
setShowDataSourceDialog(true);
|
||||||
|
}}>
|
||||||
|
<Edit className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={handleDataPreview}>
|
||||||
|
<Eye className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{/* 数据源配置对话框 */}
|
||||||
|
<Dialog open={showDataSourceDialog} onOpenChange={setShowDataSourceDialog}>
|
||||||
|
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Database className="w-5 h-5" />
|
||||||
|
{selectedDataSource ? '编辑数据源' : '添加数据源'}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
配置外部数据源的接入方式和参数
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>数据源名称</Label>
|
||||||
|
<Input placeholder="输入数据源名称" defaultValue={selectedDataSource?.name} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>数据类型</Label>
|
||||||
|
<Select defaultValue={selectedDataSource?.type}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="选择数据类型" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="气象数据">气象数据</SelectItem>
|
||||||
|
<SelectItem value="卫星遥感">卫星遥感</SelectItem>
|
||||||
|
<SelectItem value="土壤数据">土壤数据</SelectItem>
|
||||||
|
<SelectItem value="作物生长">作物生长</SelectItem>
|
||||||
|
<SelectItem value="其他">其他</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>提供商</Label>
|
||||||
|
<Input placeholder="输入数据提供商" defaultValue={selectedDataSource?.provider} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>接入方式</Label>
|
||||||
|
<Select value={selectedAccessMethod} onValueChange={(value: AccessMethod) => setSelectedAccessMethod(value)}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="选择接入方式" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="API对接">API对接</SelectItem>
|
||||||
|
<SelectItem value="FTP传输">FTP传输</SelectItem>
|
||||||
|
<SelectItem value="WebSocket">WebSocket</SelectItem>
|
||||||
|
<SelectItem value="手动上传">手动上传</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedAccessMethod !== '手动上传' && (
|
||||||
|
<div>
|
||||||
|
<Label>API端点/服务器地址</Label>
|
||||||
|
<Input placeholder="输入API端点或服务器地址" defaultValue={selectedDataSource?.apiEndpoint} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>更新频率</Label>
|
||||||
|
<Select defaultValue={selectedDataSource?.updateFrequency}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="选择更新频率" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="实时">实时</SelectItem>
|
||||||
|
<SelectItem value="每5分钟">每5分钟</SelectItem>
|
||||||
|
<SelectItem value="每30分钟">每30分钟</SelectItem>
|
||||||
|
<SelectItem value="每小时">每小时</SelectItem>
|
||||||
|
<SelectItem value="每天">每天</SelectItem>
|
||||||
|
<SelectItem value="每周">每周</SelectItem>
|
||||||
|
<SelectItem value="每月">每月</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>数据描述</Label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="输入数据源的详细描述"
|
||||||
|
className="resize-none"
|
||||||
|
rows={3}
|
||||||
|
defaultValue={selectedDataSource?.description}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setShowDataSourceDialog(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => {
|
||||||
|
toast.success(selectedDataSource ? '数据源更新成功' : '数据源添加成功');
|
||||||
|
setShowDataSourceDialog(false);
|
||||||
|
}}>
|
||||||
|
保存配置
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 手动上传数据对话框 */}
|
||||||
|
<Dialog open={showUploadDialog} onOpenChange={setShowUploadDialog}>
|
||||||
|
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Upload className="w-5 h-5 text-blue-600" />
|
||||||
|
手动上传数据
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
支持CSV、JSON、XML等多种格式数据上传,支持多文件同时上传
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label>选择文件</Label>
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept=".csv,.json,.xml,.xlsx"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{uploadProgress > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-sm">上传进度</span>
|
||||||
|
<span className="text-sm font-medium">{uploadProgress}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={uploadProgress} className="h-2" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card className="p-4 bg-blue-50 dark:bg-blue-950/30 border-blue-200 dark:border-blue-800">
|
||||||
|
<h4 className="text-sm mb-3 flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-blue-600" />
|
||||||
|
数据质量校验
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs">自动检测数据完整性</span>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs">验证数据格式规范</span>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs">异常值自动标记</span>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setShowUploadDialog(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => {
|
||||||
|
toast.success('数据上传成功');
|
||||||
|
setShowUploadDialog(false);
|
||||||
|
}}>
|
||||||
|
开始上传
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 数据校验规则对话框 */}
|
||||||
|
<Dialog open={showValidationDialog} onOpenChange={setShowValidationDialog}>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="w-5 h-5" />
|
||||||
|
数据校验规则
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
配置数据质量校验规则和异常处理策略
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-sm">数据完整性检查</Label>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-sm">数据格式验证</Label>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-sm">数值范围校验</Label>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-sm">时间戳连续性</Label>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setShowValidationDialog(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => {
|
||||||
|
toast.success('校验规则保存成功');
|
||||||
|
setShowValidationDialog(false);
|
||||||
|
}}>
|
||||||
|
保存规则
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 数据预览对话框 */}
|
||||||
|
<Dialog open={showDataPreviewDialog} onOpenChange={setShowDataPreviewDialog}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Eye className="w-5 h-5" />
|
||||||
|
数据预览
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
查看数据源的详细信息和样本数据
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{selectedDataSource && (
|
||||||
|
<>
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="font-medium mb-3">{selectedDataSource.name}</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">数据类型:</span>
|
||||||
|
<span className="ml-2">{selectedDataSource.type}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">提供商:</span>
|
||||||
|
<span className="ml-2">{selectedDataSource.provider}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">接入方式:</span>
|
||||||
|
<span className="ml-2">{selectedDataSource.accessMethod}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">更新频率:</span>
|
||||||
|
<span className="ml-2">{selectedDataSource.updateFrequency}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3">
|
||||||
|
<span className="text-muted-foreground">描述:</span>
|
||||||
|
<p className="mt-1 text-sm">{selectedDataSource.description}</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="font-medium mb-3">数据字段</h4>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{selectedDataSource.dataFields.map((field, index) => (
|
||||||
|
<Badge key={index} variant="outline">{field}</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="font-medium mb-3">统计信息</h4>
|
||||||
|
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">数据点总数:</span>
|
||||||
|
<span className="ml-2 font-medium">{selectedDataSource.dataPoints.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">最后更新:</span>
|
||||||
|
<span className="ml-2">{selectedDataSource.lastUpdateTime}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">当前状态:</span>
|
||||||
|
<Badge className={`ml-2 ${getStatusColor(selectedDataSource.status)}`}>
|
||||||
|
{selectedDataSource.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setShowDataPreviewDialog(false)}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
<Button>
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
导出数据
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
115
crop-x/src/app/(app)/ai-crop-model/data-sense-center/external/types.ts
vendored
Normal file
115
crop-x/src/app/(app)/ai-crop-model/data-sense-center/external/types.ts
vendored
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
export type DataSourceType = '气象数据' | '卫星遥感' | '土壤数据' | '作物生长' | '其他';
|
||||||
|
export type DataSourceStatus = '正常' | '异常' | '离线' | '待配置';
|
||||||
|
export type AccessMethod = '手动上传' | 'API对接' | 'FTP传输' | 'WebSocket';
|
||||||
|
|
||||||
|
export interface ExternalDataSource {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: DataSourceType;
|
||||||
|
provider: string;
|
||||||
|
accessMethod: AccessMethod;
|
||||||
|
apiEndpoint?: string;
|
||||||
|
updateFrequency: string;
|
||||||
|
lastUpdateTime: string;
|
||||||
|
dataPoints: number;
|
||||||
|
status: DataSourceStatus;
|
||||||
|
dataFields: string[];
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataSourceForm {
|
||||||
|
name: string;
|
||||||
|
type: DataSourceType;
|
||||||
|
provider: string;
|
||||||
|
accessMethod: AccessMethod;
|
||||||
|
apiEndpoint?: string;
|
||||||
|
updateFrequency: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dataSourceTypes: Array<{ key: DataSourceType; name: string; color: string }> = [
|
||||||
|
{ key: '气象数据', name: '气象数据', color: '#3b82f6' },
|
||||||
|
{ key: '卫星遥感', name: '卫星遥感', color: '#10b981' },
|
||||||
|
{ key: '土壤数据', name: '土壤数据', color: '#8b5cf6' },
|
||||||
|
{ key: '作物生长', name: '作物生长', color: '#f59e0b' },
|
||||||
|
{ key: '其他', name: '其他', color: '#6b7280' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const dataSourceStatuses: Array<{ key: DataSourceStatus; name: string; color: string }> = [
|
||||||
|
{ key: '正常', name: '正常', color: '#10b981' },
|
||||||
|
{ key: '异常', name: '异常', color: '#ef4444' },
|
||||||
|
{ key: '离线', name: '离线', color: '#6b7280' },
|
||||||
|
{ key: '待配置', name: '待配置', color: '#f59e0b' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const accessMethods: AccessMethod[] = ['手动上传', 'API对接', 'FTP传输', 'WebSocket'];
|
||||||
|
|
||||||
|
export const sampleDataSources: ExternalDataSource[] = [
|
||||||
|
{
|
||||||
|
id: 'ext-1',
|
||||||
|
name: '国家气象局API',
|
||||||
|
type: '气象数据',
|
||||||
|
provider: '中国气象局',
|
||||||
|
accessMethod: 'API对接',
|
||||||
|
apiEndpoint: 'https://api.weather.gov.cn/v1/data',
|
||||||
|
updateFrequency: '每小时',
|
||||||
|
lastUpdateTime: '2024-10-15 14:00:00',
|
||||||
|
dataPoints: 24850,
|
||||||
|
status: '正常',
|
||||||
|
dataFields: ['温度', '湿度', '气压', '降水量', '风速', '风向'],
|
||||||
|
description: '实时气象数据,包含温度、湿度、降水等多维度信息',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ext-2',
|
||||||
|
name: 'Sentinel-2卫星数据',
|
||||||
|
type: '卫星遥感',
|
||||||
|
provider: 'ESA欧空局',
|
||||||
|
accessMethod: 'API对接',
|
||||||
|
apiEndpoint: 'https://scihub.copernicus.eu/dhus',
|
||||||
|
updateFrequency: '每5天',
|
||||||
|
lastUpdateTime: '2024-10-12 08:30:00',
|
||||||
|
dataPoints: 1280,
|
||||||
|
status: '正常',
|
||||||
|
dataFields: ['NDVI', 'EVI', 'LAI', '地表温度', '土壤湿度指数'],
|
||||||
|
description: '高分辨率卫星遥感影像,用于作物长势监测',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ext-3',
|
||||||
|
name: '土壤数据库',
|
||||||
|
type: '土壤数据',
|
||||||
|
provider: '农业部土壤监测中心',
|
||||||
|
accessMethod: '手动上传',
|
||||||
|
updateFrequency: '每季度',
|
||||||
|
lastUpdateTime: '2024-09-20 10:15:00',
|
||||||
|
dataPoints: 385,
|
||||||
|
status: '正常',
|
||||||
|
dataFields: ['pH值', '有机质', '氮磷钾含量', '土壤质地', '盐分'],
|
||||||
|
description: '区域土壤理化性质数据',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ext-4',
|
||||||
|
name: '光照辐射数据',
|
||||||
|
type: '气象数据',
|
||||||
|
provider: '光伏气象站网络',
|
||||||
|
accessMethod: 'FTP传输',
|
||||||
|
updateFrequency: '每30分钟',
|
||||||
|
lastUpdateTime: '2024-10-15 13:30:00',
|
||||||
|
dataPoints: 15620,
|
||||||
|
status: '正常',
|
||||||
|
dataFields: ['总辐射', '直接辐射', '散射辐射', '光合有效辐射'],
|
||||||
|
description: '太阳辐射数据,用于光合作用分析',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ext-5',
|
||||||
|
name: '作物生长监测系统',
|
||||||
|
type: '作物生长',
|
||||||
|
provider: '农业大学作物研究所',
|
||||||
|
accessMethod: 'WebSocket',
|
||||||
|
updateFrequency: '实时',
|
||||||
|
lastUpdateTime: '2024-10-15 14:05:00',
|
||||||
|
dataPoints: 8920,
|
||||||
|
status: '异常',
|
||||||
|
dataFields: ['株高', '叶面积指数', '生物量', '产量预测', '病虫害指数'],
|
||||||
|
description: '作物生长实时监测数据,提供生长预测分析',
|
||||||
|
},
|
||||||
|
];
|
||||||
1252
crop-x/src/app/(app)/ai-crop-model/data-sense-center/iot/page.tsx
Normal file
1252
crop-x/src/app/(app)/ai-crop-model/data-sense-center/iot/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -977,17 +977,27 @@ const aiCropModel = {
|
|||||||
navMain: [
|
navMain: [
|
||||||
{
|
{
|
||||||
title: "全域数据感知中心",
|
title: "全域数据感知中心",
|
||||||
url: "/ai-crop-model/data-center",
|
url: "/ai-crop-model/data-sense-center",
|
||||||
icon: <Cloud className="w-4 h-4" />,
|
icon: <Cloud className="w-4 h-4" />,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "外部数据",
|
title: "多源数据接入",
|
||||||
url: "/ai-crop-model/data-center/external",
|
url: "/ai-crop-model/data-sense-center/external",
|
||||||
isActive: false
|
isActive: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "IoT数据",
|
title: "物联设备数据接入",
|
||||||
url: "/ai-crop-model/data-center/iot",
|
url: "/ai-crop-model/data-sense-center/iot",
|
||||||
|
isActive: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "设备类型管理",
|
||||||
|
url: "/ai-crop-model/data-sense-center/device-type",
|
||||||
|
isActive: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "设备参数管理",
|
||||||
|
url: "/ai-crop-model/data-sense-center/device-parameter",
|
||||||
isActive: false
|
isActive: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -14,5 +14,5 @@ import type { ClientOptions as ClientOptions2 } from './types.gen';
|
|||||||
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
|
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
|
||||||
|
|
||||||
export const client = createClient(createConfig<ClientOptions2>({
|
export const client = createClient(createConfig<ClientOptions2>({
|
||||||
baseUrl: 'http://pengcode.tech:8080'
|
baseUrl: 'https://gitea-admin-hm-smart-agri-app.dev.maimaiag.com'
|
||||||
}));
|
}));
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
1231
package-lock.json
generated
1231
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -35,11 +35,14 @@
|
|||||||
"date-fns": "*",
|
"date-fns": "*",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
|
"jspdf": "^3.0.3",
|
||||||
"lucide-react": "^0.487.0",
|
"lucide-react": "^0.487.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"qrcode": "*",
|
"qrcode": "*",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
|
"react-dnd": "^16.0.1",
|
||||||
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.55.0",
|
"react-hook-form": "^7.55.0",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^2.1.7",
|
||||||
|
|||||||
@@ -523,7 +523,7 @@ export function AIDataCenter({ activePath }: AIDataCenterProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h2>全域数据感知中心</h2>
|
<h2>多数据源接入</h2>
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
多源数据智能接入,构建全面精准的农业数据底座
|
多源数据智能接入,构建全面精准的农业数据底座
|
||||||
</p>
|
</p>
|
||||||
@@ -1246,62 +1246,100 @@ export function AIDataCenter({ activePath }: AIDataCenterProps) {
|
|||||||
|
|
||||||
{/* 设备配置对话框 */}
|
{/* 设备配置对话框 */}
|
||||||
<Dialog open={showDeviceDialog} onOpenChange={setShowDeviceDialog}>
|
<Dialog open={showDeviceDialog} onOpenChange={setShowDeviceDialog}>
|
||||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{selectedDevice ? '编辑设备' : '添加设备'}</DialogTitle>
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Settings className="w-5 h-5" />
|
||||||
|
{selectedDevice ? '编辑设备配置' : '添加新设备'}
|
||||||
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
配置物联网设备的连接参数和传感器信息
|
配置IoT设备的基本信息、通信参数和数据采集设置
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 基本信息 */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<h4 className="text-lg font-medium flex items-center gap-2">
|
||||||
|
<Settings className="w-4 h-4" />
|
||||||
|
基本信息
|
||||||
|
</h4>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>设备编号</Label>
|
<Label>设备编号</Label>
|
||||||
<Input placeholder="自动生成或手动输入" defaultValue={selectedDevice?.code} />
|
<Input placeholder="如: WS-2024-001" defaultValue={selectedDevice?.code} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>设备名称</Label>
|
<Label>设备名称</Label>
|
||||||
<Input placeholder="输入设备名称" defaultValue={selectedDevice?.name} />
|
<Input placeholder="如: 1号气象站" defaultValue={selectedDevice?.name} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
<div>
|
||||||
<Label>设备类型</Label>
|
<Label>设备类型</Label>
|
||||||
|
{deviceTypes.length > 0 ? (
|
||||||
<Select defaultValue={selectedDevice?.deviceTypeId}>
|
<Select defaultValue={selectedDevice?.deviceTypeId}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="选择设备类型" />
|
<SelectValue placeholder="选择设备类型" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{deviceTypes.length > 0 ? (
|
{deviceTypes.map(type => (
|
||||||
deviceTypes.map(type => (
|
|
||||||
<SelectItem key={type.id} value={type.id}>
|
<SelectItem key={type.id} value={type.id}>
|
||||||
{type.name}
|
{type.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))
|
))}
|
||||||
) : (
|
|
||||||
<SelectItem value="" disabled>暂无设备类型,请先在设备类型管理中添加</SelectItem>
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
) : (
|
||||||
<div>
|
<Select>
|
||||||
<Label>设备型号</Label>
|
<SelectTrigger>
|
||||||
<Input placeholder="输入设备型号" defaultValue={selectedDevice?.model} />
|
<SelectValue placeholder="选择设备类型" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="weather_station">气象站</SelectItem>
|
||||||
|
<SelectItem value="soil_sensor">土壤传感器</SelectItem>
|
||||||
|
<SelectItem value="camera">摄像头</SelectItem>
|
||||||
|
<SelectItem value="water_sensor">水质传感器</SelectItem>
|
||||||
|
<SelectItem value="env_station">环境监测站</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>生产厂家</Label>
|
<Label>制造商</Label>
|
||||||
<Input placeholder="输入厂家名称" defaultValue={selectedDevice?.manufacturer} />
|
<Select defaultValue={selectedDevice?.manufacturer}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="选择制造商" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="华为">华为</SelectItem>
|
||||||
|
<SelectItem value="中科院">中科院</SelectItem>
|
||||||
|
<SelectItem value="海康威视">海康威视</SelectItem>
|
||||||
|
<SelectItem value="绿盟科技">绿盟科技</SelectItem>
|
||||||
|
<SelectItem value="其他">其他</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>设备型号</Label>
|
||||||
|
<Input placeholder="如: HW-WS-Pro" defaultValue={selectedDevice?.model} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 位置信息 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-lg font-medium flex items-center gap-2">
|
||||||
|
<MapPin className="w-4 h-4" />
|
||||||
|
位置信息
|
||||||
|
</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>所属地块</Label>
|
<Label>所属地块</Label>
|
||||||
<Select defaultValue={selectedDevice?.fieldId}>
|
<Select defaultValue={selectedDevice?.fieldId}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="选择地块" />
|
<SelectValue placeholder="选择所属地块" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{fieldsList.map(field => (
|
{fieldsList.map(field => (
|
||||||
@@ -1315,14 +1353,25 @@ export function AIDataCenter({ activePath }: AIDataCenterProps) {
|
|||||||
选择地块后,设备位置将自动从地块信息中获取
|
选择地块后,设备位置将自动从地块信息中获取
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>安装位置</Label>
|
||||||
|
<Input placeholder="如: 1号大棚北侧" defaultValue={selectedDevice?.location} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 通信配置 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-lg font-medium flex items-center gap-2">
|
||||||
|
<Wifi className="w-4 h-4" />
|
||||||
|
通信配置
|
||||||
|
</h4>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>通信协议</Label>
|
<Label>通信协议</Label>
|
||||||
<Select defaultValue={selectedDevice?.protocol}>
|
<Select defaultValue={selectedDevice?.protocol}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="选择协议" />
|
<SelectValue placeholder="选择通信协议" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="MQTT">MQTT</SelectItem>
|
<SelectItem value="MQTT">MQTT</SelectItem>
|
||||||
@@ -1333,31 +1382,133 @@ export function AIDataCenter({ activePath }: AIDataCenterProps) {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>上报频率</Label>
|
<Label>数据采集频率</Label>
|
||||||
<Select defaultValue={selectedDevice?.dataFrequency}>
|
<Select defaultValue={selectedDevice?.dataFrequency}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="选择上报频率" />
|
<SelectValue placeholder="选择数据频率" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="实时">实时</SelectItem>
|
<SelectItem value="实时">实时</SelectItem>
|
||||||
<SelectItem value="每分钟">每分钟</SelectItem>
|
<SelectItem value="每1分钟">每1分钟</SelectItem>
|
||||||
<SelectItem value="每5分钟">每5分钟</SelectItem>
|
<SelectItem value="每5分钟">每5分钟</SelectItem>
|
||||||
<SelectItem value="每10分钟">每10分钟</SelectItem>
|
<SelectItem value="每10分钟">每10分钟</SelectItem>
|
||||||
<SelectItem value="每30分钟">每30分钟</SelectItem>
|
<SelectItem value="每30分钟">每30分钟</SelectItem>
|
||||||
|
<SelectItem value="每小时">每小时</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>MQTT主题/IP地址</Label>
|
<Label>IP地址</Label>
|
||||||
<Input placeholder="输入MQTT主题或IP地址" defaultValue={selectedDevice?.mqttTopic || selectedDevice?.ipAddress} />
|
<Input placeholder="如: 192.168.1.100" defaultValue={selectedDevice?.ipAddress} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>MQTT主题</Label>
|
||||||
|
<Input placeholder="如: farm/weather/station01" defaultValue={selectedDevice?.mqttTopic} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between p-4 bg-success/10 rounded-lg">
|
{/* 传感器配置 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-lg font-medium flex items-center gap-2">
|
||||||
|
<Gauge className="w-4 h-4" />
|
||||||
|
传感器配置
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="grid grid-cols-4 gap-4 text-sm font-medium text-muted-foreground">
|
||||||
|
<div>传感器名称</div>
|
||||||
|
<div>数据单位</div>
|
||||||
|
<div>正常范围</div>
|
||||||
|
<div>启用状态</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 默认传感器配置 */}
|
||||||
|
<div className="grid grid-cols-4 gap-4 items-center">
|
||||||
|
<Input placeholder="温度" defaultValue="温度" />
|
||||||
|
<Input placeholder="℃" defaultValue="℃" />
|
||||||
|
<Input placeholder="-10~40" defaultValue="-10~40" />
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 gap-4 items-center">
|
||||||
|
<Input placeholder="湿度" defaultValue="湿度" />
|
||||||
|
<Input placeholder="%" defaultValue="%" />
|
||||||
|
<Input placeholder="0~100" defaultValue="0~100" />
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 gap-4 items-center">
|
||||||
|
<Input placeholder="气压" defaultValue="气压" />
|
||||||
|
<Input placeholder="hPa" defaultValue="hPa" />
|
||||||
|
<Input placeholder="950~1050" defaultValue="950~1050" />
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" className="w-full">
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
添加传感器
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 绑定配置 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-lg font-medium flex items-center gap-2">
|
||||||
|
<Link className="w-4 h-4" />
|
||||||
|
绑定配置
|
||||||
|
</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>绑定状态</Label>
|
||||||
|
<Select defaultValue={selectedDevice?.bindingStatus}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="选择绑定状态" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="未绑定">未绑定</SelectItem>
|
||||||
|
<SelectItem value="已绑定">已绑定</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>绑定系统</Label>
|
||||||
|
<Select defaultValue={selectedDevice?.bindingSystem}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="选择绑定系统" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="智能灌溉控制系统">智能灌溉控制系统</SelectItem>
|
||||||
|
<SelectItem value="视频监控系统">视频监控系统</SelectItem>
|
||||||
|
<SelectItem value="环境监测系统">环境监测系统</SelectItem>
|
||||||
|
<SelectItem value="生产管理系统">生产管理系统</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 备注信息 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-lg font-medium flex items-center gap-2">
|
||||||
|
<FileText className="w-4 h-4" />
|
||||||
|
备注信息
|
||||||
|
</h4>
|
||||||
|
<div>
|
||||||
|
<Label>设备描述</Label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="请输入设备的详细描述信息,包括功能特点、安装要求等"
|
||||||
|
className="resize-none"
|
||||||
|
rows={3}
|
||||||
|
defaultValue={selectedDevice ? `${selectedDevice.manufacturer} ${selectedDevice.model},${selectedDevice.sensors.length}个传感器` : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 启用和测试 */}
|
||||||
|
<div className="flex items-center justify-between p-4 bg-green-50 dark:bg-green-950/30 rounded-lg border border-green-200 dark:border-green-800">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Switch defaultChecked />
|
<Switch defaultChecked />
|
||||||
<span className="text-sm">启用设备</span>
|
<span className="text-sm font-medium">启用设备</span>
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" variant="outline" onClick={handleTestConnection}>
|
<Button size="sm" variant="outline" onClick={handleTestConnection}>
|
||||||
<Link className="w-3 h-3 mr-1" />
|
<Link className="w-3 h-3 mr-1" />
|
||||||
@@ -1366,14 +1517,14 @@ export function AIDataCenter({ activePath }: AIDataCenterProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||||
<Button variant="outline" onClick={() => setShowDeviceDialog(false)}>
|
<Button variant="outline" onClick={() => setShowDeviceDialog(false)}>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="bg-success hover:bg-success/90" onClick={handleSaveDevice}>
|
<Button className="bg-green-600 hover:bg-green-700" onClick={handleSaveDevice}>
|
||||||
保存配置
|
{selectedDevice ? '保存配置' : '添加设备'}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user