Browse Source

feat: 调整设备运行时管理路由位置并更新权限配置

- 将设备运行时管理路由从独立路由调整为放在 instruments 路由内部
- 更新菜单配置,将设备运行时管理菜单项移动到仪表管理下面
- 在 AuthContext.tsx 的 getDefaultPermissions 函数中添加设备运行时管理权限
- 更新路由路径:/dashboard/instruments/device-runtimes/list
- 更新菜单路径:仪表管理 -> 运行时状态
- 添加 deviceruntimes.view 和 deviceruntimes.manage 权限
- 更新 modify.md 记录所有修改内容
feature/x1-web-request
hyh 4 days ago
parent
commit
68a1c4202a
  1. 20
      src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimes/GetDeviceRuntimesQueryHandler.cs
  2. 27
      src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimes/GetDeviceRuntimesResponse.cs
  3. 18
      src/X1.Domain/Entities/Device/CellularDeviceRuntimeDetail.cs
  4. 106
      src/X1.Domain/Entities/Device/CellularDeviceRuntimeHistory.cs
  5. 14
      src/X1.Infrastructure/Repositories/Device/CellularDeviceRuntimeRepository.cs
  6. 3
      src/X1.WebUI/src/constants/api.ts
  7. 8
      src/X1.WebUI/src/constants/menuConfig.ts
  8. 3
      src/X1.WebUI/src/contexts/AuthContext.tsx
  9. 276
      src/X1.WebUI/src/pages/device-runtimes/DeviceRuntimeDetail.tsx
  10. 209
      src/X1.WebUI/src/pages/device-runtimes/DeviceRuntimesTable.tsx
  11. 507
      src/X1.WebUI/src/pages/device-runtimes/DeviceRuntimesView.tsx
  12. 6
      src/X1.WebUI/src/pages/instruments/DeviceForm.tsx
  13. 23
      src/X1.WebUI/src/pages/instruments/DevicesTable.tsx
  14. 25
      src/X1.WebUI/src/pages/instruments/DevicesView.tsx
  15. 20
      src/X1.WebUI/src/routes/AppRouter.tsx
  16. 178
      src/X1.WebUI/src/services/deviceRuntimeService.ts
  17. 140
      src/X1.WebUI/src/services/deviceService.ts
  18. 6
      src/X1.WebUI/src/services/instrumentService.ts
  19. 53
      src/X1.WebUI/src/services/permissionService.ts
  20. 102
      src/X1.WebUI/src/services/rolePermissionService.ts
  21. 1009
      src/modify.md

20
src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimes/GetDeviceRuntimesQueryHandler.cs

@ -73,25 +73,11 @@ public class GetDeviceRuntimesQueryHandler : IRequestHandler<GetDeviceRuntimesQu
{
return new DeviceRuntimeDto
{
Id = runtime.Id,
DeviceCode = runtime.DeviceCode,
RuntimeStatus = runtime.RuntimeStatus.ToString(),
RuntimeCode = runtime.RuntimeCode,
RuntimeStatus = (int)runtime.RuntimeStatus,
NetworkStackCode = runtime.NetworkStackCode,
CreatedAt = runtime.CreatedAt,
UpdatedAt = runtime.UpdatedAt ?? DateTime.UtcNow,
Device = runtime.Device != null ? new DeviceInfoDto
{
Id = runtime.Device.Id,
Name = runtime.Device.Name,
SerialNumber = runtime.Device.SerialNumber,
DeviceCode = runtime.Device.DeviceCode,
Description = runtime.Device.Description,
IpAddress = runtime.Device.IpAddress,
AgentPort = runtime.Device.AgentPort,
IsEnabled = runtime.Device.IsEnabled
} : null
Name = runtime.Device?.Name,
CreatedAt = runtime.CreatedAt
};
}
}

27
src/X1.Application/Features/DeviceRuntimes/Queries/GetDeviceRuntimes/GetDeviceRuntimesResponse.cs

@ -36,11 +36,6 @@ public class GetDeviceRuntimesResponse
/// </summary>
public class DeviceRuntimeDto
{
/// <summary>
/// 运行时状态ID
/// </summary>
public string Id { get; set; } = null!;
/// <summary>
/// 设备编号
/// </summary>
@ -49,34 +44,22 @@ public class DeviceRuntimeDto
/// <summary>
/// 运行时状态
/// </summary>
public string RuntimeStatus { get; set; } = null!;
/// <summary>
/// 运行编码
/// </summary>
public string? RuntimeCode { get; set; }
public int RuntimeStatus { get; set; }
/// <summary>
/// 网络栈配置编号
/// </summary>
public string? NetworkStackCode { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 更新时间
/// 设备名称
/// </summary>
public DateTime UpdatedAt { get; set; }
public string? Name { get; set; }
/// <summary>
/// 设备信息
/// 创建时间
/// </summary>
public DeviceInfoDto? Device { get; set; }
public DateTime CreatedAt { get; set; }
}
/// <summary>

18
src/X1.Domain/Entities/Device/CellularDeviceRuntimeDetail.cs

@ -10,6 +10,13 @@ public class CellularDeviceRuntimeDetail : Entity
{
private CellularDeviceRuntimeDetail() { }
/// <summary>
/// 设备编码
/// </summary>
[Required]
[MaxLength(50)]
public string DeviceCode { get; private set; } = null!;
/// <summary>
/// 运行编码
/// </summary>
@ -17,6 +24,13 @@ public class CellularDeviceRuntimeDetail : Entity
[MaxLength(50)]
public string RuntimeCode { get; private set; } = null!;
/// <summary>
/// 网络栈配置编号
/// </summary>
[Required]
[MaxLength(50)]
public string NetworkStackCode { get; private set; } = null!;
/// <summary>
/// 运行时状态(true=运行中,false=已停止)
/// </summary>
@ -37,14 +51,18 @@ public class CellularDeviceRuntimeDetail : Entity
/// 创建设备运行时明细记录
/// </summary>
public static CellularDeviceRuntimeDetail Create(
string deviceCode,
string runtimeCode,
string networkStackCode,
string createdBy,
bool runtimeStatus = false)
{
var detail = new CellularDeviceRuntimeDetail
{
Id = Guid.NewGuid().ToString(),
DeviceCode = deviceCode,
RuntimeCode = runtimeCode,
NetworkStackCode = networkStackCode,
RuntimeStatus = runtimeStatus,
CreatedBy = createdBy,
CreatedAt = DateTime.UtcNow

106
src/X1.Domain/Entities/Device/CellularDeviceRuntimeHistory.cs

@ -1,106 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using CellularManagement.Domain.Entities.Common;
namespace CellularManagement.Domain.Entities.Device;
/// <summary>
/// 蜂窝设备运行时历史记录实体
/// </summary>
public class CellularDeviceRuntimeHistory : BaseEntity
{
private CellularDeviceRuntimeHistory() { }
/// <summary>
/// 运行时ID(外键)
/// </summary>
[Required]
[MaxLength(450)]
public string RuntimeId { get; private set; } = null!;
/// <summary>
/// 设备编号
/// </summary>
[Required]
[MaxLength(50)]
public string DeviceCode { get; private set; } = null!;
/// <summary>
/// 操作前状态
/// </summary>
public DeviceRuntimeStatus PreviousStatus { get; private set; }
/// <summary>
/// 操作后状态
/// </summary>
public DeviceRuntimeStatus NewStatus { get; private set; }
/// <summary>
/// 运行编码
/// </summary>
[MaxLength(50)]
public string? RuntimeCode { get; private set; }
/// <summary>
/// 网络栈配置编号
/// </summary>
[MaxLength(50)]
public string? NetworkStackCode { get; private set; }
/// <summary>
/// 操作人ID
/// </summary>
[Required]
[MaxLength(450)]
public string OperatorId { get; private set; } = null!;
/// <summary>
/// 操作描述
/// </summary>
[Required]
[MaxLength(200)]
public string OperationDescription { get; private set; } = null!;
/// <summary>
/// 操作时间
/// </summary>
public DateTime OperationAt { get; private set; }
/// <summary>
/// 运行时实例
/// </summary>
public virtual CellularDeviceRuntime Runtime { get; private set; } = null!;
/// <summary>
/// 创建设备运行时历史记录
/// </summary>
public static CellularDeviceRuntimeHistory Create(
string runtimeId,
string deviceCode,
DeviceRuntimeStatus previousStatus,
DeviceRuntimeStatus newStatus,
string? runtimeCode,
string? networkStackCode,
string operatorId,
string operationDescription,
DateTime operationAt)
{
var history = new CellularDeviceRuntimeHistory
{
Id = Guid.NewGuid().ToString(),
RuntimeId = runtimeId,
DeviceCode = deviceCode,
PreviousStatus = previousStatus,
NewStatus = newStatus,
RuntimeCode = runtimeCode,
NetworkStackCode = networkStackCode,
OperatorId = operatorId,
OperationDescription = operationDescription,
OperationAt = operationAt,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
return history;
}
}

14
src/X1.Infrastructure/Repositories/Device/CellularDeviceRuntimeRepository.cs

@ -151,7 +151,10 @@ public class CellularDeviceRuntimeRepository : BaseRepository<CellularDeviceRunt
string? keyword,
CancellationToken cancellationToken = default)
{
var query = await QueryRepository.FindAsync(r => true, cancellationToken: cancellationToken);
var query = await QueryRepository.FindAsync(
r => true,
include: q => q.Include(r => r.Device),
cancellationToken: cancellationToken);
if (!string.IsNullOrWhiteSpace(keyword))
{
@ -184,8 +187,13 @@ public class CellularDeviceRuntimeRepository : BaseRepository<CellularDeviceRunt
(r.NetworkStackCode != null && r.NetworkStackCode.Contains(keyword));
}
// 执行分页查询
var result = await QueryRepository.GetPagedAsync(predicate, pageNumber, pageSize, null, cancellationToken);
// 执行分页查询,包含设备信息
var result = await QueryRepository.GetPagedAsync(
predicate,
pageNumber,
pageSize,
include: q => q.Include(r => r.Device),
cancellationToken);
return (result.TotalCount, result.Items.ToList());
}

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

@ -2,6 +2,7 @@
export const API_PATHS = {
// 设备相关
DEVICES: '/devices',
DEVICE_RUNTIMES: '/api/device-runtimes',
// 协议相关
PROTOCOLS: '/protocolversions',
@ -21,6 +22,8 @@ export const API_PATHS = {
// 用户相关
USERS: '/users',
ROLES: '/roles',
PERMISSIONS: '/api/permissions',
ROLE_PERMISSIONS: '/api/role-permissions',
// 认证相关
AUTH: {

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

@ -54,6 +54,9 @@ export type Permission =
| 'corenetworkconfigs.manage'
| 'networkstackconfigs.view'
| 'networkstackconfigs.manage'
// 设备运行时管理权限
| 'deviceruntimes.view'
| 'deviceruntimes.manage'
export interface MenuItem {
@ -187,6 +190,11 @@ export const menuItems: MenuItem[] = [
title: '协议列表',
href: '/dashboard/instruments/protocols',
permission: 'protocols.view',
},
{
title: '运行时状态',
href: '/dashboard/instruments/device-runtimes/list',
permission: 'deviceruntimes.view',
}
],
},

3
src/X1.WebUI/src/contexts/AuthContext.tsx

@ -81,6 +81,9 @@ const getDefaultPermissions = (userPermissions: Record<string, boolean> = {}) =>
'corenetworkconfigs.manage',
'networkstackconfigs.view',
'networkstackconfigs.manage',
// 设备运行时管理权限
'deviceruntimes.view',
'deviceruntimes.manage',
])
];

276
src/X1.WebUI/src/pages/device-runtimes/DeviceRuntimeDetail.tsx

@ -0,0 +1,276 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
deviceRuntimeService,
GetDeviceRuntimeStatusResponse
} from '@/services/deviceRuntimeService';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { useToast } from '@/components/ui/use-toast';
import { ArrowLeftIcon, ReloadIcon } from '@radix-ui/react-icons';
import { format } from 'date-fns';
import { cn } from '@/lib/utils';
export default function DeviceRuntimeDetail() {
const { deviceCode } = useParams<{ deviceCode: string }>();
const navigate = useNavigate();
const { toast } = useToast();
const [deviceRuntime, setDeviceRuntime] = useState<GetDeviceRuntimeStatusResponse | null>(null);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
// 获取设备运行时状态详情
const fetchDeviceRuntimeStatus = async (isRefresh = false) => {
if (!deviceCode) return;
if (isRefresh) {
setRefreshing(true);
} else {
setLoading(true);
}
try {
const result = await deviceRuntimeService.getDeviceRuntimeStatus(deviceCode);
if (result.isSuccess && result.data) {
setDeviceRuntime(result.data);
} else {
toast({
title: '获取设备运行时状态失败',
description: result.errorMessages?.join(', ') || '未知错误',
variant: 'destructive',
});
}
} catch (error) {
console.error('获取设备运行时状态失败:', error);
toast({
title: '获取设备运行时状态失败',
description: '网络错误或服务器异常',
variant: 'destructive',
});
} finally {
setLoading(false);
setRefreshing(false);
}
};
// 格式化时间
const formatDateTime = (dateString: string) => {
try {
return format(new Date(dateString), 'yyyy-MM-dd HH:mm:ss');
} catch {
return dateString;
}
};
// 获取状态描述和颜色
const getStatusInfo = (status: string) => {
const description = deviceRuntimeService.getRuntimeStatusDescription(status);
const color = deviceRuntimeService.getRuntimeStatusColor(status);
return { description, color };
};
// 初始化加载
useEffect(() => {
fetchDeviceRuntimeStatus();
}, [deviceCode]);
if (loading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="flex items-center space-x-2">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
<span>...</span>
</div>
</div>
);
}
if (!deviceRuntime) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<h3 className="text-lg font-medium"></h3>
<p className="text-muted-foreground"> {deviceCode} </p>
<Button
onClick={() => navigate('/device-runtimes')}
className="mt-4"
variant="outline"
>
</Button>
</div>
</div>
);
}
const statusInfo = getStatusInfo(deviceRuntime.runtimeStatus);
return (
<div className="space-y-6">
{/* 页面头部 */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Button
variant="ghost"
size="sm"
onClick={() => navigate('/device-runtimes')}
>
<ArrowLeftIcon className="h-4 w-4 mr-2" />
</Button>
<div>
<h1 className="text-2xl font-bold"></h1>
<p className="text-muted-foreground">: {deviceRuntime.deviceCode}</p>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={() => fetchDeviceRuntimeStatus(true)}
disabled={refreshing}
>
<ReloadIcon className={cn("h-4 w-4 mr-2", refreshing && "animate-spin")} />
{refreshing ? '刷新中...' : '刷新'}
</Button>
</div>
{/* 状态概览卡片 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span></span>
<Badge
variant={statusInfo.color === 'success' ? 'default' :
statusInfo.color === 'warning' ? 'secondary' :
statusInfo.color === 'error' ? 'destructive' : 'outline'}
className={cn(
statusInfo.color === 'success' && 'bg-green-100 text-green-800 hover:bg-green-100',
statusInfo.color === 'warning' && 'bg-orange-100 text-orange-800 hover:bg-orange-100',
statusInfo.color === 'error' && 'bg-red-100 text-red-800 hover:bg-red-100'
)}
>
{statusInfo.description}
</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<div className="text-sm font-medium text-muted-foreground"></div>
<div className="text-lg font-semibold">{deviceRuntime.deviceCode}</div>
</div>
<div className="space-y-2">
<div className="text-sm font-medium text-muted-foreground"></div>
<div className="text-lg font-mono">
{deviceRuntime.runtimeCode || '-'}
</div>
</div>
</div>
</CardContent>
</Card>
{/* 详细信息卡片 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 网络配置信息 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="text-sm font-medium text-muted-foreground"></div>
<div className="text-base">
{deviceRuntime.networkStackCode || '-'}
</div>
</div>
<Separator />
<div className="space-y-2">
<div className="text-sm font-medium text-muted-foreground"></div>
<div className="text-base">
<Badge
variant={statusInfo.color === 'success' ? 'default' :
statusInfo.color === 'warning' ? 'secondary' :
statusInfo.color === 'error' ? 'destructive' : 'outline'}
className={cn(
statusInfo.color === 'success' && 'bg-green-100 text-green-800 hover:bg-green-100',
statusInfo.color === 'warning' && 'bg-orange-100 text-orange-800 hover:bg-orange-100',
statusInfo.color === 'error' && 'bg-red-100 text-red-800 hover:bg-red-100'
)}
>
{statusInfo.description}
</Badge>
</div>
</div>
</CardContent>
</Card>
{/* 时间信息 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="text-sm font-medium text-muted-foreground"></div>
<div className="text-base">
{deviceRuntime.updatedAt ? formatDateTime(deviceRuntime.updatedAt) : '-'}
</div>
</div>
<Separator />
<div className="space-y-2">
<div className="text-sm font-medium text-muted-foreground"></div>
<div className="text-base">
{deviceRuntime.updatedAt ? (
<span>
{(() => {
const now = new Date();
const updatedAt = new Date(deviceRuntime.updatedAt);
const diffMs = now.getTime() - updatedAt.getTime();
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
if (diffHours > 0) {
return `${diffHours}小时${diffMinutes}分钟`;
} else {
return `${diffMinutes}分钟`;
}
})()}
</span>
) : '-'}
</div>
</div>
</CardContent>
</Card>
</div>
{/* 操作按钮 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center space-x-4">
<Button
variant="outline"
onClick={() => navigate('/device-runtimes')}
>
</Button>
<Button
variant="outline"
onClick={() => fetchDeviceRuntimeStatus(true)}
disabled={refreshing}
>
<ReloadIcon className={cn("h-4 w-4 mr-2", refreshing && "animate-spin")} />
{refreshing ? '刷新中...' : '刷新状态'}
</Button>
</div>
</CardContent>
</Card>
</div>
);
}

209
src/X1.WebUI/src/pages/device-runtimes/DeviceRuntimesTable.tsx

@ -0,0 +1,209 @@
import React from 'react';
import { DeviceRuntime } from '@/services/deviceRuntimeService';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import { MoreHorizontalIcon, PlayIcon, StopIcon } from '@radix-ui/react-icons';
import { format } from 'date-fns';
import { cn } from '@/lib/utils';
interface DeviceRuntimesTableProps {
deviceRuntimes: DeviceRuntime[];
loading: boolean;
columns: { key: string; title: string; visible: boolean }[];
density: 'compact' | 'default' | 'comfortable';
selectedDevices: string[];
onDeviceSelect: (deviceCode: string, checked: boolean) => void;
onSelectAll: (checked: boolean) => void;
onStopDevice: (deviceCode: string) => void;
getRuntimeStatusDescription: (status: number | string) => string;
getRuntimeStatusColor: (status: number | string) => string;
}
export default function DeviceRuntimesTable({
deviceRuntimes,
loading,
columns,
density,
selectedDevices,
onDeviceSelect,
onSelectAll,
onStopDevice,
getRuntimeStatusDescription,
getRuntimeStatusColor,
}: DeviceRuntimesTableProps) {
// 密度样式映射
const densityStyles = {
compact: 'py-1',
default: 'py-2',
comfortable: 'py-3',
};
// 格式化时间
const formatDateTime = (dateString: string) => {
try {
return format(new Date(dateString), 'yyyy-MM-dd HH:mm:ss');
} catch {
return dateString;
}
};
// 渲染单元格内容
const renderCell = (device: DeviceRuntime, columnKey: string) => {
switch (columnKey) {
case 'deviceCode':
return (
<div className="font-medium">
{device.deviceCode}
</div>
);
case 'name':
return (
<div className="text-sm text-muted-foreground">
{device.name || '-'}
</div>
);
case 'runtimeStatus':
const statusDescription = getRuntimeStatusDescription(device.runtimeStatus);
const statusColor = getRuntimeStatusColor(device.runtimeStatus);
return (
<Badge
variant={statusColor === 'success' ? 'default' :
statusColor === 'warning' ? 'secondary' :
statusColor === 'error' ? 'destructive' : 'outline'}
className={cn(
statusColor === 'success' && 'bg-green-100 text-green-800 hover:bg-green-100',
statusColor === 'warning' && 'bg-orange-100 text-orange-800 hover:bg-orange-100',
statusColor === 'error' && 'bg-red-100 text-red-800 hover:bg-red-100'
)}
>
{statusDescription}
</Badge>
);
case 'networkStackCode':
return (
<div className="text-sm">
{device.networkStackCode || '-'}
</div>
);
case 'createdAt':
return (
<div className="text-sm text-muted-foreground">
{formatDateTime(device.createdAt)}
</div>
);
case 'actions':
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<MoreHorizontalIcon className="h-4 w-4" />
<span className="sr-only"></span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{device.runtimeStatus === 1 && (
<DropdownMenuItem
onClick={() => onStopDevice(device.deviceCode)}
className="text-red-600 focus:text-red-600"
>
<StopIcon className="h-4 w-4 mr-2" />
</DropdownMenuItem>
)}
{device.runtimeStatus !== 1 && (
<DropdownMenuItem disabled className="text-muted-foreground">
<PlayIcon className="h-4 w-4 mr-2" />
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
);
default:
return null;
}
};
// 过滤可见列
const visibleColumns = columns.filter(col => col.visible);
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
{/* 选择框列 */}
<TableHead className="w-12">
<Checkbox
checked={selectedDevices.length === deviceRuntimes.length && deviceRuntimes.length > 0}
onCheckedChange={onSelectAll}
aria-label="全选"
/>
</TableHead>
{/* 动态列 */}
{visibleColumns.map((column) => (
<TableHead key={column.key} className={cn(densityStyles[density])}>
{column.title}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={visibleColumns.length + 1} className="text-center py-8">
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
<span className="ml-2">...</span>
</div>
</TableCell>
</TableRow>
) : deviceRuntimes.length === 0 ? (
<TableRow>
<TableCell colSpan={visibleColumns.length + 1} className="text-center py-8">
<div className="text-muted-foreground">
</div>
</TableCell>
</TableRow>
) : (
deviceRuntimes.map((device) => (
<TableRow key={device.deviceCode}>
{/* 选择框 */}
<TableCell className="w-12">
<Checkbox
checked={selectedDevices.includes(device.deviceCode)}
onCheckedChange={(checked) => onDeviceSelect(device.deviceCode, checked as boolean)}
aria-label={`选择设备 ${device.deviceCode}`}
/>
</TableCell>
{/* 动态单元格 */}
{visibleColumns.map((column) => (
<TableCell key={column.key} className={cn(densityStyles[density])}>
{renderCell(device, column.key)}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
);
}

507
src/X1.WebUI/src/pages/device-runtimes/DeviceRuntimesView.tsx

@ -0,0 +1,507 @@
import React, { useState, useEffect } from 'react';
import {
getDeviceRuntimes,
DeviceRuntime,
GetDeviceRuntimesRequest,
startDevices,
stopDevice,
StartDeviceRequest,
deviceRuntimeService
} from '@/services/deviceRuntimeService';
import DeviceRuntimesTable from './DeviceRuntimesTable';
import { Input } from '@/components/ui/input';
import PaginationBar from '@/components/ui/PaginationBar';
import TableToolbar, { DensityType } from '@/components/ui/TableToolbar';
import { Button } from '@/components/ui/button';
import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons';
import { useToast } from '@/components/ui/use-toast';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger
} from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
const defaultColumns = [
{ key: 'deviceCode', title: '设备编号', visible: true },
{ key: 'name', title: '设备名称', visible: true },
{ key: 'runtimeStatus', title: '运行时状态', visible: true },
{ key: 'networkStackCode', 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: 'runtimeStatus', label: '运行时状态', type: 'select', options: [
{ value: '', label: '全部状态' },
{ value: '0', label: '初始化' },
{ value: '1', label: '运行中' },
{ value: '2', label: '已停止' },
{ value: '3', 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条/页' },
] },
];
/**
*
*
* DeviceRuntimeDto
* - deviceCode: 设备编号
* - name: 设备名称
* - runtimeStatus: 运行时状态0:初始化, 1:运行中, 2:已停止, 3:错误
* - networkStackCode: 网络栈配置编号
* - createdAt: 创建时间
*/
export default function DeviceRuntimesView() {
const [deviceRuntimes, setDeviceRuntimes] = useState<DeviceRuntime[]>([]);
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 [runtimeStatus, setRuntimeStatus] = useState<number | undefined>(undefined);
// 批量操作状态
const [selectedDevices, setSelectedDevices] = useState<string[]>([]);
const [startDialogOpen, setStartDialogOpen] = useState(false);
const [networkStackCode, setNetworkStackCode] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
// Toast 提示
const { toast } = useToast();
// 获取设备运行时状态列表
const fetchDeviceRuntimes = async (params: Partial<GetDeviceRuntimesRequest> = {}) => {
setLoading(true);
try {
const result = await getDeviceRuntimes({
pageNumber: params.pageNumber || pageNumber,
pageSize: params.pageSize || pageSize,
searchTerm: params.searchTerm !== undefined ? params.searchTerm : searchTerm,
runtimeStatus: params.runtimeStatus !== undefined ? params.runtimeStatus : runtimeStatus,
});
if (result.isSuccess && result.data) {
setDeviceRuntimes(result.data.items);
setTotal(result.data.totalCount);
if (params.pageNumber) setPageNumber(params.pageNumber);
if (params.pageSize) setPageSize(params.pageSize);
} else {
toast({
title: '获取设备运行时状态失败',
description: result.errorMessages?.join(', ') || '未知错误',
variant: 'destructive',
});
}
} catch (error) {
console.error('获取设备运行时状态失败:', error);
toast({
title: '获取设备运行时状态失败',
description: '网络错误或服务器异常',
variant: 'destructive',
});
} finally {
setLoading(false);
}
};
// 启动设备
const handleStartDevices = async () => {
if (!networkStackCode.trim()) {
toast({
title: '网络栈配置不能为空',
description: '请选择网络栈配置',
variant: 'destructive',
});
return;
}
if (selectedDevices.length === 0) {
toast({
title: '请选择要启动的设备',
description: '请至少选择一个设备',
variant: 'destructive',
});
return;
}
setIsSubmitting(true);
try {
const deviceRequests: StartDeviceRequest[] = selectedDevices.map(deviceCode => ({
deviceCode,
networkStackCode: networkStackCode.trim()
}));
const result = await startDevices(deviceRequests);
if (result.isSuccess && result.data) {
const { summary } = result.data;
toast({
title: '设备启动完成',
description: `总数: ${summary?.totalCount}, 成功: ${summary?.successCount}, 失败: ${summary?.failureCount}`,
variant: summary?.failureCount === 0 ? 'default' : 'destructive',
});
// 刷新列表
fetchDeviceRuntimes();
setStartDialogOpen(false);
setSelectedDevices([]);
setNetworkStackCode('');
} else {
toast({
title: '设备启动失败',
description: result.errorMessages?.join(', ') || '未知错误',
variant: 'destructive',
});
}
} catch (error) {
console.error('设备启动失败:', error);
toast({
title: '设备启动失败',
description: '网络错误或服务器异常',
variant: 'destructive',
});
} finally {
setIsSubmitting(false);
}
};
// 停止设备
const handleStopDevice = async (deviceCode: string) => {
try {
const result = await stopDevice(deviceCode);
if (result.isSuccess) {
toast({
title: '设备停止成功',
description: `设备 ${deviceCode} 已停止`,
});
// 刷新列表
fetchDeviceRuntimes();
} else {
toast({
title: '设备停止失败',
description: result.errorMessages?.join(', ') || '未知错误',
variant: 'destructive',
});
}
} catch (error) {
console.error('设备停止失败:', error);
toast({
title: '设备停止失败',
description: '网络错误或服务器异常',
variant: 'destructive',
});
}
};
// 处理设备选择
const handleDeviceSelect = (deviceCode: string, checked: boolean) => {
if (checked) {
setSelectedDevices(prev => [...prev, deviceCode]);
} else {
setSelectedDevices(prev => prev.filter(code => code !== deviceCode));
}
};
// 处理全选
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedDevices(deviceRuntimes.map(device => device.deviceCode));
} else {
setSelectedDevices([]);
}
};
// 查询
const handleQuery = () => {
fetchDeviceRuntimes({ pageNumber: 1 });
};
// 重置
const handleReset = () => {
setSearchTerm('');
setRuntimeStatus(undefined);
setSelectedDevices([]);
fetchDeviceRuntimes({
pageNumber: 1,
searchTerm: '',
runtimeStatus: undefined
});
};
// 页码变化
const handlePageChange = (page: number) => {
fetchDeviceRuntimes({ pageNumber: page });
};
// 每页数量变化
const handlePageSizeChange = (size: number) => {
fetchDeviceRuntimes({ pageNumber: 1, pageSize: size });
};
// 初始化加载
useEffect(() => {
fetchDeviceRuntimes();
}, []);
// 统计信息
const runningCount = deviceRuntimes.filter(d => d.runtimeStatus === 1).length;
const stoppedCount = deviceRuntimes.filter(d => d.runtimeStatus === 2).length;
const errorCount = deviceRuntimes.filter(d => d.runtimeStatus === 3).length;
return (
<div className="space-y-6">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold"></h1>
<p className="text-muted-foreground"></p>
</div>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{total}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{runningCount}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-orange-600">{stoppedCount}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">{errorCount}</div>
</CardContent>
</Card>
</div>
{/* 搜索工具栏 */}
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{/* 第一行搜索字段 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="searchTerm"></Label>
<Input
id="searchTerm"
placeholder="请输入设备编号或设备名称"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="runtimeStatus"></Label>
<Select
value={runtimeStatus?.toString() || ''}
onValueChange={(value) => setRuntimeStatus(value ? parseInt(value) : undefined)}
>
<SelectTrigger>
<SelectValue placeholder="请选择状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value=""></SelectItem>
<SelectItem value="0"></SelectItem>
<SelectItem value="1"></SelectItem>
<SelectItem value="2"></SelectItem>
<SelectItem value="3"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* 高级搜索字段 */}
{showAdvanced && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="pageSize"></Label>
<Select
value={pageSize.toString()}
onValueChange={(value) => handlePageSizeChange(parseInt(value))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10/</SelectItem>
<SelectItem value="20">20/</SelectItem>
<SelectItem value="50">50/</SelectItem>
<SelectItem value="100">100/</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)}
{/* 操作按钮 */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Button onClick={handleQuery} disabled={loading}>
</Button>
<Button variant="outline" onClick={handleReset} disabled={loading}>
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setShowAdvanced(!showAdvanced)}
>
{showAdvanced ? (
<>
<ChevronUpIcon className="h-4 w-4 mr-1" />
</>
) : (
<>
<ChevronDownIcon className="h-4 w-4 mr-1" />
</>
)}
</Button>
</div>
{/* 批量操作按钮 */}
<div className="flex items-center space-x-2">
<Dialog open={startDialogOpen} onOpenChange={setStartDialogOpen}>
<DialogTrigger asChild>
<Button
disabled={selectedDevices.length === 0}
className="bg-green-600 hover:bg-green-700"
>
({selectedDevices.length})
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="networkStackCode"></Label>
<Input
id="networkStackCode"
placeholder="请输入网络栈配置编号"
value={networkStackCode}
onChange={(e) => setNetworkStackCode(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label> ({selectedDevices.length} )</Label>
<div className="max-h-32 overflow-y-auto space-y-1">
{selectedDevices.map(deviceCode => (
<Badge key={deviceCode} variant="secondary" className="mr-1">
{deviceCode}
</Badge>
))}
</div>
</div>
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={() => setStartDialogOpen(false)}>
</Button>
<Button
onClick={handleStartDevices}
disabled={isSubmitting || !networkStackCode.trim()}
className="bg-green-600 hover:bg-green-700"
>
{isSubmitting ? '启动中...' : '确认启动'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
</div>
</div>
</CardContent>
</Card>
{/* 表格工具栏 */}
<TableToolbar
density={density}
onDensityChange={setDensity}
columns={columns}
onColumnsChange={setColumns}
selectedCount={selectedDevices.length}
totalCount={deviceRuntimes.length}
/>
{/* 设备运行时表格 */}
<DeviceRuntimesTable
deviceRuntimes={deviceRuntimes}
loading={loading}
columns={columns}
density={density}
selectedDevices={selectedDevices}
onDeviceSelect={handleDeviceSelect}
onSelectAll={handleSelectAll}
onStopDevice={handleStopDevice}
getRuntimeStatusDescription={deviceRuntimeService.getRuntimeStatusDescription}
getRuntimeStatusColor={deviceRuntimeService.getRuntimeStatusColor}
/>
{/* 分页 */}
<PaginationBar
currentPage={pageNumber}
totalPages={Math.ceil(total / pageSize)}
totalItems={total}
pageSize={pageSize}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
/>
</div>
);
}

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

@ -18,8 +18,7 @@ export default function DeviceForm({ onSubmit, initialData, isEdit = false, isSu
description: initialData?.description || '',
ipAddress: initialData?.ipAddress || '',
agentPort: initialData?.agentPort || 8080,
isEnabled: true,
isRunning: false
isEnabled: true
});
const handleSubmit = (e: React.FormEvent) => {
@ -32,8 +31,7 @@ export default function DeviceForm({ onSubmit, initialData, isEdit = false, isSu
deviceId: initialData?.deviceId || '',
deviceName: formData.deviceName,
description: formData.description,
isEnabled: formData.isEnabled,
isRunning: formData.isRunning
isEnabled: formData.isEnabled
};
onSubmit(updateData);
} else {

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

@ -16,6 +16,7 @@ import { DensityType } from '@/components/ui/TableToolbar';
* - description: 设备描述
* - agentPort: Agent端口
* - isEnabled: 是否启用
* - runtimeStatus: 运行时状态-1:未知, 0:初始化, 1:运行中, 2:停止中
* - createdAt: 创建时间
*
* IpAddress字段在创建/使
@ -44,6 +45,26 @@ function DeviceStatusBadge({ isEnabled }: { isEnabled: boolean }) {
);
}
// 运行时状态徽章组件
function RuntimeStatusBadge({ status }: { status: number }) {
const getStatusInfo = (status: number) => {
switch (status) {
case 0: return { text: '初始化', variant: 'secondary' as const };
case 1: return { text: '运行中', variant: 'default' as const };
case 2: return { text: '停止中', variant: 'outline' as const };
case -1: return { text: '未知', variant: 'outline' as const };
default: return { text: '未知', variant: 'outline' as const };
}
};
const { text, variant } = getStatusInfo(status);
return (
<Badge variant={variant}>
{text}
</Badge>
);
}
// 日期显示组件
function DateDisplay({ date }: { date: string }) {
return (
@ -100,6 +121,8 @@ export default function DevicesTable({
return <span className="font-mono text-sm">{device.agentPort}</span>;
case 'isEnabled':
return <DeviceStatusBadge isEnabled={device.isEnabled} />;
case 'runtimeStatus':
return <RuntimeStatusBadge status={device.runtimeStatus} />;
case 'createdAt':
return <DateDisplay date={device.createdAt} />;
case 'actions':

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

@ -16,7 +16,8 @@ const defaultColumns = [
{ key: 'deviceCode', title: '设备编码', visible: true },
{ key: 'description', title: '描述', visible: true },
{ key: 'agentPort', title: 'Agent端口', visible: true },
{ key: 'isEnabled', title: '状态', visible: true },
{ key: 'isEnabled', title: '启用状态', visible: true },
{ key: 'runtimeStatus', title: '运行时状态', visible: true },
{ key: 'createdAt', title: '创建时间', visible: true },
{ key: 'actions', title: '操作', visible: true },
];
@ -56,6 +57,7 @@ const advancedFields: SearchField[] = [
* - description: 设备描述
* - agentPort: Agent端口
* - isEnabled: 是否启用
* - runtimeStatus: 运行时状态-1:未知, 0:初始化, 1:运行中, 2:停止中
* - createdAt: 创建时间
*
* IpAddress字段在创建/使
@ -86,6 +88,27 @@ export default function DevicesView() {
// Toast 提示
const { toast } = useToast();
// 运行时状态映射
const getRuntimeStatusText = (status: number): string => {
switch (status) {
case 0: return '初始化';
case 1: return '运行中';
case 2: return '停止中';
case -1: return '未知';
default: return '未知';
}
};
const getRuntimeStatusColor = (status: number): string => {
switch (status) {
case 0: return 'text-yellow-500'; // 初始化 - 黄色
case 1: return 'text-green-500'; // 运行中 - 绿色
case 2: return 'text-orange-500'; // 停止中 - 橙色
case -1: return 'text-gray-400'; // 未知 - 灰色
default: return 'text-gray-400';
}
};
const fetchDevices = async (params: Partial<GetDevicesRequest> = {}) => {
setLoading(true);
const queryParams: GetDevicesRequest = {

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

@ -31,6 +31,9 @@ const UEAnalysisView = lazy(() => import('@/pages/analysis/UEAnalysisView'));
// 设备管理页面
const DevicesView = lazy(() => import('@/pages/instruments/DevicesView'));
// 设备运行时管理页面
const DeviceRuntimesView = lazy(() => import('@/pages/device-runtimes/DeviceRuntimesView'));
const DeviceRuntimeDetail = lazy(() => import('@/pages/device-runtimes/DeviceRuntimeDetail'));
// 协议管理页面
const ProtocolsView = lazy(() => import('@/pages/protocols/ProtocolsView'));
// RAN配置管理页面
@ -208,6 +211,23 @@ export function AppRouter() {
</AnimatedContainer>
</ProtectedRoute>
} />
<Route path="device-runtimes">
<Route index element={<Navigate to="list" replace />} />
<Route path="list" element={
<ProtectedRoute requiredPermission="deviceruntimes.view">
<AnimatedContainer>
<DeviceRuntimesView />
</AnimatedContainer>
</ProtectedRoute>
} />
<Route path="detail/:deviceCode" element={
<ProtectedRoute requiredPermission="deviceruntimes.view">
<AnimatedContainer>
<DeviceRuntimeDetail />
</AnimatedContainer>
</ProtectedRoute>
} />
</Route>
</Route>
{/* 网络栈配置管理路由 */}

178
src/X1.WebUI/src/services/deviceRuntimeService.ts

@ -0,0 +1,178 @@
import { httpClient } from '@/lib/http-client';
import { OperationResult } from '@/types/auth';
import { API_PATHS } from '@/constants/api';
// 设备运行时状态枚举
export enum DeviceRuntimeStatus {
Running = 'Running',
Stopped = 'Stopped',
Error = 'Error',
Unknown = 'Unknown'
}
// 设备运行时信息接口 - 对应 DeviceRuntimeDto
export interface DeviceRuntime {
deviceCode: string;
runtimeStatus: number; // 对应后端的 int 类型
networkStackCode?: string;
name?: string; // 对应后端的 Name 字段
createdAt: string; // 对应后端的 DateTime
}
// 获取设备运行时列表请求接口 - 对应 GetDeviceRuntimesQuery
export interface GetDeviceRuntimesRequest {
pageNumber?: number;
pageSize?: number;
searchTerm?: string;
runtimeStatus?: number; // 对应后端的 int 类型
}
// 获取设备运行时列表响应接口 - 对应 GetDeviceRuntimesResponse
export interface GetDeviceRuntimesResponse {
totalCount: number;
pageNumber: number;
pageSize: number;
totalPages: number;
items: DeviceRuntime[]; // 对应后端的 Items 字段
}
// 获取设备运行时状态响应接口 - 对应 GetDeviceRuntimeStatusResponse
export interface GetDeviceRuntimeStatusResponse {
deviceCode: string;
runtimeStatus: string; // 对应后端的 string 类型
runtimeCode?: string;
networkStackCode?: string;
updatedAt?: string; // 对应后端的 DateTime
}
// 启动设备请求接口 - 对应 DeviceStartRequest
export interface StartDeviceRequest {
deviceCode: string;
networkStackCode: string; // 必填字段
}
// 启动设备响应接口 - 对应 StartDeviceRuntimeResponse
export interface StartDeviceRuntimeResponse {
id?: string;
deviceCode?: string;
runtimeStatus?: string;
networkStackCode?: string;
updatedAt: string; // 对应后端的 DateTime
deviceResults?: DeviceStartResult[]; // 对应后端的 DeviceResults
summary?: BatchOperationSummary; // 对应后端的 Summary
}
// 设备启动结果 - 对应 DeviceStartResult
export interface DeviceStartResult {
deviceCode: string;
id: string;
runtimeStatus: string;
networkStackCode?: string;
updatedAt: string; // 对应后端的 DateTime
isSuccess: boolean;
errorMessage?: string;
}
// 批量操作统计 - 对应 BatchOperationSummary
export interface BatchOperationSummary {
totalCount: number;
successCount: number;
failureCount: number;
successRate: number; // 对应后端的计算属性
}
// 停止设备响应接口 - 对应 StopDeviceRuntimeResponse
export interface StopDeviceRuntimeResponse {
id: string;
deviceCode: string;
runtimeStatus: string;
runtimeCode?: string;
networkStackCode?: string;
updatedAt: string; // 对应后端的 DateTime
}
class DeviceRuntimeService {
private readonly baseUrl = API_PATHS.DEVICE_RUNTIMES;
// 获取设备运行时状态列表
async getDeviceRuntimes(params: GetDeviceRuntimesRequest = {}): Promise<OperationResult<GetDeviceRuntimesResponse>> {
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.runtimeStatus !== undefined) queryParams.append('runtimeStatus', params.runtimeStatus.toString());
const url = `${this.baseUrl}?${queryParams.toString()}`;
return httpClient.get<GetDeviceRuntimesResponse>(url);
}
// 根据设备编号获取设备运行时状态
async getDeviceRuntimeStatus(deviceCode: string): Promise<OperationResult<GetDeviceRuntimeStatusResponse>> {
return httpClient.get<GetDeviceRuntimeStatusResponse>(`${this.baseUrl}/${deviceCode}`);
}
// 批量启动设备
async startDevices(deviceRequests: StartDeviceRequest[]): Promise<OperationResult<StartDeviceRuntimeResponse>> {
return httpClient.post<StartDeviceRuntimeResponse>(`${this.baseUrl}/start`, {
deviceRequests
});
}
// 停止单个设备
async stopDevice(deviceCode: string): Promise<OperationResult<StopDeviceRuntimeResponse>> {
return httpClient.post<StopDeviceRuntimeResponse>(`${this.baseUrl}/${deviceCode}/stop`);
}
// 获取设备运行时状态的可读描述
getRuntimeStatusDescription(status: number | string): string {
const statusStr = typeof status === 'number' ? this.getRuntimeStatusString(status) : status;
switch (statusStr) {
case 'Running':
return '运行中';
case 'Stopped':
return '已停止';
case 'Error':
return '错误';
case 'Unknown':
return '未知';
default:
return '未知状态';
}
}
// 获取设备运行时状态的颜色
getRuntimeStatusColor(status: number | string): string {
const statusStr = typeof status === 'number' ? this.getRuntimeStatusString(status) : status;
switch (statusStr) {
case 'Running':
return 'success';
case 'Stopped':
return 'warning';
case 'Error':
return 'error';
case 'Unknown':
return 'default';
default:
return 'default';
}
}
// 将数字状态转换为字符串状态
private getRuntimeStatusString(status: number): string {
switch (status) {
case 0:
return 'Initializing';
case 1:
return 'Running';
case 2:
return 'Stopped';
case 3:
return 'Error';
default:
return 'Unknown';
}
}
}
export const deviceRuntimeService = new DeviceRuntimeService();

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

@ -0,0 +1,140 @@
import { httpClient } from '@/lib/http-client';
import { OperationResult } from '@/types/auth';
import { API_PATHS } from '@/constants/api';
// 设备信息接口
export interface Device {
deviceId: string;
deviceName: string;
deviceCode: string;
deviceType: string;
description?: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
// 获取设备列表请求接口
export interface GetDevicesRequest {
pageNumber?: number;
pageSize?: number;
searchTerm?: string;
deviceType?: string;
isActive?: boolean;
}
// 获取设备列表响应接口
export interface GetDevicesResponse {
totalCount: number;
pageNumber: number;
pageSize: number;
totalPages: number;
hasPreviousPage: boolean;
hasNextPage: boolean;
items: Device[];
}
// 获取设备详情响应接口
export interface GetDeviceByIdResponse {
deviceId: string;
deviceName: string;
deviceCode: string;
deviceType: string;
description?: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
// 创建设备请求接口
export interface CreateDeviceRequest {
deviceName: string;
deviceCode: string;
deviceType: string;
description?: string;
isActive?: boolean;
}
// 创建设备响应接口
export interface CreateDeviceResponse {
deviceId: string;
deviceName: string;
deviceCode: string;
deviceType: string;
description?: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
// 更新设备请求接口
export interface UpdateDeviceRequest {
deviceId: string;
deviceName: string;
deviceCode: string;
deviceType: string;
description?: string;
isActive?: boolean;
}
// 更新设备响应接口
export interface UpdateDeviceResponse {
deviceId: string;
deviceName: string;
deviceCode: string;
deviceType: string;
description?: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
class DeviceService {
private readonly baseUrl = API_PATHS.DEVICES;
// 获取设备列表
async getDevices(params: GetDevicesRequest = {}): Promise<OperationResult<GetDevicesResponse>> {
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.deviceType) queryParams.append('deviceType', params.deviceType);
if (params.isActive !== undefined) queryParams.append('isActive', params.isActive.toString());
const url = `${this.baseUrl}?${queryParams.toString()}`;
return httpClient.get<GetDevicesResponse>(url);
}
// 根据ID获取设备详情
async getDeviceById(deviceId: string): Promise<OperationResult<GetDeviceByIdResponse>> {
return httpClient.get<GetDeviceByIdResponse>(`${this.baseUrl}/${deviceId}`);
}
// 创建设备
async createDevice(data: CreateDeviceRequest): Promise<OperationResult<CreateDeviceResponse>> {
return httpClient.post<CreateDeviceResponse>(this.baseUrl, data);
}
// 更新设备
async updateDevice(deviceId: string, data: UpdateDeviceRequest): Promise<OperationResult<UpdateDeviceResponse>> {
return httpClient.put<UpdateDeviceResponse>(`${this.baseUrl}/${deviceId}`, data);
}
// 删除设备
async deleteDevice(deviceId: string): Promise<OperationResult<boolean>> {
return httpClient.delete<boolean>(`${this.baseUrl}/${deviceId}`);
}
// 获取设备状态的可读描述
getDeviceStatusDescription(isActive: boolean): string {
return isActive ? '启用' : '禁用';
}
// 获取设备状态的颜色
getDeviceStatusColor(isActive: boolean): string {
return isActive ? 'success' : 'error';
}
}
export const deviceService = new DeviceService();

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

@ -10,8 +10,8 @@ export interface Device {
description: string;
agentPort: number;
isEnabled: boolean;
isRunning: boolean;
createdAt: string;
runtimeStatus: number; // 运行时状态(-1表示未知状态)
}
// 获取设备列表请求接口 - 匹配后端GetDevicesQuery
@ -40,7 +40,6 @@ export interface CreateDeviceRequest {
ipAddress: string;
agentPort: number;
isEnabled?: boolean;
isRunning?: boolean;
}
// 创建设备响应接口 - 匹配后端CreateDeviceResponse
@ -51,7 +50,6 @@ export interface CreateDeviceResponse {
description: string;
agentPort: number;
isEnabled: boolean;
isRunning: boolean;
createdAt: string;
}
@ -61,7 +59,6 @@ export interface UpdateDeviceRequest {
deviceName: string;
description?: string;
isEnabled?: boolean;
isRunning?: boolean;
}
// 更新设备响应接口 - 匹配后端UpdateDeviceResponse
@ -72,7 +69,6 @@ export interface UpdateDeviceResponse {
description: string;
agentPort: number;
isEnabled: boolean;
isRunning: boolean;
updatedAt?: string;
}

53
src/X1.WebUI/src/services/permissionService.ts

@ -0,0 +1,53 @@
import { httpClient } from '@/lib/http-client';
import { OperationResult } from '@/types/auth';
import { API_PATHS } from '@/constants/api';
// 权限信息接口
export interface Permission {
id: string;
name: string;
description?: string;
createdAt: string;
updatedAt: string;
}
// 创建权限请求接口
export interface CreatePermissionRequest {
name: string;
description?: string;
}
// 创建权限响应接口
export interface CreatePermissionResponse {
id: string;
name: string;
description?: string;
createdAt: string;
updatedAt: string;
}
class PermissionService {
private readonly baseUrl = API_PATHS.PERMISSIONS;
// 创建权限
async createPermission(data: CreatePermissionRequest): Promise<OperationResult<CreatePermissionResponse>> {
return httpClient.post<CreatePermissionResponse>(this.baseUrl, data);
}
// 获取权限列表(如果需要的话)
async getPermissions(): Promise<OperationResult<Permission[]>> {
return httpClient.get<Permission[]>(this.baseUrl);
}
// 根据ID获取权限详情(如果需要的话)
async getPermissionById(permissionId: string): Promise<OperationResult<Permission>> {
return httpClient.get<Permission>(`${this.baseUrl}/${permissionId}`);
}
// 删除权限(如果需要的话)
async deletePermission(permissionId: string): Promise<OperationResult<boolean>> {
return httpClient.delete<boolean>(`${this.baseUrl}/${permissionId}`);
}
}
export const permissionService = new PermissionService();

102
src/X1.WebUI/src/services/rolePermissionService.ts

@ -0,0 +1,102 @@
import { httpClient } from '@/lib/http-client';
import { OperationResult } from '@/types/auth';
import { API_PATHS } from '@/constants/api';
// 权限信息接口
export interface Permission {
id: string;
name: string;
description?: string;
}
// 角色权限信息接口
export interface RolePermission {
roleId: string;
roleName: string;
permissions: Permission[];
}
// 获取角色权限请求接口
export interface GetRolePermissionsRequest {
roleId: string;
includeDetails?: boolean;
}
// 获取角色权限响应接口
export interface GetRolePermissionsResponse {
roleId: string;
roleName: string;
permissions: Permission[];
}
// 添加角色权限请求接口
export interface AddRolePermissionsRequest {
roleId: string;
permissionIds: string[];
}
// 添加角色权限响应接口
export interface AddRolePermissionsResponse {
roleId: string;
addedPermissionIds: string[];
failedPermissionIds: string[];
addedCount: number;
failedCount: number;
}
// 删除角色权限请求接口
export interface DeleteRolePermissionsRequest {
roleId: string;
permissionIds: string[];
}
// 删除角色权限响应接口
export interface DeleteRolePermissionsResponse {
roleId: string;
deletedCount: number;
failedPermissionIds: string[];
failedCount: number;
}
class RolePermissionService {
private readonly baseUrl = API_PATHS.ROLE_PERMISSIONS;
// 获取角色权限
async getRolePermissions(roleId: string, includeDetails: boolean = true): Promise<OperationResult<GetRolePermissionsResponse>> {
const url = `${this.baseUrl}/${roleId}?includeDetails=${includeDetails}`;
return httpClient.get<GetRolePermissionsResponse>(url);
}
// 添加角色权限
async addRolePermissions(data: AddRolePermissionsRequest): Promise<OperationResult<AddRolePermissionsResponse>> {
return httpClient.post<AddRolePermissionsResponse>(this.baseUrl, data);
}
// 删除角色权限
async deleteRolePermissions(data: DeleteRolePermissionsRequest): Promise<OperationResult<DeleteRolePermissionsResponse>> {
return httpClient.delete<DeleteRolePermissionsResponse>(this.baseUrl, { data });
}
// 批量添加权限到角色
async batchAddPermissions(roleId: string, permissionIds: string[]): Promise<OperationResult<AddRolePermissionsResponse>> {
return this.addRolePermissions({ roleId, permissionIds });
}
// 批量从角色删除权限
async batchRemovePermissions(roleId: string, permissionIds: string[]): Promise<OperationResult<DeleteRolePermissionsResponse>> {
return this.deleteRolePermissions({ roleId, permissionIds });
}
// 获取角色权限统计信息
getRolePermissionStats(response: GetRolePermissionsResponse): {
totalPermissions: number;
permissionNames: string[];
} {
return {
totalPermissions: response.permissions.length,
permissionNames: response.permissions.map(p => p.name)
};
}
}
export const rolePermissionService = new RolePermissionService();

1009
src/modify.md

File diff suppressed because it is too large
Loading…
Cancel
Save