Browse Source

更新WebUI组件和页面,优化设备运行时管理功能

feature/x1-web-request
root 4 days ago
parent
commit
bcfec713c9
  1. 2
      src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigs/GetNetworkStackConfigsQuery.cs
  2. 1
      src/X1.WebUI/package.json
  3. 78
      src/X1.WebUI/src/components/ui/card.tsx
  4. 15
      src/X1.WebUI/src/components/ui/dialog.tsx
  5. 198
      src/X1.WebUI/src/components/ui/dropdown-menu.tsx
  6. 11
      src/X1.WebUI/src/components/ui/index.ts
  7. 28
      src/X1.WebUI/src/components/ui/separator.tsx
  8. 2
      src/X1.WebUI/src/constants/api.ts
  9. 2
      src/X1.WebUI/src/constants/menuConfig.ts
  10. 151
      src/X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigsView.tsx
  11. 276
      src/X1.WebUI/src/pages/device-runtimes/DeviceRuntimeDetail.tsx
  12. 210
      src/X1.WebUI/src/pages/device-runtimes/DeviceRuntimesTable.tsx
  13. 436
      src/X1.WebUI/src/pages/device-runtimes/DeviceRuntimesView.tsx
  14. 151
      src/X1.WebUI/src/pages/ims-configurations/IMSConfigurationsView.tsx
  15. 99
      src/X1.WebUI/src/pages/instruments/DevicesView.tsx
  16. 114
      src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsView.tsx
  17. 113
      src/X1.WebUI/src/pages/protocols/ProtocolsView.tsx
  18. 100
      src/X1.WebUI/src/pages/ran-configurations/RANConfigurationsView.tsx
  19. 8
      src/X1.WebUI/src/routes/AppRouter.tsx
  20. 36
      src/X1.WebUI/src/services/deviceRuntimeService.ts
  21. 5
      src/X1.WebUI/yarn.lock
  22. 719
      src/modify.md

2
src/X1.Application/Features/NetworkStackConfigs/Queries/GetNetworkStackConfigs/GetNetworkStackConfigsQuery.cs

@ -18,7 +18,7 @@ public class GetNetworkStackConfigsQuery : IRequest<OperationResult<GetNetworkSt
/// <summary>
/// 每页大小
/// </summary>
[Range(1, 100, ErrorMessage = "每页大小必须在1-100之间")]
[Range(1, 1000, ErrorMessage = "每页大小必须在1-1000之间")]
public int PageSize { get; set; } = 10;
/// <summary>

1
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",

78
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<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

15
src/X1.WebUI/src/components/ui/dialog.tsx

@ -26,3 +26,18 @@ export const DialogContent = React.forwardRef<
</DialogPrimitive.Portal>
));
DialogContent.displayName = 'DialogContent';
export const DialogHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'flex flex-col space-y-1.5 text-center sm:text-left',
className
)}
{...props}
/>
));
DialogHeader.displayName = 'DialogHeader';

198
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<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

11
src/X1.WebUI/src/components/ui/index.ts

@ -5,3 +5,14 @@ export { Label } from './label';
export { Table } from './table';
export { Dialog } from './dialog';
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';

28
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<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

2
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',

2
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',
}

151
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<CoreNetworkConfig[]>([]);
const [loading, setLoading] = useState(false);
@ -52,7 +26,6 @@ export default function CoreNetworkConfigsView() {
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('');
@ -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 (
<main className="flex-1 p-4 transition-all duration-300 ease-in-out sm:p-6">
<div className="w-full space-y-4">
{/* 搜索栏 */}
<div className="flex flex-col bg-white p-4 rounded-md border mb-2">
{/* 搜索工具栏 */}
<div className="flex flex-col bg-background p-4 rounded-md border mb-2">
<form
className="grid gap-x-8 gap-y-4 items-center md:grid-cols-3 grid-cols-1"
className="flex gap-x-8 gap-y-4 items-center flex-wrap"
onSubmit={e => {
e.preventDefault();
handleQuery();
}}
>
{/* 第一行字段 */}
{firstRowFields.map((field: SearchField) => (
<div className="flex flex-row items-center min-w-[200px] flex-1" key={field.key}>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right"
style={{ width: 80, minWidth: 80 }}
>
{field.label}
</label>
{field.type === 'input' && (
<Input
className="input flex-1"
placeholder={field.placeholder}
value={field.key === 'searchTerm' ? searchTerm : ''}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (field.key === 'searchTerm') setSearchTerm(e.target.value);
}}
className="flex-1 bg-background text-foreground placeholder:text-muted-foreground border border-border focus:outline-none focus:ring-0 focus:border-border transition-all"
placeholder="请输入配置名称或描述"
value={searchTerm}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
/>
)}
{field.type === 'select' && (
<select
className="input h-10 rounded border border-border bg-background px-3 text-sm flex-1"
value={field.key === 'isDisabled' ? (isDisabled === undefined ? '' : isDisabled.toString()) : ''}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
if (field.key === 'isDisabled') {
const value = e.target.value;
setIsDisabled(value === '' ? undefined : value === 'true');
}
}}
>
{field.options.map(opt => (
<option value={opt.value} key={opt.value}>{opt.label}</option>
))}
</select>
)}
</div>
))}
{/* 按钮组直接作为表单项之一,紧跟在最后一个表单项后面 */}
<div className="flex flex-row items-center min-w-[200px] flex-1 justify-end gap-2">
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={handleReset}></button>
<button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"></button>
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={() => setShowAdvanced(v => !v)}>
{showAdvanced ? (
<>
<ChevronUpIcon className="inline-block ml-1 w-4 h-4 align-middle" />
</>
) : (
<>
<ChevronDownIcon className="inline-block ml-1 w-4 h-4 align-middle" />
</>
)}
</button>
</div>
</form>
{/* 高级搜索字段 */}
{showAdvanced && (
<div className="grid gap-x-8 gap-y-4 items-center md:grid-cols-3 grid-cols-1 mt-4 pt-4 border-t">
{advancedFields.map((field: SearchField) => (
<div className="flex flex-row items-center min-w-[200px] flex-1" key={field.key}>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right"
style={{ width: 80, minWidth: 80 }}
>
{field.label}
</label>
{field.type === 'select' && (
<select
className="input h-10 rounded border border-border bg-background px-3 text-sm flex-1"
value={field.key === 'pageSize' ? pageSize.toString() : ''}
className="h-10 rounded border border-border bg-background px-3 text-sm flex-1 text-foreground focus:outline-none focus:ring-0 focus:border-border transition-all"
value={isDisabled === undefined ? '' : isDisabled.toString()}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
if (field.key === 'pageSize') {
setPageSize(parseInt(e.target.value));
}
const value = e.target.value;
setIsDisabled(value === '' ? undefined : value === 'true');
}}
>
{field.options.map(opt => (
<option value={opt.value} key={opt.value}>{opt.label}</option>
))}
<option value=""></option>
<option value="false"></option>
<option value="true"></option>
</select>
)}
</div>
))}
{/* 按钮组 */}
<div className="flex flex-row items-center gap-2">
<Button variant="outline" onClick={handleReset}></Button>
<Button onClick={handleQuery}></Button>
</div>
)}
</form>
</div>
{/* 表格整体卡片区域,包括工具栏、表格、分页 */}
<div className="rounded-md border bg-background p-4">
{/* 顶部操作栏:添加核心网络配置+工具栏 */}
@ -389,6 +312,7 @@ export default function CoreNetworkConfigsView() {
density={density}
/>
</div>
{/* 表格区域 */}
<CoreNetworkConfigsTable
coreNetworkConfigs={coreNetworkConfigs}
@ -405,6 +329,7 @@ export default function CoreNetworkConfigsView() {
density={density}
columns={columns}
/>
{/* 分页 */}
<PaginationBar
page={pageNumber}

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

@ -1,276 +0,0 @@
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>
);
}

210
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 (
<div className="text-sm">
{device.networkStackCode || '-'}
<div className="relative network-stack-cell-dropdown">
<div
className={cn(
"flex h-8 w-full items-center justify-between rounded border px-2 py-1 text-xs",
device.runtimeStatus === 1
? "border-muted bg-muted cursor-not-allowed opacity-60"
: "border-input bg-background cursor-pointer hover:bg-accent"
)}
onClick={(e) => device.runtimeStatus !== 1 && toggleDropdown(device.deviceCode, e)}
>
<span className="text-xs truncate text-foreground">
{currentConfig ? `${currentConfig.networkStackName} (${currentConfig.networkStackCode})` :
device.networkStackCode ? `${device.networkStackCode} (未找到配置)` : '请选择网络栈配置'}
</span>
<svg className="h-3 w-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
{isDropdownOpen && device.runtimeStatus !== 1 && (
<div className="fixed z-[9999] bg-popover border rounded-md shadow-lg max-h-40 overflow-hidden"
style={{
top: `${dropdownPositions[device.deviceCode]?.top || 0}px`,
left: `${dropdownPositions[device.deviceCode]?.left || 0}px`,
width: '300px'
}}>
<div className="p-1 border-b">
<Input
placeholder="搜索配置..."
value={currentSearchTerm}
onChange={(e) => handleNetworkStackSearch(device.deviceCode, e.target.value)}
className="h-6 text-xs border-0 focus:ring-0 focus:border-0"
/>
</div>
<div className="max-h-32 overflow-y-auto">
{currentFilteredConfigs.map((config) => (
<div
key={config.networkStackConfigId}
className="px-2 py-1 text-xs cursor-pointer hover:bg-accent hover:text-accent-foreground"
onClick={() => handleNetworkStackSelect(device.deviceCode, config.networkStackCode)}
>
{config.networkStackName} ({config.networkStackCode})
</div>
))}
{currentFilteredConfigs.length === 0 && currentSearchTerm && (
<div className="px-2 py-1 text-xs text-muted-foreground">
</div>
)}
</div>
</div>
)}
</div>
);
@ -106,31 +239,38 @@ export default function DeviceRuntimesTable({
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">
<div className="flex items-center justify-center gap-2">
{/* 运行中状态:显示停止按钮 */}
{device.runtimeStatus === 1 && (
<DropdownMenuItem
<Button
variant="ghost"
size="sm"
onClick={() => onStopDevice(device.deviceCode)}
className="text-red-600 focus:text-red-600"
className="text-red-600 hover:text-red-700 hover:bg-red-50"
title="停止设备"
>
<StopIcon className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<StopIcon className="h-4 w-4" />
</Button>
)}
{/* 未知状态(-1)、初始化(0)、已停止(2)、错误(3):显示启动按钮 */}
{device.runtimeStatus !== 1 && (
<DropdownMenuItem disabled className="text-muted-foreground">
<PlayIcon className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<Button
variant="ghost"
size="sm"
onClick={() => onStartDevice(device.deviceCode)}
disabled={!device.networkStackCode}
className={cn(
"hover:bg-green-50",
device.networkStackCode
? "text-green-600 hover:text-green-700"
: "text-gray-400 cursor-not-allowed"
)}
</DropdownMenuContent>
</DropdownMenu>
title={device.networkStackCode ? "启动设备" : "请先选择网络栈配置"}
>
<PlayIcon className="h-4 w-4" />
</Button>
)}
</div>
);
default:
@ -147,7 +287,7 @@ export default function DeviceRuntimesTable({
<TableHeader>
<TableRow>
{/* 选择框列 */}
<TableHead className="w-12">
<TableHead className="w-12 text-center">
<Checkbox
checked={selectedDevices.length === deviceRuntimes.length && deviceRuntimes.length > 0}
onCheckedChange={onSelectAll}
@ -157,7 +297,7 @@ export default function DeviceRuntimesTable({
{/* 动态列 */}
{visibleColumns.map((column) => (
<TableHead key={column.key} className={cn(densityStyles[density])}>
<TableHead key={column.key} className={cn("text-center", densityStyles[density])}>
{column.title}
</TableHead>
))}
@ -185,7 +325,7 @@ export default function DeviceRuntimesTable({
deviceRuntimes.map((device) => (
<TableRow key={device.deviceCode}>
{/* 选择框 */}
<TableCell className="w-12">
<TableCell className="w-12 text-center">
<Checkbox
checked={selectedDevices.includes(device.deviceCode)}
onCheckedChange={(checked) => onDeviceSelect(device.deviceCode, checked as boolean)}
@ -195,7 +335,7 @@ export default function DeviceRuntimesTable({
{/* 动态单元格 */}
{visibleColumns.map((column) => (
<TableCell key={column.key} className={cn(densityStyles[density])}>
<TableCell key={column.key} className={cn("text-center", densityStyles[density])}>
{renderCell(device, column.key)}
</TableCell>
))}

436
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<DensityType>('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<NetworkStackConfig[]>([]);
const [filteredNetworkStackConfigs, setFilteredNetworkStackConfigs] = useState<NetworkStackConfig[]>([]);
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<GetDeviceRuntimesRequest> = {}) => {
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,158 +348,95 @@ 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 (
<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>
<main className="flex-1 p-4 transition-all duration-300 ease-in-out sm:p-6">
<div className="w-full space-y-4">
{/* 搜索工具栏 */}
<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>
<div className="flex flex-col bg-background p-4 rounded-md border mb-2">
<form
className="flex gap-x-8 gap-y-4 items-center flex-wrap"
onSubmit={e => {
e.preventDefault();
handleQuery();
}}
>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right"
style={{ width: 80, minWidth: 80 }}
>
</label>
<Input
id="searchTerm"
className="flex-1 bg-background text-foreground placeholder:text-muted-foreground border border-border focus:outline-none focus:ring-0 focus:border-border transition-all"
placeholder="请输入设备编号或设备名称"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => 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)}
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right"
style={{ width: 80, minWidth: 80 }}
>
<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))}
</label>
<select
className="h-10 rounded border border-border bg-background px-3 text-sm flex-1 text-foreground focus:outline-none focus:ring-0 focus:border-border transition-all"
value={runtimeStatus === undefined ? 'all' : runtimeStatus.toString()}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
setRuntimeStatus(value === 'all' ? undefined : 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>
<option value="all"></option>
<option value="0"></option>
<option value="1"></option>
<option value="2"></option>
</select>
</div>
{/* 按钮组 */}
<div className="flex flex-row items-center gap-2">
<Button variant="outline" onClick={handleReset}></Button>
<Button onClick={handleQuery}></Button>
</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>
</form>
</div>
{/* 批量操作按钮 */}
<div className="flex items-center space-x-2">
{/* 表格整体卡片区域,包括工具栏、表格、分页 */}
<div className="rounded-md border bg-background p-4">
{/* 顶部操作栏:批量启动+工具栏 */}
<div className="flex items-center justify-between mb-2">
<Dialog open={startDialogOpen} onOpenChange={setStartDialogOpen}>
<DialogTrigger asChild>
<Button
disabled={selectedDevices.length === 0}
className="bg-green-600 hover:bg-green-700"
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
({selectedDevices.length})
</Button>
@ -430,13 +447,60 @@ export default function DeviceRuntimesView() {
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="networkStackCode"></Label>
<Label htmlFor="networkStackCode"></Label>
<div className="relative network-stack-dropdown">
<div
className="flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 cursor-pointer"
onClick={() => setIsNetworkStackDropdownOpen(!isNetworkStackDropdownOpen)}
>
<span className="text-sm text-foreground">
{networkStackCode ?
(() => {
const config = networkStackConfigs.find(c => c.networkStackCode === networkStackCode);
return config ? `${config.networkStackName} (${config.networkStackCode})` : `${networkStackCode} (未找到配置)`;
})() :
"请选择网络栈配置"
}
</span>
<svg className="h-4 w-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
{isNetworkStackDropdownOpen && (
<div className="absolute z-50 w-full mt-1 bg-popover border rounded-md shadow-md max-h-60 overflow-hidden">
<div className="p-2 border-b">
<Input
id="networkStackCode"
placeholder="请输入网络栈配置编号"
value={networkStackCode}
onChange={(e) => setNetworkStackCode(e.target.value)}
placeholder="搜索网络栈配置..."
value={networkStackSearchTerm}
onChange={(e) => handleNetworkStackSearchChange(e.target.value)}
className="h-8 border-0 focus:ring-0 focus:border-0"
/>
</div>
<div className="max-h-48 overflow-y-auto">
{filteredNetworkStackConfigs.map((config) => (
<div
key={config.networkStackConfigId}
className="px-2 py-1.5 text-sm cursor-pointer hover:bg-accent hover:text-accent-foreground"
onClick={() => handleNetworkStackSelect(config.networkStackCode)}
>
{config.networkStackName} ({config.networkStackCode})
{config.description && (
<div className="text-xs text-muted-foreground mt-1">
{config.description}
</div>
)}
</div>
))}
{filteredNetworkStackConfigs.length === 0 && networkStackSearchTerm && (
<div className="px-2 py-1.5 text-sm text-muted-foreground">
</div>
)}
</div>
</div>
)}
</div>
</div>
<div className="space-y-2">
<Label> ({selectedDevices.length} )</Label>
@ -455,7 +519,7 @@ export default function DeviceRuntimesView() {
<Button
onClick={handleStartDevices}
disabled={isSubmitting || !networkStackCode.trim()}
className="bg-green-600 hover:bg-green-700"
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
{isSubmitting ? '启动中...' : '确认启动'}
</Button>
@ -463,45 +527,43 @@ export default function DeviceRuntimesView() {
</div>
</DialogContent>
</Dialog>
</div>
</div>
</div>
</CardContent>
</Card>
{/* 表格工具栏 */}
<TableToolbar
density={density}
onRefresh={() => fetchDeviceRuntimes()}
onDensityChange={setDensity}
columns={columns}
onColumnsChange={setColumns}
selectedCount={selectedDevices.length}
totalCount={deviceRuntimes.length}
onColumnsReset={() => setColumns(defaultColumns)}
columns={columns}
density={density}
/>
</div>
{/* 设备运行时表格 */}
{/* 表格区域 */}
<DeviceRuntimesTable
deviceRuntimes={deviceRuntimes}
loading={loading}
columns={columns}
density={density}
selectedDevices={selectedDevices}
networkStackConfigs={networkStackConfigs}
onDeviceSelect={handleDeviceSelect}
onSelectAll={handleSelectAll}
onStartDevice={handleStartDevice}
onStopDevice={handleStopDevice}
onNetworkStackChange={handleNetworkStackChange}
getRuntimeStatusDescription={deviceRuntimeService.getRuntimeStatusDescription}
getRuntimeStatusColor={deviceRuntimeService.getRuntimeStatusColor}
/>
{/* 分页 */}
<PaginationBar
currentPage={pageNumber}
totalPages={Math.ceil(total / pageSize)}
totalItems={total}
page={pageNumber}
pageSize={pageSize}
total={total}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
/>
</div>
</div>
</main>
);
}

151
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<IMSConfiguration[]>([]);
const [loading, setLoading] = useState(false);
@ -52,7 +26,6 @@ export default function IMSConfigurationsView() {
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('');
@ -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 (
<main className="flex-1 p-4 transition-all duration-300 ease-in-out sm:p-6">
<div className="w-full space-y-4">
{/* 搜索栏 */}
<div className="flex flex-col bg-white p-4 rounded-md border mb-2">
{/* 搜索工具栏 */}
<div className="flex flex-col bg-background p-4 rounded-md border mb-2">
<form
className="grid gap-x-8 gap-y-4 items-center md:grid-cols-3 grid-cols-1"
className="flex gap-x-8 gap-y-4 items-center flex-wrap"
onSubmit={e => {
e.preventDefault();
handleQuery();
}}
>
{/* 第一行字段 */}
{firstRowFields.map((field: SearchField) => (
<div className="flex flex-row items-center min-w-[200px] flex-1" key={field.key}>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right"
style={{ width: 80, minWidth: 80 }}
>
{field.label}
</label>
{field.type === 'input' && (
<Input
className="input flex-1"
placeholder={field.placeholder}
value={field.key === 'searchTerm' ? searchTerm : ''}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (field.key === 'searchTerm') setSearchTerm(e.target.value);
}}
className="flex-1 bg-background text-foreground placeholder:text-muted-foreground border border-border focus:outline-none focus:ring-0 focus:border-border transition-all"
placeholder="请输入配置名称或描述"
value={searchTerm}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
/>
)}
{field.type === 'select' && (
<select
className="input h-10 rounded border border-border bg-background px-3 text-sm flex-1"
value={field.key === 'isDisabled' ? (isDisabled === undefined ? '' : isDisabled.toString()) : ''}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
if (field.key === 'isDisabled') {
const value = e.target.value;
setIsDisabled(value === '' ? undefined : value === 'true');
}
}}
>
{field.options.map(opt => (
<option value={opt.value} key={opt.value}>{opt.label}</option>
))}
</select>
)}
</div>
))}
{/* 按钮组直接作为表单项之一,紧跟在最后一个表单项后面 */}
<div className="flex flex-row items-center min-w-[200px] flex-1 justify-end gap-2">
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={handleReset}></button>
<button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"></button>
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={() => setShowAdvanced(v => !v)}>
{showAdvanced ? (
<>
<ChevronUpIcon className="inline-block ml-1 w-4 h-4 align-middle" />
</>
) : (
<>
<ChevronDownIcon className="inline-block ml-1 w-4 h-4 align-middle" />
</>
)}
</button>
</div>
</form>
{/* 高级搜索字段 */}
{showAdvanced && (
<div className="grid gap-x-8 gap-y-4 items-center md:grid-cols-3 grid-cols-1 mt-4 pt-4 border-t">
{advancedFields.map((field: SearchField) => (
<div className="flex flex-row items-center min-w-[200px] flex-1" key={field.key}>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right"
style={{ width: 80, minWidth: 80 }}
>
{field.label}
</label>
{field.type === 'select' && (
<select
className="input h-10 rounded border border-border bg-background px-3 text-sm flex-1"
value={field.key === 'pageSize' ? pageSize.toString() : ''}
className="h-10 rounded border border-border bg-background px-3 text-sm flex-1 text-foreground focus:outline-none focus:ring-0 focus:border-border transition-all"
value={isDisabled === undefined ? '' : isDisabled.toString()}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
if (field.key === 'pageSize') {
setPageSize(parseInt(e.target.value));
}
const value = e.target.value;
setIsDisabled(value === '' ? undefined : value === 'true');
}}
>
{field.options.map(opt => (
<option value={opt.value} key={opt.value}>{opt.label}</option>
))}
<option value=""></option>
<option value="false"></option>
<option value="true"></option>
</select>
)}
</div>
))}
{/* 按钮组 */}
<div className="flex flex-row items-center gap-2">
<Button variant="outline" onClick={handleReset}></Button>
<Button onClick={handleQuery}></Button>
</div>
)}
</form>
</div>
{/* 表格整体卡片区域,包括工具栏、表格、分页 */}
<div className="rounded-md border bg-background p-4">
{/* 顶部操作栏:添加IMS配置+工具栏 */}
@ -389,6 +312,7 @@ export default function IMSConfigurationsView() {
density={density}
/>
</div>
{/* 表格区域 */}
<IMSConfigurationsTable
imsConfigurations={imsConfigurations}
@ -405,6 +329,7 @@ export default function IMSConfigurationsView() {
density={density}
columns={columns}
/>
{/* 分页 */}
<PaginationBar
page={pageNumber}

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

@ -7,7 +7,6 @@ import PaginationBar from '@/components/ui/PaginationBar';
import TableToolbar, { DensityType } from '@/components/ui/TableToolbar';
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons';
import { useToast } from '@/components/ui/use-toast';
const defaultColumns = [
@ -22,31 +21,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: 'isEnabled', 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条/页' },
] },
];
/**
*
*
@ -71,7 +45,6 @@ export default function DevicesView() {
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('');
@ -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 (
<main className="flex-1 p-4 transition-all duration-300 ease-in-out sm:p-6">
<div className="w-full space-y-4">
{/* 丰富美化后的搜索栏 */}
<div className="flex flex-col bg-white p-4 rounded-md border mb-2">
{/* 搜索工具栏 */}
<div className="flex flex-col bg-background p-4 rounded-md border mb-2">
<form
className={`grid gap-x-8 gap-y-4 items-center ${showAdvanced ? 'md:grid-cols-3' : 'md:grid-cols-3'} grid-cols-1`}
className="flex gap-x-8 gap-y-4 items-center flex-wrap"
onSubmit={e => {
e.preventDefault();
handleQuery();
}}
>
{(showAdvanced ? [...firstRowFields, ...advancedFields] : firstRowFields).map(field => (
<div className="flex flex-row items-center min-w-[200px] flex-1" key={field.key}>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right"
style={{ width: 80, minWidth: 80 }}
>
{field.label}
</label>
{field.type === 'input' && (
<Input
className="input flex-1"
placeholder={field.placeholder}
value={field.key === 'searchTerm' ? searchTerm : ''}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (field.key === 'searchTerm') setSearchTerm(e.target.value);
}}
className="flex-1 bg-background text-foreground placeholder:text-muted-foreground border border-border focus:outline-none focus:ring-0 focus:border-border transition-all"
placeholder="请输入设备名称或设备编码"
value={searchTerm}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
/>
)}
{field.type === 'select' && (
</div>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right"
style={{ width: 80, minWidth: 80 }}
>
</label>
<select
className="input h-10 rounded border border-border bg-background px-3 text-sm flex-1"
value={field.key === 'isEnabled' ? (isEnabled === undefined ? '' : isEnabled.toString()) :
field.key === 'pageSize' ? pageSize.toString() : ''}
className="h-10 rounded border border-border bg-background px-3 text-sm flex-1 text-foreground focus:outline-none focus:ring-0 focus:border-border transition-all"
value={isEnabled === undefined ? '' : isEnabled.toString()}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
if (field.key === 'isEnabled') {
const value = e.target.value;
setIsEnabled(value === '' ? undefined : value === 'true');
} else if (field.key === 'pageSize') {
setPageSize(parseInt(e.target.value));
}
}}
>
{field.options.map(opt => (
<option value={opt.value} key={opt.value}>{opt.label}</option>
))}
<option value=""></option>
<option value="true"></option>
<option value="false"></option>
</select>
)}
</div>
))}
{/* 按钮组直接作为表单项之一,紧跟在最后一个表单项后面 */}
<div className="flex flex-row items-center min-w-[200px] flex-1 justify-end gap-2">
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={handleReset}></button>
<button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"></button>
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={() => setShowAdvanced(v => !v)}>
{showAdvanced ? (
<>
<ChevronUpIcon className="inline-block ml-1 w-4 h-4 align-middle" />
</>
) : (
<>
<ChevronDownIcon className="inline-block ml-1 w-4 h-4 align-middle" />
</>
)}
</button>
{/* 按钮组 */}
<div className="flex flex-row items-center gap-2">
<Button variant="outline" onClick={handleReset}></Button>
<Button onClick={handleQuery}></Button>
</div>
</form>
</div>

114
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<NetworkStackConfig[]>([]);
const [loading, setLoading] = useState(false);
@ -55,7 +28,6 @@ export default function NetworkStackConfigsView() {
const [pageSize, setPageSize] = useState(10);
const [density, setDensity] = useState<DensityType>('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 (
<main className="flex-1 p-4 transition-all duration-300 ease-in-out sm:p-6">
<div className="w-full space-y-4">
{/* 丰富美化后的搜索栏 */}
<div className="flex flex-col bg-white p-4 rounded-md border mb-2">
{/* 搜索工具栏 */}
<div className="flex flex-col bg-background p-4 rounded-md border mb-2">
<form
className={`grid gap-x-8 gap-y-4 items-center ${showAdvanced ? 'md:grid-cols-3' : 'md:grid-cols-3'} grid-cols-1`}
className="flex gap-x-8 gap-y-4 items-center flex-wrap"
onSubmit={e => {
e.preventDefault();
handleQuery();
}}
>
{(showAdvanced ? [...firstRowFields, ...advancedFields] : firstRowFields).map(field => (
<div className="flex flex-row items-center min-w-[200px] flex-1" key={field.key}>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right"
style={{ width: 80, minWidth: 80 }}
>
{field.label}
</label>
{field.type === 'input' && (
<Input
className="input flex-1"
placeholder={field.placeholder}
value={field.key === 'networkStackName' ? networkStackName : ''}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (field.key === 'networkStackName') setNetworkStackName(e.target.value);
}}
className="flex-1 bg-background text-foreground placeholder:text-muted-foreground border border-border focus:outline-none focus:ring-0 focus:border-border transition-all"
placeholder="请输入网络栈名称"
value={networkStackName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNetworkStackName(e.target.value)}
/>
)}
{field.type === 'select' && (
</div>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right"
style={{ width: 80, minWidth: 80 }}
>
</label>
<select
className="input h-10 rounded border border-border bg-background px-3 text-sm flex-1"
value={field.key === 'isActive' ? (isActive === undefined ? '' : isActive.toString()) :
field.key === 'pageSize' ? pageSize.toString() : ''}
className="h-10 rounded border border-border bg-background px-3 text-sm flex-1 text-foreground focus:outline-none focus:ring-0 focus:border-border transition-all"
value={isActive === undefined ? '' : isActive.toString()}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
if (field.key === 'isActive') {
const value = e.target.value;
setIsActive(value === '' ? undefined : value === 'true');
} else if (field.key === 'pageSize') {
setPageSize(parseInt(e.target.value));
}
}}
>
{field.options.map(opt => (
<option value={opt.value} key={opt.value}>{opt.label}</option>
))}
<option value=""></option>
<option value="true"></option>
<option value="false"></option>
</select>
)}
</div>
))}
{/* 按钮组直接作为表单项之一,紧跟在最后一个表单项后面 */}
<div className="flex flex-row items-center min-w-[200px] flex-1 justify-end gap-2">
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={handleReset}></button>
<button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"></button>
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={() => setShowAdvanced(v => !v)}>
{showAdvanced ? (
<>
<ChevronUpIcon className="inline-block ml-1 w-4 h-4 align-middle" />
</>
) : (
<>
<ChevronDownIcon className="inline-block ml-1 w-4 h-4 align-middle" />
</>
)}
</button>
{/* 按钮组 */}
<div className="flex flex-row items-center gap-2">
<Button variant="outline" onClick={handleReset}></Button>
<Button onClick={handleQuery}></Button>
</div>
</form>
</div>
{/* 表格整体卡片区域,包括工具栏、表格、分页 */}
<div className="rounded-md border bg-background p-4">
{/* 顶部操作栏:添加网络栈配置+工具栏 */}
@ -318,6 +274,7 @@ export default function NetworkStackConfigsView() {
density={density}
/>
</div>
{/* 表格区域 */}
<NetworkStackConfigsTable
networkStackConfigs={networkStackConfigs}
@ -332,6 +289,7 @@ export default function NetworkStackConfigsView() {
density={density}
columns={columns}
/>
{/* 分页 */}
<PaginationBar
page={pageNumber}

113
src/X1.WebUI/src/pages/protocols/ProtocolsView.tsx

@ -7,7 +7,6 @@ import PaginationBar from '@/components/ui/PaginationBar';
import TableToolbar, { DensityType } from '@/components/ui/TableToolbar';
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons';
import { useToast } from '@/components/ui/use-toast';
const defaultColumns = [
@ -21,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: 'searchTerm', label: '搜索关键词', type: 'input', placeholder: '请输入版本名称或版本号' },
{ key: 'isEnabled', 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 ProtocolsView() {
const [protocolVersions, setProtocolVersions] = useState<ProtocolVersion[]>([]);
const [loading, setLoading] = useState(false);
@ -54,7 +28,6 @@ export default function ProtocolsView() {
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('');
@ -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 (
<main className="flex-1 p-4 transition-all duration-300 ease-in-out sm:p-6">
<div className="w-full space-y-4">
{/* 丰富美化后的搜索栏 */}
<div className="flex flex-col bg-white p-4 rounded-md border mb-2">
{/* 搜索工具栏 */}
<div className="flex flex-col bg-background p-4 rounded-md border mb-2">
<form
className={`grid gap-x-8 gap-y-4 items-center ${showAdvanced ? 'md:grid-cols-3' : 'md:grid-cols-3'} grid-cols-1`}
className="flex gap-x-8 gap-y-4 items-center flex-wrap"
onSubmit={e => {
e.preventDefault();
handleQuery();
}}
>
{(showAdvanced ? [...firstRowFields, ...advancedFields] : firstRowFields).map(field => (
<div className="flex flex-row items-center min-w-[200px] flex-1" key={field.key}>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right"
style={{ width: 80, minWidth: 80 }}
>
{field.label}
</label>
{field.type === 'input' && (
<Input
className="input flex-1"
placeholder={field.placeholder}
value={field.key === 'searchTerm' ? searchTerm : ''}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (field.key === 'searchTerm') setSearchTerm(e.target.value);
}}
className="flex-1 bg-background text-foreground placeholder:text-muted-foreground border border-border focus:outline-none focus:ring-0 focus:border-border transition-all"
placeholder="请输入版本名称或版本号"
value={searchTerm}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
/>
)}
{field.type === 'select' && (
</div>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right"
style={{ width: 80, minWidth: 80 }}
>
</label>
<select
className="input h-10 rounded border border-border bg-background px-3 text-sm flex-1"
value={field.key === 'isEnabled' ? (isEnabled === undefined ? '' : isEnabled.toString()) :
field.key === 'pageSize' ? pageSize.toString() : ''}
className="h-10 rounded border border-border bg-background px-3 text-sm flex-1 text-foreground focus:outline-none focus:ring-0 focus:border-border transition-all"
value={isEnabled === undefined ? '' : isEnabled.toString()}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
if (field.key === 'isEnabled') {
const value = e.target.value;
setIsEnabled(value === '' ? undefined : value === 'true');
} else if (field.key === 'pageSize') {
setPageSize(parseInt(e.target.value));
}
}}
>
{field.options.map(opt => (
<option value={opt.value} key={opt.value}>{opt.label}</option>
))}
<option value=""></option>
<option value="true"></option>
<option value="false"></option>
</select>
)}
</div>
))}
{/* 按钮组直接作为表单项之一,紧跟在最后一个表单项后面 */}
<div className="flex flex-row items-center min-w-[200px] flex-1 justify-end gap-2">
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={handleReset}></button>
<button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"></button>
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={() => setShowAdvanced(v => !v)}>
{showAdvanced ? (
<>
<ChevronUpIcon className="inline-block ml-1 w-4 h-4 align-middle" />
</>
) : (
<>
<ChevronDownIcon className="inline-block ml-1 w-4 h-4 align-middle" />
</>
)}
</button>
{/* 按钮组 */}
<div className="flex flex-row items-center gap-2">
<Button variant="outline" onClick={handleReset}></Button>
<Button onClick={handleQuery}></Button>
</div>
</form>
</div>
{/* 表格整体卡片区域,包括工具栏、表格、分页 */}
<div className="rounded-md border bg-background p-4">
{/* 顶部操作栏:添加协议版本+工具栏 */}
@ -319,6 +274,7 @@ export default function ProtocolsView() {
density={density}
/>
</div>
{/* 表格区域 */}
<ProtocolsTable
protocolVersions={protocolVersions}
@ -333,6 +289,7 @@ export default function ProtocolsView() {
density={density}
columns={columns}
/>
{/* 分页 */}
<PaginationBar
page={pageNumber}

100
src/X1.WebUI/src/pages/ran-configurations/RANConfigurationsView.tsx

@ -19,21 +19,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 }[] };
// 搜索字段
const searchFields: SearchField[] = [
{ key: 'searchTerm', label: '搜索关键词', type: 'input', placeholder: '请输入配置名称或描述' },
{ key: 'isDisabled', label: '状态', type: 'select', options: [
{ value: '', label: '请选择' },
{ value: 'false', label: '启用' },
{ value: 'true', label: '禁用' },
] },
];
export default function RANConfigurationsView() {
const [ranConfigurations, setRANConfigurations] = useState<RANConfiguration[]>([]);
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 (
<main className="flex-1 p-4 transition-all duration-300 ease-in-out sm:p-6">
<div className="w-full space-y-4">
{/* 搜索栏 */}
<div className="flex flex-col bg-white p-4 rounded-md border mb-2">
{/* 搜索工具栏 */}
<div className="flex flex-col bg-background p-4 rounded-md border mb-2">
<form
className="grid gap-x-8 gap-y-4 items-center md:grid-cols-3 grid-cols-1"
className="flex gap-x-8 gap-y-4 items-center flex-wrap"
onSubmit={e => {
e.preventDefault();
handleQuery();
}}
>
{searchFields.map((field: SearchField) => (
<div className="flex flex-row items-center min-w-[200px] flex-1" key={field.key}>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right"
style={{ width: 80, minWidth: 80 }}
>
{field.label}
</label>
{field.type === 'input' && (
<Input
className="input flex-1"
placeholder={field.placeholder}
value={field.key === 'searchTerm' ? searchTerm : ''}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (field.key === 'searchTerm') setSearchTerm(e.target.value);
}}
className="flex-1 bg-background text-foreground placeholder:text-muted-foreground border border-border focus:outline-none focus:ring-0 focus:border-border transition-all"
placeholder="请输入配置名称或描述"
value={searchTerm}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
/>
)}
{field.type === 'select' && (
</div>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right"
style={{ width: 80, minWidth: 80 }}
>
</label>
<select
className="input h-10 rounded border border-border bg-background px-3 text-sm flex-1"
value={field.key === 'isDisabled' ? (isDisabled === undefined ? '' : isDisabled.toString()) : ''}
className="h-10 rounded border border-border bg-background px-3 text-sm flex-1 text-foreground focus:outline-none focus:ring-0 focus:border-border transition-all"
value={isDisabled === undefined ? '' : isDisabled.toString()}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
if (field.key === 'isDisabled') {
const value = e.target.value;
setIsDisabled(value === '' ? undefined : value === 'true');
}
}}
>
{field.options.map((opt: { value: string; label: string }) => (
<option value={opt.value} key={opt.value}>{opt.label}</option>
))}
<option value=""></option>
<option value="false"></option>
<option value="true"></option>
</select>
)}
</div>
))}
{/* 按钮组 */}
<div className="flex flex-row items-center min-w-[200px] flex-1 justify-end gap-2">
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={handleReset}></button>
<button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"></button>
<div className="flex flex-row items-center gap-2">
<Button variant="outline" onClick={handleReset}></Button>
<Button onClick={handleQuery}></Button>
</div>
</form>
</div>
{/* 表格整体卡片区域,包括工具栏、表格、分页 */}
<div className="rounded-md border bg-background p-4">
{/* 顶部操作栏:添加RAN配置+工具栏 */}
@ -335,6 +313,7 @@ export default function RANConfigurationsView() {
density={density}
/>
</div>
{/* 表格区域 */}
<RANConfigurationsTable
ranConfigurations={ranConfigurations}
@ -351,6 +330,7 @@ export default function RANConfigurationsView() {
density={density}
columns={columns}
/>
{/* 分页 */}
<PaginationBar
page={pageNumber}

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

@ -33,7 +33,6 @@ const UEAnalysisView = lazy(() => import('@/pages/analysis/UEAnalysisView'));
const DevicesView = lazy(() => import('@/pages/instruments/DevicesView'));
// 设备运行时管理页面
const DeviceRuntimesView = lazy(() => import('@/pages/device-runtimes/DeviceRuntimesView'));
const DeviceRuntimeDetail = lazy(() => import('@/pages/device-runtimes/DeviceRuntimeDetail'));
// 协议管理页面
const ProtocolsView = lazy(() => import('@/pages/protocols/ProtocolsView'));
// RAN配置管理页面
@ -220,13 +219,6 @@ export function AppRouter() {
</AnimatedContainer>
</ProtectedRoute>
} />
<Route path="detail/:deviceCode" element={
<ProtectedRoute requiredPermission="deviceruntimes.view">
<AnimatedContainer>
<DeviceRuntimeDetail />
</AnimatedContainer>
</ProtectedRoute>
} />
</Route>
</Route>

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

@ -124,8 +124,24 @@ class DeviceRuntimeService {
return httpClient.post<StopDeviceRuntimeResponse>(`${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();

5
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"

719
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
### 修改原因
@ -4400,3 +4541,581 @@ private static DeviceRuntimeDto MapToDto(CellularDeviceRuntime runtime)
- 需要更新任何硬编码的链接或书签
- 权限检查仍然正常工作,确保安全性
- 所有设备运行时管理功能保持不变
## 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 <Select.Item /> 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 修复**:将 `<SelectItem value="">全部状态</SelectItem>` 改为 `<SelectItem value="all">全部状态</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"` 让按钮自动适配浅色/深色主题
- **设计一致性**:与系统其他按钮保持一致的视觉风格
- **可维护性**:移除硬编码样式,提高代码的可维护性
- **用户体验**:在不同主题下都能提供良好的视觉体验
### 影响范围
- **视觉效果**:批量启动按钮现在与主题系统完全兼容
- **用户体验**:在浅色和深色主题下都有良好的显示效果
- **代码质量**:移除了硬编码样式,提高了代码质量
- **设计一致性**:与系统其他组件保持一致的视觉风格
Loading…
Cancel
Save