diff --git a/src/X1.WebUI/src/components/testcases/TestCaseDetailDrawer.tsx b/src/X1.WebUI/src/components/testcases/TestCaseDetailDrawer.tsx index ea8da39..eecf8d2 100644 --- a/src/X1.WebUI/src/components/testcases/TestCaseDetailDrawer.tsx +++ b/src/X1.WebUI/src/components/testcases/TestCaseDetailDrawer.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect } from 'react'; import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'; import ReactFlow, { Node, @@ -13,23 +13,7 @@ import ReactFlow, { } from 'reactflow'; import { testcaseService, TestCaseFlowDetail } from '@/services/testcaseService'; import 'reactflow/dist/style.css'; -import { - Play, - Square, - GitBranch, - Smartphone, - Wifi, - WifiOff, - Phone, - PhoneCall, - PhoneOff, - Network, - Activity, - Signal, - SignalHigh, - SignalLow, - Settings -} from 'lucide-react'; +import { Settings } from 'lucide-react'; interface TestCaseDetailDrawerProps { testCaseId: string | null; @@ -37,281 +21,45 @@ interface TestCaseDetailDrawerProps { onOpenChange: (open: boolean) => void; } -// 自定义节点组件 - 与 ReactFlowDesigner 完全一致 +// 简化的自定义节点组件 const TestStepNode = ({ data, selected }: { data: any; selected?: boolean }) => { - console.log('TestStepNode 接收到的数据:', data); - console.log('TestStepNode selected:', selected); - - const getIconComponent = (iconName: string) => { - // 根据图标名称返回对应的图标组件 - switch (iconName) { - case 'play-circle': return ; - case 'stop-circle': return ; - case 'git-branch': return ; - case 'settings': return ; - case 'smartphone': return ; - case 'wifi': return ; - case 'wifi-off': return ; - case 'phone': return ; - case 'phone-call': return ; - case 'phone-off': return ; - case 'network': return ; - case 'activity': return ; - case 'signal': return ; - case 'signal-high': return ; - case 'signal-low': return ; - default: return ; - } - }; - const getNodeStyle = (stepType: number) => { switch (stepType) { - case 1: // 开始步骤 - 圆形 - return { - shape: 'rounded-full', - bgColor: 'bg-blue-50 dark:bg-blue-900/30', - textColor: 'text-blue-600 dark:text-blue-400', - borderColor: 'border-blue-200 dark:border-blue-600/50', - hoverBorderColor: 'hover:border-blue-300 dark:hover:border-blue-500' - }; - case 2: // 结束步骤 - 圆形 - return { - shape: 'rounded-full', - bgColor: 'bg-red-50 dark:bg-red-900/30', - textColor: 'text-red-600 dark:text-red-400', - borderColor: 'border-red-200 dark:border-red-600/50', - hoverBorderColor: 'hover:border-red-300 dark:hover:border-red-500' - }; - case 3: // 处理步骤 - 矩形 - return { - shape: 'rounded-md', - bgColor: 'bg-green-50 dark:bg-green-900/30', - textColor: 'text-green-600 dark:text-green-400', - borderColor: 'border-green-200 dark:border-green-600/50', - hoverBorderColor: 'hover:border-green-300 dark:hover:border-green-500' - }; - case 4: // 判断步骤 - 菱形 - return { - shape: 'transform rotate-45', - bgColor: 'bg-purple-50 dark:bg-purple-900/30', - textColor: 'text-purple-600 dark:text-purple-400', - borderColor: 'border-purple-200 dark:border-purple-600/50', - hoverBorderColor: 'hover:border-purple-300 dark:hover:border-purple-500' - }; - default: - return { - shape: 'rounded-md', - bgColor: 'bg-gray-50 dark:bg-gray-800', - textColor: 'text-gray-600 dark:text-gray-400', - borderColor: 'border-gray-200 dark:border-gray-700', - hoverBorderColor: 'hover:border-gray-300 dark:hover:border-gray-600' - }; - } - }; - - const getIconBgColor = (iconName: string) => { - // 设备相关图标 - 蓝色 - const deviceIcons = ['smartphone', 'phone', 'phone-call', 'phone-off', 'wifi', 'wifi-off', 'signal', 'signal-high', 'signal-low']; - // 网络相关图标 - 绿色 - const networkIcons = ['network', 'activity']; - // 控制相关图标 - 橙色 - const controlIcons = ['play-circle', 'stop-circle']; - // 配置相关图标 - 紫色 - const configIcons = ['settings', 'git-branch']; - - if (deviceIcons.includes(iconName)) { - return 'bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400'; - } else if (networkIcons.includes(iconName)) { - return 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400'; - } else if (controlIcons.includes(iconName)) { - return 'bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400'; - } else if (configIcons.includes(iconName)) { - return 'bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400'; + case 1: return { bgColor: 'bg-blue-50', borderColor: 'border-blue-200' }; + case 2: return { bgColor: 'bg-red-50', borderColor: 'border-red-200' }; + case 3: return { bgColor: 'bg-green-50', borderColor: 'border-green-200' }; + case 4: return { bgColor: 'bg-purple-50', borderColor: 'border-purple-200' }; + default: return { bgColor: 'bg-gray-50', borderColor: 'border-gray-200' }; } - - return 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400'; }; const nodeStyle = getNodeStyle(data.stepType); return ( -
- {/* 开始和结束步骤使用圆形 */} - {(data.stepType === 1 || data.stepType === 2) && ( -
-
-
- {getIconComponent(data.icon || 'settings')} -
-
-
- {data.stepName} -
-
-
-
- )} +
+
+ + {data.stepName} +
- {/* 处理步骤使用矩形 */} - {data.stepType === 3 && ( -
-
-
- {getIconComponent(data.icon || 'settings')} -
-
-
- {data.stepName} -
-
-
-
- )} - - {/* 判断步骤使用菱形 */} - {data.stepType === 4 && ( -
-
-
- {getIconComponent(data.icon || 'settings')} -
-
-
- {data.stepName} -
-
-
-
- )} - - {/* 连接点 - 根据节点类型显示不同的连接点 */} - {/* 开始节点 (type=1) - 只有输出连接点 */} - {data.stepType === 1 && ( - <> - - - - )} - - {/* 结束节点 (type=2) - 只有输入连接点 */} - {data.stepType === 2 && ( - <> - - - - )} - - {/* 处理节点 (type=3) - 有输入和输出连接点 */} - {data.stepType === 3 && ( - <> - - - - - - )} - - {/* 判断节点 (type=4) - 有输入和输出连接点 */} - {data.stepType === 4 && ( - <> - - - - - - )} + {/* 连接点 */} + +
); }; -// 节点类型定义 -const nodeTypes = { - testStep: TestStepNode, -}; +const nodeTypes = { testStep: TestStepNode }; function TestCaseDetailDrawerInner({ testCaseId, open, onOpenChange }: TestCaseDetailDrawerProps) { const [selectedTestCase, setSelectedTestCase] = useState(null); @@ -325,7 +73,6 @@ function TestCaseDetailDrawerInner({ testCaseId, open, onOpenChange }: TestCaseD if (open && testCaseId) { loadTestCaseDetail(testCaseId); } else { - // 关闭抽屉时清理状态 setSelectedTestCase(null); setError(null); setNodes([]); @@ -340,76 +87,40 @@ function TestCaseDetailDrawerInner({ testCaseId, open, onOpenChange }: TestCaseD const result = await testcaseService.getTestCaseFlowById(id); if (result.isSuccess && result.data) { setSelectedTestCase(result.data.testCaseFlow); - // 转换节点和连线为ReactFlow格式 const flowData = getReactFlowData(result.data.testCaseFlow); setNodes(flowData.nodes); setEdges(flowData.edges); - // 延迟执行 fitView,确保节点已渲染 - setTimeout(() => { - fitView({ padding: 0.1 }); - }, 100); + setTimeout(() => fitView({ padding: 0.1 }), 100); } else { setError('加载测试用例详情失败'); - console.error('加载测试用例详情失败:', result.errorMessages); } } catch (error) { setError('加载测试用例详情出错'); - console.error('加载测试用例详情出错:', error); } finally { setFlowLoading(false); } }; - // 转换节点和连线为ReactFlow格式 const getReactFlowData = (testCase: TestCaseFlowDetail) => { - console.log('原始节点数据:', testCase.nodes); - console.log('原始连线数据:', testCase.edges); - - const flowNodes: Node[] = testCase.nodes.map(node => { - console.log('处理节点:', node); - console.log('节点数据:', node.data); - console.log('节点位置:', node.position); - - // 检查数据字段是否存在,如果不存在则使用默认值 - const nodeData = { + const flowNodes: Node[] = testCase.nodes.map(node => ({ + id: node.id, + type: 'testStep', + position: node.position, + data: { stepId: node.data?.stepId || node.id, stepName: node.data?.stepName || 'Unknown', stepType: node.data?.stepType || 3, - stepTypeName: node.data?.stepTypeName || '处理步骤', - description: node.data?.description || '', icon: node.data?.icon || 'settings' - }; - - console.log('处理后的节点数据:', nodeData); - - return { - id: node.id, - type: 'testStep', // 使用自定义节点类型 - position: node.position, - data: nodeData, - width: node.width || 150, - height: node.height || 50, - selected: node.selected || false, - positionAbsolute: node.positionAbsolute, - dragging: node.dragging || false - }; - }); + } + })); - const flowEdges: Edge[] = testCase.edges.map(edge => { - console.log('处理连线:', edge); - - return { - id: edge.id, - source: edge.source, - sourceHandle: edge.sourceHandle || null, - target: edge.target, - targetHandle: edge.targetHandle || null, - type: edge.type || 'smoothstep', - animated: edge.animated || false, - style: edge.style || { stroke: '#333', strokeWidth: 2 }, - data: edge.data || {} - }; - }); + const flowEdges: Edge[] = testCase.edges.map(edge => ({ + id: edge.id, + source: edge.source, + target: edge.target, + type: 'smoothstep', + style: { stroke: '#3b82f6', strokeWidth: 2 } + })); return { nodes: flowNodes, edges: flowEdges }; }; @@ -434,11 +145,6 @@ function TestCaseDetailDrawerInner({ testCaseId, open, onOpenChange }: TestCaseD ) : error ? (
-
- - - -

{error}

) : selectedTestCase ? ( -
+
+
) : ( diff --git a/src/X1.WebUI/src/pages/users/UserForm.tsx b/src/X1.WebUI/src/pages/users/UserForm.tsx index c242141..591506b 100644 --- a/src/X1.WebUI/src/pages/users/UserForm.tsx +++ b/src/X1.WebUI/src/pages/users/UserForm.tsx @@ -1,8 +1,10 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { Checkbox } from '@/components/ui/checkbox'; import { CreateUserRequest } from '@/services/userService'; +import { roleService, Role } from '@/services/roleService'; interface UserFormProps { onSubmit: (data: CreateUserRequest) => void; @@ -18,11 +20,35 @@ export default function UserForm({ onSubmit, initialData }: UserFormProps) { roles: initialData?.roles || [] }); + const [roles, setRoles] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + const fetchRoles = async () => { + setLoading(true); + const result = await roleService.getAllRoles(); + if (result.isSuccess && result.data) { + setRoles(result.data.roles || []); + } + setLoading(false); + }; + fetchRoles(); + }, []); + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSubmit(formData); }; + const handleRoleChange = (roleName: string, checked: boolean) => { + const currentRoles = formData.roles || []; + if (checked) { + setFormData({ ...formData, roles: [...currentRoles, roleName] }); + } else { + setFormData({ ...formData, roles: currentRoles.filter(r => r !== roleName) }); + } + }; + return (
@@ -62,6 +88,29 @@ export default function UserForm({ onSubmit, initialData }: UserFormProps) { required={!initialData} />
+ +
+ + {loading ? ( +
加载角色中...
+ ) : ( +
+ {roles.map((role) => ( +
+ handleRoleChange(role.name, checked)} + /> + +
+ ))} +
+ )} +
+ diff --git a/src/modify.md b/src/modify.md index 55c582c..652f5f8 100644 --- a/src/modify.md +++ b/src/modify.md @@ -1,5 +1,48 @@ # 修改记录 +## 2025-01-21 - UserForm 组件添加角色选择功能 + +#### 修改文件: +`X1.WebUI/src/pages/users/UserForm.tsx` - 为用户创建和编辑表单添加角色选择功能 + +#### 修改内容: + +1. **角色选择功能添加**: + - **导入依赖**:添加 `useEffect`, `useState` 和 `roleService`, `Role` 导入 + - **状态管理**:添加 `roles` 和 `loading` 状态管理角色数据和加载状态 + - **数据获取**:在组件挂载时自动获取所有可用角色列表 + - **角色选择**:使用 `Checkbox` 组件实现多角色选择功能 + +2. **角色选择界面**: + - **加载状态**:显示"加载角色中..."提示 + - **网格布局**:使用 `grid grid-cols-2 gap-4` 布局,每行显示两个角色 + - **复选框**:每个角色使用独立的复选框,支持多选 + - **标签显示**:显示角色名称,使用 `text-sm font-normal` 样式 + +3. **角色数据处理**: + - **初始值设置**:在 `formData` 中初始化 `roles` 字段为 `initialData?.roles || []` + - **角色变更处理**:实现 `handleRoleChange` 方法处理角色选择/取消选择 + - **数据同步**:确保表单数据与角色选择状态保持同步 + +4. **技术特性**: + - **异步加载**:使用 `useEffect` 异步加载角色数据 + - **错误处理**:通过 `roleService.getAllRoles()` 的错误处理机制 + - **用户体验**:加载状态提示,防止用户困惑 + - **数据完整性**:确保角色数据正确传递给后端 + +5. **界面布局**: + - **表单结构**:在密码字段后添加角色选择区域 + - **响应式设计**:使用网格布局适应不同屏幕尺寸 + - **视觉一致性**:与现有表单字段保持一致的样式和间距 + +#### 修改时间: +2025-01-21 + +#### 修改原因: +用户发现虽然 `userService.ts` 中的 `CreateUserRequest` 接口支持 `roles?: string[]` 字段,但 `UserForm.tsx` 组件中缺少角色选择功能。需要添加角色选择功能,使用户能够在创建和编辑用户时选择多个角色,确保前端界面与后端 `CreateUserCommand` 完全匹配。 + +--- + ## 2025-01-21 - NodeData 添加 stepId 必填字段和 TestCaseNode 实体修复 #### 修改文件: