15 changed files with 1010 additions and 790 deletions
@ -1,7 +1,7 @@ |
|||
import React from 'react'; |
|||
import { Button } from '@/components/ui/button'; |
|||
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 { 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'; |
|||
|
|||
// 设备状态类型
|
|||
export type DeviceStatus = 'online' | 'offline' | 'maintenance' | 'error'; |
|||
|
|||
// 设备类型
|
|||
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(); |
|||
// 为了向后兼容,保留原有的 instrumentService 导出
|
|||
export { deviceService as instrumentService } from './deviceService'; |
@ -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