15 changed files with 1010 additions and 790 deletions
@ -1,7 +1,7 @@ |
|||||
import React from 'react'; |
import React from 'react'; |
||||
import { Button } from '@/components/ui/button'; |
import { Button } from '@/components/ui/button'; |
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; |
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; |
||||
import { Config, ConfigType } from '@/services/instrumentService'; |
import { Config, ConfigType } from '@/services/configService'; |
||||
import { Badge } from '@/components/ui/badge'; |
import { Badge } from '@/components/ui/badge'; |
||||
import { EyeOpenIcon, CheckIcon, PlayIcon } from '@radix-ui/react-icons'; |
import { EyeOpenIcon, CheckIcon, PlayIcon } from '@radix-ui/react-icons'; |
||||
|
|
@ -1,254 +0,0 @@ |
|||||
import React from 'react'; |
|
||||
import { Button } from '@/components/ui/button'; |
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; |
|
||||
import { Protocol, ProtocolType } from '@/services/instrumentService'; |
|
||||
import { Badge } from '@/components/ui/badge'; |
|
||||
import { EyeOpenIcon, FileTextIcon } from '@radix-ui/react-icons'; |
|
||||
|
|
||||
interface ProtocolsTableProps { |
|
||||
protocols: Protocol[]; |
|
||||
loading: boolean; |
|
||||
onView: (protocol: Protocol) => void; |
|
||||
onViewDocumentation: (protocol: Protocol) => void; |
|
||||
page: number; |
|
||||
pageSize: number; |
|
||||
total: number; |
|
||||
onPageChange: (page: number) => void; |
|
||||
hideCard?: boolean; |
|
||||
density?: 'compact' | 'default' | 'comfortable'; |
|
||||
columns?: { key: string; title: string; visible: boolean }[]; |
|
||||
} |
|
||||
|
|
||||
// 协议类型徽章组件
|
|
||||
const ProtocolTypeBadge: React.FC<{ type: ProtocolType }> = ({ type }) => { |
|
||||
const typeConfig = { |
|
||||
modbus: { label: 'Modbus', className: 'bg-blue-100 text-blue-800' }, |
|
||||
opcua: { label: 'OPC UA', className: 'bg-green-100 text-green-800' }, |
|
||||
mqtt: { label: 'MQTT', className: 'bg-purple-100 text-purple-800' }, |
|
||||
http: { label: 'HTTP', className: 'bg-orange-100 text-orange-800' }, |
|
||||
tcp: { label: 'TCP', className: 'bg-indigo-100 text-indigo-800' }, |
|
||||
udp: { label: 'UDP', className: 'bg-teal-100 text-teal-800' }, |
|
||||
serial: { label: 'Serial', className: 'bg-gray-100 text-gray-800' }, |
|
||||
}; |
|
||||
|
|
||||
const config = typeConfig[type]; |
|
||||
return ( |
|
||||
<Badge className={config.className}> |
|
||||
{config.label} |
|
||||
</Badge> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
// 状态徽章组件
|
|
||||
const StatusBadge: React.FC<{ isActive: boolean }> = ({ isActive }) => { |
|
||||
return ( |
|
||||
<Badge className={isActive ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}> |
|
||||
{isActive ? '启用' : '禁用'} |
|
||||
</Badge> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
// 安全级别徽章组件
|
|
||||
const SecurityLevelBadge: React.FC<{ level: 'low' | 'medium' | 'high' }> = ({ level }) => { |
|
||||
const levelConfig = { |
|
||||
low: { label: '低', className: 'bg-green-100 text-green-800' }, |
|
||||
medium: { label: '中', className: 'bg-yellow-100 text-yellow-800' }, |
|
||||
high: { label: '高', className: 'bg-red-100 text-red-800' }, |
|
||||
}; |
|
||||
|
|
||||
const config = levelConfig[level]; |
|
||||
return ( |
|
||||
<Badge className={config.className}> |
|
||||
{config.label} |
|
||||
</Badge> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
// 加密类型徽章组件
|
|
||||
const EncryptionTypeBadge: React.FC<{ type: 'none' | 'ssl' | 'tls' | 'aes' }> = ({ type }) => { |
|
||||
const typeConfig = { |
|
||||
none: { label: '无', className: 'bg-gray-100 text-gray-800' }, |
|
||||
ssl: { label: 'SSL', className: 'bg-blue-100 text-blue-800' }, |
|
||||
tls: { label: 'TLS', className: 'bg-green-100 text-green-800' }, |
|
||||
aes: { label: 'AES', className: 'bg-purple-100 text-purple-800' }, |
|
||||
}; |
|
||||
|
|
||||
const config = typeConfig[type]; |
|
||||
return ( |
|
||||
<Badge className={config.className}> |
|
||||
{config.label} |
|
||||
</Badge> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
// 认证类型徽章组件
|
|
||||
const AuthenticationTypeBadge: React.FC<{ type: 'none' | 'basic' | 'token' | 'certificate' }> = ({ type }) => { |
|
||||
const typeConfig = { |
|
||||
none: { label: '无', className: 'bg-gray-100 text-gray-800' }, |
|
||||
basic: { label: '基本', className: 'bg-blue-100 text-blue-800' }, |
|
||||
token: { label: '令牌', className: 'bg-green-100 text-green-800' }, |
|
||||
certificate: { label: '证书', className: 'bg-purple-100 text-purple-800' }, |
|
||||
}; |
|
||||
|
|
||||
const config = typeConfig[type]; |
|
||||
return ( |
|
||||
<Badge className={config.className}> |
|
||||
{config.label} |
|
||||
</Badge> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
// 数据格式徽章组件
|
|
||||
const DataFormatBadge: React.FC<{ format: 'json' | 'xml' | 'binary' | 'csv' }> = ({ format }) => { |
|
||||
const formatConfig = { |
|
||||
json: { label: 'JSON', className: 'bg-blue-100 text-blue-800' }, |
|
||||
xml: { label: 'XML', className: 'bg-green-100 text-green-800' }, |
|
||||
binary: { label: 'Binary', className: 'bg-purple-100 text-purple-800' }, |
|
||||
csv: { label: 'CSV', className: 'bg-orange-100 text-orange-800' }, |
|
||||
}; |
|
||||
|
|
||||
const config = formatConfig[format]; |
|
||||
return ( |
|
||||
<Badge className={config.className}> |
|
||||
{config.label} |
|
||||
</Badge> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
export default function ProtocolsTable({ |
|
||||
protocols, |
|
||||
loading, |
|
||||
onView, |
|
||||
onViewDocumentation, |
|
||||
page, |
|
||||
pageSize, |
|
||||
total, |
|
||||
onPageChange, |
|
||||
hideCard = false, |
|
||||
density = 'default', |
|
||||
columns = [] |
|
||||
}: ProtocolsTableProps) { |
|
||||
const densityClasses = { |
|
||||
compact: 'py-1', |
|
||||
default: 'py-2', |
|
||||
comfortable: 'py-3', |
|
||||
}; |
|
||||
|
|
||||
const visibleColumns = columns.filter(col => col.visible); |
|
||||
|
|
||||
const renderCell = (protocol: Protocol, columnKey: string) => { |
|
||||
switch (columnKey) { |
|
||||
case 'protocolId': |
|
||||
return ( |
|
||||
<div className="font-medium"> |
|
||||
{protocol.protocolId} |
|
||||
</div> |
|
||||
); |
|
||||
case 'protocolName': |
|
||||
return ( |
|
||||
<div className="max-w-xs truncate" title={protocol.protocolName}> |
|
||||
{protocol.protocolName} |
|
||||
</div> |
|
||||
); |
|
||||
case 'protocolType': |
|
||||
return <ProtocolTypeBadge type={protocol.protocolType} />; |
|
||||
case 'version': |
|
||||
return <span>{protocol.version}</span>; |
|
||||
case 'isActive': |
|
||||
return <StatusBadge isActive={protocol.isActive} />; |
|
||||
case 'defaultPort': |
|
||||
return <span>{protocol.defaultPort}</span>; |
|
||||
case 'securityLevel': |
|
||||
return <SecurityLevelBadge level={protocol.securityLevel} />; |
|
||||
case 'encryptionType': |
|
||||
return <EncryptionTypeBadge type={protocol.encryptionType} />; |
|
||||
case 'authenticationType': |
|
||||
return <AuthenticationTypeBadge type={protocol.authenticationType} />; |
|
||||
case 'connectionTimeout': |
|
||||
return <span>{protocol.connectionTimeout}ms</span>; |
|
||||
case 'dataFormat': |
|
||||
return <DataFormatBadge format={protocol.dataFormat} />; |
|
||||
case 'createdBy': |
|
||||
return <span>{protocol.createdBy}</span>; |
|
||||
case 'actions': |
|
||||
return ( |
|
||||
<div className="flex items-center gap-2"> |
|
||||
<Button |
|
||||
variant="ghost" |
|
||||
size="sm" |
|
||||
onClick={() => onView(protocol)} |
|
||||
className="h-8 w-8 p-0" |
|
||||
> |
|
||||
<EyeOpenIcon className="h-4 w-4" /> |
|
||||
</Button> |
|
||||
<Button |
|
||||
variant="ghost" |
|
||||
size="sm" |
|
||||
onClick={() => onViewDocumentation(protocol)} |
|
||||
className="h-8 w-8 p-0" |
|
||||
> |
|
||||
<FileTextIcon className="h-4 w-4" /> |
|
||||
</Button> |
|
||||
</div> |
|
||||
); |
|
||||
default: |
|
||||
return null; |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
if (loading) { |
|
||||
return ( |
|
||||
<div className="flex items-center justify-center py-8"> |
|
||||
<div className="text-center"> |
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div> |
|
||||
<p className="mt-2 text-sm text-muted-foreground">加载中...</p> |
|
||||
</div> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
if (protocols.length === 0) { |
|
||||
return ( |
|
||||
<div className="flex items-center justify-center py-8"> |
|
||||
<div className="text-center"> |
|
||||
<p className="text-sm text-muted-foreground">暂无协议数据</p> |
|
||||
</div> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
const tableContent = ( |
|
||||
<Table> |
|
||||
<TableHeader> |
|
||||
<TableRow> |
|
||||
{visibleColumns.map((column) => ( |
|
||||
<TableHead key={column.key} className={densityClasses[density]}> |
|
||||
{column.title} |
|
||||
</TableHead> |
|
||||
))} |
|
||||
</TableRow> |
|
||||
</TableHeader> |
|
||||
<TableBody> |
|
||||
{protocols.map((protocol) => ( |
|
||||
<TableRow key={protocol.id} className={densityClasses[density]}> |
|
||||
{visibleColumns.map((column) => ( |
|
||||
<TableCell key={column.key}> |
|
||||
{renderCell(protocol, column.key)} |
|
||||
</TableCell> |
|
||||
))} |
|
||||
</TableRow> |
|
||||
))} |
|
||||
</TableBody> |
|
||||
</Table> |
|
||||
); |
|
||||
|
|
||||
if (hideCard) { |
|
||||
return tableContent; |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<div className="rounded-md border"> |
|
||||
{tableContent} |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
@ -1,209 +0,0 @@ |
|||||
import React, { useEffect, useState } from 'react'; |
|
||||
import { instrumentService, Protocol } from '@/services/instrumentService'; |
|
||||
import ProtocolsTable from './ProtocolsTable'; |
|
||||
import { Input } from '@/components/ui/input'; |
|
||||
import PaginationBar from '@/components/ui/PaginationBar'; |
|
||||
import TableToolbar, { DensityType } from '@/components/ui/TableToolbar'; |
|
||||
import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'; |
|
||||
|
|
||||
const defaultColumns = [ |
|
||||
{ key: 'protocolId', title: '协议ID', visible: true }, |
|
||||
{ key: 'protocolName', title: '协议名称', visible: true }, |
|
||||
{ key: 'protocolType', title: '协议类型', visible: true }, |
|
||||
{ key: 'version', title: '版本', visible: true }, |
|
||||
{ key: 'isActive', title: '状态', visible: true }, |
|
||||
{ key: 'defaultPort', title: '默认端口', visible: true }, |
|
||||
{ key: 'securityLevel', title: '安全级别', visible: true }, |
|
||||
{ key: 'encryptionType', title: '加密类型', visible: true }, |
|
||||
{ key: 'authenticationType', title: '认证类型', visible: true }, |
|
||||
{ key: 'connectionTimeout', title: '连接超时(ms)', visible: true }, |
|
||||
{ key: 'dataFormat', title: '数据格式', visible: true }, |
|
||||
{ key: 'createdBy', title: '创建人', visible: true }, |
|
||||
{ key: 'actions', title: '操作', visible: true } |
|
||||
]; |
|
||||
|
|
||||
// 字段类型声明
|
|
||||
type SearchField = |
|
||||
| { key: string; label: string; type: 'input'; placeholder: string } |
|
||||
| { key: string; label: string; type: 'select'; options: { value: string; label: string }[] }; |
|
||||
|
|
||||
// 第一行字段(收起时只显示这3个)
|
|
||||
const firstRowFields: SearchField[] = [ |
|
||||
{ key: 'protocolId', label: '协议ID', type: 'input', placeholder: '请输入' }, |
|
||||
{ key: 'protocolType', label: '协议类型', type: 'select', options: [ |
|
||||
{ value: '', label: '请选择' }, |
|
||||
{ value: 'modbus', label: 'Modbus' }, |
|
||||
{ value: 'opcua', label: 'OPC UA' }, |
|
||||
{ value: 'mqtt', label: 'MQTT' }, |
|
||||
{ value: 'http', label: 'HTTP' }, |
|
||||
{ value: 'tcp', label: 'TCP' }, |
|
||||
{ value: 'udp', label: 'UDP' }, |
|
||||
{ value: 'serial', label: 'Serial' }, |
|
||||
] }, |
|
||||
{ key: 'isActive', label: '状态', type: 'select', options: [ |
|
||||
{ value: '', label: '请选择' }, |
|
||||
{ value: 'true', label: '启用' }, |
|
||||
{ value: 'false', label: '禁用' }, |
|
||||
] }, |
|
||||
]; |
|
||||
|
|
||||
// 高级字段(展开时才显示)
|
|
||||
const advancedFields: SearchField[] = [ |
|
||||
{ key: 'version', label: '版本', type: 'input', placeholder: '请输入' }, |
|
||||
{ key: 'createdBy', label: '创建人', type: 'input', placeholder: '请输入' }, |
|
||||
]; |
|
||||
|
|
||||
export default function ProtocolsView() { |
|
||||
const [protocols, setProtocols] = useState<Protocol[]>([]); |
|
||||
const [loading, setLoading] = useState(false); |
|
||||
const [total, setTotal] = useState(0); |
|
||||
const [protocolId, setProtocolId] = useState(''); |
|
||||
const [page, setPage] = useState(1); |
|
||||
const [pageSize, setPageSize] = useState(10); |
|
||||
const [density, setDensity] = useState<DensityType>('default'); |
|
||||
const [columns, setColumns] = useState(defaultColumns); |
|
||||
const [showAdvanced, setShowAdvanced] = useState(false); |
|
||||
|
|
||||
const fetchProtocols = async (params = {}) => { |
|
||||
setLoading(true); |
|
||||
const result = await instrumentService.getAllProtocols({ protocolId, page, pageSize, ...params }); |
|
||||
if (result.isSuccess && result.data) { |
|
||||
setProtocols(result.data.protocols || []); |
|
||||
setTotal(result.data.totalCount || 0); |
|
||||
} |
|
||||
setLoading(false); |
|
||||
}; |
|
||||
|
|
||||
useEffect(() => { |
|
||||
fetchProtocols(); |
|
||||
// eslint-disable-next-line
|
|
||||
}, [page, pageSize]); |
|
||||
|
|
||||
const handleView = (protocol: Protocol) => { |
|
||||
// 这里可以实现查看协议详情的逻辑
|
|
||||
console.log('查看协议:', protocol); |
|
||||
}; |
|
||||
|
|
||||
const handleViewDocumentation = (protocol: Protocol) => { |
|
||||
// 这里可以实现查看协议文档的逻辑
|
|
||||
console.log('查看协议文档:', protocol); |
|
||||
}; |
|
||||
|
|
||||
// 查询按钮
|
|
||||
const handleQuery = () => { |
|
||||
setPage(1); |
|
||||
fetchProtocols({ page: 1 }); |
|
||||
}; |
|
||||
|
|
||||
// 重置按钮
|
|
||||
const handleReset = () => { |
|
||||
setProtocolId(''); |
|
||||
setPage(1); |
|
||||
fetchProtocols({ protocolId: '', page: 1 }); |
|
||||
}; |
|
||||
|
|
||||
// 每页条数选择
|
|
||||
const handlePageSizeChange = (size: number) => { |
|
||||
setPageSize(size); |
|
||||
setPage(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`} |
|
||||
onSubmit={e => { |
|
||||
e.preventDefault(); |
|
||||
handleQuery(); |
|
||||
}} |
|
||||
> |
|
||||
{(showAdvanced ? [...firstRowFields, ...advancedFields] : firstRowFields).map(field => ( |
|
||||
<div className="flex flex-row items-center min-w-[200px] flex-1" key={field.key}> |
|
||||
<label |
|
||||
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right" |
|
||||
style={{ width: 80, minWidth: 80 }} |
|
||||
> |
|
||||
{field.label}: |
|
||||
</label> |
|
||||
{field.type === 'input' && ( |
|
||||
<Input |
|
||||
className="input flex-1" |
|
||||
placeholder={field.placeholder} |
|
||||
value={field.key === 'protocolId' ? protocolId : ''} |
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { |
|
||||
if (field.key === 'protocolId') setProtocolId(e.target.value); |
|
||||
}} |
|
||||
/> |
|
||||
)} |
|
||||
{field.type === 'select' && ( |
|
||||
<select className="input h-10 rounded border border-border bg-background px-3 text-sm flex-1"> |
|
||||
{field.options.map(opt => ( |
|
||||
<option value={opt.value} key={opt.value}>{opt.label}</option> |
|
||||
))} |
|
||||
</select> |
|
||||
)} |
|
||||
</div> |
|
||||
))} |
|
||||
{/* 按钮组直接作为表单项之一,紧跟在最后一个表单项后面 */} |
|
||||
<div className="flex flex-row items-center min-w-[200px] flex-1 justify-end gap-2"> |
|
||||
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={handleReset}>重置</button> |
|
||||
<button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700">查询</button> |
|
||||
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={() => setShowAdvanced(v => !v)}> |
|
||||
{showAdvanced ? ( |
|
||||
<> |
|
||||
收起 <ChevronUpIcon className="inline-block ml-1 w-4 h-4 align-middle" /> |
|
||||
</> |
|
||||
) : ( |
|
||||
<> |
|
||||
展开 <ChevronDownIcon className="inline-block ml-1 w-4 h-4 align-middle" /> |
|
||||
</> |
|
||||
)} |
|
||||
</button> |
|
||||
</div> |
|
||||
</form> |
|
||||
</div> |
|
||||
{/* 表格整体卡片区域,包括工具栏、表格、分页 */} |
|
||||
<div className="rounded-md border bg-background p-4"> |
|
||||
{/* 顶部工具栏 */} |
|
||||
<div className="flex items-center justify-end mb-2"> |
|
||||
<TableToolbar |
|
||||
onRefresh={() => fetchProtocols()} |
|
||||
onDensityChange={setDensity} |
|
||||
onColumnsChange={setColumns} |
|
||||
onColumnsReset={() => setColumns(defaultColumns)} |
|
||||
columns={columns} |
|
||||
density={density} |
|
||||
/> |
|
||||
</div> |
|
||||
{/* 表格区域 */} |
|
||||
<ProtocolsTable |
|
||||
protocols={protocols} |
|
||||
loading={loading} |
|
||||
onView={handleView} |
|
||||
onViewDocumentation={handleViewDocumentation} |
|
||||
page={page} |
|
||||
pageSize={pageSize} |
|
||||
total={total} |
|
||||
onPageChange={setPage} |
|
||||
hideCard={true} |
|
||||
density={density} |
|
||||
columns={columns} |
|
||||
/> |
|
||||
{/* 分页 */} |
|
||||
<PaginationBar |
|
||||
page={page} |
|
||||
pageSize={pageSize} |
|
||||
total={total} |
|
||||
onPageChange={setPage} |
|
||||
onPageSizeChange={handlePageSizeChange} |
|
||||
/> |
|
||||
</div> |
|
||||
</div> |
|
||||
</main> |
|
||||
); |
|
||||
} |
|
@ -0,0 +1,109 @@ |
|||||
|
import React from 'react'; |
||||
|
import { Button } from '@/components/ui/button'; |
||||
|
import { Input } from '@/components/ui/input'; |
||||
|
import { Label } from '@/components/ui/label'; |
||||
|
import { Textarea } from '@/components/ui/textarea'; |
||||
|
import { Checkbox } from '@/components/ui/checkbox'; |
||||
|
import { CreateProtocolVersionRequest, UpdateProtocolVersionRequest } from '@/services/protocolService'; |
||||
|
|
||||
|
interface ProtocolFormProps { |
||||
|
onSubmit: (data: CreateProtocolVersionRequest | UpdateProtocolVersionRequest) => void; |
||||
|
initialData?: Partial<CreateProtocolVersionRequest>; |
||||
|
isEdit?: boolean; |
||||
|
isSubmitting?: boolean; |
||||
|
} |
||||
|
|
||||
|
export default function ProtocolForm({ onSubmit, initialData, isEdit = false, isSubmitting = false }: ProtocolFormProps) { |
||||
|
const [formData, setFormData] = React.useState<CreateProtocolVersionRequest>({ |
||||
|
name: initialData?.name || '', |
||||
|
version: initialData?.version || '', |
||||
|
description: initialData?.description || '', |
||||
|
isEnabled: initialData?.isEnabled ?? true, |
||||
|
releaseDate: initialData?.releaseDate || '', |
||||
|
minimumSupportedVersion: initialData?.minimumSupportedVersion || '' |
||||
|
}); |
||||
|
|
||||
|
const handleSubmit = (e: React.FormEvent) => { |
||||
|
e.preventDefault(); |
||||
|
if (isSubmitting) return; // 防止重复提交
|
||||
|
onSubmit(formData); |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<form onSubmit={handleSubmit} className="space-y-4"> |
||||
|
<div className="space-y-2"> |
||||
|
<Label htmlFor="name">协议名称</Label> |
||||
|
<Input |
||||
|
id="name" |
||||
|
value={formData.name} |
||||
|
onChange={e => setFormData({ ...formData, name: e.target.value })} |
||||
|
placeholder="请输入协议名称" |
||||
|
required |
||||
|
disabled={isSubmitting} |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div className="space-y-2"> |
||||
|
<Label htmlFor="version">版本号</Label> |
||||
|
<Input |
||||
|
id="version" |
||||
|
value={formData.version} |
||||
|
onChange={e => setFormData({ ...formData, version: e.target.value })} |
||||
|
placeholder="例如: 1.0.0" |
||||
|
required |
||||
|
disabled={isSubmitting} |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div className="space-y-2"> |
||||
|
<Label htmlFor="description">描述</Label> |
||||
|
<Textarea |
||||
|
id="description" |
||||
|
value={formData.description} |
||||
|
onChange={e => setFormData({ ...formData, description: e.target.value })} |
||||
|
placeholder="请输入协议描述" |
||||
|
rows={3} |
||||
|
disabled={isSubmitting} |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div className="space-y-2"> |
||||
|
<Label htmlFor="releaseDate">发布日期</Label> |
||||
|
<Input |
||||
|
id="releaseDate" |
||||
|
type="date" |
||||
|
value={formData.releaseDate} |
||||
|
onChange={e => setFormData({ ...formData, releaseDate: e.target.value })} |
||||
|
disabled={isSubmitting} |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div className="space-y-2"> |
||||
|
<Label htmlFor="minimumSupportedVersion">最低支持版本</Label> |
||||
|
<Input |
||||
|
id="minimumSupportedVersion" |
||||
|
value={formData.minimumSupportedVersion} |
||||
|
onChange={e => setFormData({ ...formData, minimumSupportedVersion: e.target.value })} |
||||
|
placeholder="例如: 1.0.0" |
||||
|
disabled={isSubmitting} |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div className="flex items-center space-x-2"> |
||||
|
<Checkbox |
||||
|
id="isEnabled" |
||||
|
checked={formData.isEnabled} |
||||
|
onCheckedChange={(checked) => |
||||
|
setFormData({ ...formData, isEnabled: checked as boolean }) |
||||
|
} |
||||
|
disabled={isSubmitting} |
||||
|
/> |
||||
|
<Label htmlFor="isEnabled">启用协议版本</Label> |
||||
|
</div> |
||||
|
|
||||
|
<Button type="submit" className="w-full" disabled={isSubmitting}> |
||||
|
{isSubmitting ? '提交中...' : (isEdit ? '更新协议版本' : '创建协议版本')} |
||||
|
</Button> |
||||
|
</form> |
||||
|
); |
||||
|
} |
@ -0,0 +1,167 @@ |
|||||
|
import React from 'react'; |
||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; |
||||
|
import { ProtocolVersion } from '@/services/protocolService'; |
||||
|
import { Badge } from '@/components/ui/badge'; |
||||
|
|
||||
|
interface ProtocolsTableProps { |
||||
|
protocolVersions: ProtocolVersion[]; |
||||
|
loading: boolean; |
||||
|
onEdit: (protocolVersion: ProtocolVersion) => void; |
||||
|
onDelete: (protocolVersion: ProtocolVersion) => void; |
||||
|
page: number; |
||||
|
pageSize: number; |
||||
|
total: number; |
||||
|
onPageChange: (page: number) => void; |
||||
|
hideCard?: boolean; |
||||
|
density?: 'relaxed' | 'default' | 'compact'; |
||||
|
columns?: { key: string; title: string; visible: boolean }[]; |
||||
|
} |
||||
|
|
||||
|
// 状态徽章组件
|
||||
|
const StatusBadge: React.FC<{ isEnabled: boolean }> = ({ isEnabled }) => { |
||||
|
return ( |
||||
|
<Badge className={isEnabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}> |
||||
|
{isEnabled ? '启用' : '禁用'} |
||||
|
</Badge> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
// 日期格式化组件
|
||||
|
const DateDisplay: React.FC<{ date?: string }> = ({ date }) => { |
||||
|
if (!date) return <span className="text-gray-400">-</span>; |
||||
|
|
||||
|
const formatDate = (dateString: string) => { |
||||
|
const date = new Date(dateString); |
||||
|
return date.toLocaleDateString('zh-CN', { |
||||
|
year: 'numeric', |
||||
|
month: '2-digit', |
||||
|
day: '2-digit', |
||||
|
hour: '2-digit', |
||||
|
minute: '2-digit' |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
return <span>{formatDate(date)}</span>; |
||||
|
}; |
||||
|
|
||||
|
export default function ProtocolsTable({ |
||||
|
protocolVersions, |
||||
|
loading, |
||||
|
onEdit, |
||||
|
onDelete, |
||||
|
page, |
||||
|
pageSize, |
||||
|
total, |
||||
|
onPageChange, |
||||
|
hideCard = false, |
||||
|
density = 'default', |
||||
|
columns = [] |
||||
|
}: ProtocolsTableProps) { |
||||
|
const densityClasses = { |
||||
|
relaxed: 'py-3', |
||||
|
default: 'py-2', |
||||
|
compact: 'py-1', |
||||
|
}; |
||||
|
|
||||
|
const visibleColumns = columns.filter(col => col.visible); |
||||
|
|
||||
|
const renderCell = (protocolVersion: ProtocolVersion, columnKey: string) => { |
||||
|
switch (columnKey) { |
||||
|
case 'name': |
||||
|
return ( |
||||
|
<div className="max-w-xs truncate" title={protocolVersion.name}> |
||||
|
{protocolVersion.name} |
||||
|
</div> |
||||
|
); |
||||
|
case 'version': |
||||
|
return <span className="font-mono text-sm">{protocolVersion.version}</span>; |
||||
|
case 'description': |
||||
|
return ( |
||||
|
<div className="max-w-xs truncate text-gray-600" title={protocolVersion.description || ''}> |
||||
|
{protocolVersion.description || '-'} |
||||
|
</div> |
||||
|
); |
||||
|
case 'isEnabled': |
||||
|
return <StatusBadge isEnabled={protocolVersion.isEnabled} />; |
||||
|
case 'releaseDate': |
||||
|
return <DateDisplay date={protocolVersion.releaseDate} />; |
||||
|
case 'minimumSupportedVersion': |
||||
|
return ( |
||||
|
<span className="text-sm"> |
||||
|
{protocolVersion.minimumSupportedVersion || '-'} |
||||
|
</span> |
||||
|
); |
||||
|
case 'createdAt': |
||||
|
return <DateDisplay date={protocolVersion.createdAt} />; |
||||
|
case 'actions': |
||||
|
return ( |
||||
|
<div className="flex justify-end gap-4"> |
||||
|
<span |
||||
|
className="cursor-pointer text-blue-600 hover:underline select-none" |
||||
|
onClick={() => onEdit(protocolVersion)} |
||||
|
> |
||||
|
修改 |
||||
|
</span> |
||||
|
<span |
||||
|
className="cursor-pointer text-red-500 hover:underline select-none" |
||||
|
onClick={() => onDelete(protocolVersion)} |
||||
|
> |
||||
|
删除 |
||||
|
</span> |
||||
|
</div> |
||||
|
); |
||||
|
default: |
||||
|
return null; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const totalPages = Math.ceil(total / pageSize); |
||||
|
const Wrapper = hideCard ? React.Fragment : 'div'; |
||||
|
const wrapperProps = hideCard ? {} : { className: 'rounded-md border bg-background' }; |
||||
|
const rowClass = density === 'relaxed' ? 'h-20' : density === 'compact' ? 'h-8' : 'h-12'; |
||||
|
const cellPadding = density === 'relaxed' ? 'py-5' : density === 'compact' ? 'py-1' : 'py-3'; |
||||
|
|
||||
|
return ( |
||||
|
<Wrapper {...wrapperProps}> |
||||
|
<Table> |
||||
|
<TableHeader key="header"> |
||||
|
<TableRow className={rowClass}> |
||||
|
{visibleColumns.map(col => ( |
||||
|
<TableHead |
||||
|
key={col.key} |
||||
|
className={`text-foreground text-center ${col.key === 'actions' ? 'text-right' : ''} ${cellPadding}`} |
||||
|
> |
||||
|
{col.title} |
||||
|
</TableHead> |
||||
|
))} |
||||
|
</TableRow> |
||||
|
</TableHeader> |
||||
|
<TableBody key="body"> |
||||
|
{loading ? ( |
||||
|
<TableRow key="loading" className={rowClass}> |
||||
|
<TableCell colSpan={visibleColumns.length} className={`text-center text-muted-foreground ${cellPadding}`}> |
||||
|
加载中... |
||||
|
</TableCell> |
||||
|
</TableRow> |
||||
|
) : protocolVersions.length === 0 ? ( |
||||
|
<TableRow key="empty" className={rowClass}> |
||||
|
<TableCell colSpan={visibleColumns.length} className={`text-center text-muted-foreground ${cellPadding}`}> |
||||
|
暂无数据 |
||||
|
</TableCell> |
||||
|
</TableRow> |
||||
|
) : ( |
||||
|
protocolVersions.map((protocolVersion) => ( |
||||
|
<TableRow key={protocolVersion.protocolVersionId} className={rowClass}> |
||||
|
{visibleColumns.map((column) => ( |
||||
|
<TableCell key={column.key} className={`text-foreground text-center ${cellPadding}`}> |
||||
|
{renderCell(protocolVersion, column.key)} |
||||
|
</TableCell> |
||||
|
))} |
||||
|
</TableRow> |
||||
|
)) |
||||
|
)} |
||||
|
</TableBody> |
||||
|
</Table> |
||||
|
</Wrapper> |
||||
|
); |
||||
|
} |
@ -0,0 +1,360 @@ |
|||||
|
import React, { useEffect, useState } from 'react'; |
||||
|
import { protocolService, ProtocolVersion, GetProtocolVersionsRequest, CreateProtocolVersionRequest, UpdateProtocolVersionRequest } from '@/services/protocolService'; |
||||
|
import ProtocolsTable from './ProtocolsTable'; |
||||
|
import ProtocolForm from './ProtocolForm'; |
||||
|
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: 'name', title: '版本名称', visible: true }, |
||||
|
{ key: 'version', title: '版本号', visible: true }, |
||||
|
{ key: 'description', title: '描述', visible: true }, |
||||
|
{ key: 'isEnabled', title: '状态', visible: true }, |
||||
|
{ key: 'releaseDate', title: '发布日期', visible: true }, |
||||
|
{ key: 'minimumSupportedVersion', title: '最低支持版本', visible: true }, |
||||
|
{ key: 'createdAt', title: '创建时间', visible: true }, |
||||
|
{ key: 'actions', title: '操作', visible: true } |
||||
|
]; |
||||
|
|
||||
|
// 字段类型声明
|
||||
|
type SearchField = |
||||
|
| { key: string; label: string; type: 'input'; placeholder: string } |
||||
|
| { key: string; label: string; type: 'select'; options: { value: string; label: string }[] }; |
||||
|
|
||||
|
// 第一行字段(收起时只显示这3个)
|
||||
|
const firstRowFields: SearchField[] = [ |
||||
|
{ key: 'searchTerm', label: '搜索关键词', type: 'input', placeholder: '请输入版本名称或版本号' }, |
||||
|
{ key: 'isEnabled', label: '状态', type: 'select', options: [ |
||||
|
{ value: '', label: '请选择' }, |
||||
|
{ value: 'true', label: '启用' }, |
||||
|
{ value: 'false', label: '禁用' }, |
||||
|
] }, |
||||
|
]; |
||||
|
|
||||
|
// 高级字段(展开时才显示)
|
||||
|
const advancedFields: SearchField[] = [ |
||||
|
{ key: 'pageSize', label: '每页数量', type: 'select', options: [ |
||||
|
{ value: '10', label: '10条/页' }, |
||||
|
{ value: '20', label: '20条/页' }, |
||||
|
{ value: '50', label: '50条/页' }, |
||||
|
{ value: '100', label: '100条/页' }, |
||||
|
] }, |
||||
|
]; |
||||
|
|
||||
|
export default function ProtocolsView() { |
||||
|
const [protocolVersions, setProtocolVersions] = useState<ProtocolVersion[]>([]); |
||||
|
const [loading, setLoading] = useState(false); |
||||
|
const [total, setTotal] = useState(0); |
||||
|
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 [searchTerm, setSearchTerm] = useState(''); |
||||
|
const [isEnabled, setIsEnabled] = useState<boolean | undefined>(undefined); |
||||
|
|
||||
|
// 表单对话框状态
|
||||
|
const [open, setOpen] = useState(false); |
||||
|
const [editOpen, setEditOpen] = useState(false); |
||||
|
const [selectedProtocol, setSelectedProtocol] = useState<ProtocolVersion | null>(null); |
||||
|
|
||||
|
// 提交状态
|
||||
|
const [isSubmitting, setIsSubmitting] = useState(false); |
||||
|
|
||||
|
// Toast 提示
|
||||
|
const { toast } = useToast(); |
||||
|
|
||||
|
const fetchProtocolVersions = async (params: Partial<GetProtocolVersionsRequest> = {}) => { |
||||
|
setLoading(true); |
||||
|
const queryParams: GetProtocolVersionsRequest = { |
||||
|
pageNumber, |
||||
|
pageSize, |
||||
|
searchTerm, |
||||
|
isEnabled, |
||||
|
...params |
||||
|
}; |
||||
|
|
||||
|
const result = await protocolService.getProtocolVersions(queryParams); |
||||
|
if (result.isSuccess && result.data) { |
||||
|
setProtocolVersions(result.data.items || []); |
||||
|
setTotal(result.data.totalCount || 0); |
||||
|
} |
||||
|
setLoading(false); |
||||
|
}; |
||||
|
|
||||
|
useEffect(() => { |
||||
|
fetchProtocolVersions(); |
||||
|
// eslint-disable-next-line
|
||||
|
}, [pageNumber, pageSize]); |
||||
|
|
||||
|
|
||||
|
|
||||
|
const handleEdit = (protocolVersion: ProtocolVersion) => { |
||||
|
setSelectedProtocol(protocolVersion); |
||||
|
setEditOpen(true); |
||||
|
}; |
||||
|
|
||||
|
const handleDelete = async (protocolVersion: ProtocolVersion) => { |
||||
|
if (confirm(`确定要删除协议版本 "${protocolVersion.name}" 吗?`)) { |
||||
|
try { |
||||
|
const result = await protocolService.deleteProtocolVersion(protocolVersion.protocolVersionId); |
||||
|
if (result.isSuccess) { |
||||
|
toast({ |
||||
|
title: "删除成功", |
||||
|
description: `协议版本 "${protocolVersion.name}" 删除成功`, |
||||
|
}); |
||||
|
fetchProtocolVersions(); |
||||
|
} 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: CreateProtocolVersionRequest) => { |
||||
|
if (isSubmitting) return; // 防止重复提交
|
||||
|
|
||||
|
console.log('开始创建协议版本:', data); |
||||
|
setIsSubmitting(true); |
||||
|
try { |
||||
|
const result = await protocolService.createProtocolVersion(data); |
||||
|
console.log('创建协议版本结果:', result); |
||||
|
|
||||
|
if (result.isSuccess) { |
||||
|
toast({ |
||||
|
title: "创建成功", |
||||
|
description: `协议版本 "${data.name}" 创建成功`, |
||||
|
}); |
||||
|
setOpen(false); |
||||
|
fetchProtocolVersions(); |
||||
|
} 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 handleUpdate = async (data: UpdateProtocolVersionRequest) => { |
||||
|
if (!selectedProtocol || isSubmitting) return; // 防止重复提交
|
||||
|
|
||||
|
setIsSubmitting(true); |
||||
|
try { |
||||
|
const result = await protocolService.updateProtocolVersion(selectedProtocol.protocolVersionId, data); |
||||
|
if (result.isSuccess) { |
||||
|
toast({ |
||||
|
title: "更新成功", |
||||
|
description: `协议版本 "${data.name}" 更新成功`, |
||||
|
}); |
||||
|
setEditOpen(false); |
||||
|
setSelectedProtocol(null); |
||||
|
fetchProtocolVersions(); |
||||
|
} 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 = () => { |
||||
|
setPageNumber(1); |
||||
|
fetchProtocolVersions({ pageNumber: 1 }); |
||||
|
}; |
||||
|
|
||||
|
// 重置按钮
|
||||
|
const handleReset = () => { |
||||
|
setSearchTerm(''); |
||||
|
setIsEnabled(undefined); |
||||
|
setPageNumber(1); |
||||
|
fetchProtocolVersions({ |
||||
|
searchTerm: '', |
||||
|
isEnabled: undefined, |
||||
|
pageNumber: 1 |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
// 每页条数选择
|
||||
|
const handlePageSizeChange = (size: number) => { |
||||
|
setPageSize(size); |
||||
|
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-3'} grid-cols-1`} |
||||
|
onSubmit={e => { |
||||
|
e.preventDefault(); |
||||
|
handleQuery(); |
||||
|
}} |
||||
|
> |
||||
|
{(showAdvanced ? [...firstRowFields, ...advancedFields] : firstRowFields).map(field => ( |
||||
|
<div className="flex flex-row items-center min-w-[200px] flex-1" key={field.key}> |
||||
|
<label |
||||
|
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right" |
||||
|
style={{ width: 80, minWidth: 80 }} |
||||
|
> |
||||
|
{field.label}: |
||||
|
</label> |
||||
|
{field.type === 'input' && ( |
||||
|
<Input |
||||
|
className="input flex-1" |
||||
|
placeholder={field.placeholder} |
||||
|
value={field.key === 'searchTerm' ? searchTerm : ''} |
||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { |
||||
|
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" |
||||
|
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> |
||||
|
))} |
||||
|
</select> |
||||
|
)} |
||||
|
</div> |
||||
|
))} |
||||
|
{/* 按钮组直接作为表单项之一,紧跟在最后一个表单项后面 */} |
||||
|
<div className="flex flex-row items-center min-w-[200px] flex-1 justify-end gap-2"> |
||||
|
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={handleReset}>重置</button> |
||||
|
<button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700">查询</button> |
||||
|
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={() => setShowAdvanced(v => !v)}> |
||||
|
{showAdvanced ? ( |
||||
|
<> |
||||
|
收起 <ChevronUpIcon className="inline-block ml-1 w-4 h-4 align-middle" /> |
||||
|
</> |
||||
|
) : ( |
||||
|
<> |
||||
|
展开 <ChevronDownIcon className="inline-block ml-1 w-4 h-4 align-middle" /> |
||||
|
</> |
||||
|
)} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
{/* 表格整体卡片区域,包括工具栏、表格、分页 */} |
||||
|
<div className="rounded-md border bg-background p-4"> |
||||
|
{/* 顶部操作栏:添加协议版本+工具栏 */} |
||||
|
<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"> |
||||
|
<ProtocolForm onSubmit={handleCreate} isSubmitting={isSubmitting} /> |
||||
|
</DialogContent> |
||||
|
</Dialog> |
||||
|
<TableToolbar |
||||
|
onRefresh={() => fetchProtocolVersions()} |
||||
|
onDensityChange={setDensity} |
||||
|
onColumnsChange={setColumns} |
||||
|
onColumnsReset={() => setColumns(defaultColumns)} |
||||
|
columns={columns} |
||||
|
density={density} |
||||
|
/> |
||||
|
</div> |
||||
|
{/* 表格区域 */} |
||||
|
<ProtocolsTable |
||||
|
protocolVersions={protocolVersions} |
||||
|
loading={loading} |
||||
|
onEdit={handleEdit} |
||||
|
onDelete={handleDelete} |
||||
|
page={pageNumber} |
||||
|
pageSize={pageSize} |
||||
|
total={total} |
||||
|
onPageChange={setPageNumber} |
||||
|
hideCard={true} |
||||
|
density={density} |
||||
|
columns={columns} |
||||
|
/> |
||||
|
{/* 分页 */} |
||||
|
<PaginationBar |
||||
|
page={pageNumber} |
||||
|
pageSize={pageSize} |
||||
|
total={total} |
||||
|
onPageChange={setPageNumber} |
||||
|
onPageSizeChange={handlePageSizeChange} |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
{/* 编辑协议版本对话框 */} |
||||
|
<Dialog open={editOpen} onOpenChange={setEditOpen}> |
||||
|
<DialogContent className="bg-background"> |
||||
|
<ProtocolForm |
||||
|
onSubmit={(data) => handleUpdate(data as UpdateProtocolVersionRequest)} |
||||
|
initialData={selectedProtocol || undefined} |
||||
|
isEdit={true} |
||||
|
isSubmitting={isSubmitting} |
||||
|
/> |
||||
|
</DialogContent> |
||||
|
</Dialog> |
||||
|
</main> |
||||
|
); |
||||
|
} |
@ -0,0 +1,119 @@ |
|||||
|
import { httpClient } from '@/lib/http-client'; |
||||
|
import { OperationResult } from '@/types/auth'; |
||||
|
|
||||
|
// 配置类型
|
||||
|
export type ConfigType = 'system' | 'network' | 'security' | 'communication' | 'monitoring'; |
||||
|
|
||||
|
// 配置接口定义
|
||||
|
export interface Config { |
||||
|
id: string; |
||||
|
configId: string; |
||||
|
configName: string; |
||||
|
configType: ConfigType; |
||||
|
description: string; |
||||
|
isActive: boolean; |
||||
|
isDefault: boolean; |
||||
|
version: string; |
||||
|
parameters: { |
||||
|
[key: string]: any; |
||||
|
}; |
||||
|
networkSettings: { |
||||
|
ipAddress: string; |
||||
|
subnet: string; |
||||
|
gateway: string; |
||||
|
dns: string[]; |
||||
|
port: number; |
||||
|
}; |
||||
|
securitySettings: { |
||||
|
encryptionEnabled: boolean; |
||||
|
authenticationRequired: boolean; |
||||
|
sslEnabled: boolean; |
||||
|
certificatePath: string; |
||||
|
keyPath: string; |
||||
|
}; |
||||
|
communicationSettings: { |
||||
|
protocol: string; |
||||
|
timeout: number; |
||||
|
retryCount: number; |
||||
|
heartbeatInterval: number; |
||||
|
}; |
||||
|
monitoringSettings: { |
||||
|
dataCollectionInterval: number; |
||||
|
alertThresholds: { |
||||
|
[key: string]: number; |
||||
|
}; |
||||
|
logLevel: 'debug' | 'info' | 'warn' | 'error'; |
||||
|
}; |
||||
|
appliedTo: string[]; // 应用此配置的设备ID列表
|
||||
|
createdAt: string; |
||||
|
updatedAt: string; |
||||
|
createdBy: string; |
||||
|
updatedBy: string; |
||||
|
} |
||||
|
|
||||
|
// 获取配置列表请求接口
|
||||
|
export interface GetAllConfigsRequest { |
||||
|
configId?: string; |
||||
|
configName?: string; |
||||
|
configType?: ConfigType; |
||||
|
isActive?: boolean; |
||||
|
isDefault?: boolean; |
||||
|
version?: string; |
||||
|
createdBy?: string; |
||||
|
page?: number; |
||||
|
pageSize?: number; |
||||
|
} |
||||
|
|
||||
|
// 获取配置列表响应接口
|
||||
|
export interface GetAllConfigsResponse { |
||||
|
configs: Config[]; |
||||
|
totalCount: number; |
||||
|
} |
||||
|
|
||||
|
class ConfigService { |
||||
|
private readonly baseUrl = '/api/instruments/configs'; |
||||
|
|
||||
|
// 获取所有配置
|
||||
|
async getAllConfigs(params: GetAllConfigsRequest = {}): Promise<OperationResult<GetAllConfigsResponse>> { |
||||
|
const queryParams = new URLSearchParams(); |
||||
|
|
||||
|
if (params.configId) queryParams.append('configId', params.configId); |
||||
|
if (params.configName) queryParams.append('configName', params.configName); |
||||
|
if (params.configType) queryParams.append('configType', params.configType); |
||||
|
if (params.isActive !== undefined) queryParams.append('isActive', params.isActive.toString()); |
||||
|
if (params.isDefault !== undefined) queryParams.append('isDefault', params.isDefault.toString()); |
||||
|
if (params.version) queryParams.append('version', params.version); |
||||
|
if (params.createdBy) queryParams.append('createdBy', params.createdBy); |
||||
|
if (params.page) queryParams.append('page', params.page.toString()); |
||||
|
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()); |
||||
|
|
||||
|
const url = `${this.baseUrl}?${queryParams.toString()}`; |
||||
|
return httpClient.get<GetAllConfigsResponse>(url); |
||||
|
} |
||||
|
|
||||
|
// 根据ID获取配置
|
||||
|
async getConfigById(id: string): Promise<OperationResult<Config>> { |
||||
|
return httpClient.get<Config>(`${this.baseUrl}/${id}`); |
||||
|
} |
||||
|
|
||||
|
// 验证配置
|
||||
|
async validateConfig(configId: string): Promise<OperationResult<{ |
||||
|
valid: boolean; |
||||
|
errors: string[]; |
||||
|
warnings: string[]; |
||||
|
}>> { |
||||
|
return httpClient.post(`${this.baseUrl}/${configId}/validate`); |
||||
|
} |
||||
|
|
||||
|
// 应用配置到设备
|
||||
|
async applyConfigToDevice(configId: string, deviceIds: string[]): Promise<OperationResult<void>> { |
||||
|
return httpClient.post(`${this.baseUrl}/${configId}/apply`, { deviceIds }); |
||||
|
} |
||||
|
|
||||
|
// 导出配置列表
|
||||
|
async exportConfigs(format: 'pdf' | 'excel' | 'csv', filters?: any): Promise<OperationResult<{ downloadUrl: string }>> { |
||||
|
return httpClient.post(`${this.baseUrl}/export`, { format, filters }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const configService = new ConfigService(); |
@ -0,0 +1,113 @@ |
|||||
|
import { httpClient } from '@/lib/http-client'; |
||||
|
import { OperationResult } from '@/types/auth'; |
||||
|
|
||||
|
// 设备状态类型
|
||||
|
export type DeviceStatus = 'online' | 'offline' | 'maintenance' | 'error'; |
||||
|
|
||||
|
// 设备类型
|
||||
|
export type DeviceType = 'sensor' | 'controller' | 'monitor' | 'actuator' | 'gateway'; |
||||
|
|
||||
|
// 设备接口定义
|
||||
|
export interface Device { |
||||
|
id: string; |
||||
|
deviceId: string; |
||||
|
deviceName: string; |
||||
|
deviceType: DeviceType; |
||||
|
status: DeviceStatus; |
||||
|
protocolId: string; |
||||
|
protocolName: string; |
||||
|
ipAddress: string; |
||||
|
port: number; |
||||
|
location: string; |
||||
|
description: string; |
||||
|
manufacturer: string; |
||||
|
model: string; |
||||
|
serialNumber: string; |
||||
|
firmwareVersion: string; |
||||
|
lastHeartbeat: string; |
||||
|
lastDataUpdate: string; |
||||
|
configId: string; |
||||
|
configName: string; |
||||
|
tags: string[]; |
||||
|
properties: { |
||||
|
[key: string]: any; |
||||
|
}; |
||||
|
createdAt: string; |
||||
|
updatedAt: string; |
||||
|
createdBy: string; |
||||
|
updatedBy: string; |
||||
|
} |
||||
|
|
||||
|
// 获取设备列表请求接口
|
||||
|
export interface GetAllDevicesRequest { |
||||
|
deviceId?: string; |
||||
|
deviceName?: string; |
||||
|
deviceType?: DeviceType; |
||||
|
status?: DeviceStatus; |
||||
|
protocolId?: string; |
||||
|
location?: string; |
||||
|
manufacturer?: string; |
||||
|
createdBy?: string; |
||||
|
page?: number; |
||||
|
pageSize?: number; |
||||
|
} |
||||
|
|
||||
|
// 获取设备列表响应接口
|
||||
|
export interface GetAllDevicesResponse { |
||||
|
devices: Device[]; |
||||
|
totalCount: number; |
||||
|
} |
||||
|
|
||||
|
class DeviceService { |
||||
|
private readonly baseUrl = '/api/instruments/devices'; |
||||
|
|
||||
|
// 获取所有设备
|
||||
|
async getAllDevices(params: GetAllDevicesRequest = {}): Promise<OperationResult<GetAllDevicesResponse>> { |
||||
|
const queryParams = new URLSearchParams(); |
||||
|
|
||||
|
if (params.deviceId) queryParams.append('deviceId', params.deviceId); |
||||
|
if (params.deviceName) queryParams.append('deviceName', params.deviceName); |
||||
|
if (params.deviceType) queryParams.append('deviceType', params.deviceType); |
||||
|
if (params.status) queryParams.append('status', params.status); |
||||
|
if (params.protocolId) queryParams.append('protocolId', params.protocolId); |
||||
|
if (params.location) queryParams.append('location', params.location); |
||||
|
if (params.manufacturer) queryParams.append('manufacturer', params.manufacturer); |
||||
|
if (params.createdBy) queryParams.append('createdBy', params.createdBy); |
||||
|
if (params.page) queryParams.append('page', params.page.toString()); |
||||
|
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()); |
||||
|
|
||||
|
const url = `${this.baseUrl}?${queryParams.toString()}`; |
||||
|
return httpClient.get<GetAllDevicesResponse>(url); |
||||
|
} |
||||
|
|
||||
|
// 根据ID获取设备
|
||||
|
async getDeviceById(id: string): Promise<OperationResult<Device>> { |
||||
|
return httpClient.get<Device>(`${this.baseUrl}/${id}`); |
||||
|
} |
||||
|
|
||||
|
// 获取设备状态
|
||||
|
async getDeviceStatus(deviceId: string): Promise<OperationResult<{ |
||||
|
status: DeviceStatus; |
||||
|
lastHeartbeat: string; |
||||
|
lastDataUpdate: string; |
||||
|
connectionInfo: any; |
||||
|
}>> { |
||||
|
return httpClient.get(`${this.baseUrl}/${deviceId}/status`); |
||||
|
} |
||||
|
|
||||
|
// 测试设备连接
|
||||
|
async testDeviceConnection(deviceId: string): Promise<OperationResult<{ |
||||
|
connected: boolean; |
||||
|
responseTime: number; |
||||
|
error?: string; |
||||
|
}>> { |
||||
|
return httpClient.post(`${this.baseUrl}/${deviceId}/test-connection`); |
||||
|
} |
||||
|
|
||||
|
// 导出设备列表
|
||||
|
async exportDevices(format: 'pdf' | 'excel' | 'csv', filters?: any): Promise<OperationResult<{ downloadUrl: string }>> { |
||||
|
return httpClient.post(`${this.baseUrl}/export`, { format, filters }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const deviceService = new DeviceService(); |
@ -1,316 +1,7 @@ |
|||||
import { httpClient } from '@/lib/http-client'; |
// 重新导出所有类型和服务,保持向后兼容
|
||||
import { OperationResult } from '@/types/auth'; |
export * from './deviceService'; |
||||
|
export * from './protocolService'; |
||||
|
export * from './configService'; |
||||
|
|
||||
// 设备状态类型
|
// 为了向后兼容,保留原有的 instrumentService 导出
|
||||
export type DeviceStatus = 'online' | 'offline' | 'maintenance' | 'error'; |
export { deviceService as instrumentService } from './deviceService'; |
||||
|
|
||||
// 设备类型
|
|
||||
export type DeviceType = 'sensor' | 'controller' | 'monitor' | 'actuator' | 'gateway'; |
|
||||
|
|
||||
// 协议类型
|
|
||||
export type ProtocolType = 'modbus' | 'opcua' | 'mqtt' | 'http' | 'tcp' | 'udp' | 'serial'; |
|
||||
|
|
||||
// 配置类型
|
|
||||
export type ConfigType = 'system' | 'network' | 'security' | 'communication' | 'monitoring'; |
|
||||
|
|
||||
// 设备接口定义
|
|
||||
export interface Device { |
|
||||
id: string; |
|
||||
deviceId: string; |
|
||||
deviceName: string; |
|
||||
deviceType: DeviceType; |
|
||||
status: DeviceStatus; |
|
||||
protocolId: string; |
|
||||
protocolName: string; |
|
||||
ipAddress: string; |
|
||||
port: number; |
|
||||
location: string; |
|
||||
description: string; |
|
||||
manufacturer: string; |
|
||||
model: string; |
|
||||
serialNumber: string; |
|
||||
firmwareVersion: string; |
|
||||
lastHeartbeat: string; |
|
||||
lastDataUpdate: string; |
|
||||
configId: string; |
|
||||
configName: string; |
|
||||
tags: string[]; |
|
||||
properties: { |
|
||||
[key: string]: any; |
|
||||
}; |
|
||||
createdAt: string; |
|
||||
updatedAt: string; |
|
||||
createdBy: string; |
|
||||
updatedBy: string; |
|
||||
} |
|
||||
|
|
||||
// 协议接口定义
|
|
||||
export interface Protocol { |
|
||||
id: string; |
|
||||
protocolId: string; |
|
||||
protocolName: string; |
|
||||
protocolType: ProtocolType; |
|
||||
version: string; |
|
||||
description: string; |
|
||||
isActive: boolean; |
|
||||
defaultPort: number; |
|
||||
supportedFeatures: string[]; |
|
||||
securityLevel: 'low' | 'medium' | 'high'; |
|
||||
encryptionType: 'none' | 'ssl' | 'tls' | 'aes'; |
|
||||
authenticationType: 'none' | 'basic' | 'token' | 'certificate'; |
|
||||
connectionTimeout: number; |
|
||||
retryAttempts: number; |
|
||||
heartbeatInterval: number; |
|
||||
dataFormat: 'json' | 'xml' | 'binary' | 'csv'; |
|
||||
documentation: string; |
|
||||
examples: string[]; |
|
||||
createdAt: string; |
|
||||
updatedAt: string; |
|
||||
createdBy: string; |
|
||||
updatedBy: string; |
|
||||
} |
|
||||
|
|
||||
// 配置接口定义
|
|
||||
export interface Config { |
|
||||
id: string; |
|
||||
configId: string; |
|
||||
configName: string; |
|
||||
configType: ConfigType; |
|
||||
description: string; |
|
||||
isActive: boolean; |
|
||||
isDefault: boolean; |
|
||||
version: string; |
|
||||
parameters: { |
|
||||
[key: string]: any; |
|
||||
}; |
|
||||
networkSettings: { |
|
||||
ipAddress: string; |
|
||||
subnet: string; |
|
||||
gateway: string; |
|
||||
dns: string[]; |
|
||||
port: number; |
|
||||
}; |
|
||||
securitySettings: { |
|
||||
encryptionEnabled: boolean; |
|
||||
authenticationRequired: boolean; |
|
||||
sslEnabled: boolean; |
|
||||
certificatePath: string; |
|
||||
keyPath: string; |
|
||||
}; |
|
||||
communicationSettings: { |
|
||||
protocol: string; |
|
||||
timeout: number; |
|
||||
retryCount: number; |
|
||||
heartbeatInterval: number; |
|
||||
}; |
|
||||
monitoringSettings: { |
|
||||
dataCollectionInterval: number; |
|
||||
alertThresholds: { |
|
||||
[key: string]: number; |
|
||||
}; |
|
||||
logLevel: 'debug' | 'info' | 'warn' | 'error'; |
|
||||
}; |
|
||||
appliedTo: string[]; // 应用此配置的设备ID列表
|
|
||||
createdAt: string; |
|
||||
updatedAt: string; |
|
||||
createdBy: string; |
|
||||
updatedBy: string; |
|
||||
} |
|
||||
|
|
||||
// 获取设备列表请求接口
|
|
||||
export interface GetAllDevicesRequest { |
|
||||
deviceId?: string; |
|
||||
deviceName?: string; |
|
||||
deviceType?: DeviceType; |
|
||||
status?: DeviceStatus; |
|
||||
protocolId?: string; |
|
||||
location?: string; |
|
||||
manufacturer?: string; |
|
||||
createdBy?: string; |
|
||||
page?: number; |
|
||||
pageSize?: number; |
|
||||
} |
|
||||
|
|
||||
// 获取设备列表响应接口
|
|
||||
export interface GetAllDevicesResponse { |
|
||||
devices: Device[]; |
|
||||
totalCount: number; |
|
||||
} |
|
||||
|
|
||||
// 获取协议列表请求接口
|
|
||||
export interface GetAllProtocolsRequest { |
|
||||
protocolId?: string; |
|
||||
protocolName?: string; |
|
||||
protocolType?: ProtocolType; |
|
||||
isActive?: boolean; |
|
||||
version?: string; |
|
||||
createdBy?: string; |
|
||||
page?: number; |
|
||||
pageSize?: number; |
|
||||
} |
|
||||
|
|
||||
// 获取协议列表响应接口
|
|
||||
export interface GetAllProtocolsResponse { |
|
||||
protocols: Protocol[]; |
|
||||
totalCount: number; |
|
||||
} |
|
||||
|
|
||||
// 获取配置列表请求接口
|
|
||||
export interface GetAllConfigsRequest { |
|
||||
configId?: string; |
|
||||
configName?: string; |
|
||||
configType?: ConfigType; |
|
||||
isActive?: boolean; |
|
||||
isDefault?: boolean; |
|
||||
version?: string; |
|
||||
createdBy?: string; |
|
||||
page?: number; |
|
||||
pageSize?: number; |
|
||||
} |
|
||||
|
|
||||
// 获取配置列表响应接口
|
|
||||
export interface GetAllConfigsResponse { |
|
||||
configs: Config[]; |
|
||||
totalCount: number; |
|
||||
} |
|
||||
|
|
||||
class InstrumentService { |
|
||||
private readonly baseUrl = '/api/instruments'; |
|
||||
|
|
||||
// 获取所有设备
|
|
||||
async getAllDevices(params: GetAllDevicesRequest = {}): Promise<OperationResult<GetAllDevicesResponse>> { |
|
||||
const queryParams = new URLSearchParams(); |
|
||||
|
|
||||
if (params.deviceId) queryParams.append('deviceId', params.deviceId); |
|
||||
if (params.deviceName) queryParams.append('deviceName', params.deviceName); |
|
||||
if (params.deviceType) queryParams.append('deviceType', params.deviceType); |
|
||||
if (params.status) queryParams.append('status', params.status); |
|
||||
if (params.protocolId) queryParams.append('protocolId', params.protocolId); |
|
||||
if (params.location) queryParams.append('location', params.location); |
|
||||
if (params.manufacturer) queryParams.append('manufacturer', params.manufacturer); |
|
||||
if (params.createdBy) queryParams.append('createdBy', params.createdBy); |
|
||||
if (params.page) queryParams.append('page', params.page.toString()); |
|
||||
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()); |
|
||||
|
|
||||
const url = `${this.baseUrl}/devices?${queryParams.toString()}`; |
|
||||
return httpClient.get<GetAllDevicesResponse>(url); |
|
||||
} |
|
||||
|
|
||||
// 根据ID获取设备
|
|
||||
async getDeviceById(id: string): Promise<OperationResult<Device>> { |
|
||||
return httpClient.get<Device>(`${this.baseUrl}/devices/${id}`); |
|
||||
} |
|
||||
|
|
||||
// 获取所有协议
|
|
||||
async getAllProtocols(params: GetAllProtocolsRequest = {}): Promise<OperationResult<GetAllProtocolsResponse>> { |
|
||||
const queryParams = new URLSearchParams(); |
|
||||
|
|
||||
if (params.protocolId) queryParams.append('protocolId', params.protocolId); |
|
||||
if (params.protocolName) queryParams.append('protocolName', params.protocolName); |
|
||||
if (params.protocolType) queryParams.append('protocolType', params.protocolType); |
|
||||
if (params.isActive !== undefined) queryParams.append('isActive', params.isActive.toString()); |
|
||||
if (params.version) queryParams.append('version', params.version); |
|
||||
if (params.createdBy) queryParams.append('createdBy', params.createdBy); |
|
||||
if (params.page) queryParams.append('page', params.page.toString()); |
|
||||
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()); |
|
||||
|
|
||||
const url = `${this.baseUrl}/protocols?${queryParams.toString()}`; |
|
||||
return httpClient.get<GetAllProtocolsResponse>(url); |
|
||||
} |
|
||||
|
|
||||
// 根据ID获取协议
|
|
||||
async getProtocolById(id: string): Promise<OperationResult<Protocol>> { |
|
||||
return httpClient.get<Protocol>(`${this.baseUrl}/protocols/${id}`); |
|
||||
} |
|
||||
|
|
||||
// 获取所有配置
|
|
||||
async getAllConfigs(params: GetAllConfigsRequest = {}): Promise<OperationResult<GetAllConfigsResponse>> { |
|
||||
const queryParams = new URLSearchParams(); |
|
||||
|
|
||||
if (params.configId) queryParams.append('configId', params.configId); |
|
||||
if (params.configName) queryParams.append('configName', params.configName); |
|
||||
if (params.configType) queryParams.append('configType', params.configType); |
|
||||
if (params.isActive !== undefined) queryParams.append('isActive', params.isActive.toString()); |
|
||||
if (params.isDefault !== undefined) queryParams.append('isDefault', params.isDefault.toString()); |
|
||||
if (params.version) queryParams.append('version', params.version); |
|
||||
if (params.createdBy) queryParams.append('createdBy', params.createdBy); |
|
||||
if (params.page) queryParams.append('page', params.page.toString()); |
|
||||
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()); |
|
||||
|
|
||||
const url = `${this.baseUrl}/configs?${queryParams.toString()}`; |
|
||||
return httpClient.get<GetAllConfigsResponse>(url); |
|
||||
} |
|
||||
|
|
||||
// 根据ID获取配置
|
|
||||
async getConfigById(id: string): Promise<OperationResult<Config>> { |
|
||||
return httpClient.get<Config>(`${this.baseUrl}/configs/${id}`); |
|
||||
} |
|
||||
|
|
||||
// 获取仪表统计信息
|
|
||||
async getInstrumentStatistics(): Promise<OperationResult<{ |
|
||||
devices: { total: number; online: number; offline: number; error: number }; |
|
||||
protocols: { total: number; active: number; inactive: number }; |
|
||||
configs: { total: number; active: number; default: number }; |
|
||||
}>> { |
|
||||
return httpClient.get(`${this.baseUrl}/statistics`); |
|
||||
} |
|
||||
|
|
||||
// 获取设备状态
|
|
||||
async getDeviceStatus(deviceId: string): Promise<OperationResult<{ |
|
||||
status: DeviceStatus; |
|
||||
lastHeartbeat: string; |
|
||||
lastDataUpdate: string; |
|
||||
connectionInfo: any; |
|
||||
}>> { |
|
||||
return httpClient.get(`${this.baseUrl}/devices/${deviceId}/status`); |
|
||||
} |
|
||||
|
|
||||
// 测试设备连接
|
|
||||
async testDeviceConnection(deviceId: string): Promise<OperationResult<{ |
|
||||
connected: boolean; |
|
||||
responseTime: number; |
|
||||
error?: string; |
|
||||
}>> { |
|
||||
return httpClient.post(`${this.baseUrl}/devices/${deviceId}/test-connection`); |
|
||||
} |
|
||||
|
|
||||
// 获取协议文档
|
|
||||
async getProtocolDocumentation(protocolId: string): Promise<OperationResult<{ |
|
||||
documentation: string; |
|
||||
examples: string[]; |
|
||||
specifications: any; |
|
||||
}>> { |
|
||||
return httpClient.get(`${this.baseUrl}/protocols/${protocolId}/documentation`); |
|
||||
} |
|
||||
|
|
||||
// 验证配置
|
|
||||
async validateConfig(configId: string): Promise<OperationResult<{ |
|
||||
valid: boolean; |
|
||||
errors: string[]; |
|
||||
warnings: string[]; |
|
||||
}>> { |
|
||||
return httpClient.post(`${this.baseUrl}/configs/${configId}/validate`); |
|
||||
} |
|
||||
|
|
||||
// 应用配置到设备
|
|
||||
async applyConfigToDevice(configId: string, deviceIds: string[]): Promise<OperationResult<void>> { |
|
||||
return httpClient.post(`${this.baseUrl}/configs/${configId}/apply`, { deviceIds }); |
|
||||
} |
|
||||
|
|
||||
// 导出设备列表
|
|
||||
async exportDevices(format: 'pdf' | 'excel' | 'csv', filters?: any): Promise<OperationResult<{ downloadUrl: string }>> { |
|
||||
return httpClient.post(`${this.baseUrl}/devices/export`, { format, filters }); |
|
||||
} |
|
||||
|
|
||||
// 导出协议列表
|
|
||||
async exportProtocols(format: 'pdf' | 'excel' | 'csv', filters?: any): Promise<OperationResult<{ downloadUrl: string }>> { |
|
||||
return httpClient.post(`${this.baseUrl}/protocols/export`, { format, filters }); |
|
||||
} |
|
||||
|
|
||||
// 导出配置列表
|
|
||||
async exportConfigs(format: 'pdf' | 'excel' | 'csv', filters?: any): Promise<OperationResult<{ downloadUrl: string }>> { |
|
||||
return httpClient.post(`${this.baseUrl}/configs/export`, { format, filters }); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export const instrumentService = new InstrumentService(); |
|
@ -0,0 +1,122 @@ |
|||||
|
import { httpClient } from '@/lib/http-client'; |
||||
|
import { OperationResult } from '@/types/auth'; |
||||
|
|
||||
|
|
||||
|
|
||||
|
// 协议版本接口定义
|
||||
|
export interface ProtocolVersion { |
||||
|
protocolVersionId: string; |
||||
|
name: string; |
||||
|
version: string; |
||||
|
description?: string; |
||||
|
isEnabled: boolean; |
||||
|
releaseDate?: string; |
||||
|
minimumSupportedVersion?: string; |
||||
|
createdAt: string; |
||||
|
updatedAt?: string; |
||||
|
} |
||||
|
|
||||
|
// 获取协议版本列表请求接口
|
||||
|
export interface GetProtocolVersionsRequest { |
||||
|
pageNumber?: number; |
||||
|
pageSize?: number; |
||||
|
searchTerm?: string; |
||||
|
isEnabled?: boolean; |
||||
|
} |
||||
|
|
||||
|
// 获取协议版本列表响应接口
|
||||
|
export interface GetProtocolVersionsResponse { |
||||
|
totalCount: number; |
||||
|
pageNumber: number; |
||||
|
pageSize: number; |
||||
|
totalPages: number; |
||||
|
hasPreviousPage: boolean; |
||||
|
hasNextPage: boolean; |
||||
|
items: ProtocolVersion[]; |
||||
|
} |
||||
|
|
||||
|
// 创建协议版本请求接口
|
||||
|
export interface CreateProtocolVersionRequest { |
||||
|
name: string; |
||||
|
version: string; |
||||
|
description?: string; |
||||
|
isEnabled?: boolean; |
||||
|
releaseDate?: string; |
||||
|
minimumSupportedVersion?: string; |
||||
|
} |
||||
|
|
||||
|
// 创建协议版本响应接口
|
||||
|
export interface CreateProtocolVersionResponse { |
||||
|
protocolVersionId: string; |
||||
|
name: string; |
||||
|
version: string; |
||||
|
description?: string; |
||||
|
isEnabled: boolean; |
||||
|
releaseDate?: string; |
||||
|
minimumSupportedVersion?: string; |
||||
|
createdAt: string; |
||||
|
} |
||||
|
|
||||
|
// 更新协议版本请求接口
|
||||
|
export interface UpdateProtocolVersionRequest { |
||||
|
protocolVersionId: string; |
||||
|
name: string; |
||||
|
version: string; |
||||
|
description?: string; |
||||
|
isEnabled?: boolean; |
||||
|
releaseDate?: string; |
||||
|
minimumSupportedVersion?: string; |
||||
|
} |
||||
|
|
||||
|
// 更新协议版本响应接口
|
||||
|
export interface UpdateProtocolVersionResponse { |
||||
|
protocolVersionId: string; |
||||
|
name: string; |
||||
|
version: string; |
||||
|
description?: string; |
||||
|
isEnabled: boolean; |
||||
|
releaseDate?: string; |
||||
|
minimumSupportedVersion?: string; |
||||
|
updatedAt: string; |
||||
|
} |
||||
|
|
||||
|
class ProtocolService { |
||||
|
private readonly baseUrl = '/protocolversions'; |
||||
|
|
||||
|
|
||||
|
|
||||
|
// 获取协议版本列表
|
||||
|
async getProtocolVersions(params: GetProtocolVersionsRequest = {}): Promise<OperationResult<GetProtocolVersionsResponse>> { |
||||
|
const queryParams = new URLSearchParams(); |
||||
|
|
||||
|
if (params.pageNumber) queryParams.append('pageNumber', params.pageNumber.toString()); |
||||
|
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()); |
||||
|
if (params.searchTerm) queryParams.append('searchTerm', params.searchTerm); |
||||
|
if (params.isEnabled !== undefined) queryParams.append('isEnabled', params.isEnabled.toString()); |
||||
|
|
||||
|
const url = `${this.baseUrl}?${queryParams.toString()}`; |
||||
|
return httpClient.get<GetProtocolVersionsResponse>(url); |
||||
|
} |
||||
|
|
||||
|
// 根据ID获取协议版本详情
|
||||
|
async getProtocolVersionById(id: string): Promise<OperationResult<ProtocolVersion>> { |
||||
|
return httpClient.get<ProtocolVersion>(`${this.baseUrl}/${id}`); |
||||
|
} |
||||
|
|
||||
|
// 创建协议版本
|
||||
|
async createProtocolVersion(data: CreateProtocolVersionRequest): Promise<OperationResult<CreateProtocolVersionResponse>> { |
||||
|
return httpClient.post<CreateProtocolVersionResponse>(this.baseUrl, data); |
||||
|
} |
||||
|
|
||||
|
// 更新协议版本
|
||||
|
async updateProtocolVersion(id: string, data: UpdateProtocolVersionRequest): Promise<OperationResult<UpdateProtocolVersionResponse>> { |
||||
|
return httpClient.put<UpdateProtocolVersionResponse>(`${this.baseUrl}/${id}`, data); |
||||
|
} |
||||
|
|
||||
|
// 删除协议版本
|
||||
|
async deleteProtocolVersion(id: string): Promise<OperationResult<boolean>> { |
||||
|
return httpClient.delete<boolean>(`${this.baseUrl}/${id}`); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const protocolService = new ProtocolService(); |
Loading…
Reference in new issue