Browse Source
- 将设备运行时管理路由从独立路由调整为放在 instruments 路由内部 - 更新菜单配置,将设备运行时管理菜单项移动到仪表管理下面 - 在 AuthContext.tsx 的 getDefaultPermissions 函数中添加设备运行时管理权限 - 更新路由路径:/dashboard/instruments/device-runtimes/list - 更新菜单路径:仪表管理 -> 运行时状态 - 添加 deviceruntimes.view 和 deviceruntimes.manage 权限 - 更新 modify.md 记录所有修改内容feature/x1-web-request
21 changed files with 2596 additions and 159 deletions
@ -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; |
|
||||
} |
|
||||
} |
|
@ -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> |
||||
|
); |
||||
|
} |
@ -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> |
||||
|
); |
||||
|
} |
@ -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> |
||||
|
); |
||||
|
} |
@ -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(); |
@ -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(); |
@ -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(); |
@ -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(); |
File diff suppressed because it is too large
Loading…
Reference in new issue