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