生产管理系统 - 全领域数据感知中心开发
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:统计分析页面开发经验
|
||||
|
||||
### 总体开发经验总结
|
||||
|
||||
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
|
||||
|
||||
# 后端 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_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: [
|
||||
{
|
||||
title: "全域数据感知中心",
|
||||
url: "/ai-crop-model/data-center",
|
||||
url: "/ai-crop-model/data-sense-center",
|
||||
icon: <Cloud className="w-4 h-4" />,
|
||||
items: [
|
||||
{
|
||||
title: "外部数据",
|
||||
url: "/ai-crop-model/data-center/external",
|
||||
title: "多源数据接入",
|
||||
url: "/ai-crop-model/data-sense-center/external",
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
title: "IoT数据",
|
||||
url: "/ai-crop-model/data-center/iot",
|
||||
title: "物联设备数据接入",
|
||||
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
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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 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": "*",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"jspdf": "^3.0.3",
|
||||
"lucide-react": "^0.487.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"qrcode": "*",
|
||||
"react": "^18.3.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-hook-form": "^7.55.0",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
|
||||
@@ -523,7 +523,7 @@ export function AIDataCenter({ activePath }: AIDataCenterProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2>全域数据感知中心</h2>
|
||||
<h2>多数据源接入</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
多源数据智能接入,构建全面精准的农业数据底座
|
||||
</p>
|
||||
@@ -1246,62 +1246,100 @@ export function AIDataCenter({ activePath }: AIDataCenterProps) {
|
||||
|
||||
{/* 设备配置对话框 */}
|
||||
<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>
|
||||
<DialogTitle>{selectedDevice ? '编辑设备' : '添加设备'}</DialogTitle>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Settings className="w-5 h-5" />
|
||||
{selectedDevice ? '编辑设备配置' : '添加新设备'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
配置物联网设备的连接参数和传感器信息
|
||||
配置IoT设备的基本信息、通信参数和数据采集设置
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 基本信息 */}
|
||||
<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>
|
||||
<Label>设备编号</Label>
|
||||
<Input placeholder="自动生成或手动输入" defaultValue={selectedDevice?.code} />
|
||||
<Input placeholder="如: WS-2024-001" defaultValue={selectedDevice?.code} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>设备名称</Label>
|
||||
<Input placeholder="输入设备名称" defaultValue={selectedDevice?.name} />
|
||||
<Input placeholder="如: 1号气象站" defaultValue={selectedDevice?.name} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>设备类型</Label>
|
||||
{deviceTypes.length > 0 ? (
|
||||
<Select defaultValue={selectedDevice?.deviceTypeId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择设备类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{deviceTypes.length > 0 ? (
|
||||
deviceTypes.map(type => (
|
||||
{deviceTypes.map(type => (
|
||||
<SelectItem key={type.id} value={type.id}>
|
||||
{type.name}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="" disabled>暂无设备类型,请先在设备类型管理中添加</SelectItem>
|
||||
)}
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>设备型号</Label>
|
||||
<Input placeholder="输入设备型号" defaultValue={selectedDevice?.model} />
|
||||
) : (
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<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 className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>生产厂家</Label>
|
||||
<Input placeholder="输入厂家名称" defaultValue={selectedDevice?.manufacturer} />
|
||||
<Label>制造商</Label>
|
||||
<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>
|
||||
<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>
|
||||
<Label>所属地块</Label>
|
||||
<Select defaultValue={selectedDevice?.fieldId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择地块" />
|
||||
<SelectValue placeholder="选择所属地块" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fieldsList.map(field => (
|
||||
@@ -1315,14 +1353,25 @@ export function AIDataCenter({ activePath }: AIDataCenterProps) {
|
||||
选择地块后,设备位置将自动从地块信息中获取
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>安装位置</Label>
|
||||
<Input placeholder="如: 1号大棚北侧" defaultValue={selectedDevice?.location} />
|
||||
</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>
|
||||
<Label>通信协议</Label>
|
||||
<Select defaultValue={selectedDevice?.protocol}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择协议" />
|
||||
<SelectValue placeholder="选择通信协议" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="MQTT">MQTT</SelectItem>
|
||||
@@ -1333,31 +1382,133 @@ export function AIDataCenter({ activePath }: AIDataCenterProps) {
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>上报频率</Label>
|
||||
<Label>数据采集频率</Label>
|
||||
<Select defaultValue={selectedDevice?.dataFrequency}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择上报频率" />
|
||||
<SelectValue placeholder="选择数据频率" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="实时">实时</SelectItem>
|
||||
<SelectItem value="每分钟">每分钟</SelectItem>
|
||||
<SelectItem value="每1分钟">每1分钟</SelectItem>
|
||||
<SelectItem value="每5分钟">每5分钟</SelectItem>
|
||||
<SelectItem value="每10分钟">每10分钟</SelectItem>
|
||||
<SelectItem value="每30分钟">每30分钟</SelectItem>
|
||||
<SelectItem value="每小时">每小时</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>MQTT主题/IP地址</Label>
|
||||
<Input placeholder="输入MQTT主题或IP地址" defaultValue={selectedDevice?.mqttTopic || selectedDevice?.ipAddress} />
|
||||
<Label>IP地址</Label>
|
||||
<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 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">
|
||||
<Switch defaultChecked />
|
||||
<span className="text-sm">启用设备</span>
|
||||
<span className="text-sm font-medium">启用设备</span>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" onClick={handleTestConnection}>
|
||||
<Link className="w-3 h-3 mr-1" />
|
||||
@@ -1366,14 +1517,14 @@ export function AIDataCenter({ activePath }: AIDataCenterProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||
<Button variant="outline" onClick={() => setShowDeviceDialog(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button className="bg-success hover:bg-success/90" onClick={handleSaveDevice}>
|
||||
保存配置
|
||||
<Button className="bg-green-600 hover:bg-green-700" onClick={handleSaveDevice}>
|
||||
{selectedDevice ? '保存配置' : '添加设备'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user