Browse Source

feat(protocols): 优化协议版本表格,移除查看操作和ID列,完善表单防重复提交与错误提示

feature/x1-owen-debug
root 4 weeks ago
parent
commit
9cd07b2ab2
  1. 2
      src/X1.WebUI/src/constants/menuConfig.ts
  2. 2
      src/X1.WebUI/src/pages/configs/ConfigsTable.tsx
  3. 4
      src/X1.WebUI/src/pages/configs/ConfigsView.tsx
  4. 2
      src/X1.WebUI/src/pages/instruments/DevicesTable.tsx
  5. 4
      src/X1.WebUI/src/pages/instruments/DevicesView.tsx
  6. 254
      src/X1.WebUI/src/pages/instruments/ProtocolsTable.tsx
  7. 209
      src/X1.WebUI/src/pages/instruments/ProtocolsView.tsx
  8. 109
      src/X1.WebUI/src/pages/protocols/ProtocolForm.tsx
  9. 167
      src/X1.WebUI/src/pages/protocols/ProtocolsTable.tsx
  10. 360
      src/X1.WebUI/src/pages/protocols/ProtocolsView.tsx
  11. 12
      src/X1.WebUI/src/routes/AppRouter.tsx
  12. 119
      src/X1.WebUI/src/services/configService.ts
  13. 113
      src/X1.WebUI/src/services/deviceService.ts
  14. 321
      src/X1.WebUI/src/services/instrumentService.ts
  15. 122
      src/X1.WebUI/src/services/protocolService.ts

2
src/X1.WebUI/src/constants/menuConfig.ts

@ -173,7 +173,7 @@ export const menuItems: MenuItem[] = [
children: [ children: [
{ {
title: '设备列表', title: '设备列表',
href: '/dashboard/instruments/devices', href: '/dashboard/instruments/list',
permission: 'devices.view', permission: 'devices.view',
}, },
{ {

2
src/X1.WebUI/src/pages/instruments/ConfigsTable.tsx → src/X1.WebUI/src/pages/configs/ConfigsTable.tsx

@ -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';

4
src/X1.WebUI/src/pages/instruments/ConfigsView.tsx → src/X1.WebUI/src/pages/configs/ConfigsView.tsx

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { instrumentService, Config } from '@/services/instrumentService'; import { configService, Config } from '@/services/configService';
import ConfigsTable from './ConfigsTable'; import ConfigsTable from './ConfigsTable';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import PaginationBar from '@/components/ui/PaginationBar'; import PaginationBar from '@/components/ui/PaginationBar';
@ -68,7 +68,7 @@ export default function ConfigsView() {
const fetchConfigs = async (params = {}) => { const fetchConfigs = async (params = {}) => {
setLoading(true); setLoading(true);
const result = await instrumentService.getAllConfigs({ configId, page, pageSize, ...params }); const result = await configService.getAllConfigs({ configId, page, pageSize, ...params });
if (result.isSuccess && result.data) { if (result.isSuccess && result.data) {
setConfigs(result.data.configs || []); setConfigs(result.data.configs || []);
setTotal(result.data.totalCount || 0); setTotal(result.data.totalCount || 0);

2
src/X1.WebUI/src/pages/instruments/DevicesTable.tsx

@ -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 { Device, DeviceStatus, DeviceType } from '@/services/instrumentService'; import { Device, DeviceStatus, DeviceType } from '@/services/deviceService';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { EyeOpenIcon, PlayIcon } from '@radix-ui/react-icons'; import { EyeOpenIcon, PlayIcon } from '@radix-ui/react-icons';

4
src/X1.WebUI/src/pages/instruments/DevicesView.tsx

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { instrumentService, Device } from '@/services/instrumentService'; import { deviceService, Device } from '@/services/deviceService';
import DevicesTable from './DevicesTable'; import DevicesTable from './DevicesTable';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import PaginationBar from '@/components/ui/PaginationBar'; import PaginationBar from '@/components/ui/PaginationBar';
@ -65,7 +65,7 @@ export default function DevicesView() {
const fetchDevices = async (params = {}) => { const fetchDevices = async (params = {}) => {
setLoading(true); setLoading(true);
const result = await instrumentService.getAllDevices({ deviceId, page, pageSize, ...params }); const result = await deviceService.getAllDevices({ deviceId, page, pageSize, ...params });
if (result.isSuccess && result.data) { if (result.isSuccess && result.data) {
setDevices(result.data.devices || []); setDevices(result.data.devices || []);
setTotal(result.data.totalCount || 0); setTotal(result.data.totalCount || 0);

254
src/X1.WebUI/src/pages/instruments/ProtocolsTable.tsx

@ -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>
);
}

209
src/X1.WebUI/src/pages/instruments/ProtocolsView.tsx

@ -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>
);
}

109
src/X1.WebUI/src/pages/protocols/ProtocolForm.tsx

@ -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>
);
}

167
src/X1.WebUI/src/pages/protocols/ProtocolsTable.tsx

@ -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>
);
}

360
src/X1.WebUI/src/pages/protocols/ProtocolsView.tsx

@ -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>
);
}

12
src/X1.WebUI/src/routes/AppRouter.tsx

@ -29,10 +29,12 @@ const PerformanceAnalysisView = lazy(() => import('@/pages/analysis/PerformanceA
const IssueAnalysisView = lazy(() => import('@/pages/analysis/IssueAnalysisView')); const IssueAnalysisView = lazy(() => import('@/pages/analysis/IssueAnalysisView'));
const UEAnalysisView = lazy(() => import('@/pages/analysis/UEAnalysisView')); const UEAnalysisView = lazy(() => import('@/pages/analysis/UEAnalysisView'));
// 仪表管理页面 // 设备管理页面
const DevicesView = lazy(() => import('@/pages/instruments/DevicesView')); const DevicesView = lazy(() => import('@/pages/instruments/DevicesView'));
const ProtocolsView = lazy(() => import('@/pages/instruments/ProtocolsView')); // 协议管理页面
const ConfigsView = lazy(() => import('@/pages/instruments/ConfigsView')); const ProtocolsView = lazy(() => import('@/pages/protocols/ProtocolsView'));
// 配置管理页面
const ConfigsView = lazy(() => import('@/pages/configs/ConfigsView'));
// 加载中的占位组件 // 加载中的占位组件
const LoadingFallback = () => ( const LoadingFallback = () => (
@ -184,8 +186,8 @@ export function AppRouter() {
{/* 仪表管理路由 */} {/* 仪表管理路由 */}
<Route path="instruments"> <Route path="instruments">
<Route index element={<Navigate to="devices" replace />} /> <Route index element={<Navigate to="list" replace />} />
<Route path="devices" element={ <Route path="list" element={
<ProtectedRoute requiredPermission="devices.view"> <ProtectedRoute requiredPermission="devices.view">
<AnimatedContainer> <AnimatedContainer>
<DevicesView /> <DevicesView />

119
src/X1.WebUI/src/services/configService.ts

@ -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();

113
src/X1.WebUI/src/services/deviceService.ts

@ -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();

321
src/X1.WebUI/src/services/instrumentService.ts

@ -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();

122
src/X1.WebUI/src/services/protocolService.ts

@ -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…
Cancel
Save