|
|
@ -1,24 +1,26 @@ |
|
|
|
import React, { useEffect, useState } from 'react'; |
|
|
|
import { deviceService, Device } from '@/services/deviceService'; |
|
|
|
import React, { useState, useEffect } from 'react'; |
|
|
|
import { getDevices, Device, GetDevicesRequest, createDevice, updateDevice, deleteDevice, CreateDeviceRequest, UpdateDeviceRequest } from '@/services/instrumentService'; |
|
|
|
import DevicesTable from './DevicesTable'; |
|
|
|
import DeviceForm from './DeviceForm'; |
|
|
|
import { Input } from '@/components/ui/input'; |
|
|
|
import PaginationBar from '@/components/ui/PaginationBar'; |
|
|
|
import TableToolbar, { DensityType } from '@/components/ui/TableToolbar'; |
|
|
|
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'; |
|
|
|
import { Button } from '@/components/ui/button'; |
|
|
|
import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'; |
|
|
|
import { useToast } from '@/components/ui/use-toast'; |
|
|
|
|
|
|
|
const defaultColumns = [ |
|
|
|
{ key: 'deviceId', title: '设备ID', visible: true }, |
|
|
|
{ key: 'deviceId', title: '设备ID', visible: false }, |
|
|
|
{ key: 'deviceName', title: '设备名称', visible: true }, |
|
|
|
{ key: 'deviceType', title: '设备类型', visible: true }, |
|
|
|
{ key: 'status', title: '状态', visible: true }, |
|
|
|
{ key: 'protocolName', title: '协议', visible: true }, |
|
|
|
{ key: 'ipAddress', title: 'IP地址', visible: true }, |
|
|
|
{ key: 'location', title: '位置', visible: true }, |
|
|
|
{ key: 'manufacturer', title: '制造商', visible: true }, |
|
|
|
{ key: 'model', title: '型号', visible: true }, |
|
|
|
{ key: 'lastHeartbeat', title: '最后心跳', visible: true }, |
|
|
|
{ key: 'createdBy', title: '创建人', visible: true }, |
|
|
|
{ key: 'actions', title: '操作', visible: true } |
|
|
|
{ key: 'serialNumber', title: '序列号', visible: true }, |
|
|
|
{ key: 'description', title: '描述', visible: true }, |
|
|
|
{ key: 'protocolVersion', title: '协议版本', visible: true }, |
|
|
|
{ key: 'agentPort', title: 'Agent端口', visible: true }, |
|
|
|
{ key: 'isEnabled', title: '状态', visible: true }, |
|
|
|
{ key: 'isRunning', title: '运行状态', visible: true }, |
|
|
|
{ key: 'createdAt', title: '创建时间', visible: true }, |
|
|
|
{ key: 'actions', title: '操作', visible: true }, |
|
|
|
]; |
|
|
|
|
|
|
|
// 字段类型声明
|
|
|
@ -28,94 +30,246 @@ type SearchField = |
|
|
|
|
|
|
|
// 第一行字段(收起时只显示这3个)
|
|
|
|
const firstRowFields: SearchField[] = [ |
|
|
|
{ key: 'deviceId', label: '设备ID', type: 'input', placeholder: '请输入' }, |
|
|
|
{ key: 'deviceType', label: '设备类型', type: 'select', options: [ |
|
|
|
{ key: 'searchTerm', label: '搜索关键词', type: 'input', placeholder: '请输入设备名称或序列号' }, |
|
|
|
{ key: 'isEnabled', label: '状态', type: 'select', options: [ |
|
|
|
{ value: '', label: '请选择' }, |
|
|
|
{ value: 'sensor', label: '传感器' }, |
|
|
|
{ value: 'controller', label: '控制器' }, |
|
|
|
{ value: 'monitor', label: '监视器' }, |
|
|
|
{ value: 'actuator', label: '执行器' }, |
|
|
|
{ value: 'gateway', label: '网关' }, |
|
|
|
] }, |
|
|
|
{ key: 'status', label: '状态', type: 'select', options: [ |
|
|
|
{ value: '', label: '请选择' }, |
|
|
|
{ value: 'online', label: '在线' }, |
|
|
|
{ value: 'offline', label: '离线' }, |
|
|
|
{ value: 'maintenance', label: '维护中' }, |
|
|
|
{ value: 'error', label: '错误' }, |
|
|
|
{ value: 'true', label: '启用' }, |
|
|
|
{ value: 'false', label: '禁用' }, |
|
|
|
] }, |
|
|
|
]; |
|
|
|
|
|
|
|
// 高级字段(展开时才显示)
|
|
|
|
const advancedFields: SearchField[] = [ |
|
|
|
{ key: 'location', label: '位置', type: 'input', placeholder: '请输入' }, |
|
|
|
{ key: 'manufacturer', label: '制造商', type: 'input', placeholder: '请输入' }, |
|
|
|
{ key: 'pageSize', label: '每页数量', type: 'select', options: [ |
|
|
|
{ value: '10', label: '10条/页' }, |
|
|
|
{ value: '20', label: '20条/页' }, |
|
|
|
{ value: '50', label: '50条/页' }, |
|
|
|
{ value: '100', label: '100条/页' }, |
|
|
|
] }, |
|
|
|
]; |
|
|
|
|
|
|
|
/** |
|
|
|
* 设备管理页面 |
|
|
|
* |
|
|
|
* 表格字段映射关系(基于后端GetDeviceByIdResponse): |
|
|
|
* - deviceId: 设备ID |
|
|
|
* - deviceName: 设备名称 |
|
|
|
* - serialNumber: 序列号 |
|
|
|
* - description: 设备描述 |
|
|
|
* - protocolVersion: 协议版本 |
|
|
|
* - agentPort: Agent端口 |
|
|
|
* - isEnabled: 是否启用 |
|
|
|
* - isRunning: 设备状态(启动/未启动) |
|
|
|
* - createdAt: 创建时间 |
|
|
|
* |
|
|
|
* 注意:IpAddress字段在创建/更新时使用,但查询响应中不返回 |
|
|
|
*/ |
|
|
|
|
|
|
|
export default function DevicesView() { |
|
|
|
const [devices, setDevices] = useState<Device[]>([]); |
|
|
|
const [loading, setLoading] = useState(false); |
|
|
|
const [total, setTotal] = useState(0); |
|
|
|
const [deviceId, setDeviceId] = useState(''); |
|
|
|
const [page, setPage] = useState(1); |
|
|
|
const [pageNumber, setPageNumber] = useState(1); |
|
|
|
const [pageSize, setPageSize] = useState(10); |
|
|
|
const [density, setDensity] = useState<DensityType>('default'); |
|
|
|
const [columns, setColumns] = useState(defaultColumns); |
|
|
|
const [showAdvanced, setShowAdvanced] = useState(false); |
|
|
|
|
|
|
|
const fetchDevices = async (params = {}) => { |
|
|
|
// 搜索参数
|
|
|
|
const [searchTerm, setSearchTerm] = useState(''); |
|
|
|
const [isEnabled, setIsEnabled] = useState<boolean | undefined>(undefined); |
|
|
|
|
|
|
|
// 表单对话框状态
|
|
|
|
const [open, setOpen] = useState(false); |
|
|
|
const [editOpen, setEditOpen] = useState(false); |
|
|
|
const [selectedDevice, setSelectedDevice] = useState<Device | null>(null); |
|
|
|
|
|
|
|
// 提交状态
|
|
|
|
const [isSubmitting, setIsSubmitting] = useState(false); |
|
|
|
|
|
|
|
// Toast 提示
|
|
|
|
const { toast } = useToast(); |
|
|
|
|
|
|
|
const fetchDevices = async (params: Partial<GetDevicesRequest> = {}) => { |
|
|
|
setLoading(true); |
|
|
|
const result = await deviceService.getAllDevices({ deviceId, page, pageSize, ...params }); |
|
|
|
if (result.isSuccess && result.data) { |
|
|
|
setDevices(result.data.devices || []); |
|
|
|
setTotal(result.data.totalCount || 0); |
|
|
|
const queryParams: GetDevicesRequest = { |
|
|
|
pageNumber, |
|
|
|
pageSize, |
|
|
|
searchTerm, |
|
|
|
isDisabled: isEnabled === false, // 注意:后端使用isDisabled,前端使用isEnabled
|
|
|
|
...params |
|
|
|
}; |
|
|
|
|
|
|
|
try { |
|
|
|
const result = await getDevices(queryParams); |
|
|
|
if (result.isSuccess && result.data) { |
|
|
|
setDevices(result.data.items); |
|
|
|
setTotal(result.data.totalCount); |
|
|
|
} else { |
|
|
|
console.error('获取设备列表失败:', result.errorMessages); |
|
|
|
setDevices([]); |
|
|
|
setTotal(0); |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
console.error('获取设备列表异常:', error); |
|
|
|
setDevices([]); |
|
|
|
setTotal(0); |
|
|
|
} finally { |
|
|
|
setLoading(false); |
|
|
|
} |
|
|
|
setLoading(false); |
|
|
|
}; |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
fetchDevices(); |
|
|
|
// eslint-disable-next-line
|
|
|
|
}, [page, pageSize]); |
|
|
|
}, [pageNumber, pageSize]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const handleEdit = (device: Device) => { |
|
|
|
setSelectedDevice(device); |
|
|
|
setEditOpen(true); |
|
|
|
}; |
|
|
|
|
|
|
|
const handleDelete = async (device: Device) => { |
|
|
|
if (confirm(`确定要删除设备 "${device.deviceName}" 吗?`)) { |
|
|
|
try { |
|
|
|
const result = await deleteDevice(device.deviceId); |
|
|
|
if (result.isSuccess) { |
|
|
|
toast({ |
|
|
|
title: "删除成功", |
|
|
|
description: `设备 "${device.deviceName}" 删除成功`, |
|
|
|
}); |
|
|
|
fetchDevices(); |
|
|
|
} else { |
|
|
|
const errorMessage = result.errorMessages?.join(', ') || "删除设备时发生错误"; |
|
|
|
console.error('删除设备失败:', errorMessage); |
|
|
|
toast({ |
|
|
|
title: "删除失败", |
|
|
|
description: errorMessage, |
|
|
|
variant: "destructive", |
|
|
|
}); |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
console.error('删除设备异常:', error); |
|
|
|
toast({ |
|
|
|
title: "删除失败", |
|
|
|
description: "网络错误,请稍后重试", |
|
|
|
variant: "destructive", |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const handleCreate = async (data: CreateDeviceRequest) => { |
|
|
|
if (isSubmitting) return; // 防止重复提交
|
|
|
|
|
|
|
|
const handleView = (device: Device) => { |
|
|
|
// 这里可以实现查看设备详情的逻辑
|
|
|
|
console.log('查看设备:', device); |
|
|
|
console.log('开始创建设备:', data); |
|
|
|
setIsSubmitting(true); |
|
|
|
try { |
|
|
|
const result = await createDevice(data); |
|
|
|
console.log('创建设备结果:', result); |
|
|
|
|
|
|
|
if (result.isSuccess) { |
|
|
|
toast({ |
|
|
|
title: "创建成功", |
|
|
|
description: `设备 "${data.deviceName}" 创建成功`, |
|
|
|
}); |
|
|
|
setOpen(false); |
|
|
|
fetchDevices(); |
|
|
|
} else { |
|
|
|
const errorMessage = result.errorMessages?.join(', ') || "创建设备时发生错误"; |
|
|
|
console.error('创建设备失败:', errorMessage, result); |
|
|
|
toast({ |
|
|
|
title: "创建失败", |
|
|
|
description: errorMessage, |
|
|
|
variant: "destructive", |
|
|
|
}); |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
console.error('创建设备异常:', error); |
|
|
|
toast({ |
|
|
|
title: "创建失败", |
|
|
|
description: "网络错误,请稍后重试", |
|
|
|
variant: "destructive", |
|
|
|
}); |
|
|
|
} finally { |
|
|
|
setIsSubmitting(false); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const handleTestConnection = (device: Device) => { |
|
|
|
// 这里可以实现测试设备连接的逻辑
|
|
|
|
console.log('测试设备连接:', device); |
|
|
|
const handleUpdate = async (data: UpdateDeviceRequest) => { |
|
|
|
if (!selectedDevice || isSubmitting) return; // 防止重复提交
|
|
|
|
|
|
|
|
setIsSubmitting(true); |
|
|
|
try { |
|
|
|
const updateData: UpdateDeviceRequest = { |
|
|
|
...data, |
|
|
|
deviceId: selectedDevice.deviceId |
|
|
|
}; |
|
|
|
const result = await updateDevice(selectedDevice.deviceId, updateData); |
|
|
|
if (result.isSuccess) { |
|
|
|
toast({ |
|
|
|
title: "更新成功", |
|
|
|
description: `设备 "${data.deviceName}" 更新成功`, |
|
|
|
}); |
|
|
|
setEditOpen(false); |
|
|
|
setSelectedDevice(null); |
|
|
|
fetchDevices(); |
|
|
|
} else { |
|
|
|
const errorMessage = result.errorMessages?.join(', ') || "更新设备时发生错误"; |
|
|
|
console.error('更新设备失败:', errorMessage); |
|
|
|
toast({ |
|
|
|
title: "更新失败", |
|
|
|
description: errorMessage, |
|
|
|
variant: "destructive", |
|
|
|
}); |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
console.error('更新设备异常:', error); |
|
|
|
toast({ |
|
|
|
title: "更新失败", |
|
|
|
description: "网络错误,请稍后重试", |
|
|
|
variant: "destructive", |
|
|
|
}); |
|
|
|
} finally { |
|
|
|
setIsSubmitting(false); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 查询按钮
|
|
|
|
const handleQuery = () => { |
|
|
|
setPage(1); |
|
|
|
fetchDevices({ page: 1 }); |
|
|
|
setPageNumber(1); |
|
|
|
fetchDevices({ pageNumber: 1 }); |
|
|
|
}; |
|
|
|
|
|
|
|
// 重置按钮
|
|
|
|
const handleReset = () => { |
|
|
|
setDeviceId(''); |
|
|
|
setPage(1); |
|
|
|
fetchDevices({ deviceId: '', page: 1 }); |
|
|
|
setSearchTerm(''); |
|
|
|
setIsEnabled(undefined); |
|
|
|
setPageNumber(1); |
|
|
|
fetchDevices({ |
|
|
|
searchTerm: '', |
|
|
|
isDisabled: undefined, |
|
|
|
pageNumber: 1 |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
// 每页条数选择
|
|
|
|
const handlePageSizeChange = (size: number) => { |
|
|
|
setPageSize(size); |
|
|
|
setPage(1); |
|
|
|
setPageNumber(1); |
|
|
|
}; |
|
|
|
|
|
|
|
const totalPages = Math.ceil(total / pageSize); |
|
|
|
|
|
|
|
return ( |
|
|
|
<main className="flex-1 p-4 transition-all duration-300 ease-in-out sm:p-6"> |
|
|
|
<div className="w-full space-y-4"> |
|
|
|
{/* 丰富美化后的搜索栏 */} |
|
|
|
<div className="flex flex-col bg-white p-4 rounded-md border mb-2"> |
|
|
|
<form |
|
|
|
className={`grid gap-x-8 gap-y-4 items-center ${showAdvanced ? 'md:grid-cols-3' : 'md:grid-cols-4'} grid-cols-1`} |
|
|
|
className={`grid gap-x-8 gap-y-4 items-center ${showAdvanced ? 'md:grid-cols-3' : 'md:grid-cols-3'} grid-cols-1`} |
|
|
|
onSubmit={e => { |
|
|
|
e.preventDefault(); |
|
|
|
handleQuery(); |
|
|
@ -133,14 +287,26 @@ export default function DevicesView() { |
|
|
|
<Input |
|
|
|
className="input flex-1" |
|
|
|
placeholder={field.placeholder} |
|
|
|
value={field.key === 'deviceId' ? deviceId : ''} |
|
|
|
value={field.key === 'searchTerm' ? searchTerm : ''} |
|
|
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { |
|
|
|
if (field.key === 'deviceId') setDeviceId(e.target.value); |
|
|
|
if (field.key === 'searchTerm') setSearchTerm(e.target.value); |
|
|
|
}} |
|
|
|
/> |
|
|
|
)} |
|
|
|
{field.type === 'select' && ( |
|
|
|
<select className="input h-10 rounded border border-border bg-background px-3 text-sm flex-1"> |
|
|
|
<select |
|
|
|
className="input h-10 rounded border border-border bg-background px-3 text-sm flex-1" |
|
|
|
value={field.key === 'isEnabled' ? (isEnabled === undefined ? '' : isEnabled.toString()) : |
|
|
|
field.key === 'pageSize' ? pageSize.toString() : ''} |
|
|
|
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => { |
|
|
|
if (field.key === 'isEnabled') { |
|
|
|
const value = e.target.value; |
|
|
|
setIsEnabled(value === '' ? undefined : value === 'true'); |
|
|
|
} else if (field.key === 'pageSize') { |
|
|
|
setPageSize(parseInt(e.target.value)); |
|
|
|
} |
|
|
|
}} |
|
|
|
> |
|
|
|
{field.options.map(opt => ( |
|
|
|
<option value={opt.value} key={opt.value}>{opt.label}</option> |
|
|
|
))} |
|
|
@ -166,10 +332,19 @@ export default function DevicesView() { |
|
|
|
</div> |
|
|
|
</form> |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* 表格整体卡片区域,包括工具栏、表格、分页 */} |
|
|
|
<div className="rounded-md border bg-background p-4"> |
|
|
|
{/* 顶部工具栏 */} |
|
|
|
<div className="flex items-center justify-end mb-2"> |
|
|
|
{/* 顶部操作栏:添加设备+工具栏 */} |
|
|
|
<div className="flex items-center justify-between mb-2"> |
|
|
|
<Dialog open={open} onOpenChange={setOpen}> |
|
|
|
<DialogTrigger asChild> |
|
|
|
<Button className="bg-primary text-primary-foreground hover:bg-primary/90">+ 添加仪表</Button> |
|
|
|
</DialogTrigger> |
|
|
|
<DialogContent className="bg-background"> |
|
|
|
<DeviceForm onSubmit={handleCreate} isSubmitting={isSubmitting} /> |
|
|
|
</DialogContent> |
|
|
|
</Dialog> |
|
|
|
<TableToolbar |
|
|
|
onRefresh={() => fetchDevices()} |
|
|
|
onDensityChange={setDensity} |
|
|
@ -179,30 +354,53 @@ export default function DevicesView() { |
|
|
|
density={density} |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* 表格区域 */} |
|
|
|
<DevicesTable |
|
|
|
devices={devices} |
|
|
|
loading={loading} |
|
|
|
onView={handleView} |
|
|
|
onTestConnection={handleTestConnection} |
|
|
|
page={page} |
|
|
|
onEdit={handleEdit} |
|
|
|
onDelete={handleDelete} |
|
|
|
page={pageNumber} |
|
|
|
pageSize={pageSize} |
|
|
|
total={total} |
|
|
|
onPageChange={setPage} |
|
|
|
onPageChange={setPageNumber} |
|
|
|
hideCard={true} |
|
|
|
density={density} |
|
|
|
columns={columns} |
|
|
|
/> |
|
|
|
|
|
|
|
{/* 分页 */} |
|
|
|
<PaginationBar |
|
|
|
page={page} |
|
|
|
page={pageNumber} |
|
|
|
pageSize={pageSize} |
|
|
|
total={total} |
|
|
|
onPageChange={setPage} |
|
|
|
onPageChange={setPageNumber} |
|
|
|
onPageSizeChange={handlePageSizeChange} |
|
|
|
/> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* 编辑设备对话框 */} |
|
|
|
<Dialog open={editOpen} onOpenChange={setEditOpen}> |
|
|
|
<DialogContent className="bg-background"> |
|
|
|
<DeviceForm |
|
|
|
onSubmit={(data) => handleUpdate(data as UpdateDeviceRequest)} |
|
|
|
initialData={selectedDevice ? { |
|
|
|
deviceName: selectedDevice.deviceName, |
|
|
|
serialNumber: selectedDevice.serialNumber, |
|
|
|
description: selectedDevice.description, |
|
|
|
protocolVersionId: '', // 需要从后端获取协议版本ID
|
|
|
|
ipAddress: '', // 需要从后端获取IP地址
|
|
|
|
agentPort: selectedDevice.agentPort, |
|
|
|
isEnabled: selectedDevice.isEnabled, |
|
|
|
isRunning: selectedDevice.isRunning |
|
|
|
} : undefined} |
|
|
|
isEdit={true} |
|
|
|
isSubmitting={isSubmitting} |
|
|
|
/> |
|
|
|
</DialogContent> |
|
|
|
</Dialog> |
|
|
|
</main> |
|
|
|
); |
|
|
|
} |