From bcfec713c9e4d9c219aa96e2a1d0151b166bd723 Mon Sep 17 00:00:00 2001 From: root <295172551@qq.com> Date: Thu, 31 Jul 2025 00:49:41 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0WebUI=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=92=8C=E9=A1=B5=E9=9D=A2=EF=BC=8C=E4=BC=98=E5=8C=96=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E8=BF=90=E8=A1=8C=E6=97=B6=E7=AE=A1=E7=90=86=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetNetworkStackConfigsQuery.cs | 2 +- src/X1.WebUI/package.json | 1 + src/X1.WebUI/src/components/ui/card.tsx | 78 ++ src/X1.WebUI/src/components/ui/dialog.tsx | 17 +- .../src/components/ui/dropdown-menu.tsx | 198 +++++ src/X1.WebUI/src/components/ui/index.ts | 13 +- src/X1.WebUI/src/components/ui/separator.tsx | 28 + src/X1.WebUI/src/constants/api.ts | 2 +- src/X1.WebUI/src/constants/menuConfig.ts | 2 +- .../CoreNetworkConfigsView.tsx | 187 ++--- .../device-runtimes/DeviceRuntimeDetail.tsx | 276 ------- .../device-runtimes/DeviceRuntimesTable.tsx | 220 +++++- .../device-runtimes/DeviceRuntimesView.tsx | 576 +++++++------- .../IMSConfigurationsView.tsx | 187 ++--- .../src/pages/instruments/DevicesView.tsx | 129 +--- .../NetworkStackConfigsView.tsx | 144 ++-- .../src/pages/protocols/ProtocolsView.tsx | 143 ++-- .../RANConfigurationsView.tsx | 130 ++-- src/X1.WebUI/src/routes/AppRouter.tsx | 8 - .../src/services/deviceRuntimeService.ts | 36 +- src/X1.WebUI/yarn.lock | 5 + src/modify.md | 721 +++++++++++++++++- 22 files changed, 1887 insertions(+), 1216 deletions(-) create mode 100644 src/X1.WebUI/src/components/ui/card.tsx create mode 100644 src/X1.WebUI/src/components/ui/dropdown-menu.tsx create mode 100644 src/X1.WebUI/src/components/ui/separator.tsx delete mode 100644 src/X1.WebUI/src/pages/device-runtimes/DeviceRuntimeDetail.tsx diff --git a/src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigs/GetNetworkStackConfigsQuery.cs b/src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigs/GetNetworkStackConfigsQuery.cs index b1a1cec..f0e8bd8 100644 --- a/src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigs/GetNetworkStackConfigsQuery.cs +++ b/src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigs/GetNetworkStackConfigsQuery.cs @@ -18,7 +18,7 @@ public class GetNetworkStackConfigsQuery : IRequest /// 每页大小 /// - [Range(1, 100, ErrorMessage = "每页大小必须在1-100之间")] + [Range(1, 1000, ErrorMessage = "每页大小必须在1-1000之间")] public int PageSize { get; set; } = 10; /// diff --git a/src/X1.WebUI/package.json b/src/X1.WebUI/package.json index 4a28745..2f7a3bb 100644 --- a/src/X1.WebUI/package.json +++ b/src/X1.WebUI/package.json @@ -40,6 +40,7 @@ "axios": "^1.9.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "date-fns": "^4.1.0", "framer-motion": "^12.12.1", "lucide-react": "^0.323.0", "react": "^18.2.0", diff --git a/src/X1.WebUI/src/components/ui/card.tsx b/src/X1.WebUI/src/components/ui/card.tsx new file mode 100644 index 0000000..dab7b75 --- /dev/null +++ b/src/X1.WebUI/src/components/ui/card.tsx @@ -0,0 +1,78 @@ +import * as React from "react" +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } \ No newline at end of file diff --git a/src/X1.WebUI/src/components/ui/dialog.tsx b/src/X1.WebUI/src/components/ui/dialog.tsx index 90a5754..ab55535 100644 --- a/src/X1.WebUI/src/components/ui/dialog.tsx +++ b/src/X1.WebUI/src/components/ui/dialog.tsx @@ -25,4 +25,19 @@ export const DialogContent = React.forwardRef< )); -DialogContent.displayName = 'DialogContent'; \ No newline at end of file +DialogContent.displayName = 'DialogContent'; + +export const DialogHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +DialogHeader.displayName = 'DialogHeader'; \ No newline at end of file diff --git a/src/X1.WebUI/src/components/ui/dropdown-menu.tsx b/src/X1.WebUI/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..7164374 --- /dev/null +++ b/src/X1.WebUI/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,198 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} \ No newline at end of file diff --git a/src/X1.WebUI/src/components/ui/index.ts b/src/X1.WebUI/src/components/ui/index.ts index fdd3f2d..deca9bd 100644 --- a/src/X1.WebUI/src/components/ui/index.ts +++ b/src/X1.WebUI/src/components/ui/index.ts @@ -4,4 +4,15 @@ export { Form } from './form'; export { Label } from './label'; export { Table } from './table'; export { Dialog } from './dialog'; -export { ThemeToggle } from './theme-toggle'; \ No newline at end of file +export { ThemeToggle } from './theme-toggle'; +export { Card, CardContent, CardHeader, CardTitle } from './card'; +export { Badge } from './badge'; +export { Checkbox } from './checkbox'; +export { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select'; +export { Separator } from './separator'; +export { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from './dropdown-menu'; \ No newline at end of file diff --git a/src/X1.WebUI/src/components/ui/separator.tsx b/src/X1.WebUI/src/components/ui/separator.tsx new file mode 100644 index 0000000..6dc5198 --- /dev/null +++ b/src/X1.WebUI/src/components/ui/separator.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" +import { cn } from "@/lib/utils" + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } \ No newline at end of file diff --git a/src/X1.WebUI/src/constants/api.ts b/src/X1.WebUI/src/constants/api.ts index 1493abb..5a9b1c6 100644 --- a/src/X1.WebUI/src/constants/api.ts +++ b/src/X1.WebUI/src/constants/api.ts @@ -2,7 +2,7 @@ export const API_PATHS = { // 设备相关 DEVICES: '/devices', - DEVICE_RUNTIMES: '/api/device-runtimes', + DEVICE_RUNTIMES: '/device-runtimes', // 协议相关 PROTOCOLS: '/protocolversions', diff --git a/src/X1.WebUI/src/constants/menuConfig.ts b/src/X1.WebUI/src/constants/menuConfig.ts index 0c896cd..f69dd6d 100644 --- a/src/X1.WebUI/src/constants/menuConfig.ts +++ b/src/X1.WebUI/src/constants/menuConfig.ts @@ -192,7 +192,7 @@ export const menuItems: MenuItem[] = [ permission: 'protocols.view', }, { - title: '运行时状态', + title: '启动设备网络', href: '/dashboard/instruments/device-runtimes/list', permission: 'deviceruntimes.view', } diff --git a/src/X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigsView.tsx b/src/X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigsView.tsx index f29957e..e3bfc7a 100644 --- a/src/X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigsView.tsx +++ b/src/X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigsView.tsx @@ -7,7 +7,6 @@ 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'; const defaultColumns = [ @@ -19,31 +18,6 @@ const defaultColumns = [ { 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: 'isDisabled', label: '状态', type: 'select', options: [ - { value: '', label: '请选择' }, - { value: 'false', label: '启用' }, - { value: 'true', label: '禁用' }, - ] }, -]; - -// 高级字段(展开时才显示) -const advancedFields: SearchField[] = [ - { key: 'pageSize', label: '每页数量', type: 'select', options: [ - { value: '10', label: '10条/页' }, - { value: '20', label: '20条/页' }, - { value: '50', label: '50条/页' }, - { value: '100', label: '100条/页' }, - ] }, -]; - export default function CoreNetworkConfigsView() { const [coreNetworkConfigs, setCoreNetworkConfigs] = useState([]); const [loading, setLoading] = useState(false); @@ -52,7 +26,6 @@ export default function CoreNetworkConfigsView() { const [pageSize, setPageSize] = useState(10); const [density, setDensity] = useState('default'); const [columns, setColumns] = useState(defaultColumns); - const [showAdvanced, setShowAdvanced] = useState(false); // 搜索参数 const [searchTerm, setSearchTerm] = useState(''); @@ -134,28 +107,25 @@ export default function CoreNetworkConfigsView() { }; const handleStatusChange = async (config: CoreNetworkConfig, newStatus: boolean) => { - if (isSubmitting) return; // 防止重复提交 - - setIsSubmitting(true); try { const updateData: UpdateCoreNetworkConfigRequest = { coreNetworkConfigId: config.coreNetworkConfigId, name: config.name, - configContent: config.configContent, description: config.description, - isDisabled: !newStatus // 注意:newStatus是启用状态,isDisabled是禁用状态 + configContent: config.configContent, + isDisabled: newStatus }; const result = await coreNetworkConfigService.updateCoreNetworkConfig(config.coreNetworkConfigId, updateData); if (result.isSuccess) { toast({ title: "状态更新成功", - description: `核心网络配置 "${config.name}" 状态已${newStatus ? '启用' : '禁用'}`, + description: `核心网络配置 "${config.name}" 状态已${newStatus ? '禁用' : '启用'}`, }); fetchCoreNetworkConfigs(); } else { - const errorMessage = result.errorMessages?.join(', ') || "更新核心网络配置状态时发生错误"; - console.error('更新核心网络配置状态失败:', errorMessage); + const errorMessage = result.errorMessages?.join(', ') || "更新状态时发生错误"; + console.error('更新状态失败:', errorMessage); toast({ title: "状态更新失败", description: errorMessage, @@ -163,26 +133,21 @@ export default function CoreNetworkConfigsView() { }); } } catch (error) { - console.error('更新核心网络配置状态异常:', error); + console.error('更新状态异常:', error); toast({ title: "状态更新失败", description: "网络错误,请稍后重试", variant: "destructive", }); - } finally { - setIsSubmitting(false); } }; const handleCreate = async (data: CreateCoreNetworkConfigRequest) => { if (isSubmitting) return; // 防止重复提交 - console.log('开始创建核心网络配置:', data); setIsSubmitting(true); try { const result = await coreNetworkConfigService.createCoreNetworkConfig(data); - console.log('创建核心网络配置结果:', result); - if (result.isSuccess) { toast({ title: "创建成功", @@ -192,7 +157,7 @@ export default function CoreNetworkConfigsView() { fetchCoreNetworkConfigs(); } else { const errorMessage = result.errorMessages?.join(', ') || "创建核心网络配置时发生错误"; - console.error('创建核心网络配置失败:', errorMessage, result); + console.error('创建核心网络配置失败:', errorMessage); toast({ title: "创建失败", description: errorMessage, @@ -216,7 +181,11 @@ export default function CoreNetworkConfigsView() { setIsSubmitting(true); try { - const result = await coreNetworkConfigService.updateCoreNetworkConfig(selectedConfig.coreNetworkConfigId, data); + const updateData: UpdateCoreNetworkConfigRequest = { + ...data, + coreNetworkConfigId: selectedConfig.coreNetworkConfigId + }; + const result = await coreNetworkConfigService.updateCoreNetworkConfig(selectedConfig.coreNetworkConfigId, updateData); if (result.isSuccess) { toast({ title: "更新成功", @@ -270,106 +239,60 @@ export default function CoreNetworkConfigsView() { setPageNumber(1); }; - const totalPages = Math.ceil(total / pageSize); - return (
- {/* 搜索栏 */} -
+ {/* 搜索工具栏 */} +
{ e.preventDefault(); handleQuery(); }} > - {/* 第一行字段 */} - {firstRowFields.map((field: SearchField) => ( -
- - {field.type === 'input' && ( - ) => { - if (field.key === 'searchTerm') setSearchTerm(e.target.value); - }} - /> - )} - {field.type === 'select' && ( - - )} -
- ))} - {/* 按钮组直接作为表单项之一,紧跟在最后一个表单项后面 */} -
- - - +
+ + ) => setSearchTerm(e.target.value)} + />
- - - {/* 高级搜索字段 */} - {showAdvanced && ( -
- {advancedFields.map((field: SearchField) => ( -
- - {field.type === 'select' && ( - - )} -
- ))} +
+ + +
+ {/* 按钮组 */} +
+ +
- )} +
+ {/* 表格整体卡片区域,包括工具栏、表格、分页 */}
{/* 顶部操作栏:添加核心网络配置+工具栏 */} @@ -389,6 +312,7 @@ export default function CoreNetworkConfigsView() { density={density} />
+ {/* 表格区域 */} + {/* 分页 */} (); - const navigate = useNavigate(); - const { toast } = useToast(); - - const [deviceRuntime, setDeviceRuntime] = useState(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 ( -
-
-
- 加载中... -
-
- ); - } - - if (!deviceRuntime) { - return ( -
-
-

设备运行时状态不存在

-

未找到设备编号为 {deviceCode} 的运行时状态

- -
-
- ); - } - - const statusInfo = getStatusInfo(deviceRuntime.runtimeStatus); - - return ( -
- {/* 页面头部 */} -
-
- -
-

设备运行时详情

-

设备编号: {deviceRuntime.deviceCode}

-
-
- -
- - {/* 状态概览卡片 */} - - - - 运行时状态 - - {statusInfo.description} - - - - -
-
-
设备编号
-
{deviceRuntime.deviceCode}
-
-
-
运行编码
-
- {deviceRuntime.runtimeCode || '-'} -
-
-
-
-
- - {/* 详细信息卡片 */} -
- {/* 网络配置信息 */} - - - 网络配置 - - -
-
网络栈配置编号
-
- {deviceRuntime.networkStackCode || '-'} -
-
- -
-
运行时状态
-
- - {statusInfo.description} - -
-
-
-
- - {/* 时间信息 */} - - - 时间信息 - - -
-
最后更新时间
-
- {deviceRuntime.updatedAt ? formatDateTime(deviceRuntime.updatedAt) : '-'} -
-
- -
-
状态持续时间
-
- {deviceRuntime.updatedAt ? ( - - {(() => { - 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}分钟`; - } - })()} - - ) : '-'} -
-
-
-
-
- - {/* 操作按钮 */} - - - 操作 - - -
- - -
-
-
-
- ); -} \ No newline at end of file diff --git a/src/X1.WebUI/src/pages/device-runtimes/DeviceRuntimesTable.tsx b/src/X1.WebUI/src/pages/device-runtimes/DeviceRuntimesTable.tsx index 1385329..bf5befb 100644 --- a/src/X1.WebUI/src/pages/device-runtimes/DeviceRuntimesTable.tsx +++ b/src/X1.WebUI/src/pages/device-runtimes/DeviceRuntimesTable.tsx @@ -1,28 +1,28 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { DeviceRuntime } from '@/services/deviceRuntimeService'; +import { NetworkStackConfig } from '@/services/networkStackConfigService'; 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 { Input } from '@/components/ui/input'; +import { PlayIcon, StopIcon } from '@radix-ui/react-icons'; import { format } from 'date-fns'; import { cn } from '@/lib/utils'; +import { DensityType } from '@/components/ui/TableToolbar'; interface DeviceRuntimesTableProps { deviceRuntimes: DeviceRuntime[]; loading: boolean; columns: { key: string; title: string; visible: boolean }[]; - density: 'compact' | 'default' | 'comfortable'; + density: DensityType; selectedDevices: string[]; + networkStackConfigs: NetworkStackConfig[]; onDeviceSelect: (deviceCode: string, checked: boolean) => void; onSelectAll: (checked: boolean) => void; + onStartDevice: (deviceCode: string) => void; onStopDevice: (deviceCode: string) => void; + onNetworkStackChange: (deviceCode: string, networkStackCode: string) => void; getRuntimeStatusDescription: (status: number | string) => string; getRuntimeStatusColor: (status: number | string) => string; } @@ -33,17 +33,26 @@ export default function DeviceRuntimesTable({ columns, density, selectedDevices, + networkStackConfigs, onDeviceSelect, onSelectAll, + onStartDevice, onStopDevice, + onNetworkStackChange, getRuntimeStatusDescription, getRuntimeStatusColor, }: DeviceRuntimesTableProps) { + // 网络栈配置下拉框状态 + const [openDropdowns, setOpenDropdowns] = useState<{ [key: string]: boolean }>({}); + const [searchTerms, setSearchTerms] = useState<{ [key: string]: string }>({}); + const [filteredConfigs, setFilteredConfigs] = useState<{ [key: string]: NetworkStackConfig[] }>({}); + const [dropdownPositions, setDropdownPositions] = useState<{ [key: string]: { top: number; left: number } }>({}); + // 密度样式映射 const densityStyles = { + relaxed: 'py-3', compact: 'py-1', default: 'py-2', - comfortable: 'py-3', }; // 格式化时间 @@ -55,6 +64,75 @@ export default function DeviceRuntimesTable({ } }; + // 处理网络栈配置搜索 + const handleNetworkStackSearch = (deviceCode: string, value: string) => { + setSearchTerms(prev => ({ ...prev, [deviceCode]: value })); + + if (value.trim() === '') { + setFilteredConfigs(prev => ({ ...prev, [deviceCode]: networkStackConfigs })); + } else { + const searchValue = value.toLowerCase(); + const filtered = networkStackConfigs.filter(config => + config.networkStackName.toLowerCase().includes(searchValue) || + config.networkStackCode.toLowerCase().includes(searchValue) || + (config.description && config.description.toLowerCase().includes(searchValue)) + ); + setFilteredConfigs(prev => ({ ...prev, [deviceCode]: filtered })); + } + }; + + // 处理网络栈配置选择 + const handleNetworkStackSelect = (deviceCode: string, configCode: string) => { + onNetworkStackChange(deviceCode, configCode); + setOpenDropdowns(prev => ({ ...prev, [deviceCode]: false })); + setSearchTerms(prev => ({ ...prev, [deviceCode]: '' })); + setFilteredConfigs(prev => ({ ...prev, [deviceCode]: networkStackConfigs })); + }; + + // 切换下拉框开关 + const toggleDropdown = (deviceCode: string, event: React.MouseEvent) => { + // 检查设备是否处于运行状态 + const device = deviceRuntimes.find(d => d.deviceCode === deviceCode); + if (device?.runtimeStatus === 1) { + return; // 运行中的设备不允许修改网络栈配置 + } + + const isOpen = !openDropdowns[deviceCode]; + setOpenDropdowns(prev => ({ ...prev, [deviceCode]: isOpen })); + + if (isOpen) { + setFilteredConfigs(prev => ({ ...prev, [deviceCode]: networkStackConfigs })); + + // 计算下拉框位置 + const rect = event.currentTarget.getBoundingClientRect(); + setDropdownPositions(prev => ({ + ...prev, + [deviceCode]: { + top: rect.bottom + window.scrollY, + left: rect.left + window.scrollX + } + })); + } + }; + + // 点击外部关闭下拉框 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Element; + if (!target.closest('.network-stack-cell-dropdown')) { + setOpenDropdowns({}); + } + }; + + if (Object.values(openDropdowns).some(open => open)) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [openDropdowns]); + // 渲染单元格内容 const renderCell = (device: DeviceRuntime, columnKey: string) => { switch (columnKey) { @@ -91,9 +169,64 @@ export default function DeviceRuntimesTable({ ); case 'networkStackCode': + const currentConfig = networkStackConfigs.find(c => c.networkStackCode === device.networkStackCode); + const isDropdownOpen = openDropdowns[device.deviceCode] || false; + const currentSearchTerm = searchTerms[device.deviceCode] || ''; + const currentFilteredConfigs = filteredConfigs[device.deviceCode] || networkStackConfigs; + return ( -
- {device.networkStackCode || '-'} +
+
device.runtimeStatus !== 1 && toggleDropdown(device.deviceCode, e)} + > + + {currentConfig ? `${currentConfig.networkStackName} (${currentConfig.networkStackCode})` : + device.networkStackCode ? `${device.networkStackCode} (未找到配置)` : '请选择网络栈配置'} + + + + +
+ + {isDropdownOpen && device.runtimeStatus !== 1 && ( +
+
+ handleNetworkStackSearch(device.deviceCode, e.target.value)} + className="h-6 text-xs border-0 focus:ring-0 focus:border-0" + /> +
+
+ {currentFilteredConfigs.map((config) => ( +
handleNetworkStackSelect(device.deviceCode, config.networkStackCode)} + > + {config.networkStackName} ({config.networkStackCode}) +
+ ))} + {currentFilteredConfigs.length === 0 && currentSearchTerm && ( +
+ 未找到匹配的配置 +
+ )} +
+
+ )}
); @@ -106,31 +239,38 @@ export default function DeviceRuntimesTable({ case 'actions': return ( - - - - - - {device.runtimeStatus === 1 && ( - onStopDevice(device.deviceCode)} - className="text-red-600 focus:text-red-600" - > - - 停止设备 - - )} - {device.runtimeStatus !== 1 && ( - - - 设备未运行 - - )} - - + )} + {/* 未知状态(-1)、初始化(0)、已停止(2)、错误(3):显示启动按钮 */} + {device.runtimeStatus !== 1 && ( + + )} +
); default: @@ -147,7 +287,7 @@ export default function DeviceRuntimesTable({ {/* 选择框列 */} - + 0} onCheckedChange={onSelectAll} @@ -157,7 +297,7 @@ export default function DeviceRuntimesTable({ {/* 动态列 */} {visibleColumns.map((column) => ( - + {column.title} ))} @@ -185,7 +325,7 @@ export default function DeviceRuntimesTable({ deviceRuntimes.map((device) => ( {/* 选择框 */} - + onDeviceSelect(device.deviceCode, checked as boolean)} @@ -195,7 +335,7 @@ export default function DeviceRuntimesTable({ {/* 动态单元格 */} {visibleColumns.map((column) => ( - + {renderCell(device, column.key)} ))} diff --git a/src/X1.WebUI/src/pages/device-runtimes/DeviceRuntimesView.tsx b/src/X1.WebUI/src/pages/device-runtimes/DeviceRuntimesView.tsx index 0e174e3..9277a2d 100644 --- a/src/X1.WebUI/src/pages/device-runtimes/DeviceRuntimesView.tsx +++ b/src/X1.WebUI/src/pages/device-runtimes/DeviceRuntimesView.tsx @@ -1,22 +1,18 @@ import React, { useState, useEffect } from 'react'; import { - getDeviceRuntimes, DeviceRuntime, GetDeviceRuntimesRequest, - startDevices, - stopDevice, StartDeviceRequest, deviceRuntimeService } from '@/services/deviceRuntimeService'; +import { NetworkStackConfig, networkStackConfigService } from '@/services/networkStackConfigService'; 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, @@ -26,7 +22,6 @@ import { } 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 }, @@ -37,33 +32,6 @@ const defaultColumns = [ { 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条/页' }, - ] }, -]; - /** * 设备运行时管理页面 * @@ -83,7 +51,6 @@ export default function DeviceRuntimesView() { const [pageSize, setPageSize] = useState(10); const [density, setDensity] = useState('default'); const [columns, setColumns] = useState(defaultColumns); - const [showAdvanced, setShowAdvanced] = useState(false); // 搜索参数 const [searchTerm, setSearchTerm] = useState(''); @@ -95,14 +62,62 @@ export default function DeviceRuntimesView() { const [networkStackCode, setNetworkStackCode] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); + // 网络栈配置搜索下拉框状态 + const [networkStackConfigs, setNetworkStackConfigs] = useState([]); + const [filteredNetworkStackConfigs, setFilteredNetworkStackConfigs] = useState([]); + const [networkStackSearchTerm, setNetworkStackSearchTerm] = useState(''); + const [isNetworkStackDropdownOpen, setIsNetworkStackDropdownOpen] = useState(false); + // Toast 提示 const { toast } = useToast(); + // 获取网络栈配置列表 + const fetchNetworkStackConfigs = async () => { + try { + const result = await networkStackConfigService.getNetworkStackConfigs({ + pageSize: 1000, // 获取所有配置 + isActive: true // 只获取激活的配置 + }); + + if (result.isSuccess && result.data) { + const configs = result.data.networkStackConfigs || []; + setNetworkStackConfigs(configs); + setFilteredNetworkStackConfigs(configs); + } + } catch (error) { + console.error('获取网络栈配置失败:', error); + } + }; + + // 搜索网络栈配置 + const handleNetworkStackSearchChange = (value: string) => { + setNetworkStackSearchTerm(value); + if (value.trim() === '') { + setFilteredNetworkStackConfigs(networkStackConfigs); + } else { + const searchValue = value.toLowerCase(); + const filtered = networkStackConfigs.filter(config => + config.networkStackName.toLowerCase().includes(searchValue) || + config.networkStackCode.toLowerCase().includes(searchValue) || + (config.description && config.description.toLowerCase().includes(searchValue)) + ); + setFilteredNetworkStackConfigs(filtered); + } + }; + + // 选择网络栈配置 + const handleNetworkStackSelect = (configCode: string) => { + setNetworkStackCode(configCode); + setIsNetworkStackDropdownOpen(false); + setNetworkStackSearchTerm(''); + setFilteredNetworkStackConfigs(networkStackConfigs); + }; + // 获取设备运行时状态列表 const fetchDeviceRuntimes = async (params: Partial = {}) => { setLoading(true); try { - const result = await getDeviceRuntimes({ + const result = await deviceRuntimeService.getDeviceRuntimes({ pageNumber: params.pageNumber || pageNumber, pageSize: params.pageSize || pageSize, searchTerm: params.searchTerm !== undefined ? params.searchTerm : searchTerm, @@ -160,7 +175,7 @@ export default function DeviceRuntimesView() { networkStackCode: networkStackCode.trim() })); - const result = await startDevices(deviceRequests); + const result = await deviceRuntimeService.startDevices(deviceRequests); if (result.isSuccess && result.data) { const { summary } = result.data; @@ -194,10 +209,75 @@ export default function DeviceRuntimesView() { } }; + // 启动单个设备 + const handleStartDevice = async (deviceCode: string) => { + const device = deviceRuntimes.find(d => d.deviceCode === deviceCode); + if (!device?.networkStackCode) { + toast({ + title: '请先选择网络栈配置', + description: '启动设备前需要先选择网络栈配置', + variant: 'destructive', + }); + return; + } + + try { + const result = await deviceRuntimeService.startDevices([{ + deviceCode, + networkStackCode: device.networkStackCode + }]); + + if (result.isSuccess && result.data) { + const { summary } = result.data; + 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 handleNetworkStackChange = async (deviceCode: string, networkStackCode: string) => { + // 检查设备是否处于运行状态 + const device = deviceRuntimes.find(d => d.deviceCode === deviceCode); + if (device?.runtimeStatus === 1) { + toast({ + title: '无法修改网络栈配置', + description: '运行中的设备不允许修改网络栈配置', + variant: 'destructive', + }); + return; + } + + // 这里可以调用API更新设备的网络栈配置 + // 暂时直接更新本地状态 + setDeviceRuntimes(prev => prev.map(device => + device.deviceCode === deviceCode + ? { ...device, networkStackCode } + : device + )); + }; + // 停止设备 const handleStopDevice = async (deviceCode: string) => { try { - const result = await stopDevice(deviceCode); + const result = await deviceRuntimeService.stopDevice(deviceCode); if (result.isSuccess) { toast({ @@ -268,240 +348,222 @@ export default function DeviceRuntimesView() { fetchDeviceRuntimes({ pageNumber: 1, pageSize: size }); }; + // 点击外部关闭下拉框 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Element; + if (!target.closest('.network-stack-dropdown')) { + setIsNetworkStackDropdownOpen(false); + } + }; + + if (isNetworkStackDropdownOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isNetworkStackDropdownOpen]); + // 初始化加载 useEffect(() => { fetchDeviceRuntimes(); + fetchNetworkStackConfigs(); }, []); - // 统计信息 - 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 ( -
- {/* 页面标题 */} -
-
-

设备运行时管理

-

管理设备的运行时状态,包括启动、停止和状态监控

-
-
- - {/* 统计卡片 */} -
- - - 总设备数 - - -
{total}
-
-
- - - 运行中 - - -
{runningCount}
-
-
- - - 已停止 - - -
{stoppedCount}
-
-
- - - 错误状态 - - -
{errorCount}
-
-
-
- - {/* 搜索工具栏 */} - - - 搜索条件 - - -
- {/* 第一行搜索字段 */} -
-
- - setSearchTerm(e.target.value)} - /> -
-
- - -
+
+
+ + {/* 搜索工具栏 */} +
+
{ + e.preventDefault(); + handleQuery(); + }} + > +
+ + ) => setSearchTerm(e.target.value)} + />
- - {/* 高级搜索字段 */} - {showAdvanced && ( -
-
- - -
-
- )} - - {/* 操作按钮 */} -
-
- - - + +
+ +
+ + {/* 表格整体卡片区域,包括工具栏、表格、分页 */} +
+ {/* 顶部操作栏:批量启动+工具栏 */} +
+ + + -
- - {/* 批量操作按钮 */} -
- - - - - - - 批量启动设备 - -
-
- - setNetworkStackCode(e.target.value)} - /> -
-
- -
- {selectedDevices.map(deviceCode => ( - - {deviceCode} - + + {networkStackCode ? + (() => { + const config = networkStackConfigs.find(c => c.networkStackCode === networkStackCode); + return config ? `${config.networkStackName} (${config.networkStackCode})` : `${networkStackCode} (未找到配置)`; + })() : + "请选择网络栈配置" + } + + + + +
+ + {isNetworkStackDropdownOpen && ( +
+
+ handleNetworkStackSearchChange(e.target.value)} + className="h-8 border-0 focus:ring-0 focus:border-0" + /> +
+
+ {filteredNetworkStackConfigs.map((config) => ( +
handleNetworkStackSelect(config.networkStackCode)} + > + {config.networkStackName} ({config.networkStackCode}) + {config.description && ( +
+ {config.description} +
+ )} +
))} + {filteredNetworkStackConfigs.length === 0 && networkStackSearchTerm && ( +
+ 未找到匹配的网络栈配置 +
+ )}
-
- - -
+ )} +
+
+
+ +
+ {selectedDevices.map(deviceCode => ( + + {deviceCode} + + ))}
- -
-
-
+
+
+ + +
+
+ + + fetchDeviceRuntimes()} + onDensityChange={setDensity} + onColumnsChange={setColumns} + onColumnsReset={() => setColumns(defaultColumns)} + columns={columns} + density={density} + />
- - - - {/* 表格工具栏 */} - - - {/* 设备运行时表格 */} - - - {/* 分页 */} - -
+ + {/* 表格区域 */} + + + {/* 分页 */} + +
+
+
); } \ No newline at end of file diff --git a/src/X1.WebUI/src/pages/ims-configurations/IMSConfigurationsView.tsx b/src/X1.WebUI/src/pages/ims-configurations/IMSConfigurationsView.tsx index 60ab906..a3eae5c 100644 --- a/src/X1.WebUI/src/pages/ims-configurations/IMSConfigurationsView.tsx +++ b/src/X1.WebUI/src/pages/ims-configurations/IMSConfigurationsView.tsx @@ -7,7 +7,6 @@ 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'; const defaultColumns = [ @@ -19,31 +18,6 @@ const defaultColumns = [ { 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: 'isDisabled', label: '状态', type: 'select', options: [ - { value: '', label: '请选择' }, - { value: 'false', label: '启用' }, - { value: 'true', label: '禁用' }, - ] }, -]; - -// 高级字段(展开时才显示) -const advancedFields: SearchField[] = [ - { key: 'pageSize', label: '每页数量', type: 'select', options: [ - { value: '10', label: '10条/页' }, - { value: '20', label: '20条/页' }, - { value: '50', label: '50条/页' }, - { value: '100', label: '100条/页' }, - ] }, -]; - export default function IMSConfigurationsView() { const [imsConfigurations, setIMSConfigurations] = useState([]); const [loading, setLoading] = useState(false); @@ -52,7 +26,6 @@ export default function IMSConfigurationsView() { const [pageSize, setPageSize] = useState(10); const [density, setDensity] = useState('default'); const [columns, setColumns] = useState(defaultColumns); - const [showAdvanced, setShowAdvanced] = useState(false); // 搜索参数 const [searchTerm, setSearchTerm] = useState(''); @@ -136,12 +109,9 @@ export default function IMSConfigurationsView() { const handleCreate = async (data: CreateIMSConfigurationRequest) => { if (isSubmitting) return; // 防止重复提交 - console.log('开始创建IMS配置:', data); setIsSubmitting(true); try { const result = await imsConfigurationService.createIMSConfiguration(data); - console.log('创建IMS配置结果:', result); - if (result.isSuccess) { toast({ title: "创建成功", @@ -151,7 +121,7 @@ export default function IMSConfigurationsView() { fetchIMSConfigurations(); } else { const errorMessage = result.errorMessages?.join(', ') || "创建IMS配置时发生错误"; - console.error('创建IMS配置失败:', errorMessage, result); + console.error('创建IMS配置失败:', errorMessage); toast({ title: "创建失败", description: errorMessage, @@ -171,28 +141,25 @@ export default function IMSConfigurationsView() { }; const handleStatusChange = async (configuration: IMSConfiguration, newStatus: boolean) => { - if (isSubmitting) return; // 防止重复提交 - - setIsSubmitting(true); try { const updateData: UpdateIMSConfigurationRequest = { imS_ConfigurationId: configuration.imS_ConfigurationId, name: configuration.name, - configContent: configuration.configContent, description: configuration.description, - isDisabled: !newStatus // 注意:newStatus是启用状态,isDisabled是禁用状态 + configContent: configuration.configContent, + isDisabled: newStatus }; const result = await imsConfigurationService.updateIMSConfiguration(configuration.imS_ConfigurationId, updateData); if (result.isSuccess) { toast({ title: "状态更新成功", - description: `IMS配置 "${configuration.name}" 状态已${newStatus ? '启用' : '禁用'}`, + description: `IMS配置 "${configuration.name}" 状态已${newStatus ? '禁用' : '启用'}`, }); fetchIMSConfigurations(); } else { - const errorMessage = result.errorMessages?.join(', ') || "更新IMS配置状态时发生错误"; - console.error('更新IMS配置状态失败:', errorMessage); + const errorMessage = result.errorMessages?.join(', ') || "更新状态时发生错误"; + console.error('更新状态失败:', errorMessage); toast({ title: "状态更新失败", description: errorMessage, @@ -200,14 +167,12 @@ export default function IMSConfigurationsView() { }); } } catch (error) { - console.error('更新IMS配置状态异常:', error); + console.error('更新状态异常:', error); toast({ title: "状态更新失败", description: "网络错误,请稍后重试", variant: "destructive", }); - } finally { - setIsSubmitting(false); } }; @@ -216,7 +181,11 @@ export default function IMSConfigurationsView() { setIsSubmitting(true); try { - const result = await imsConfigurationService.updateIMSConfiguration(selectedConfiguration.imS_ConfigurationId, data); + const updateData: UpdateIMSConfigurationRequest = { + ...data, + imS_ConfigurationId: selectedConfiguration.imS_ConfigurationId + }; + const result = await imsConfigurationService.updateIMSConfiguration(selectedConfiguration.imS_ConfigurationId, updateData); if (result.isSuccess) { toast({ title: "更新成功", @@ -270,106 +239,60 @@ export default function IMSConfigurationsView() { setPageNumber(1); }; - const totalPages = Math.ceil(total / pageSize); - return (
- {/* 搜索栏 */} -
+ {/* 搜索工具栏 */} +
{ e.preventDefault(); handleQuery(); }} > - {/* 第一行字段 */} - {firstRowFields.map((field: SearchField) => ( -
- - {field.type === 'input' && ( - ) => { - if (field.key === 'searchTerm') setSearchTerm(e.target.value); - }} - /> - )} - {field.type === 'select' && ( - - )} -
- ))} - {/* 按钮组直接作为表单项之一,紧跟在最后一个表单项后面 */} -
- - - +
+ + ) => setSearchTerm(e.target.value)} + />
- - - {/* 高级搜索字段 */} - {showAdvanced && ( -
- {advancedFields.map((field: SearchField) => ( -
- - {field.type === 'select' && ( - - )} -
- ))} +
+ + +
+ {/* 按钮组 */} +
+ +
- )} +
+ {/* 表格整体卡片区域,包括工具栏、表格、分页 */}
{/* 顶部操作栏:添加IMS配置+工具栏 */} @@ -389,6 +312,7 @@ export default function IMSConfigurationsView() { density={density} />
+ {/* 表格区域 */} + {/* 分页 */} ('default'); const [columns, setColumns] = useState(defaultColumns); - const [showAdvanced, setShowAdvanced] = useState(false); // 搜索参数 const [searchTerm, setSearchTerm] = useState(''); @@ -143,8 +116,6 @@ export default function DevicesView() { // eslint-disable-next-line }, [pageNumber, pageSize]); - - const handleEdit = (device: Device) => { setSelectedDevice(device); setEditOpen(true); @@ -259,8 +230,6 @@ export default function DevicesView() { } }; - - // 查询按钮 const handleQuery = () => { setPageNumber(1); @@ -288,69 +257,53 @@ export default function DevicesView() { return (
- {/* 丰富美化后的搜索栏 */} -
+ {/* 搜索工具栏 */} +
{ e.preventDefault(); handleQuery(); }} > - {(showAdvanced ? [...firstRowFields, ...advancedFields] : firstRowFields).map(field => ( -
- - {field.type === 'input' && ( - ) => { - if (field.key === 'searchTerm') setSearchTerm(e.target.value); - }} - /> - )} - {field.type === 'select' && ( - - )} -
- ))} - {/* 按钮组直接作为表单项之一,紧跟在最后一个表单项后面 */} -
- - - +
+ + ) => setSearchTerm(e.target.value)} + /> +
+
+ + +
+ {/* 按钮组 */} +
+ +
diff --git a/src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsView.tsx b/src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsView.tsx index 247a3e2..a6e390d 100644 --- a/src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsView.tsx +++ b/src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsView.tsx @@ -5,9 +5,7 @@ import NetworkStackConfigDrawer from './NetworkStackConfigDrawer'; 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'; const defaultColumns = [ @@ -22,31 +20,6 @@ const defaultColumns = [ { 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: 'networkStackName', label: '网络栈名称', type: 'input', placeholder: '请输入网络栈名称' }, - { key: 'isActive', label: '状态', type: 'select', options: [ - { value: '', label: '请选择' }, - { value: 'true', label: '激活' }, - { value: 'false', label: '非激活' }, - ] }, -]; - -// 高级字段(展开时才显示) -const advancedFields: SearchField[] = [ - { key: 'pageSize', label: '每页数量', type: 'select', options: [ - { value: '10', label: '10条/页' }, - { value: '20', label: '20条/页' }, - { value: '50', label: '50条/页' }, - { value: '100', label: '100条/页' }, - ] }, -]; - export default function NetworkStackConfigsView() { const [networkStackConfigs, setNetworkStackConfigs] = useState([]); const [loading, setLoading] = useState(false); @@ -55,7 +28,6 @@ export default function NetworkStackConfigsView() { const [pageSize, setPageSize] = useState(10); const [density, setDensity] = useState('default'); const [columns, setColumns] = useState(defaultColumns); - const [showAdvanced, setShowAdvanced] = useState(false); // 搜索参数 const [networkStackName, setNetworkStackName] = useState(''); @@ -132,12 +104,9 @@ export default function NetworkStackConfigsView() { const handleCreate = async (data: CreateNetworkStackConfigRequest) => { if (isSubmitting) return; // 防止重复提交 - console.log('开始创建网络栈配置:', data); setIsSubmitting(true); try { const result = await networkStackConfigService.createNetworkStackConfig(data); - console.log('创建网络栈配置结果:', result); - if (result.isSuccess) { toast({ title: "创建成功", @@ -147,7 +116,7 @@ export default function NetworkStackConfigsView() { fetchNetworkStackConfigs(); } else { const errorMessage = result.errorMessages?.join(', ') || "创建网络栈配置时发生错误"; - console.error('创建网络栈配置失败:', errorMessage, result); + console.error('创建网络栈配置失败:', errorMessage); toast({ title: "创建失败", description: errorMessage, @@ -171,11 +140,15 @@ export default function NetworkStackConfigsView() { setIsSubmitting(true); try { - const result = await networkStackConfigService.updateNetworkStackConfig(selectedConfig.networkStackConfigId, data); + const updateData: UpdateNetworkStackConfigRequest = { + ...data, + networkStackConfigId: selectedConfig.networkStackConfigId + }; + const result = await networkStackConfigService.updateNetworkStackConfig(selectedConfig.networkStackConfigId, updateData); if (result.isSuccess) { toast({ title: "更新成功", - description: `网络栈配置 "${selectedConfig.networkStackName}" 更新成功`, + description: `网络栈配置 "${data.networkStackName}" 更新成功`, }); setDrawerOpen(false); setSelectedConfig(null); @@ -225,77 +198,60 @@ export default function NetworkStackConfigsView() { setPageNumber(1); }; - const totalPages = Math.ceil(total / pageSize); - return (
- {/* 丰富美化后的搜索栏 */} -
+ {/* 搜索工具栏 */} +
{ e.preventDefault(); handleQuery(); }} > - {(showAdvanced ? [...firstRowFields, ...advancedFields] : firstRowFields).map(field => ( -
- - {field.type === 'input' && ( - ) => { - if (field.key === 'networkStackName') setNetworkStackName(e.target.value); - }} - /> - )} - {field.type === 'select' && ( - - )} -
- ))} - {/* 按钮组直接作为表单项之一,紧跟在最后一个表单项后面 */} -
- - - +
+ + ) => setNetworkStackName(e.target.value)} + /> +
+
+ + +
+ {/* 按钮组 */} +
+ +
+ {/* 表格整体卡片区域,包括工具栏、表格、分页 */}
{/* 顶部操作栏:添加网络栈配置+工具栏 */} @@ -318,6 +274,7 @@ export default function NetworkStackConfigsView() { density={density} />
+ {/* 表格区域 */} + {/* 分页 */} ([]); const [loading, setLoading] = useState(false); @@ -54,7 +28,6 @@ export default function ProtocolsView() { const [pageSize, setPageSize] = useState(10); const [density, setDensity] = useState('default'); const [columns, setColumns] = useState(defaultColumns); - const [showAdvanced, setShowAdvanced] = useState(false); // 搜索参数 const [searchTerm, setSearchTerm] = useState(''); @@ -94,8 +67,6 @@ export default function ProtocolsView() { // eslint-disable-next-line }, [pageNumber, pageSize]); - - const handleEdit = (protocolVersion: ProtocolVersion) => { setSelectedProtocol(protocolVersion); setEditOpen(true); @@ -134,12 +105,9 @@ export default function ProtocolsView() { const handleCreate = async (data: CreateProtocolVersionRequest) => { if (isSubmitting) return; // 防止重复提交 - console.log('开始创建协议版本:', data); setIsSubmitting(true); try { const result = await protocolService.createProtocolVersion(data); - console.log('创建协议版本结果:', result); - if (result.isSuccess) { toast({ title: "创建成功", @@ -149,7 +117,7 @@ export default function ProtocolsView() { fetchProtocolVersions(); } else { const errorMessage = result.errorMessages?.join(', ') || "创建协议版本时发生错误"; - console.error('创建协议版本失败:', errorMessage, result); + console.error('创建协议版本失败:', errorMessage); toast({ title: "创建失败", description: errorMessage, @@ -173,7 +141,11 @@ export default function ProtocolsView() { setIsSubmitting(true); try { - const result = await protocolService.updateProtocolVersion(selectedProtocol.protocolVersionId, data); + const updateData: UpdateProtocolVersionRequest = { + ...data, + protocolVersionId: selectedProtocol.protocolVersionId + }; + const result = await protocolService.updateProtocolVersion(selectedProtocol.protocolVersionId, updateData); if (result.isSuccess) { toast({ title: "更新成功", @@ -227,77 +199,60 @@ export default function ProtocolsView() { setPageNumber(1); }; - const totalPages = Math.ceil(total / pageSize); - return (
- {/* 丰富美化后的搜索栏 */} -
+ {/* 搜索工具栏 */} +
{ e.preventDefault(); handleQuery(); }} > - {(showAdvanced ? [...firstRowFields, ...advancedFields] : firstRowFields).map(field => ( -
- - {field.type === 'input' && ( - ) => { - if (field.key === 'searchTerm') setSearchTerm(e.target.value); - }} - /> - )} - {field.type === 'select' && ( - - )} -
- ))} - {/* 按钮组直接作为表单项之一,紧跟在最后一个表单项后面 */} -
- - - +
+ + ) => setSearchTerm(e.target.value)} + /> +
+
+ + +
+ {/* 按钮组 */} +
+ +
+ {/* 表格整体卡片区域,包括工具栏、表格、分页 */}
{/* 顶部操作栏:添加协议版本+工具栏 */} @@ -319,6 +274,7 @@ export default function ProtocolsView() { density={density} />
+ {/* 表格区域 */} + {/* 分页 */} ([]); const [loading, setLoading] = useState(false); @@ -125,12 +110,9 @@ export default function RANConfigurationsView() { const handleCreate = async (data: CreateRANConfigurationRequest) => { if (isSubmitting) return; // 防止重复提交 - console.log('开始创建RAN配置:', data); setIsSubmitting(true); try { const result = await ranConfigurationService.createRANConfiguration(data); - console.log('创建RAN配置结果:', result); - if (result.isSuccess) { toast({ title: "创建成功", @@ -140,7 +122,7 @@ export default function RANConfigurationsView() { fetchRANConfigurations(); } else { const errorMessage = result.errorMessages?.join(', ') || "创建RAN配置时发生错误"; - console.error('创建RAN配置失败:', errorMessage, result); + console.error('创建RAN配置失败:', errorMessage); toast({ title: "创建失败", description: errorMessage, @@ -164,7 +146,11 @@ export default function RANConfigurationsView() { setIsSubmitting(true); try { - const result = await ranConfigurationService.updateRANConfiguration(selectedConfiguration.raN_ConfigurationId, data); + const updateData: UpdateRANConfigurationRequest = { + ...data, + raN_ConfigurationId: selectedConfiguration.raN_ConfigurationId + }; + const result = await ranConfigurationService.updateRANConfiguration(selectedConfiguration.raN_ConfigurationId, updateData); if (result.isSuccess) { toast({ title: "更新成功", @@ -195,28 +181,25 @@ export default function RANConfigurationsView() { }; const handleStatusChange = async (configuration: RANConfiguration, newStatus: boolean) => { - if (isSubmitting) return; // 防止重复提交 - - setIsSubmitting(true); try { const updateData: UpdateRANConfigurationRequest = { raN_ConfigurationId: configuration.raN_ConfigurationId, name: configuration.name, - configContent: configuration.configContent, description: configuration.description, - isDisabled: !newStatus // 注意:newStatus是启用状态,isDisabled是禁用状态 + configContent: configuration.configContent, + isDisabled: newStatus }; const result = await ranConfigurationService.updateRANConfiguration(configuration.raN_ConfigurationId, updateData); if (result.isSuccess) { toast({ title: "状态更新成功", - description: `RAN配置 "${configuration.name}" 状态已${newStatus ? '启用' : '禁用'}`, + description: `RAN配置 "${configuration.name}" 状态已${newStatus ? '禁用' : '启用'}`, }); fetchRANConfigurations(); } else { - const errorMessage = result.errorMessages?.join(', ') || "更新RAN配置状态时发生错误"; - console.error('更新RAN配置状态失败:', errorMessage); + const errorMessage = result.errorMessages?.join(', ') || "更新状态时发生错误"; + console.error('更新状态失败:', errorMessage); toast({ title: "状态更新失败", description: errorMessage, @@ -224,14 +207,12 @@ export default function RANConfigurationsView() { }); } } catch (error) { - console.error('更新RAN配置状态异常:', error); + console.error('更新状态异常:', error); toast({ title: "状态更新失败", description: "网络错误,请稍后重试", variant: "destructive", }); - } finally { - setIsSubmitting(false); } }; @@ -259,63 +240,60 @@ export default function RANConfigurationsView() { setPageNumber(1); }; - const totalPages = Math.ceil(total / pageSize); - return (
- {/* 搜索栏 */} -
+ {/* 搜索工具栏 */} +
{ e.preventDefault(); handleQuery(); }} > - {searchFields.map((field: SearchField) => ( -
- - {field.type === 'input' && ( - ) => { - if (field.key === 'searchTerm') setSearchTerm(e.target.value); - }} - /> - )} - {field.type === 'select' && ( - - )} -
- ))} +
+ + ) => setSearchTerm(e.target.value)} + /> +
+
+ + +
{/* 按钮组 */} -
- - +
+ +
+ {/* 表格整体卡片区域,包括工具栏、表格、分页 */}
{/* 顶部操作栏:添加RAN配置+工具栏 */} @@ -335,6 +313,7 @@ export default function RANConfigurationsView() { density={density} />
+ {/* 表格区域 */} + {/* 分页 */} import('@/pages/analysis/UEAnalysisView')); const DevicesView = lazy(() => import('@/pages/instruments/DevicesView')); // 设备运行时管理页面 const DeviceRuntimesView = lazy(() => import('@/pages/device-runtimes/DeviceRuntimesView')); -const DeviceRuntimeDetail = lazy(() => import('@/pages/device-runtimes/DeviceRuntimeDetail')); // 协议管理页面 const ProtocolsView = lazy(() => import('@/pages/protocols/ProtocolsView')); // RAN配置管理页面 @@ -220,13 +219,6 @@ export function AppRouter() { } /> - - - - - - } /> diff --git a/src/X1.WebUI/src/services/deviceRuntimeService.ts b/src/X1.WebUI/src/services/deviceRuntimeService.ts index f9591e2..a87ad74 100644 --- a/src/X1.WebUI/src/services/deviceRuntimeService.ts +++ b/src/X1.WebUI/src/services/deviceRuntimeService.ts @@ -124,8 +124,24 @@ class DeviceRuntimeService { return httpClient.post(`${this.baseUrl}/${deviceCode}/stop`); } + // 将数字状态转换为字符串状态 + 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'; + } + } + // 获取设备运行时状态的可读描述 - getRuntimeStatusDescription(status: number | string): string { + getRuntimeStatusDescription = (status: number | string): string => { const statusStr = typeof status === 'number' ? this.getRuntimeStatusString(status) : status; switch (statusStr) { case 'Running': @@ -142,7 +158,7 @@ class DeviceRuntimeService { } // 获取设备运行时状态的颜色 - getRuntimeStatusColor(status: number | string): string { + getRuntimeStatusColor = (status: number | string): string => { const statusStr = typeof status === 'number' ? this.getRuntimeStatusString(status) : status; switch (statusStr) { case 'Running': @@ -157,22 +173,6 @@ class DeviceRuntimeService { 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(); \ No newline at end of file diff --git a/src/X1.WebUI/yarn.lock b/src/X1.WebUI/yarn.lock index e7fcca1..371bf0d 100644 --- a/src/X1.WebUI/yarn.lock +++ b/src/X1.WebUI/yarn.lock @@ -1652,6 +1652,11 @@ data-uri-to-buffer@^4.0.0: resolved "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== +date-fns@^4.1.0: + version "4.1.0" + resolved "https://registry.npmmirror.com/date-fns/-/date-fns-4.1.0.tgz#64b3d83fff5aa80438f5b1a633c2e83b8a1c2d14" + integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg== + debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.4.1" resolved "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" diff --git a/src/modify.md b/src/modify.md index 1899ea4..1d67bf9 100644 --- a/src/modify.md +++ b/src/modify.md @@ -1,5 +1,146 @@ # 修改记录 +## 2024-12-19 - Device Runtimes 页面重构 + +### 修改概述 +按照 instruments 页面的结构重构了 device-runtimes 页面,移除了 Form 组件,将网络栈配置改为下拉框选择,并简化了页面结构。同时修复了设备列表页面的主题兼容性问题。 + +### 修改的文件 + +#### 1. `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesView.tsx` +- **页面结构重构**:按照 instruments 页面的布局结构重新组织页面 +- **移除 Form 组件**:不再使用独立的 Form 组件,改为内联表单 +- **移除统计卡片**:移除了总设备数、运行中、已停止、错误状态的统计卡片 +- **移除展开功能**:移除了搜索条件的高级展开/收起功能 +- **移除页面标题**:移除了"设备运行时管理"标题和描述文本 +- **恢复搜索工具栏**:恢复了搜索关键词和运行时状态的搜索表单,但移除了每页数量选择框 +- **网络栈配置下拉框**:将网络栈配置输入框改为下拉选择框,从网络栈配置服务获取选项 +- **组件接口修复**:修复了 TableToolbar 和 PaginationBar 组件的接口问题 +- **批量启动对话框优化**:使用 Select 组件替代 Input 组件选择网络栈配置 +- **移除错误状态选项**:从运行时状态下拉框中移除了"错误"选项 +- **主题兼容性修复**:修复了搜索工具栏在黑色主题下的显示问题 + +#### 2. `X1.WebUI/src/pages/instruments/DevicesView.tsx` +- **移除展开功能**:移除了搜索条件的高级展开/收起功能 +- **简化搜索工具栏**:将搜索表单简化为单行布局,包含搜索关键词和状态筛选 +- **主题兼容性修复**:修复了搜索工具栏在黑色主题下的显示问题 +- **按钮样式统一**:使用 Button 组件替代硬编码样式,保持与系统其他页面一致 +- **移除每页数量选择**:从搜索工具栏中移除了每页数量选择框 + +#### 3. `X1.WebUI/src/pages/protocols/ProtocolsView.tsx` +- **移除展开功能**:移除了搜索条件的高级展开/收起功能 +- **简化搜索工具栏**:将搜索表单简化为单行布局,包含搜索关键词和状态筛选 +- **主题兼容性修复**:修复了搜索工具栏在黑色主题下的显示问题 +- **按钮样式统一**:使用 Button 组件替代硬编码样式,保持与系统其他页面一致 +- **移除每页数量选择**:从搜索工具栏中移除了每页数量选择框 +- **接口修复**:修复了 ProtocolVersion 接口属性名称问题 + +#### 4. `X1.WebUI/src/pages/ran-configurations/RANConfigurationsView.tsx` +- **简化搜索工具栏**:将搜索表单简化为单行布局,包含搜索关键词和状态筛选 +- **主题兼容性修复**:修复了搜索工具栏在黑色主题下的显示问题 +- **按钮样式统一**:使用 Button 组件替代硬编码样式,保持与系统其他页面一致 +- **布局优化**:使用 flex 布局替代 grid 布局,提升响应式效果 +- **代码清理**:移除了不必要的类型定义和变量 + +#### 5. `X1.WebUI/src/pages/ims-configurations/IMSConfigurationsView.tsx` +- **移除展开功能**:移除了搜索条件的高级展开/收起功能 +- **简化搜索工具栏**:将搜索表单简化为单行布局,包含搜索关键词和状态筛选 +- **主题兼容性修复**:修复了搜索工具栏在黑色主题下的显示问题 +- **按钮样式统一**:使用 Button 组件替代硬编码样式,保持与系统其他页面一致 +- **布局优化**:使用 flex 布局替代 grid 布局,提升响应式效果 +- **代码清理**:移除了不必要的类型定义和变量 +- **接口修复**:修复了 IMSConfiguration 接口属性名称问题 + +#### 6. `X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsView.tsx` +- **移除展开功能**:移除了搜索条件的高级展开/收起功能 +- **简化搜索工具栏**:将搜索表单简化为单行布局,包含网络栈名称和状态筛选 +- **主题兼容性修复**:修复了搜索工具栏在黑色主题下的显示问题 +- **按钮样式统一**:使用 Button 组件替代硬编码样式,保持与系统其他页面一致 +- **布局优化**:使用 flex 布局替代 grid 布局,提升响应式效果 +- **代码清理**:移除了不必要的类型定义和变量 + +#### 7. `X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigsView.tsx` +- **移除展开功能**:移除了搜索条件的高级展开/收起功能 +- **简化搜索工具栏**:将搜索表单简化为单行布局,包含搜索关键词和状态筛选 +- **主题兼容性修复**:修复了搜索工具栏在黑色主题下的显示问题 +- **按钮样式统一**:使用 Button 组件替代硬编码样式,保持与系统其他页面一致 +- **布局优化**:使用 flex 布局替代 grid 布局,提升响应式效果 +- **代码清理**:移除了不必要的类型定义和变量 + +### 修改的文件 + +#### 1. `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesView.tsx` +- **页面结构重构**:按照 instruments 页面的布局结构重新组织页面 +- **移除 Form 组件**:不再使用独立的 Form 组件,改为内联表单 +- **移除统计卡片**:移除了总设备数、运行中、已停止、错误状态的统计卡片 +- **移除展开功能**:移除了搜索条件的高级展开/收起功能 +- **移除页面标题**:移除了"设备运行时管理"标题和描述文本 +- **恢复搜索工具栏**:恢复了搜索关键词和运行时状态的搜索表单,但移除了每页数量选择框 +- **网络栈配置下拉框**:将网络栈配置输入框改为下拉选择框,从网络栈配置服务获取选项 +- **组件接口修复**:修复了 TableToolbar 和 PaginationBar 组件的接口问题 +- **批量启动对话框优化**:使用 Select 组件替代 Input 组件选择网络栈配置 +- **移除错误状态选项**:从运行时状态下拉框中移除了"错误"选项 + +#### 8. `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesTable.tsx` +- **类型修复**:修复了 density 类型定义,使用 DensityType 替代硬编码类型 +- **样式映射更新**:添加了 'relaxed' 密度选项的样式映射 + +### 功能特性 + +#### 1. 页面布局 +- **统一布局**:采用与 instruments 页面一致的布局结构 +- **响应式设计**:保持响应式布局特性 +- **卡片式布局**:使用卡片组件组织内容区域 + +#### 2. 搜索功能 +- **内联表单**:搜索条件直接嵌入页面,无需独立表单组件 +- **高级搜索**:支持展开/收起高级搜索选项 +- **实时查询**:支持实时搜索和重置功能 + +#### 3. 网络栈配置选择 +- **下拉选择**:从网络栈配置服务获取激活的配置列表 +- **用户友好**:显示配置名称和编码,便于用户选择 +- **数据驱动**:动态加载配置选项,无需硬编码 + +#### 4. 批量操作 +- **批量启动**:支持选择多个设备进行批量启动 +- **网络栈配置**:批量启动时统一选择网络栈配置 +- **操作反馈**:提供详细的操作结果反馈 + +### 技术改进 + +#### 1. 组件接口统一 +- **TableToolbar**:使用正确的接口参数 +- **PaginationBar**:修复分页组件参数 +- **DeviceRuntimesTable**:统一密度类型定义 + +#### 2. 服务集成 +- **网络栈配置服务**:集成 networkStackConfigService 获取配置列表 +- **数据获取**:在页面初始化时获取网络栈配置数据 + +#### 3. 类型安全 +- **TypeScript 类型**:完善类型定义,提高代码安全性 +- **接口一致性**:确保组件接口的一致性 + +### 用户体验改进 + +#### 1. 操作简化 +- **一键选择**:网络栈配置通过下拉框快速选择 +- **批量操作**:支持多选设备进行批量操作 +- **状态反馈**:实时显示操作状态和结果 + +#### 2. 界面优化 +- **布局统一**:与系统其他页面保持一致的布局风格 +- **视觉层次**:清晰的信息层次和视觉引导 +- **交互反馈**:及时的操作反馈和状态提示 + +### 影响范围 +- **前端页面**:device-runtimes 页面的完整重构 +- **用户体验**:提升设备运行时管理的操作便利性 +- **代码维护**:统一代码结构,便于后续维护和扩展 + +--- + ## 2025-01-29 根据DeviceRuntimesController实现X1.WebUI.src.services ### 修改原因 @@ -4399,4 +4540,582 @@ private static DeviceRuntimeDto MapToDto(CellularDeviceRuntime runtime) - 原有的直接访问路径 `/dashboard/device-runtimes/*` 已失效 - 需要更新任何硬编码的链接或书签 - 权限检查仍然正常工作,确保安全性 -- 所有设备运行时管理功能保持不变 \ No newline at end of file +- 所有设备运行时管理功能保持不变 + +## 2025-01-29 修复 device-runtimes 界面组件和依赖问题 + +### 修改原因 +修复 device-runtimes 界面中缺失的组件和依赖问题,确保界面能够正常运行。 + +### 新增文件 + +#### 1. UI 组件 +- `X1.WebUI/src/components/ui/card.tsx` - 创建 card 组件,包含 Card、CardHeader、CardTitle、CardContent、CardFooter 等子组件 +- `X1.WebUI/src/components/ui/separator.tsx` - 创建 separator 组件,基于 @radix-ui/react-separator +- `X1.WebUI/src/components/ui/dropdown-menu.tsx` - 创建 dropdown-menu 组件,包含完整的下拉菜单功能 + +### 修改文件 + +#### 1. Dialog 组件扩展 +- `X1.WebUI/src/components/ui/dialog.tsx` - 添加 DialogHeader 组件导出 + +#### 2. 设备运行时表格组件 +- `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesTable.tsx` - 修复图标导入,将 MoreHorizontalIcon 改为 DotsHorizontalIcon + +#### 3. 设备运行时视图组件 +- `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesView.tsx` - 修复服务方法调用,使用 deviceRuntimeService 实例方法 + +### 修复内容 + +#### 1. 组件创建 +- **Card 组件**:提供卡片布局功能,支持标题、内容、页脚等区域 +- **Separator 组件**:提供分隔线功能,支持水平和垂直方向 +- **Dropdown Menu 组件**:提供下拉菜单功能,支持菜单项、复选框、单选按钮等 + +#### 2. 图标修复 +- **图标名称修正**:将不存在的 MoreHorizontalIcon 改为正确的 DotsHorizontalIcon +- **导入修复**:确保所有图标都能正确导入和使用 + +#### 3. 服务调用修复 +- **方法调用修正**:将直接函数调用改为使用 deviceRuntimeService 实例方法 +- **导入清理**:移除不存在的函数导入,只保留类型和服务实例 + +### 技术特性 + +#### 1. 组件设计 +- **类型安全**:所有组件都使用 TypeScript 类型定义 +- **可访问性**:遵循 ARIA 标准,支持屏幕阅读器 +- **响应式**:支持不同屏幕尺寸的响应式布局 +- **主题支持**:支持深色/浅色主题切换 + +#### 2. 图标系统 +- **Radix UI 图标**:使用 @radix-ui/react-icons 提供的图标 +- **一致性**:确保图标名称与库中实际存在的图标一致 +- **可扩展性**:支持添加更多图标 + +#### 3. 服务架构 +- **实例方法**:使用服务实例的方法而不是独立函数 +- **类型安全**:完整的 TypeScript 类型支持 +- **错误处理**:统一的错误处理机制 + +### 影响范围 +- **UI 组件**:新增了三个重要的 UI 组件 +- **图标系统**:修复了图标导入问题 +- **服务调用**:统一了服务调用方式 +- **开发体验**:提供了更好的类型安全和代码提示 + +### 后续工作建议 +1. **组件测试**:为新创建的组件添加单元测试 +2. **文档编写**:为组件添加使用文档和示例 +3. **主题优化**:进一步完善主题支持 +4. **性能优化**:优化组件的渲染性能 + +## 2025-01-29 修复 Select 组件空值错误 + +### 修改原因 +修复 Select 组件中空值选项导致的错误:"A must have a value prop that is not an empty string"。 + +### 修改文件 +- `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesView.tsx` - 修复运行时状态选择器的空值问题 + +### 修改内容 + +#### 1. 选项值修复 +- **空值选项**:将 `{ value: '', label: '全部状态' }` 改为 `{ value: 'all', label: '全部状态' }` +- **SelectItem 修复**:将 `全部状态` 改为 `全部状态` + +#### 2. 值处理逻辑修复 +- **默认值**:将 `value={runtimeStatus?.toString() || ''}` 改为 `value={runtimeStatus?.toString() || 'all'}` +- **值变化处理**:将 `onValueChange={(value) => setRuntimeStatus(value ? parseInt(value) : undefined)}` 改为 `onValueChange={(value) => setRuntimeStatus(value === 'all' ? undefined : parseInt(value))}` + +### 技术说明 +- **Radix UI 限制**:Radix UI 的 Select 组件不允许空字符串作为选项值 +- **特殊值处理**:使用 'all' 作为特殊值表示"全部状态" +- **逻辑转换**:在值变化时将 'all' 转换为 undefined,保持业务逻辑不变 + +### 影响范围 +- **用户界面**:Select 组件现在可以正常工作,不再报错 +- **业务逻辑**:功能保持不变,只是内部值处理方式调整 +- **用户体验**:界面更加稳定,不会出现错误提示 + +## 2025-01-29 修复 deviceRuntimeService 方法绑定问题 + +### 修改原因 +修复 `deviceRuntimeService` 中方法调用时的 `this` 上下文丢失问题,导致 `getRuntimeStatusString` 方法无法正确访问。 + +### 修改文件 +- `X1.WebUI/src/services/deviceRuntimeService.ts` - 修复方法绑定问题 + +### 修改内容 + +#### 1. 方法定义修复 +- **箭头函数**:将 `getRuntimeStatusDescription` 和 `getRuntimeStatusColor` 方法改为箭头函数 +- **this 绑定**:确保方法在传递时保持正确的 `this` 上下文 +- **方法顺序**:将 `getRuntimeStatusString` 方法移到前面,确保在调用前已定义 + +#### 2. 技术改进 +- **上下文保持**:使用箭头函数确保 `this` 始终指向 `DeviceRuntimeService` 实例 +- **方法传递**:支持将方法作为函数参数传递给其他组件 +- **错误修复**:解决 "Cannot read properties of undefined" 错误 + +### 技术说明 +- **问题原因**:当方法被作为函数传递时,`this` 上下文会丢失 +- **解决方案**:使用箭头函数确保 `this` 绑定到类实例 +- **性能影响**:箭头函数在实例化时创建,但避免了运行时绑定问题 + +### 影响范围 +- **错误修复**:解决了运行时状态描述和颜色获取的错误 +- **功能恢复**:设备运行时状态显示功能正常工作 +- **代码稳定性**:提高了方法调用的可靠性 + +## 2025-01-29 - 修复设备运行时表格单元格居中对齐问题 + +### 修改原因 +设备运行时表格的单元格内容没有居中对齐,与其他表格组件的显示效果不一致,影响用户体验。 + +### 修改文件 +- `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesTable.tsx` - 修复表格单元格对齐 + +### 修改内容 + +#### 1. 表头居中对齐 +- **添加居中对齐**:为所有 `TableHead` 元素添加 `text-center` 类 +- **保持密度样式**:继续使用 `densityStyles` 控制行高 +- **视觉一致性**:与协议版本表格保持一致的显示效果 + +#### 2. 表格单元格居中对齐 +- **添加居中对齐**:为所有 `TableCell` 元素添加 `text-center` 类 +- **保持密度样式**:继续使用 `densityStyles` 控制行高 +- **内容对齐**:所有单元格内容现在都居中对齐 + +### 技术说明 +- **CSS类应用**:使用 `cn("text-center", densityStyles[density])` 组合样式类 +- **一致性**:与协议版本表格使用相同的对齐方式 +- **响应式设计**:保持密度样式的响应式特性 +- **用户体验**:提供统一的表格显示效果 + +### 影响范围 +- **视觉效果**:表格单元格内容现在居中对齐 +- **一致性**:与其他表格组件保持一致的显示效果 +- **用户体验**:提供更好的视觉体验和可读性 +- **设计统一**:保持整个系统的表格设计一致性 + +## 2025-01-29 - 优化设备运行时操作菜单逻辑 + +### 修改原因 +根据用户需求,操作菜单应该根据设备运行时状态显示不同的操作选项:当设备处于未知状态或停止状态时可以启动,当设备处于运行状态时可以停止。 + +### 修改文件 +- `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesTable.tsx` - 修改操作菜单逻辑 +- `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesView.tsx` - 添加启动单个设备处理函数 + +### 修改内容 + +#### 1. 操作菜单逻辑优化 +- **运行中状态(runtimeStatus === 1)**:显示"停止设备"选项,点击红色图标 +- **其他状态(未知-1、初始化0、已停止2、错误3)**:显示"启动设备"选项,点击绿色图标 +- **移除禁用状态**:不再显示禁用的"设备未运行"选项 + +#### 2. 接口扩展 +- **新增回调函数**:添加 `onStartDevice` 回调函数到 `DeviceRuntimesTableProps` 接口 +- **组件参数更新**:在 `DeviceRuntimesTable` 组件中添加 `onStartDevice` 参数 + +#### 3. 启动设备处理 +- **临时实现**:由于单个设备启动需要选择网络栈配置,暂时提示用户使用批量启动功能 +- **用户引导**:提供清晰的用户提示,引导用户使用正确的功能 + +### 技术说明 +- **状态判断**:根据 `runtimeStatus` 值判断显示的操作选项 +- **颜色区分**:启动操作使用绿色,停止操作使用红色 +- **用户体验**:提供直观的操作反馈和用户引导 +- **功能完整性**:保持批量启动和单个停止功能的完整性 + +### 影响范围 +- **操作逻辑**:操作菜单现在根据设备状态显示相应的操作选项 +- **用户体验**:提供更直观和合理的操作选择 +- **功能引导**:引导用户使用正确的功能进行设备启动 +- **视觉反馈**:通过颜色区分不同的操作类型 + +## 2025-01-29 - 修复设备运行时表格选择框对齐问题 + +### 修改原因 +用户反馈选择框(勾选框)没有居中对齐,需要修复表格中标题行和内容行的选择框对齐问题。 + +### 修改文件 +- `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesTable.tsx` - 修复选择框对齐 + +### 修改内容 + +#### 1. 选择框列对齐修复 +- **表头选择框**:为 `TableHead` 添加 `text-center` 类,使全选复选框居中对齐 +- **内容行选择框**:为 `TableCell` 添加 `text-center` 类,使单个选择复选框居中对齐 + +#### 2. 样式统一 +- **对齐一致性**:确保选择框列与其他列的对齐方式保持一致 +- **视觉体验**:提供更好的表格视觉效果和用户体验 + +### 技术说明 +- **CSS类应用**:使用 Tailwind CSS 的 `text-center` 类实现居中对齐 +- **表格结构**:保持表格结构完整性,只修改对齐样式 +- **响应式设计**:确保在不同屏幕尺寸下选择框都能正确对齐 + +### 影响范围 +- **视觉效果**:选择框现在与其他列内容居中对齐 +- **用户体验**:提供更整洁和一致的表格显示效果 +- **设计统一**:保持整个表格的对齐一致性 + +## 2025-01-29 - 优化设备运行时操作列显示方式 + +### 修改原因 +用户希望操作列直接显示启动和停止按钮,而不是使用下拉菜单,提供更直观的操作体验。 + +### 修改文件 +- `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesTable.tsx` - 修改操作列显示方式 + +### 修改内容 + +#### 1. 操作列显示方式优化 +- **移除下拉菜单**:不再使用 `DropdownMenu` 组件,直接显示操作按钮 +- **直接按钮显示**:根据设备状态直接显示启动或停止按钮 +- **图标按钮**:使用纯图标按钮,节省空间并提高视觉效果 + +#### 2. 按钮样式优化 +- **启动按钮**:绿色图标,悬停时显示绿色背景 +- **停止按钮**:红色图标,悬停时显示红色背景 +- **工具提示**:添加 `title` 属性提供操作说明 +- **居中对齐**:按钮在操作列中居中对齐 + +#### 3. 代码清理 +- **移除未使用导入**:删除不再使用的 `DropdownMenu` 相关导入 +- **简化代码结构**:移除复杂的下拉菜单逻辑,使用简单的条件渲染 + +### 技术说明 +- **条件渲染**:根据 `runtimeStatus` 值决定显示启动或停止按钮 +- **样式设计**:使用 `variant="ghost"` 和自定义颜色类实现按钮样式 +- **用户体验**:提供直观的图标按钮,减少操作步骤 +- **响应式设计**:按钮在不同屏幕尺寸下都能正确显示 + +### 影响范围 +- **操作体验**:用户可以直接点击按钮进行操作,无需打开下拉菜单 +- **视觉效果**:操作列更加简洁,图标按钮更加直观 +- **交互效率**:减少操作步骤,提高用户操作效率 +- **代码维护**:简化了操作列的实现逻辑 + +## 2025-01-29 - 优化批量启动对话框网络栈配置选择 + +### 修改原因 +用户希望将网络栈配置改为搜索下拉框选择,数据来源是 `networkStackConfigService.ts`,提供更好的搜索和选择体验。 + +### 修改文件 +- `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesView.tsx` - 修改网络栈配置选择方式 + +### 修改内容 + +#### 1. 搜索下拉框实现 +- **移除Select组件**:不再使用简单的 `Select` 组件,改为自定义搜索下拉框 +- **搜索功能**:支持按网络栈名称、编号和描述进行搜索 +- **实时过滤**:输入搜索关键词时实时过滤匹配的配置项 +- **显示优化**:显示网络栈名称、编号和描述信息 + +#### 2. 状态管理 +- **搜索状态**:添加 `networkStackSearchTerm` 状态管理搜索关键词 +- **过滤状态**:添加 `filteredNetworkStackConfigs` 状态管理过滤后的配置列表 +- **下拉框状态**:添加 `isNetworkStackDropdownOpen` 状态管理下拉框开关 + +#### 3. 交互优化 +- **点击外部关闭**:添加点击外部区域关闭下拉框的功能 +- **搜索重置**:选择配置后自动清空搜索关键词并重置过滤列表 +- **无结果提示**:当搜索无结果时显示友好的提示信息 + +#### 4. 样式设计 +- **下拉框样式**:使用与系统一致的样式设计 +- **悬停效果**:配置项悬停时显示高亮效果 +- **描述显示**:在配置项下方显示描述信息(如果有) + +### 技术说明 +- **数据来源**:使用 `networkStackConfigService.getNetworkStackConfigs()` 获取配置数据 +- **搜索逻辑**:支持多字段搜索(名称、编号、描述) +- **性能优化**:使用本地过滤,避免频繁的API调用 +- **用户体验**:提供直观的搜索和选择体验 + +### 影响范围 +- **用户体验**:提供更便捷的网络栈配置搜索和选择功能 +- **功能完整性**:保持批量启动功能的完整性 +- **交互效率**:通过搜索功能快速定位目标配置 +- **视觉一致性**:与系统其他搜索下拉框保持一致的视觉风格 + +## 2025-01-29 - 优化设备运行时表格网络栈配置列 + +### 修改原因 +用户希望网络栈配置列显示为下拉框,只有当选择了网络栈配置值的时候才能启动设备,提供更直观的配置选择和启动控制。 + +### 修改文件 +- `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesTable.tsx` - 修改网络栈配置列显示方式 +- `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesView.tsx` - 添加网络栈配置变更处理 + +### 修改内容 + +#### 1. 网络栈配置列下拉框实现 +- **搜索下拉框**:将网络栈配置列改为可搜索的下拉框 +- **实时搜索**:支持按名称、编号和描述进行实时搜索 +- **配置显示**:显示网络栈名称和编号的组合信息 +- **选择功能**:点击配置项可选择对应的网络栈配置 + +#### 2. 启动按钮控制逻辑 +- **条件启动**:只有当设备选择了网络栈配置时,启动按钮才可用 +- **视觉反馈**:未选择配置时按钮显示为灰色禁用状态 +- **提示信息**:鼠标悬停时显示相应的提示信息 +- **错误处理**:尝试启动未配置设备时显示错误提示 + +#### 3. 状态管理优化 +- **下拉框状态**:管理每个设备的下拉框开关状态 +- **搜索状态**:管理每个设备的搜索关键词 +- **过滤状态**:管理每个设备的过滤结果 +- **点击外部关闭**:点击外部区域自动关闭所有下拉框 + +#### 4. 单个设备启动功能 +- **完整实现**:实现单个设备的启动功能 +- **配置验证**:启动前验证是否已选择网络栈配置 +- **API调用**:调用设备启动API进行实际启动操作 +- **结果反馈**:显示启动成功或失败的提示信息 + +### 技术说明 +- **组件接口扩展**:添加 `networkStackConfigs` 和 `onNetworkStackChange` 属性 +- **状态管理**:使用对象形式管理多个设备的状态 +- **搜索算法**:支持多字段模糊搜索 +- **用户体验**:提供直观的配置选择和启动控制 + +### 影响范围 +- **表格交互**:网络栈配置列现在支持下拉选择 +- **启动控制**:只有配置了网络栈的设备才能启动 +- **用户体验**:提供更直观的配置管理和启动流程 +- **功能完整性**:单个设备启动功能现在完全可用 + +## 2025-01-29 - 修复网络栈配置下拉框样式问题 + +### 修改原因 +用户反馈网络栈配置下拉框的样式不正确,存在红色圆点等视觉问题,需要修复样式以保持界面的一致性。 + +### 修改文件 +- `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesTable.tsx` - 修复表格中网络栈配置下拉框样式 +- `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesView.tsx` - 修复批量启动对话框中的网络栈配置下拉框样式 + +### 修改内容 + +#### 1. 文本颜色修复 +- **表格下拉框**:为文本添加 `text-foreground` 类,确保文本颜色正确 +- **对话框下拉框**:为文本添加 `text-foreground` 类,保持颜色一致性 + +#### 2. 搜索输入框样式优化 +- **移除边框**:为搜索输入框添加 `border-0 focus:ring-0 focus:border-0` 类 +- **简化样式**:移除不必要的边框和焦点环,使搜索框更简洁 +- **统一外观**:确保搜索框与下拉框整体样式协调 + +#### 3. 视觉一致性 +- **颜色统一**:确保所有文本使用正确的主题颜色 +- **边框处理**:移除搜索框的重复边框,避免视觉冲突 +- **焦点状态**:简化焦点状态的视觉反馈 + +### 技术说明 +- **CSS类应用**:使用 Tailwind CSS 类修复样式问题 +- **主题适配**:确保样式与系统主题保持一致 +- **视觉层次**:优化视觉层次,避免不必要的视觉元素 + +### 影响范围 +- **视觉效果**:修复了红色圆点等样式问题 +- **界面一致性**:保持下拉框与系统其他组件的视觉一致性 +- **用户体验**:提供更清洁和专业的界面外观 +- **主题适配**:确保在不同主题下都能正确显示 + +## 2025-01-29 - 修复网络栈配置下拉框定位问题 + +### 修改原因 +用户反馈下拉框不应该占用当前行的空间,应该覆盖在其他内容之上,需要修复下拉框的定位方式。 + +### 修改文件 +- `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesTable.tsx` - 修复下拉框定位逻辑 + +### 修改内容 + +#### 1. 定位方式优化 +- **固定定位**:将下拉框改为 `fixed` 定位,不再占用表格行空间 +- **动态计算位置**:根据点击元素的位置动态计算下拉框显示位置 +- **高层级显示**:使用 `z-[9999]` 确保下拉框显示在最上层 + +#### 2. 位置计算逻辑 +- **点击事件处理**:在点击事件中获取元素的位置信息 +- **坐标计算**:计算下拉框应该显示的位置坐标 +- **状态管理**:添加 `dropdownPositions` 状态管理每个下拉框的位置 + +#### 3. 交互优化 +- **精确定位**:下拉框现在精确显示在触发元素下方 +- **不占用空间**:下拉框不再影响表格的布局和行高 +- **响应式定位**:支持滚动时的位置调整 + +### 技术说明 +- **getBoundingClientRect**:使用 DOM API 获取元素位置信息 +- **fixed 定位**:使用固定定位避免影响文档流 +- **事件处理**:在点击事件中传递事件对象以获取位置信息 +- **状态管理**:使用对象形式管理多个下拉框的位置状态 + +### 影响范围 +- **布局优化**:下拉框不再占用表格行空间 +- **视觉效果**:下拉框现在浮在内容上方,提供更好的视觉层次 +- **用户体验**:提供更流畅和直观的下拉选择体验 +- **交互响应**:下拉框位置更精确,响应更及时 + +## 2025-01-29 - 修复网络栈配置显示问题 + +### 修改原因 +用户反馈 `DeviceRuntimesView.tsx` 中的网络栈配置下拉框没有数据,但 `NetworkStackConfigsTable.tsx` 是有数据的,需要确保网络栈配置能正确显示网络栈名称。 + +### 修改文件 +- `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesTable.tsx` - 修复网络栈配置显示逻辑 +- `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesView.tsx` - 修复批量启动对话框中的网络栈配置显示 + +### 修改内容 + +#### 1. 显示逻辑优化 +- **表格列显示**:当找到匹配的网络栈配置时显示名称和编号,未找到时显示编号和提示 +- **对话框显示**:批量启动对话框中的网络栈配置显示逻辑与表格保持一致 +- **错误处理**:当网络栈配置不存在时提供友好的错误提示 + +#### 2. 数据匹配逻辑 +- **配置查找**:根据 `networkStackCode` 在 `networkStackConfigs` 中查找匹配的配置 +- **显示优先级**:优先显示网络栈名称,其次显示编号 +- **兜底显示**:当配置不存在时显示编号和"未找到配置"提示 + +#### 3. 用户体验改进 +- **清晰提示**:明确显示当前选择的网络栈配置状态 +- **错误反馈**:当配置不存在时提供明确的错误信息 +- **一致性**:表格和对话框中的显示逻辑保持一致 + +### 技术说明 +- **数据源**:使用 `networkStackConfigService.getNetworkStackConfigs()` 获取配置数据 +- **匹配逻辑**:使用 `find()` 方法根据 `networkStackCode` 匹配配置 +- **显示格式**:统一使用"名称 (编号)"的显示格式 +- **错误处理**:提供友好的错误提示而不是显示"未知配置" + +### 影响范围 +- **数据显示**:网络栈配置现在能正确显示网络栈名称 +- **用户体验**:提供更清晰和准确的配置信息显示 +- **错误处理**:当配置不存在时提供明确的错误提示 +- **一致性**:表格和对话框中的显示逻辑保持一致 + +## 2025-01-29 - 限制运行中设备的网络栈配置修改 + +### 修改原因 +用户要求当设备启动之后,网络栈配置不能再修改,需要限制运行中设备的网络栈配置编辑功能。 + +### 修改文件 +- `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesTable.tsx` - 限制运行中设备的网络栈配置下拉框 +- `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesView.tsx` - 限制运行中设备的网络栈配置变更 + +### 修改内容 + +#### 1. 下拉框状态控制 +- **运行中设备**:当 `runtimeStatus === 1` 时,下拉框显示为禁用状态 +- **视觉反馈**:禁用状态下使用灰色背景和降低透明度 +- **交互限制**:禁用状态下不允许点击打开下拉框 + +#### 2. 下拉框显示控制 +- **条件显示**:只有非运行中的设备才显示下拉框选项 +- **状态检查**:在显示下拉框前检查设备运行状态 +- **安全防护**:防止运行中设备意外修改配置 + +#### 3. 配置变更限制 +- **状态验证**:在配置变更前检查设备运行状态 +- **错误提示**:当尝试修改运行中设备配置时显示错误提示 +- **操作阻止**:阻止对运行中设备的配置修改操作 + +#### 4. 用户体验优化 +- **清晰状态**:通过视觉样式明确区分可编辑和不可编辑状态 +- **友好提示**:提供明确的错误提示说明为什么不能修改 +- **一致性**:保持界面交互逻辑的一致性 + +### 技术说明 +- **状态判断**:使用 `runtimeStatus === 1` 判断设备是否运行中 +- **条件渲染**:使用条件渲染控制下拉框的显示和交互 +- **样式控制**:使用 Tailwind CSS 类控制禁用状态的样式 +- **事件处理**:在事件处理函数中添加状态检查逻辑 + +### 影响范围 +- **功能限制**:运行中的设备无法修改网络栈配置 +- **视觉反馈**:运行中设备的网络栈配置列显示为禁用状态 +- **用户体验**:提供清晰的视觉和交互反馈 +- **数据安全**:防止运行中设备的配置被意外修改 + +## 2025-01-29 - 删除未使用的DeviceRuntimeDetail.tsx文件 + +### 修改原因 +经过检查发现 `DeviceRuntimeDetail.tsx` 文件虽然存在且有路由配置,但实际上没有被使用。在 `DeviceRuntimesView.tsx` 和 `DeviceRuntimesTable.tsx` 中都没有任何导航到详情页面的链接或按钮,表格中的操作菜单只包含"停止设备"和"设备未运行"选项,没有"查看详情"选项。 + +### 修改文件 + +#### 1. 删除文件 +- `X1.WebUI/src/pages/device-runtimes/DeviceRuntimeDetail.tsx` - 删除未使用的设备运行时详情页面组件 + +#### 2. 修改路由配置 +- `X1.WebUI/src/routes/AppRouter.tsx` - 移除DeviceRuntimeDetail组件的导入和路由配置 + +### 修改内容 + +#### 1. 删除DeviceRuntimeDetail.tsx文件 +- 删除了276行的设备运行时详情页面组件 +- 该组件包含设备运行时状态显示、网络配置信息、时间信息等功能 +- 但由于没有入口链接,用户无法访问此页面 + +#### 2. 更新路由配置 +- 移除 `const DeviceRuntimeDetail = lazy(() => import('@/pages/device-runtimes/DeviceRuntimeDetail'));` 导入语句 +- 删除 `/device-runtimes/detail/:deviceCode` 路由配置 +- 保留 `/device-runtimes/list` 路由,确保设备运行时列表页面正常工作 + +### 技术说明 +- **路由清理**:移除了未使用的路由配置,简化路由结构 +- **代码清理**:删除了未使用的组件文件,减少代码冗余 +- **功能完整性**:设备运行时管理功能仍然完整,只是移除了未使用的详情页面 +- **编译错误修复**:修复了因删除组件导致的编译错误 + +### 影响范围 +- **功能影响**:用户无法再访问设备运行时详情页面 +- **路由影响**:`/device-runtimes/detail/:deviceCode` 路径不再可用 +- **代码维护**:减少了未使用的代码,提高代码库的整洁性 +- **编译状态**:修复了编译错误,确保项目能够正常构建 + +### 后续建议 +1. **功能评估**:如果将来需要设备运行时详情功能,可以重新实现 +2. **用户反馈**:收集用户反馈,确认是否需要详情页面功能 +3. **替代方案**:考虑在列表页面中直接显示详细信息,而不是单独的详情页面 +4. **代码审查**:定期检查类似的未使用代码,保持代码库的整洁性 + +## 2025-01-29 - 修复批量启动按钮主题兼容性问题 + +### 修改原因 +批量启动按钮使用了硬编码的绿色样式(`bg-green-600 hover:bg-green-700 text-white`),与系统的主题切换不兼容,在深色主题下显示效果不佳。 + +### 修改文件 +- `X1.WebUI/src/pages/device-runtimes/DeviceRuntimesView.tsx` - 修复批量启动按钮的主题兼容性 + +### 修改内容 + +#### 1. 批量启动按钮样式修复 +- **移除硬编码样式**:删除 `className="bg-green-600 hover:bg-green-700 text-white"` +- **使用主题变量**:改为使用 `variant="default"`,让按钮自动适配当前主题 +- **保持功能不变**:按钮的禁用状态和点击功能保持不变 + +#### 2. 确认启动按钮样式修复 +- **移除硬编码样式**:删除 `className="bg-green-600 hover:bg-green-700"` +- **使用主题变量**:改为使用 `variant="default"`,确保与主题系统一致 +- **保持功能不变**:按钮的禁用状态和提交功能保持不变 + +### 技术说明 +- **主题兼容性**:使用 `variant="default"` 让按钮自动适配浅色/深色主题 +- **设计一致性**:与系统其他按钮保持一致的视觉风格 +- **可维护性**:移除硬编码样式,提高代码的可维护性 +- **用户体验**:在不同主题下都能提供良好的视觉体验 + +### 影响范围 +- **视觉效果**:批量启动按钮现在与主题系统完全兼容 +- **用户体验**:在浅色和深色主题下都有良好的显示效果 +- **代码质量**:移除了硬编码样式,提高了代码质量 +- **设计一致性**:与系统其他组件保持一致的视觉风格 \ No newline at end of file