Browse Source

修复设备管理中的ProtocolVersion导航属性问题 - 重构仓储设计,避免N+1查询 - 在命令和查询处理器中正确注入IProtocolVersionRepository - 优化批量查询性能,避免为每个设备单独查询协议版本 - 符合单一职责原则,CellularDeviceRepository只负责设备操作

feature/x1-owen-debug
root 4 weeks ago
parent
commit
0960773766
  1. 5
      src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommand.cs
  2. 9
      src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs
  3. 2
      src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommand.cs
  4. 9
      src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommandHandler.cs
  5. 8
      src/X1.Application/Features/Devices/Queries/GetDeviceById/GetDeviceByIdQueryHandler.cs
  6. 28
      src/X1.Application/Features/Devices/Queries/GetDevices/GetDevicesQueryHandler.cs
  7. 8
      src/X1.Domain/Repositories/Device/ICellularDeviceRepository.cs
  8. 8
      src/X1.Infrastructure/Repositories/Device/CellularDeviceRepository.cs
  9. 64
      src/X1.WebUI/src/constants/api.ts
  10. 160
      src/X1.WebUI/src/pages/instruments/DeviceForm.tsx
  11. 204
      src/X1.WebUI/src/pages/instruments/DevicesTable.tsx
  12. 322
      src/X1.WebUI/src/pages/instruments/DevicesView.tsx
  13. 3
      src/X1.WebUI/src/services/configService.ts
  14. 113
      src/X1.WebUI/src/services/deviceService.ts
  15. 232
      src/X1.WebUI/src/services/instrumentService.ts
  16. 7
      src/X1.WebUI/src/services/protocolService.ts

5
src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommand.cs

@ -38,8 +38,9 @@ public class CreateDeviceCommand : IRequest<OperationResult<CreateDeviceResponse
/// <summary>
/// IP地址
/// </summary>
[MaxLength(45, ErrorMessage = "Agent端口不能为空")]
public string IpAddress { get; private set; } = null!;
[Required(ErrorMessage = "设备IP不能为空")]
[MaxLength(45, ErrorMessage = "IP地址不能超过45个字符")]
public string IpAddress { get; set; } = null!;
/// <summary>
/// Agent端口

9
src/X1.Application/Features/Devices/Commands/CreateDevice/CreateDeviceCommandHandler.cs

@ -15,6 +15,7 @@ namespace CellularManagement.Application.Features.Devices.Commands.CreateDevice;
public class CreateDeviceCommandHandler : IRequestHandler<CreateDeviceCommand, OperationResult<CreateDeviceResponse>>
{
private readonly ICellularDeviceRepository _deviceRepository;
private readonly IProtocolVersionRepository _protocolVersionRepository;
private readonly ILogger<CreateDeviceCommandHandler> _logger;
private readonly IUnitOfWork _unitOfWork;
private readonly ICurrentUserService _currentUserService;
@ -24,11 +25,13 @@ public class CreateDeviceCommandHandler : IRequestHandler<CreateDeviceCommand, O
/// </summary>
public CreateDeviceCommandHandler(
ICellularDeviceRepository deviceRepository,
IProtocolVersionRepository protocolVersionRepository,
ILogger<CreateDeviceCommandHandler> logger,
IUnitOfWork unitOfWork,
ICurrentUserService currentUserService)
{
_deviceRepository = deviceRepository;
_protocolVersionRepository = protocolVersionRepository;
_logger = logger;
_unitOfWork = unitOfWork;
_currentUserService = currentUserService;
@ -77,8 +80,8 @@ public class CreateDeviceCommandHandler : IRequestHandler<CreateDeviceCommand, O
// 保存更改到数据库
await _unitOfWork.SaveChangesAsync(cancellationToken);
// 加载导航属性
//await _deviceRepository.LoadNavigationPropertiesAsync(device, cancellationToken);
// 获取协议版本信息
var protocolVersion = await _protocolVersionRepository.GetProtocolVersionByIdAsync(device.ProtocolVersionId, cancellationToken);
// 构建响应
var response = new CreateDeviceResponse
@ -87,7 +90,7 @@ public class CreateDeviceCommandHandler : IRequestHandler<CreateDeviceCommand, O
DeviceName = device.Name,
SerialNumber = device.SerialNumber,
Description = device.Description,
ProtocolVersion = device.ProtocolVersion?.Version ?? "未知",
ProtocolVersion = protocolVersion?.Version ?? "未知",
AgentPort = device.AgentPort,
IsEnabled = device.IsEnabled,
IsRunning = device.IsRunning,

2
src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommand.cs

@ -46,7 +46,7 @@ public class UpdateDeviceCommand : IRequest<OperationResult<UpdateDeviceResponse
/// </summary>
[Required]
[MaxLength(45)]
public string IpAddress { get; private set; } = null!;
public string IpAddress { get; set; } = null!;
/// <summary>
/// Agent端口
/// </summary>

9
src/X1.Application/Features/Devices/Commands/UpdateDevice/UpdateDeviceCommandHandler.cs

@ -17,6 +17,7 @@ namespace CellularManagement.Application.Features.Devices.Commands.UpdateDevice;
public class UpdateDeviceCommandHandler : IRequestHandler<UpdateDeviceCommand, OperationResult<UpdateDeviceResponse>>
{
private readonly ICellularDeviceRepository _deviceRepository;
private readonly IProtocolVersionRepository _protocolVersionRepository;
private readonly ILogger<UpdateDeviceCommandHandler> _logger;
private readonly IUnitOfWork _unitOfWork;
private readonly ICurrentUserService _currentUserService;
@ -26,11 +27,13 @@ public class UpdateDeviceCommandHandler : IRequestHandler<UpdateDeviceCommand, O
/// </summary>
public UpdateDeviceCommandHandler(
ICellularDeviceRepository deviceRepository,
IProtocolVersionRepository protocolVersionRepository,
ILogger<UpdateDeviceCommandHandler> logger,
IUnitOfWork unitOfWork,
ICurrentUserService currentUserService)
{
_deviceRepository = deviceRepository;
_protocolVersionRepository = protocolVersionRepository;
_logger = logger;
_unitOfWork = unitOfWork;
_currentUserService = currentUserService;
@ -89,8 +92,8 @@ public class UpdateDeviceCommandHandler : IRequestHandler<UpdateDeviceCommand, O
// 保存更改到数据库
await _unitOfWork.SaveChangesAsync(cancellationToken);
// 加载导航属性
//await _deviceRepository.LoadNavigationPropertiesAsync(existingDevice, cancellationToken);
// 获取协议版本信息
var protocolVersion = await _protocolVersionRepository.GetProtocolVersionByIdAsync(existingDevice.ProtocolVersionId, cancellationToken);
// 构建响应
var response = new UpdateDeviceResponse
@ -99,7 +102,7 @@ public class UpdateDeviceCommandHandler : IRequestHandler<UpdateDeviceCommand, O
DeviceName = existingDevice.Name,
SerialNumber = existingDevice.SerialNumber,
Description = existingDevice.Description,
ProtocolVersion = existingDevice.ProtocolVersion?.Version ?? "未知",
ProtocolVersion = protocolVersion?.Version ?? "未知",
AgentPort = existingDevice.AgentPort,
IsEnabled = existingDevice.IsEnabled,
IsRunning = existingDevice.IsRunning,

8
src/X1.Application/Features/Devices/Queries/GetDeviceById/GetDeviceByIdQueryHandler.cs

@ -13,6 +13,7 @@ namespace CellularManagement.Application.Features.Devices.Queries.GetDeviceById;
public class GetDeviceByIdQueryHandler : IRequestHandler<GetDeviceByIdQuery, OperationResult<GetDeviceByIdResponse>>
{
private readonly ICellularDeviceRepository _deviceRepository;
private readonly IProtocolVersionRepository _protocolVersionRepository;
private readonly ILogger<GetDeviceByIdQueryHandler> _logger;
/// <summary>
@ -20,9 +21,11 @@ public class GetDeviceByIdQueryHandler : IRequestHandler<GetDeviceByIdQuery, Ope
/// </summary>
public GetDeviceByIdQueryHandler(
ICellularDeviceRepository deviceRepository,
IProtocolVersionRepository protocolVersionRepository,
ILogger<GetDeviceByIdQueryHandler> logger)
{
_deviceRepository = deviceRepository;
_protocolVersionRepository = protocolVersionRepository;
_logger = logger;
}
@ -43,13 +46,16 @@ public class GetDeviceByIdQueryHandler : IRequestHandler<GetDeviceByIdQuery, Ope
return OperationResult<GetDeviceByIdResponse>.CreateFailure($"未找到ID为 {request.DeviceId} 的设备");
}
// 获取协议版本信息
var protocolVersion = await _protocolVersionRepository.GetProtocolVersionByIdAsync(device.ProtocolVersionId, cancellationToken);
var response = new GetDeviceByIdResponse
{
DeviceId = device.Id,
DeviceName = device.Name,
SerialNumber = device.SerialNumber,
Description = device.Description,
ProtocolVersion = device.ProtocolVersion?.Version ?? "未知",
ProtocolVersion = protocolVersion?.Version ?? "未知",
AgentPort = device.AgentPort,
IsEnabled = device.IsEnabled,
IsRunning = device.IsRunning,

28
src/X1.Application/Features/Devices/Queries/GetDevices/GetDevicesQueryHandler.cs

@ -16,6 +16,7 @@ namespace CellularManagement.Application.Features.Devices.Queries.GetDevices;
public class GetDevicesQueryHandler : IRequestHandler<GetDevicesQuery, OperationResult<GetDevicesResponse>>
{
private readonly ICellularDeviceRepository _deviceRepository;
private readonly IProtocolVersionRepository _protocolVersionRepository;
private readonly ILogger<GetDevicesQueryHandler> _logger;
/// <summary>
@ -23,9 +24,11 @@ public class GetDevicesQueryHandler : IRequestHandler<GetDevicesQuery, Operation
/// </summary>
public GetDevicesQueryHandler(
ICellularDeviceRepository deviceRepository,
IProtocolVersionRepository protocolVersionRepository,
ILogger<GetDevicesQueryHandler> logger)
{
_deviceRepository = deviceRepository;
_protocolVersionRepository = protocolVersionRepository;
_logger = logger;
}
@ -61,6 +64,11 @@ public class GetDevicesQueryHandler : IRequestHandler<GetDevicesQuery, Operation
.Take(request.PageSize)
.ToList();
// 批量获取协议版本信息,避免 N+1 查询
var protocolVersionIds = items.Select(d => d.ProtocolVersionId).Distinct().ToList();
var allProtocolVersions = await _protocolVersionRepository.GetAllProtocolVersionsAsync(cancellationToken);
var protocolVersionDict = allProtocolVersions.ToDictionary(pv => pv.Id, pv => pv.Version);
// 构建响应
var response = new GetDevicesResponse
{
@ -70,17 +78,17 @@ public class GetDevicesQueryHandler : IRequestHandler<GetDevicesQuery, Operation
TotalPages = (int)Math.Ceiling(totalCount / (double)request.PageSize),
HasPreviousPage = request.PageNumber > 1,
HasNextPage = request.PageNumber < (int)Math.Ceiling(totalCount / (double)request.PageSize),
Items = items.Select(d => new GetDeviceByIdResponse
Items = items.Select(device => new GetDeviceByIdResponse
{
DeviceId = d.Id,
DeviceName = d.Name,
SerialNumber = d.SerialNumber,
Description = d.Description,
ProtocolVersion = d.ProtocolVersion?.Version ?? "未知",
AgentPort = d.AgentPort,
IsEnabled = d.IsEnabled,
IsRunning = d.IsRunning,
CreatedAt = d.CreatedAt
DeviceId = device.Id,
DeviceName = device.Name,
SerialNumber = device.SerialNumber,
Description = device.Description,
ProtocolVersion = protocolVersionDict.TryGetValue(device.ProtocolVersionId, out var version) ? version : "未知",
AgentPort = device.AgentPort,
IsEnabled = device.IsEnabled,
IsRunning = device.IsRunning,
CreatedAt = device.CreatedAt
}).ToList()
};

8
src/X1.Domain/Repositories/Device/ICellularDeviceRepository.cs

@ -35,6 +35,10 @@ public interface ICellularDeviceRepository : IBaseRepository<CellularDevice>
/// </summary>
Task<CellularDevice?> GetDeviceByIdAsync(string id, CancellationToken cancellationToken = default);
/// <summary>
/// 根据序列号获取蜂窝设备
/// </summary>
@ -47,6 +51,10 @@ public interface ICellularDeviceRepository : IBaseRepository<CellularDevice>
string? keyword,
CancellationToken cancellationToken = default);
/// <summary>
/// 检查蜂窝设备是否存在
/// </summary>

8
src/X1.Infrastructure/Repositories/Device/CellularDeviceRepository.cs

@ -76,6 +76,10 @@ public class CellularDeviceRepository : BaseRepository<CellularDevice>, ICellula
return await QueryRepository.GetByIdAsync(id, cancellationToken);
}
/// <summary>
/// 根据序列号获取蜂窝设备
/// </summary>
@ -105,6 +109,10 @@ public class CellularDeviceRepository : BaseRepository<CellularDevice>, ICellula
return devices.ToList();
}
/// <summary>
/// 检查蜂窝设备是否存在
/// </summary>

64
src/X1.WebUI/src/constants/api.ts

@ -0,0 +1,64 @@
// API 路径常量
export const API_PATHS = {
// 设备相关
DEVICES: '/devices',
// 协议相关
PROTOCOLS: '/protocolversions',
// 配置相关
CONFIGS: '/instruments/configs',
// 用户相关
USERS: '/users',
ROLES: '/roles',
// 认证相关
AUTH: {
LOGIN: '/auth/login',
REGISTER: '/auth/register',
REFRESH: '/auth/refresh',
LOGOUT: '/auth/logout',
},
// 任务相关
TASKS: '/tasks',
TASK_EXECUTIONS: '/task-executions',
TASK_REVIEWS: '/task-reviews',
// 场景相关
SCENARIOS: '/scenarios',
// 测试用例相关
TEST_CASES: '/test-cases',
TEST_STEPS: '/test-steps',
// 分析相关
ANALYSIS: {
FUNCTIONAL: '/analysis/functional',
PERFORMANCE: '/analysis/performance',
ISSUE: '/analysis/issue',
UE: '/analysis/ue',
}
} as const;
// API 响应状态码
export const API_STATUS = {
SUCCESS: 200,
CREATED: 201,
NO_CONTENT: 204,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
INTERNAL_SERVER_ERROR: 500,
} as const;
// HTTP 方法
export const HTTP_METHODS = {
GET: 'GET',
POST: 'POST',
PUT: 'PUT',
DELETE: 'DELETE',
PATCH: 'PATCH',
} as const;

160
src/X1.WebUI/src/pages/instruments/DeviceForm.tsx

@ -0,0 +1,160 @@
import React, { useEffect, useState } 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 { CreateDeviceRequest, UpdateDeviceRequest } from '@/services/instrumentService';
import { protocolService, ProtocolVersion } from '@/services/protocolService';
interface DeviceFormProps {
onSubmit: (data: CreateDeviceRequest | UpdateDeviceRequest) => void;
initialData?: Partial<CreateDeviceRequest>;
isEdit?: boolean;
isSubmitting?: boolean;
}
export default function DeviceForm({ onSubmit, initialData, isEdit = false, isSubmitting = false }: DeviceFormProps) {
const [formData, setFormData] = React.useState<CreateDeviceRequest>({
deviceName: initialData?.deviceName || '',
serialNumber: initialData?.serialNumber || '',
description: initialData?.description || '',
protocolVersionId: initialData?.protocolVersionId || '',
ipAddress: initialData?.ipAddress || '',
agentPort: initialData?.agentPort || 8080,
isEnabled: initialData?.isEnabled ?? false,
isRunning: initialData?.isRunning ?? false
});
const [protocolVersions, setProtocolVersions] = useState<ProtocolVersion[]>([]);
const [loadingProtocols, setLoadingProtocols] = useState(false);
// 加载协议版本列表
useEffect(() => {
const loadProtocolVersions = async () => {
setLoadingProtocols(true);
try {
const result = await protocolService.getProtocolVersions({
pageSize: 100,
isEnabled: true
});
if (result.isSuccess && result.data) {
setProtocolVersions(result.data.items || []);
// 如果是编辑模式且有初始数据,尝试根据协议版本名称找到对应的ID
if (isEdit && initialData && !initialData.protocolVersionId) {
// 这里需要根据实际情况来匹配协议版本
// 由于后端返回的是协议版本名称,我们需要找到对应的ID
// 暂时设置为空,需要后端提供更详细的设备信息
}
}
} catch (error) {
console.error('加载协议版本失败:', error);
} finally {
setLoadingProtocols(false);
}
};
loadProtocolVersions();
}, [isEdit, initialData]);
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="deviceName"></Label>
<Input
id="deviceName"
value={formData.deviceName}
onChange={e => setFormData({ ...formData, deviceName: e.target.value })}
placeholder="请输入设备名称"
required
disabled={isSubmitting}
/>
</div>
<div className="space-y-2">
<Label htmlFor="serialNumber"></Label>
<Input
id="serialNumber"
value={formData.serialNumber}
onChange={e => setFormData({ ...formData, serialNumber: e.target.value })}
placeholder="请输入设备序列号"
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="protocolVersionId"></Label>
<select
id="protocolVersionId"
className="input h-10 rounded border border-border bg-background px-3 text-sm w-full"
value={formData.protocolVersionId}
onChange={e => setFormData({ ...formData, protocolVersionId: e.target.value })}
required
disabled={isSubmitting || loadingProtocols}
>
<option value=""></option>
{protocolVersions.map(protocol => (
<option key={protocol.protocolVersionId} value={protocol.protocolVersionId}>
{protocol.name} - {protocol.version}
</option>
))}
</select>
{loadingProtocols && <div className="text-sm text-muted-foreground">...</div>}
</div>
<div className="space-y-2">
<Label htmlFor="ipAddress">IP地址</Label>
<Input
id="ipAddress"
type="text"
value={formData.ipAddress}
onChange={e => setFormData({ ...formData, ipAddress: e.target.value })}
placeholder="例如: 192.168.1.100"
required
disabled={isSubmitting}
/>
</div>
<div className="space-y-2">
<Label htmlFor="agentPort">Agent端口</Label>
<Input
id="agentPort"
type="number"
value={formData.agentPort}
onChange={e => setFormData({ ...formData, agentPort: parseInt(e.target.value) || 0 })}
placeholder="例如: 8080"
min="1"
max="65535"
required
disabled={isSubmitting}
/>
</div>
<Button type="submit" className="w-full" disabled={isSubmitting}>
{isSubmitting ? '提交中...' : (isEdit ? '更新设备' : '创建设备')}
</Button>
</form>
);
}

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

@ -1,64 +1,83 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Device, DeviceStatus, DeviceType } from '@/services/deviceService';
import { Device, DeviceStatus, DeviceRunningStatus } from '@/services/instrumentService';
import { Badge } from '@/components/ui/badge';
import { EyeOpenIcon, PlayIcon } from '@radix-ui/react-icons';
import { Pencil1Icon, TrashIcon } from '@radix-ui/react-icons';
import { DensityType } from '@/components/ui/TableToolbar';
/**
*
*
* GetDeviceByIdResponse
* - deviceId: 设备ID
* - deviceName: 设备名称
* - serialNumber: 序列号
* - description: 设备描述
* - protocolVersion: 协议版本
* - agentPort: Agent端口
* - isEnabled: 是否启用
* - isRunning: 设备状态/
* - createdAt: 创建时间
*
* IpAddress字段在创建/使
*/
interface DevicesTableProps {
devices: Device[];
loading: boolean;
onView: (device: Device) => void;
onTestConnection: (device: Device) => void;
onEdit: (device: Device) => void;
onDelete: (device: Device) => void;
page: number;
pageSize: number;
total: number;
onPageChange: (page: number) => void;
hideCard?: boolean;
density?: 'compact' | 'default' | 'comfortable';
density?: DensityType;
columns?: { key: string; title: string; visible: boolean }[];
}
// 设备状态徽章组件
const DeviceStatusBadge: React.FC<{ status: DeviceStatus }> = ({ status }) => {
const statusConfig = {
online: { label: '在线', className: 'bg-green-100 text-green-800' },
offline: { label: '离线', className: 'bg-gray-100 text-gray-800' },
maintenance: { label: '维护中', className: 'bg-yellow-100 text-yellow-800' },
error: { label: '错误', className: 'bg-red-100 text-red-800' },
const DeviceStatusBadge: React.FC<{ isEnabled: boolean }> = ({ isEnabled }) => {
return (
<Badge className={isEnabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}>
{isEnabled ? '启用' : '禁用'}
</Badge>
);
};
const config = statusConfig[status];
// 设备运行状态徽章组件
const DeviceRunningStatusBadge: React.FC<{ isRunning: boolean }> = ({ isRunning }) => {
return (
<Badge className={config.className}>
{config.label}
<Badge className={isRunning ? 'bg-blue-100 text-blue-800' : 'bg-orange-100 text-orange-800'}>
{isRunning ? '运行中' : '已停止'}
</Badge>
);
};
// 设备类型徽章组件
const DeviceTypeBadge: React.FC<{ type: DeviceType }> = ({ type }) => {
const typeConfig = {
sensor: { label: '传感器', className: 'bg-blue-100 text-blue-800' },
controller: { label: '控制器', className: 'bg-purple-100 text-purple-800' },
monitor: { label: '监视器', className: 'bg-indigo-100 text-indigo-800' },
actuator: { label: '执行器', className: 'bg-orange-100 text-orange-800' },
gateway: { label: '网关', className: 'bg-teal-100 text-teal-800' },
// 日期格式化组件
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'
});
};
const config = typeConfig[type];
return (
<Badge className={config.className}>
{config.label}
</Badge>
);
return <span>{formatDate(date)}</span>;
};
export default function DevicesTable({
devices,
loading,
onView,
onTestConnection,
onEdit,
onDelete,
page,
pageSize,
total,
@ -70,7 +89,7 @@ export default function DevicesTable({
const densityClasses = {
compact: 'py-1',
default: 'py-2',
comfortable: 'py-3',
relaxed: 'py-3',
};
const visibleColumns = columns.filter(col => col.visible);
@ -89,51 +108,42 @@ export default function DevicesTable({
{device.deviceName}
</div>
);
case 'deviceType':
return <DeviceTypeBadge type={device.deviceType} />;
case 'status':
return <DeviceStatusBadge status={device.status} />;
case 'protocolName':
return <span>{device.protocolName}</span>;
case 'ipAddress':
case 'serialNumber':
return <span className="font-mono text-sm">{device.serialNumber}</span>;
case 'description':
return (
<div className="font-mono text-sm">
{device.ipAddress}:{device.port}
<div className="max-w-xs truncate text-gray-600" title={device.description || ''}>
{device.description || '-'}
</div>
);
case 'location':
return (
<div className="max-w-xs truncate" title={device.location}>
{device.location}
</div>
);
case 'manufacturer':
return <span>{device.manufacturer}</span>;
case 'model':
return <span>{device.model}</span>;
case 'lastHeartbeat':
return <span>{new Date(device.lastHeartbeat).toLocaleString()}</span>;
case 'createdBy':
return <span>{device.createdBy}</span>;
case 'protocolVersion':
return <span className="text-sm">{device.protocolVersion}</span>;
case 'agentPort':
return <span className="font-mono text-sm">{device.agentPort}</span>;
case 'isEnabled':
return <DeviceStatusBadge isEnabled={device.isEnabled} />;
case 'isRunning':
return <DeviceRunningStatusBadge isRunning={device.isRunning} />;
case 'createdAt':
return <DateDisplay date={device.createdAt} />;
case 'actions':
return (
<div className="flex items-center gap-2">
<div className="flex justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => onView(device)}
onClick={() => onEdit(device)}
className="h-8 w-8 p-0"
>
<EyeOpenIcon className="h-4 w-4" />
<Pencil1Icon className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onTestConnection(device)}
onClick={() => onDelete(device)}
className="h-8 w-8 p-0"
disabled={device.status === 'offline'}
>
<PlayIcon className="h-4 w-4" />
<TrashIcon className="h-4 w-4" />
</Button>
</div>
);
@ -142,59 +152,53 @@ export default function DevicesTable({
}
};
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>
);
}
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';
if (devices.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 = (
<Wrapper {...wrapperProps}>
<Table>
<TableHeader>
<TableRow>
{visibleColumns.map((column) => (
<TableHead key={column.key} className={densityClasses[density]}>
{column.title}
<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>
{devices.map((device) => (
<TableRow key={device.id} className={densityClasses[density]}>
<TableBody key="body">
{loading ? (
<TableRow key="loading" className={rowClass}>
<TableCell colSpan={visibleColumns.length} className={`text-center text-muted-foreground ${cellPadding}`}>
...
</TableCell>
</TableRow>
) : devices.length === 0 ? (
<TableRow key="empty" className={rowClass}>
<TableCell colSpan={visibleColumns.length} className={`text-center text-muted-foreground ${cellPadding}`}>
</TableCell>
</TableRow>
) : (
devices.map((device) => (
<TableRow key={device.deviceId} className={rowClass}>
{visibleColumns.map((column) => (
<TableCell key={column.key}>
<TableCell key={column.key} className={`text-foreground text-center ${cellPadding}`}>
{renderCell(device, column.key)}
</TableCell>
))}
</TableRow>
))}
))
)}
</TableBody>
</Table>
);
if (hideCard) {
return tableContent;
}
return (
<div className="rounded-md border">
{tableContent}
</div>
</Wrapper>
);
}

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

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

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

@ -1,5 +1,6 @@
import { httpClient } from '@/lib/http-client';
import { OperationResult } from '@/types/auth';
import { API_PATHS } from '@/constants/api';
// 配置类型
export type ConfigType = 'system' | 'network' | 'security' | 'communication' | 'monitoring';
@ -71,7 +72,7 @@ export interface GetAllConfigsResponse {
}
class ConfigService {
private readonly baseUrl = '/api/instruments/configs';
private readonly baseUrl = API_PATHS.CONFIGS;
// 获取所有配置
async getAllConfigs(params: GetAllConfigsRequest = {}): Promise<OperationResult<GetAllConfigsResponse>> {

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

@ -1,113 +0,0 @@
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();

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

@ -1,7 +1,233 @@
// 重新导出所有类型和服务,保持向后兼容
export * from './deviceService';
import { httpClient } from '@/lib/http-client';
import { OperationResult } from '@/types/auth';
import { API_PATHS } from '@/constants/api';
// 设备状态类型 - 根据后端实体定义
export type DeviceStatus = 'enabled' | 'disabled';
// 设备运行状态
export type DeviceRunningStatus = 'running' | 'stopped';
// 设备接口定义 - 匹配后端GetDeviceByIdResponse
export interface Device {
deviceId: string;
deviceName: string;
serialNumber: string;
description: string;
protocolVersion: string;
agentPort: number;
isEnabled: boolean;
isRunning: boolean;
createdAt: string;
}
// 获取设备列表请求接口 - 匹配后端GetDevicesQuery
export interface GetDevicesRequest {
pageNumber?: number;
pageSize?: number;
searchTerm?: string;
isDisabled?: boolean;
}
// 获取设备列表响应接口 - 匹配后端GetDevicesResponse
export interface GetDevicesResponse {
totalCount: number;
pageNumber: number;
pageSize: number;
totalPages: number;
hasPreviousPage: boolean;
hasNextPage: boolean;
items: Device[];
}
// 创建设备请求接口 - 匹配后端CreateDeviceCommand
export interface CreateDeviceRequest {
deviceName: string;
serialNumber: string;
description?: string;
protocolVersionId: string;
ipAddress: string;
agentPort: number;
isEnabled?: boolean;
isRunning?: boolean;
}
// 创建设备响应接口 - 匹配后端CreateDeviceResponse
export interface CreateDeviceResponse {
deviceId: string;
deviceName: string;
serialNumber: string;
description: string;
protocolVersion: string;
agentPort: number;
isEnabled: boolean;
isRunning: boolean;
createdAt: string;
}
// 更新设备请求接口 - 匹配后端UpdateDeviceCommand
export interface UpdateDeviceRequest {
deviceId: string;
deviceName: string;
serialNumber: string;
description?: string;
protocolVersionId: string;
ipAddress: string;
agentPort: number;
isEnabled?: boolean;
isRunning?: boolean;
}
// 更新设备响应接口 - 匹配后端UpdateDeviceResponse
export interface UpdateDeviceResponse {
deviceId: string;
deviceName: string;
serialNumber: string;
description: string;
protocolVersion: string;
agentPort: number;
isEnabled: boolean;
isRunning: boolean;
updatedAt?: string;
}
// 获取设备列表
export function getDevices(params: GetDevicesRequest = {}): Promise<OperationResult<GetDevicesResponse>> {
return httpClient.get(API_PATHS.DEVICES, { params });
}
// 获取设备详情
export function getDeviceById(id: string): Promise<OperationResult<Device>> {
return httpClient.get(`${API_PATHS.DEVICES}/${id}`);
}
// 创建设备
export function createDevice(data: CreateDeviceRequest): Promise<OperationResult<CreateDeviceResponse>> {
return httpClient.post(API_PATHS.DEVICES, data);
}
// 更新设备
export function updateDevice(id: string, data: UpdateDeviceRequest): Promise<OperationResult<UpdateDeviceResponse>> {
return httpClient.put(`${API_PATHS.DEVICES}/${id}`, data);
}
// 删除设备
export function deleteDevice(id: string): Promise<OperationResult<boolean>> {
return httpClient.delete(`${API_PATHS.DEVICES}/${id}`);
}
// 为了向后兼容,保留原有的类型和服务
export type LegacyDeviceStatus = 'online' | 'offline' | 'maintenance' | 'error';
export type DeviceType = 'sensor' | 'controller' | 'monitor' | 'actuator' | 'gateway';
// 旧版设备接口定义
export interface LegacyDevice {
id: string;
deviceId: string;
deviceName: string;
deviceType: DeviceType;
status: LegacyDeviceStatus;
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?: LegacyDeviceStatus;
protocolId?: string;
location?: string;
manufacturer?: string;
createdBy?: string;
page?: number;
pageSize?: number;
}
// 旧版获取设备列表响应接口
export interface GetAllDevicesResponse {
devices: LegacyDevice[];
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<LegacyDevice>> {
return httpClient.get<LegacyDevice>(`${this.baseUrl}/${id}`);
}
// 获取设备状态
async getDeviceStatus(deviceId: string): Promise<OperationResult<{
status: LegacyDeviceStatus;
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();
// 重新导出其他服务,保持向后兼容
export * from './protocolService';
export * from './configService';
// 为了向后兼容,保留原有的 instrumentService 导出
export { deviceService as instrumentService } from './deviceService';
export { deviceService as instrumentService };

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

@ -1,7 +1,6 @@
import { httpClient } from '@/lib/http-client';
import { OperationResult } from '@/types/auth';
import { API_PATHS } from '@/constants/api';
// 协议版本接口定义
export interface ProtocolVersion {
@ -81,9 +80,7 @@ export interface UpdateProtocolVersionResponse {
}
class ProtocolService {
private readonly baseUrl = '/protocolversions';
private readonly baseUrl = API_PATHS.PROTOCOLS;
// 获取协议版本列表
async getProtocolVersions(params: GetProtocolVersionsRequest = {}): Promise<OperationResult<GetProtocolVersionsResponse>> {

Loading…
Cancel
Save