Browse Source
- 将 TestStepForm 改为 Drawer 方式,提升用户体验 - 创建 TestStepDrawer 组件,支持创建和编辑模式 - 提取步骤配置到独立文件 stepConfigs.ts,增强可维护性 - 优化表单类型数据加载,避免重复请求 - 完善步骤映射功能,支持自动设置和双向绑定 - 修复映射字段类型问题,使用数值类型匹配后端枚举 - 优化步骤映射选择界面,为不同步骤类型提供专门映射选项 - 更新 TestStepsView 组件,集成新的抽屉组件 - 删除旧的 TestStepForm 文件,清理冗余代码 技术特性: - 响应式设计,支持不同屏幕尺寸 - 状态隔离,创建和编辑状态完全独立 - 生命周期管理,自动重置表单数据 - 配置化架构,便于后续扩展和维护 - 性能优化,减少不必要的网络请求release/web-ui-v1.0.0
11 changed files with 2944 additions and 535 deletions
File diff suppressed because it is too large
@ -0,0 +1,188 @@ |
|||
using System; |
|||
using Microsoft.EntityFrameworkCore.Migrations; |
|||
|
|||
#nullable disable |
|||
|
|||
namespace X1.Infrastructure.Migrations |
|||
{ |
|||
/// <inheritdoc />
|
|||
public partial class UpdateCaseStepConfigMappingFieldType : Migration |
|||
{ |
|||
/// <inheritdoc />
|
|||
protected override void Up(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.DropIndex( |
|||
name: "ix_tb_casestepconfig_stepname", |
|||
table: "tb_casestepconfig"); |
|||
|
|||
migrationBuilder.RenameIndex( |
|||
name: "ix_tb_casestepconfig_steptype", |
|||
table: "tb_casestepconfig", |
|||
newName: "IX_tb_casestepconfig_steptype"); |
|||
|
|||
migrationBuilder.RenameIndex( |
|||
name: "ix_tb_casestepconfig_isenabled", |
|||
table: "tb_casestepconfig", |
|||
newName: "IX_tb_casestepconfig_isenabled"); |
|||
|
|||
// 首先更新现有的mapping数据,将字符串值转换为对应的整数值
|
|||
migrationBuilder.Sql(@"
|
|||
UPDATE tb_casestepconfig |
|||
SET mapping = CASE |
|||
WHEN mapping = 'None' THEN '0' |
|||
WHEN mapping = 'StartFlow' THEN '1' |
|||
WHEN mapping = 'EndFlow' THEN '2' |
|||
WHEN mapping = 'EnableFlightMode' THEN '3' |
|||
WHEN mapping = 'DisableFlightMode' THEN '4' |
|||
WHEN mapping = 'ImsiRegistration' THEN '5' |
|||
WHEN mapping = 'MoCall' THEN '6' |
|||
WHEN mapping = 'MtCall' THEN '7' |
|||
WHEN mapping = 'HangUpCall' THEN '8' |
|||
WHEN mapping = 'PingTest' THEN '9' |
|||
WHEN mapping = 'IperfTest' THEN '10' |
|||
ELSE '0' |
|||
END |
|||
WHERE mapping IS NOT NULL; |
|||
");
|
|||
|
|||
// 然后修改列类型,使用USING子句明确指定转换
|
|||
migrationBuilder.Sql(@"
|
|||
ALTER TABLE tb_casestepconfig |
|||
ALTER COLUMN mapping TYPE integer |
|||
USING mapping::integer; |
|||
");
|
|||
|
|||
migrationBuilder.CreateTable( |
|||
name: "tb_imsi_registration_records", |
|||
columns: table => new |
|||
{ |
|||
Id = table.Column<string>(type: "text", nullable: false), |
|||
TestCaseId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false), |
|||
NodeId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false), |
|||
IsDualSim = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false), |
|||
Sim1Plmn = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true), |
|||
Sim1CellId = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true), |
|||
Sim1RegistrationWaitTime = table.Column<int>(type: "integer", nullable: true), |
|||
Sim2Plmn = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true), |
|||
Sim2CellId = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true), |
|||
Sim2RegistrationWaitTime = table.Column<int>(type: "integer", nullable: true), |
|||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"), |
|||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true) |
|||
}, |
|||
constraints: table => |
|||
{ |
|||
table.PrimaryKey("PK_tb_imsi_registration_records", x => x.Id); |
|||
table.ForeignKey( |
|||
name: "FK_tb_imsi_registration_records_tb_testcaseflow_TestCaseId", |
|||
column: x => x.TestCaseId, |
|||
principalTable: "tb_testcaseflow", |
|||
principalColumn: "id", |
|||
onDelete: ReferentialAction.Cascade); |
|||
table.ForeignKey( |
|||
name: "FK_tb_imsi_registration_records_tb_testcasenode_NodeId", |
|||
column: x => x.NodeId, |
|||
principalTable: "tb_testcasenode", |
|||
principalColumn: "id", |
|||
onDelete: ReferentialAction.Cascade); |
|||
}, |
|||
comment: "IMSI注册记录表"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_tb_casestepconfig_createdat", |
|||
table: "tb_casestepconfig", |
|||
column: "createdat"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_tb_casestepconfig_formtype", |
|||
table: "tb_casestepconfig", |
|||
column: "formtype"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_tb_casestepconfig_mapping", |
|||
table: "tb_casestepconfig", |
|||
column: "mapping"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_tb_imsi_registration_records_createdat", |
|||
table: "tb_imsi_registration_records", |
|||
column: "CreatedAt"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_tb_imsi_registration_records_nodeid", |
|||
table: "tb_imsi_registration_records", |
|||
column: "NodeId"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_tb_imsi_registration_records_testcaseid", |
|||
table: "tb_imsi_registration_records", |
|||
column: "TestCaseId"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_tb_imsi_registration_records_testcaseid_nodeid", |
|||
table: "tb_imsi_registration_records", |
|||
columns: new[] { "TestCaseId", "NodeId" }, |
|||
unique: true); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
protected override void Down(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.DropTable( |
|||
name: "tb_imsi_registration_records"); |
|||
|
|||
migrationBuilder.DropIndex( |
|||
name: "IX_tb_casestepconfig_createdat", |
|||
table: "tb_casestepconfig"); |
|||
|
|||
migrationBuilder.DropIndex( |
|||
name: "IX_tb_casestepconfig_formtype", |
|||
table: "tb_casestepconfig"); |
|||
|
|||
migrationBuilder.DropIndex( |
|||
name: "IX_tb_casestepconfig_mapping", |
|||
table: "tb_casestepconfig"); |
|||
|
|||
migrationBuilder.RenameIndex( |
|||
name: "IX_tb_casestepconfig_steptype", |
|||
table: "tb_casestepconfig", |
|||
newName: "ix_tb_casestepconfig_steptype"); |
|||
|
|||
migrationBuilder.RenameIndex( |
|||
name: "IX_tb_casestepconfig_isenabled", |
|||
table: "tb_casestepconfig", |
|||
newName: "ix_tb_casestepconfig_isenabled"); |
|||
|
|||
// 首先将整数值转换回字符串值
|
|||
migrationBuilder.Sql(@"
|
|||
UPDATE tb_casestepconfig |
|||
SET mapping = CASE |
|||
WHEN mapping = 0 THEN 'None' |
|||
WHEN mapping = 1 THEN 'StartFlow' |
|||
WHEN mapping = 2 THEN 'EndFlow' |
|||
WHEN mapping = 3 THEN 'EnableFlightMode' |
|||
WHEN mapping = 4 THEN 'DisableFlightMode' |
|||
WHEN mapping = 5 THEN 'ImsiRegistration' |
|||
WHEN mapping = 6 THEN 'MoCall' |
|||
WHEN mapping = 7 THEN 'MtCall' |
|||
WHEN mapping = 8 THEN 'HangUpCall' |
|||
WHEN mapping = 9 THEN 'PingTest' |
|||
WHEN mapping = 10 THEN 'IperfTest' |
|||
ELSE 'None' |
|||
END |
|||
WHERE mapping IS NOT NULL; |
|||
");
|
|||
|
|||
// 然后修改列类型,使用USING子句明确指定转换
|
|||
migrationBuilder.Sql(@"
|
|||
ALTER TABLE tb_casestepconfig |
|||
ALTER COLUMN mapping TYPE character varying(200) |
|||
USING mapping::character varying(200); |
|||
");
|
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "ix_tb_casestepconfig_stepname", |
|||
table: "tb_casestepconfig", |
|||
column: "stepname"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,503 @@ |
|||
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 { Textarea } from '@/components/ui/textarea'; |
|||
import { Checkbox } from '@/components/ui/checkbox'; |
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; |
|||
import { Drawer, DrawerHeader, DrawerContent, DrawerFooter } from '@/components/ui/drawer'; |
|||
import { CreateTestStepRequest, UpdateTestStepRequest, FormTypeDto, StepTypeDto, StepMappingDto } from '@/services/teststepsService'; |
|||
import { |
|||
STEP_TYPES, |
|||
PROCESSING_STEPS, |
|||
getIconComponent, |
|||
getFormTypeIcon, |
|||
getDefaultStepName, |
|||
getDefaultIcon, |
|||
getDefaultFormType |
|||
} from '@/constants/stepConfigs'; |
|||
import { X } from 'lucide-react'; |
|||
|
|||
interface TestStepDrawerProps { |
|||
open: boolean; |
|||
onOpenChange: (open: boolean) => void; |
|||
onSubmit: (data: CreateTestStepRequest | UpdateTestStepRequest) => void; |
|||
initialData?: Partial<CreateTestStepRequest> & { id?: string }; |
|||
isEdit?: boolean; |
|||
isSubmitting?: boolean; |
|||
existingSteps?: Array<{ stepType: number; stepName: string }>; |
|||
formTypes: FormTypeDto[]; |
|||
stepTypes: StepTypeDto[]; |
|||
stepMappings: StepMappingDto[]; |
|||
loadingFormTypes: boolean; |
|||
} |
|||
|
|||
export default function TestStepDrawer({ |
|||
open, |
|||
onOpenChange, |
|||
onSubmit, |
|||
initialData, |
|||
isEdit = false, |
|||
isSubmitting = false, |
|||
existingSteps = [], |
|||
formTypes, |
|||
stepTypes, |
|||
stepMappings, |
|||
loadingFormTypes |
|||
}: TestStepDrawerProps) { |
|||
|
|||
// 检查步骤类型是否已存在
|
|||
const isStepTypeExists = (stepType: number): boolean => { |
|||
return existingSteps.some(step => step.stepType === stepType); |
|||
}; |
|||
|
|||
// 检查处理步骤名称是否已存在
|
|||
const isProcessingStepNameExists = (stepName: string): boolean => { |
|||
return existingSteps.some(step => step.stepType === 3 && step.stepName === stepName); |
|||
}; |
|||
|
|||
// 根据表单类型获取支持的步骤映射
|
|||
const getSupportedStepMappings = (formType: number | undefined) => { |
|||
// 根据步骤类型和表单类型过滤步骤映射
|
|||
if (formData.stepType === 1) { |
|||
// 开始步骤:可以选择 StartFlowController(1) 和 EmptyController(0)
|
|||
return stepMappings.filter(mapping => |
|||
mapping.value === 1 || mapping.value === 0 |
|||
); |
|||
} else if (formData.stepType === 2) { |
|||
// 结束步骤:可以选择 EndFlowController(2) 和 EmptyController(0)
|
|||
return stepMappings.filter(mapping => |
|||
mapping.value === 2 || mapping.value === 0 |
|||
); |
|||
} else if (formData.stepType === 3) { |
|||
// 处理步骤:根据表单类型过滤,排除通用控制器
|
|||
const excludedControllerValues = [0, 1, 2]; // EmptyController, StartFlowController, EndFlowController
|
|||
|
|||
if (formType === undefined || formType === 0) { |
|||
return stepMappings.filter(mapping => |
|||
!excludedControllerValues.includes(mapping.value) |
|||
); |
|||
} |
|||
|
|||
return stepMappings.filter(mapping => { |
|||
if (excludedControllerValues.includes(mapping.value)) { |
|||
return false; |
|||
} |
|||
const processingStep = PROCESSING_STEPS.find(ps => ps.mapping === mapping.value); |
|||
return processingStep && processingStep.formType === formType; |
|||
}); |
|||
} else { |
|||
// 判断步骤和其他类型:只显示空控制器
|
|||
return stepMappings.filter(mapping => mapping.value === 0); |
|||
} |
|||
}; |
|||
|
|||
// 根据步骤映射获取对应的处理步骤配置
|
|||
const getProcessingStepByMapping = (mappingValue: number) => { |
|||
return PROCESSING_STEPS.find(ps => ps.mapping === mappingValue); |
|||
}; |
|||
|
|||
const [formData, setFormData] = useState<CreateTestStepRequest>({ |
|||
stepName: initialData?.stepName || getDefaultStepName(initialData?.stepType || 1), |
|||
stepType: initialData?.stepType || 1, |
|||
mapping: initialData?.mapping || 0, |
|||
description: initialData?.description || '', |
|||
icon: initialData?.icon || getDefaultIcon(initialData?.stepType || 1), |
|||
isEnabled: initialData?.isEnabled ?? true, |
|||
formType: initialData?.formType || getDefaultFormType(initialData?.stepType || 1) |
|||
}); |
|||
|
|||
// 当抽屉打开时,初始化表单数据
|
|||
useEffect(() => { |
|||
if (open) { |
|||
if (initialData && isEdit) { |
|||
setFormData({ |
|||
stepName: initialData.stepName || getDefaultStepName(initialData.stepType || 1), |
|||
stepType: initialData.stepType || 1, |
|||
mapping: initialData.mapping || 0, |
|||
description: initialData.description || '', |
|||
icon: initialData.icon || getDefaultIcon(initialData.stepType || 1), |
|||
isEnabled: initialData.isEnabled ?? true, |
|||
formType: initialData.formType || getDefaultFormType(initialData.stepType || 1) |
|||
}); |
|||
} else { |
|||
// 重置表单
|
|||
setFormData({ |
|||
stepName: getDefaultStepName(1), |
|||
stepType: 1, |
|||
mapping: 0, |
|||
description: '', |
|||
icon: getDefaultIcon(1), |
|||
isEnabled: true, |
|||
formType: getDefaultFormType(1) |
|||
}); |
|||
} |
|||
} |
|||
}, [open, initialData, isEdit]); |
|||
|
|||
const handleSubmit = (e: React.FormEvent) => { |
|||
e.preventDefault(); |
|||
if (isSubmitting) return; |
|||
|
|||
if (isEdit) { |
|||
// 编辑模式:只提交可修改的字段
|
|||
const updateData: UpdateTestStepRequest = { |
|||
id: initialData?.id || '', |
|||
description: formData.description, |
|||
icon: formData.icon, |
|||
mapping: formData.mapping, |
|||
isEnabled: formData.isEnabled, |
|||
formType: formData.formType |
|||
}; |
|||
onSubmit(updateData); |
|||
} else { |
|||
// 创建模式:提交所有字段
|
|||
onSubmit(formData); |
|||
} |
|||
}; |
|||
|
|||
// 处理步骤类型变化
|
|||
const handleStepTypeChange = (stepType: number) => { |
|||
const newFormData = { |
|||
...formData, |
|||
stepType: stepType, |
|||
stepName: getDefaultStepName(stepType), |
|||
icon: getDefaultIcon(stepType), |
|||
formType: getDefaultFormType(stepType), // 根据步骤类型设置默认表单类型
|
|||
mapping: stepType === 3 ? 0 : 0 // 默认使用空控制器(StepMapping.None)
|
|||
}; |
|||
setFormData(newFormData); |
|||
}; |
|||
|
|||
return ( |
|||
<Drawer open={open} onOpenChange={onOpenChange}> |
|||
<div className="flex flex-col h-full"> |
|||
<DrawerHeader> |
|||
<div className="flex items-center justify-between w-full"> |
|||
<h2 className="text-lg font-semibold"> |
|||
{isEdit ? '编辑测试步骤' : '创建测试步骤'} |
|||
</h2> |
|||
<Button |
|||
variant="ghost" |
|||
size="sm" |
|||
onClick={() => onOpenChange(false)} |
|||
className="h-8 w-8 p-0" |
|||
> |
|||
<X className="h-4 w-4" /> |
|||
</Button> |
|||
</div> |
|||
</DrawerHeader> |
|||
|
|||
<form onSubmit={handleSubmit} className="flex flex-col flex-1"> |
|||
<DrawerContent className="flex flex-col space-y-4 flex-1 overflow-y-auto p-4 max-h-[calc(100vh-200px)]"> |
|||
{!isEdit && ( |
|||
<div className="space-y-2"> |
|||
<Label htmlFor="stepName">步骤名称</Label> |
|||
<Input |
|||
id="stepName" |
|||
value={formData.stepName} |
|||
onChange={e => setFormData({ ...formData, stepName: e.target.value })} |
|||
placeholder="请输入步骤名称" |
|||
required |
|||
disabled={isSubmitting || (formData.stepType !== 3)} |
|||
/> |
|||
{(formData.stepType === 1 || formData.stepType === 2 || formData.stepType === 4) && ( |
|||
<p className="text-sm text-muted-foreground"> |
|||
{formData.stepType === 1 && "开始步骤使用固定名称:StartStep"} |
|||
{formData.stepType === 2 && "结束步骤使用固定名称:EndStep"} |
|||
{formData.stepType === 4 && "判断步骤使用固定名称:DecisionStep"} |
|||
</p> |
|||
)} |
|||
</div> |
|||
)} |
|||
|
|||
{!isEdit && ( |
|||
<div className="space-y-3"> |
|||
<Label htmlFor="stepType">步骤类型</Label> |
|||
<div className="grid grid-cols-4 gap-3"> |
|||
{STEP_TYPES.map((type) => { |
|||
// 处理步骤(类型3)可以创建多个,其他类型只能创建一个
|
|||
const isDisabled = isSubmitting || (!isEdit && type.value !== 3 && isStepTypeExists(type.value)); |
|||
return ( |
|||
<button |
|||
key={type.value} |
|||
type="button" |
|||
onClick={() => { |
|||
if (!isDisabled) { |
|||
handleStepTypeChange(type.value); |
|||
} |
|||
}} |
|||
disabled={isDisabled} |
|||
className={` |
|||
relative p-3 rounded-lg border-2 transition-all duration-200 hover:scale-105 |
|||
${formData.stepType === type.value |
|||
? `${type.selectedColor} shadow-md` |
|||
: `${type.color} hover:shadow-sm` |
|||
} |
|||
${isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'} |
|||
`}
|
|||
> |
|||
<div className="flex flex-col items-center space-y-1.5"> |
|||
<div className={` |
|||
p-1.5 rounded-full |
|||
${formData.stepType === type.value |
|||
? 'bg-white/80 dark:bg-white/20' |
|||
: 'bg-white/60 dark:bg-white/10' |
|||
} |
|||
`}>
|
|||
{getIconComponent(type.icon)} |
|||
</div> |
|||
<span className="text-xs font-medium">{type.label}</span> |
|||
{formData.stepType === type.value && ( |
|||
<div className="absolute top-1.5 right-1.5"> |
|||
<div className="w-2.5 h-2.5 bg-white dark:bg-white/90 rounded-full shadow-sm border border-white/20 dark:border-white/10 flex items-center justify-center"> |
|||
<div className="w-1 h-1 bg-current rounded-full"></div> |
|||
</div> |
|||
</div> |
|||
)} |
|||
</div> |
|||
</button> |
|||
); |
|||
})} |
|||
</div> |
|||
{/* 显示已存在步骤类型的提示 - 只显示特殊步骤类型(1,2,4)的限制 */} |
|||
{!isEdit && (isStepTypeExists(1) || isStepTypeExists(2) || isStepTypeExists(4)) && ( |
|||
<div className="mt-2 p-2 bg-yellow-50 dark:bg-yellow-950/30 border border-yellow-200 dark:border-yellow-600/50 rounded-lg"> |
|||
<p className="text-xs text-yellow-700 dark:text-yellow-300"> |
|||
以下步骤类型已存在,无法重复创建: |
|||
{isStepTypeExists(1) && <span className="ml-1 px-1.5 py-0.5 bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-200 rounded text-xs">开始步骤</span>} |
|||
{isStepTypeExists(2) && <span className="ml-1 px-1.5 py-0.5 bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-200 rounded text-xs">结束步骤</span>} |
|||
{isStepTypeExists(4) && <span className="ml-1 px-1.5 py-0.5 bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-200 rounded text-xs">判断步骤</span>} |
|||
</p> |
|||
</div> |
|||
)} |
|||
</div> |
|||
)} |
|||
|
|||
<div className="space-y-2"> |
|||
<Label htmlFor="icon">步骤图标</Label> |
|||
<div className="flex items-center gap-3"> |
|||
<Input |
|||
id="icon" |
|||
value={formData.icon} |
|||
onChange={e => setFormData({ ...formData, icon: e.target.value })} |
|||
placeholder="请输入图标名称或路径" |
|||
disabled={isSubmitting || formData.stepType !== 3} |
|||
className="flex-1" |
|||
/> |
|||
{formData.icon && ( |
|||
<div className="flex items-center justify-center w-10 h-10 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800/50 dark:to-gray-900/50 hover:from-gray-100 hover:to-gray-200 dark:hover:from-gray-700/50 dark:hover:to-gray-800/50 transition-all duration-200"> |
|||
<div className="p-1 rounded-full bg-white/80 dark:bg-white/10 shadow-sm"> |
|||
{getIconComponent(formData.icon)} |
|||
</div> |
|||
</div> |
|||
)} |
|||
</div> |
|||
{formData.stepType === 3 && ( |
|||
<div className="mt-3"> |
|||
<Label className="text-sm text-muted-foreground mb-2 block">选择处理类型</Label> |
|||
<div className="grid grid-cols-4 gap-3"> |
|||
{PROCESSING_STEPS.map((iconOption) => { |
|||
// 检查该处理类型是否已存在
|
|||
const isProcessingTypeExists = isProcessingStepNameExists(iconOption.stepName); |
|||
return ( |
|||
<button |
|||
key={iconOption.icon} |
|||
type="button" |
|||
onClick={() => setFormData({ |
|||
...formData, |
|||
icon: iconOption.icon, |
|||
stepName: iconOption.stepName, // 自动设置步骤名称为英文名称
|
|||
mapping: iconOption.mapping, // 自动设置映射值
|
|||
formType: iconOption.formType // 自动设置表单类型
|
|||
})} |
|||
disabled={isSubmitting || isProcessingTypeExists} |
|||
className={` |
|||
relative p-3 rounded-lg border-2 transition-all duration-200 hover:scale-105 |
|||
${formData.icon === iconOption.icon |
|||
? `${iconOption.selectedColor} shadow-md` |
|||
: `${iconOption.color} hover:shadow-sm` |
|||
} |
|||
${(isSubmitting || isProcessingTypeExists) ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'} |
|||
`}
|
|||
> |
|||
<div className="flex flex-col items-center space-y-1.5"> |
|||
<div className={` |
|||
p-1.5 rounded-full |
|||
${formData.icon === iconOption.icon |
|||
? 'bg-white/80 dark:bg-white/20' |
|||
: 'bg-white/60 dark:bg-white/10' |
|||
} |
|||
`}>
|
|||
{getIconComponent(iconOption.icon)} |
|||
</div> |
|||
<span className="text-xs font-medium">{iconOption.label}</span> |
|||
{formData.icon === iconOption.icon && ( |
|||
<div className="absolute top-1.5 right-1.5"> |
|||
<div className="w-2.5 h-2.5 bg-white dark:bg-white/90 rounded-full shadow-sm border border-white/20 dark:border-white/10 flex items-center justify-center"> |
|||
<div className="w-1 h-1 bg-current rounded-full"></div> |
|||
</div> |
|||
</div> |
|||
)} |
|||
</div> |
|||
</button> |
|||
); |
|||
})} |
|||
</div> |
|||
</div> |
|||
)} |
|||
{formData.stepType !== 3 && ( |
|||
<p className="text-sm text-muted-foreground mt-1"> |
|||
{formData.stepType === 1 && "开始步骤使用固定图标"} |
|||
{formData.stepType === 2 && "结束步骤使用固定图标"} |
|||
{formData.stepType === 4 && "判断步骤使用固定图标"} |
|||
</p> |
|||
)} |
|||
</div> |
|||
|
|||
{/* 表单类型选择 */} |
|||
<div className="space-y-2"> |
|||
<Label htmlFor="formType">表单类型</Label> |
|||
<div className="flex items-center gap-3"> |
|||
<Select |
|||
value={formData.formType?.toString() || "0"} |
|||
onValueChange={(value) => { |
|||
const newFormType = parseInt(value); |
|||
setFormData({ |
|||
...formData, |
|||
formType: newFormType, |
|||
// 如果当前映射不匹配新的表单类型,清空映射
|
|||
mapping: formData.mapping && getProcessingStepByMapping(formData.mapping)?.formType === newFormType |
|||
? formData.mapping |
|||
: 0 |
|||
}); |
|||
}} |
|||
disabled={isSubmitting || loadingFormTypes} |
|||
> |
|||
<SelectTrigger className="flex-1"> |
|||
<SelectValue placeholder="选择表单类型" /> |
|||
</SelectTrigger> |
|||
<SelectContent> |
|||
{formTypes.map((formType) => ( |
|||
<SelectItem key={formType.value} value={formType.value.toString()}> |
|||
<div className="flex items-center gap-2"> |
|||
{getFormTypeIcon(formType.value)} |
|||
<span>{formType.description}</span> |
|||
</div> |
|||
</SelectItem> |
|||
))} |
|||
</SelectContent> |
|||
</Select> |
|||
{formData.formType !== undefined && ( |
|||
<div className="flex items-center justify-center w-10 h-10 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800/50 dark:to-gray-900/50 hover:from-gray-100 hover:to-gray-200 dark:hover:from-gray-700/50 dark:hover:from-gray-800/50 transition-all duration-200"> |
|||
<div className="p-1 rounded-full bg-white/80 dark:bg-white/10 shadow-sm"> |
|||
{getFormTypeIcon(formData.formType)} |
|||
</div> |
|||
</div> |
|||
)} |
|||
</div> |
|||
<p className="text-sm text-muted-foreground"> |
|||
{formData.stepType === 1 && "开始步骤默认使用无表单类型"} |
|||
{formData.stepType === 2 && "结束步骤默认使用无表单类型"} |
|||
{formData.stepType === 4 && "判断步骤默认使用无表单类型"} |
|||
{formData.stepType === 3 && "处理步骤可以选择不同的表单类型"} |
|||
</p> |
|||
</div> |
|||
|
|||
{/* 步骤映射选择 */} |
|||
<div className="space-y-2"> |
|||
<Label htmlFor="mapping">步骤映射</Label> |
|||
<div className="flex items-center gap-3"> |
|||
<Select |
|||
value={formData.mapping?.toString() || "0"} |
|||
onValueChange={(value) => { |
|||
const mappingValue = parseInt(value); |
|||
const processingStep = getProcessingStepByMapping(mappingValue); |
|||
setFormData({ |
|||
...formData, |
|||
mapping: mappingValue, |
|||
// 自动设置对应的表单类型
|
|||
formType: processingStep?.formType || 0 |
|||
}); |
|||
}} |
|||
disabled={isSubmitting || loadingFormTypes} |
|||
> |
|||
<SelectTrigger className="flex-1"> |
|||
<SelectValue placeholder="选择步骤映射" /> |
|||
</SelectTrigger> |
|||
<SelectContent> |
|||
{getSupportedStepMappings(formData.formType).map((mapping) => ( |
|||
<SelectItem key={mapping.value} value={mapping.value.toString()}> |
|||
<div className="flex items-center gap-2"> |
|||
<span>{mapping.name}</span> |
|||
</div> |
|||
</SelectItem> |
|||
))} |
|||
</SelectContent> |
|||
</Select> |
|||
{formData.mapping && ( |
|||
<div className="flex items-center justify-center w-10 h-10 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800/50 dark:to-gray-900/50"> |
|||
<div className="p-1 rounded-full bg-white/80 dark:bg-white/10 shadow-sm"> |
|||
<span className="text-xs font-mono">M</span> |
|||
</div> |
|||
</div> |
|||
)} |
|||
</div> |
|||
<p className="text-sm text-muted-foreground"> |
|||
{formData.stepType === 1 && "开始步骤可选择启动流程控制器或空控制器"} |
|||
{formData.stepType === 2 && "结束步骤可选择结束流程控制器或空控制器"} |
|||
{formData.stepType === 3 && "处理步骤选择对应的具体操作控制器"} |
|||
{formData.stepType === 4 && "判断步骤使用空控制器,不执行具体操作"} |
|||
{formData.mapping && formData.mapping !== 0 && ( |
|||
<span className="ml-2 text-blue-600 dark:text-blue-400"> |
|||
(已选择: {stepMappings.find(m => m.value === formData.mapping)?.name || formData.mapping}) |
|||
</span> |
|||
)} |
|||
</p> |
|||
</div> |
|||
|
|||
<div className="space-y-2"> |
|||
<Label htmlFor="description">步骤描述</Label> |
|||
<Textarea |
|||
id="description" |
|||
value={formData.description} |
|||
onChange={e => setFormData({ ...formData, description: e.target.value })} |
|||
placeholder="请输入步骤描述" |
|||
rows={3} |
|||
disabled={isSubmitting} |
|||
/> |
|||
</div> |
|||
|
|||
<div className="flex items-center space-x-2"> |
|||
<Checkbox |
|||
id="isEnabled" |
|||
checked={formData.isEnabled ?? true} |
|||
onCheckedChange={(checked) => setFormData({ ...formData, isEnabled: checked as boolean })} |
|||
disabled={isSubmitting} |
|||
/> |
|||
<Label htmlFor="isEnabled">启用</Label> |
|||
</div> |
|||
</DrawerContent> |
|||
|
|||
<DrawerFooter> |
|||
<Button |
|||
type="button" |
|||
variant="outline" |
|||
onClick={() => onOpenChange(false)} |
|||
disabled={isSubmitting} |
|||
> |
|||
取消 |
|||
</Button> |
|||
<Button |
|||
type="submit" |
|||
disabled={isSubmitting} |
|||
className="bg-primary text-primary-foreground hover:bg-primary/90" |
|||
> |
|||
{isSubmitting ? '提交中...' : (isEdit ? '更新测试步骤' : '创建测试步骤')} |
|||
</Button> |
|||
</DrawerFooter> |
|||
</form> |
|||
</div> |
|||
</Drawer> |
|||
); |
|||
} |
|||
@ -1,425 +0,0 @@ |
|||
import React, { useState } from 'react'; |
|||
import { Button } from '@/components/ui/button'; |
|||
import { Input } from '@/components/ui/input'; |
|||
import { Label } from '@/components/ui/label'; |
|||
import { Textarea } from '@/components/ui/textarea'; |
|||
import { Checkbox } from '@/components/ui/checkbox'; |
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; |
|||
import { CreateTestStepRequest, UpdateTestStepRequest, FormTypeDto, StepTypeDto, StepMappingDto } from '@/services/teststepsService'; |
|||
import { |
|||
STEP_TYPES, |
|||
PROCESSING_STEPS, |
|||
getIconComponent, |
|||
getFormTypeIcon, |
|||
getDefaultStepName, |
|||
getDefaultIcon, |
|||
getDefaultFormType |
|||
} from '@/constants/stepConfigs'; |
|||
|
|||
interface TestStepFormProps { |
|||
onSubmit: (data: CreateTestStepRequest | UpdateTestStepRequest) => void; |
|||
initialData?: Partial<CreateTestStepRequest> & { id?: string }; |
|||
isEdit?: boolean; |
|||
isSubmitting?: boolean; |
|||
existingSteps?: Array<{ stepType: number; stepName: string }>; |
|||
formTypes: FormTypeDto[]; |
|||
stepTypes: StepTypeDto[]; |
|||
stepMappings: StepMappingDto[]; |
|||
loadingFormTypes: boolean; |
|||
} |
|||
|
|||
export default function TestStepForm({ |
|||
onSubmit, |
|||
initialData, |
|||
isEdit = false, |
|||
isSubmitting = false, |
|||
existingSteps = [], |
|||
formTypes, |
|||
stepTypes, |
|||
stepMappings, |
|||
loadingFormTypes |
|||
}: TestStepFormProps) { |
|||
|
|||
|
|||
|
|||
// 检查步骤类型是否已存在
|
|||
const isStepTypeExists = (stepType: number): boolean => { |
|||
return existingSteps.some(step => step.stepType === stepType); |
|||
}; |
|||
|
|||
// 检查处理步骤名称是否已存在
|
|||
const isProcessingStepNameExists = (stepName: string): boolean => { |
|||
return existingSteps.some(step => step.stepType === 3 && step.stepName === stepName); |
|||
}; |
|||
|
|||
// 根据表单类型获取支持的步骤映射
|
|||
const getSupportedStepMappings = (formType: number | undefined) => { |
|||
if (formType === undefined || formType === 0) { |
|||
return stepMappings; // 无表单类型时显示所有映射
|
|||
} |
|||
|
|||
// 根据表单类型过滤步骤映射
|
|||
return stepMappings.filter(mapping => { |
|||
const processingStep = PROCESSING_STEPS.find(ps => ps.mapping === mapping.name); |
|||
return processingStep && processingStep.formType === formType; |
|||
}); |
|||
}; |
|||
|
|||
// 根据步骤映射获取对应的处理步骤配置
|
|||
const getProcessingStepByMapping = (mappingName: string) => { |
|||
return PROCESSING_STEPS.find(ps => ps.mapping === mappingName); |
|||
}; |
|||
|
|||
const [formData, setFormData] = useState<CreateTestStepRequest>({ |
|||
stepName: initialData?.stepName || getDefaultStepName(initialData?.stepType || 1), |
|||
stepType: initialData?.stepType || 1, |
|||
mapping: initialData?.mapping || '', |
|||
description: initialData?.description || '', |
|||
icon: initialData?.icon || getDefaultIcon(initialData?.stepType || 1), |
|||
isEnabled: initialData?.isEnabled ?? true, |
|||
formType: initialData?.formType || getDefaultFormType(initialData?.stepType || 1) |
|||
}); |
|||
|
|||
const handleSubmit = (e: React.FormEvent) => { |
|||
e.preventDefault(); |
|||
if (isSubmitting) return; |
|||
|
|||
if (isEdit) { |
|||
// 编辑模式:只提交可修改的字段
|
|||
const updateData: UpdateTestStepRequest = { |
|||
id: initialData?.id || '', |
|||
description: formData.description, |
|||
icon: formData.icon, |
|||
mapping: formData.mapping, |
|||
isEnabled: formData.isEnabled, |
|||
formType: formData.formType |
|||
}; |
|||
onSubmit(updateData); |
|||
} else { |
|||
// 创建模式:提交所有字段
|
|||
onSubmit(formData); |
|||
} |
|||
}; |
|||
|
|||
// 处理步骤类型变化
|
|||
const handleStepTypeChange = (stepType: number) => { |
|||
const newFormData = { |
|||
...formData, |
|||
stepType: stepType, |
|||
stepName: getDefaultStepName(stepType), |
|||
icon: getDefaultIcon(stepType), |
|||
formType: getDefaultFormType(stepType) // 根据步骤类型设置默认表单类型
|
|||
}; |
|||
setFormData(newFormData); |
|||
}; |
|||
|
|||
return ( |
|||
<form onSubmit={handleSubmit} className="space-y-4"> |
|||
{!isEdit && ( |
|||
<div className="space-y-2"> |
|||
<Label htmlFor="stepName">步骤名称</Label> |
|||
<Input |
|||
id="stepName" |
|||
value={formData.stepName} |
|||
onChange={e => setFormData({ ...formData, stepName: e.target.value })} |
|||
placeholder="请输入步骤名称" |
|||
required |
|||
disabled={isSubmitting || (formData.stepType !== 3)} |
|||
/> |
|||
{(formData.stepType === 1 || formData.stepType === 2 || formData.stepType === 4) && ( |
|||
<p className="text-sm text-muted-foreground"> |
|||
{formData.stepType === 1 && "开始步骤使用固定名称:StartStep"} |
|||
{formData.stepType === 2 && "结束步骤使用固定名称:EndStep"} |
|||
{formData.stepType === 4 && "判断步骤使用固定名称:DecisionStep"} |
|||
</p> |
|||
)} |
|||
</div> |
|||
)} |
|||
|
|||
{!isEdit && ( |
|||
<div className="space-y-3"> |
|||
<Label htmlFor="stepType">步骤类型</Label> |
|||
<div className="grid grid-cols-4 gap-3"> |
|||
{STEP_TYPES.map((type) => { |
|||
// 处理步骤(类型3)可以创建多个,其他类型只能创建一个
|
|||
const isDisabled = isSubmitting || (!isEdit && type.value !== 3 && isStepTypeExists(type.value)); |
|||
return ( |
|||
<button |
|||
key={type.value} |
|||
type="button" |
|||
onClick={() => { |
|||
if (!isDisabled) { |
|||
handleStepTypeChange(type.value); |
|||
} |
|||
}} |
|||
disabled={isDisabled} |
|||
className={` |
|||
relative p-3 rounded-lg border-2 transition-all duration-200 hover:scale-105 |
|||
${formData.stepType === type.value |
|||
? `${type.selectedColor} shadow-md` |
|||
: `${type.color} hover:shadow-sm` |
|||
} |
|||
${isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'} |
|||
`}
|
|||
> |
|||
<div className="flex flex-col items-center space-y-1.5"> |
|||
<div className={` |
|||
p-1.5 rounded-full |
|||
${formData.stepType === type.value |
|||
? 'bg-white/80 dark:bg-white/20' |
|||
: 'bg-white/60 dark:bg-white/10' |
|||
} |
|||
`}>
|
|||
{getIconComponent(type.icon)} |
|||
</div> |
|||
<span className="text-xs font-medium">{type.label}</span> |
|||
{formData.stepType === type.value && ( |
|||
<div className="absolute top-1.5 right-1.5"> |
|||
<div className="w-2.5 h-2.5 bg-white dark:bg-white/90 rounded-full shadow-sm border border-white/20 dark:border-white/10 flex items-center justify-center"> |
|||
<div className="w-1 h-1 bg-current rounded-full"></div> |
|||
</div> |
|||
</div> |
|||
)} |
|||
</div> |
|||
</button> |
|||
); |
|||
})} |
|||
</div> |
|||
{/* 显示已存在步骤类型的提示 - 只显示特殊步骤类型(1,2,4)的限制 */} |
|||
{!isEdit && (isStepTypeExists(1) || isStepTypeExists(2) || isStepTypeExists(4)) && ( |
|||
<div className="mt-2 p-2 bg-yellow-50 dark:bg-yellow-950/30 border border-yellow-200 dark:border-yellow-600/50 rounded-lg"> |
|||
<p className="text-xs text-yellow-700 dark:text-yellow-300"> |
|||
以下步骤类型已存在,无法重复创建: |
|||
{isStepTypeExists(1) && <span className="ml-1 px-1.5 py-0.5 bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-200 rounded text-xs">开始步骤</span>} |
|||
{isStepTypeExists(2) && <span className="ml-1 px-1.5 py-0.5 bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-200 rounded text-xs">结束步骤</span>} |
|||
{isStepTypeExists(4) && <span className="ml-1 px-1.5 py-0.5 bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-200 rounded text-xs">判断步骤</span>} |
|||
</p> |
|||
</div> |
|||
)} |
|||
</div> |
|||
)} |
|||
|
|||
{!isEdit && ( |
|||
<div className="space-y-2"> |
|||
<Label htmlFor="mapping">步骤映射</Label> |
|||
<Input |
|||
id="mapping" |
|||
value={formData.mapping} |
|||
onChange={e => setFormData({ ...formData, mapping: e.target.value })} |
|||
placeholder="请输入步骤映射" |
|||
required |
|||
disabled={isSubmitting} |
|||
/> |
|||
</div> |
|||
)} |
|||
|
|||
<div className="space-y-2"> |
|||
<Label htmlFor="icon">步骤图标</Label> |
|||
<div className="flex items-center gap-3"> |
|||
<Input |
|||
id="icon" |
|||
value={formData.icon} |
|||
onChange={e => setFormData({ ...formData, icon: e.target.value })} |
|||
placeholder="请输入图标名称或路径" |
|||
disabled={isSubmitting || formData.stepType !== 3} |
|||
className="flex-1" |
|||
/> |
|||
{formData.icon && ( |
|||
<div className="flex items-center justify-center w-10 h-10 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800/50 dark:to-gray-900/50 hover:from-gray-100 hover:to-gray-200 dark:hover:from-gray-700/50 dark:hover:to-gray-800/50 transition-all duration-200"> |
|||
<div className="p-1 rounded-full bg-white/80 dark:bg-white/10 shadow-sm"> |
|||
{getIconComponent(formData.icon)} |
|||
</div> |
|||
</div> |
|||
)} |
|||
</div> |
|||
{formData.stepType === 3 && ( |
|||
<div className="mt-3"> |
|||
<Label className="text-sm text-muted-foreground mb-2 block">选择处理类型</Label> |
|||
<div className="grid grid-cols-4 gap-3"> |
|||
{PROCESSING_STEPS.map((iconOption) => { |
|||
// 检查该处理类型是否已存在
|
|||
const isProcessingTypeExists = isProcessingStepNameExists(iconOption.stepName); |
|||
return ( |
|||
<button |
|||
key={iconOption.icon} |
|||
type="button" |
|||
onClick={() => setFormData({ |
|||
...formData, |
|||
icon: iconOption.icon, |
|||
stepName: iconOption.stepName, // 自动设置步骤名称为英文名称
|
|||
mapping: iconOption.mapping, // 自动设置映射值
|
|||
formType: iconOption.formType // 自动设置表单类型
|
|||
})} |
|||
disabled={isSubmitting || isProcessingTypeExists} |
|||
className={` |
|||
relative p-3 rounded-lg border-2 transition-all duration-200 hover:scale-105 |
|||
${formData.icon === iconOption.icon |
|||
? `${iconOption.selectedColor} shadow-md` |
|||
: `${iconOption.color} hover:shadow-sm` |
|||
} |
|||
${(isSubmitting || isProcessingTypeExists) ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'} |
|||
`}
|
|||
> |
|||
<div className="flex flex-col items-center space-y-1.5"> |
|||
<div className={` |
|||
p-1.5 rounded-full |
|||
${formData.icon === iconOption.icon |
|||
? 'bg-white/80 dark:bg-white/20' |
|||
: 'bg-white/60 dark:bg-white/10' |
|||
} |
|||
`}>
|
|||
{getIconComponent(iconOption.icon)} |
|||
</div> |
|||
<span className="text-xs font-medium">{iconOption.label}</span> |
|||
{formData.icon === iconOption.icon && ( |
|||
<div className="absolute top-1.5 right-1.5"> |
|||
<div className="w-2.5 h-2.5 bg-white dark:bg-white/90 rounded-full shadow-sm border border-white/20 dark:border-white/10 flex items-center justify-center"> |
|||
<div className="w-1 h-1 bg-current rounded-full"></div> |
|||
</div> |
|||
</div> |
|||
)} |
|||
</div> |
|||
</button> |
|||
); |
|||
})} |
|||
</div> |
|||
</div> |
|||
)} |
|||
{formData.stepType !== 3 && ( |
|||
<p className="text-sm text-muted-foreground mt-1"> |
|||
{formData.stepType === 1 && "开始步骤使用固定图标"} |
|||
{formData.stepType === 2 && "结束步骤使用固定图标"} |
|||
{formData.stepType === 4 && "判断步骤使用固定图标"} |
|||
</p> |
|||
)} |
|||
</div> |
|||
|
|||
{/* 表单类型选择 */} |
|||
<div className="space-y-2"> |
|||
<Label htmlFor="formType">表单类型</Label> |
|||
<div className="flex items-center gap-3"> |
|||
<Select |
|||
value={formData.formType?.toString() || "0"} |
|||
onValueChange={(value) => { |
|||
const newFormType = parseInt(value); |
|||
setFormData({ |
|||
...formData, |
|||
formType: newFormType, |
|||
// 如果当前映射不匹配新的表单类型,清空映射
|
|||
mapping: formData.mapping && getProcessingStepByMapping(formData.mapping)?.formType === newFormType |
|||
? formData.mapping |
|||
: '' |
|||
}); |
|||
}} |
|||
disabled={isSubmitting || loadingFormTypes} |
|||
> |
|||
<SelectTrigger className="flex-1"> |
|||
<SelectValue placeholder="选择表单类型" /> |
|||
</SelectTrigger> |
|||
<SelectContent> |
|||
{formTypes.map((formType) => ( |
|||
<SelectItem key={formType.value} value={formType.value.toString()}> |
|||
<div className="flex items-center gap-2"> |
|||
{getFormTypeIcon(formType.value)} |
|||
<span>{formType.description}</span> |
|||
</div> |
|||
</SelectItem> |
|||
))} |
|||
</SelectContent> |
|||
</Select> |
|||
{formData.formType !== undefined && ( |
|||
<div className="flex items-center justify-center w-10 h-10 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800/50 dark:to-gray-900/50 hover:from-gray-100 hover:to-gray-200 dark:hover:from-gray-700/50 dark:hover:from-gray-800/50 transition-all duration-200"> |
|||
<div className="p-1 rounded-full bg-white/80 dark:bg-white/10 shadow-sm"> |
|||
{getFormTypeIcon(formData.formType)} |
|||
</div> |
|||
</div> |
|||
)} |
|||
</div> |
|||
<p className="text-sm text-muted-foreground"> |
|||
{formData.stepType === 1 && "开始步骤默认使用无表单类型"} |
|||
{formData.stepType === 2 && "结束步骤默认使用无表单类型"} |
|||
{formData.stepType === 4 && "判断步骤默认使用无表单类型"} |
|||
{formData.stepType === 3 && "处理步骤可以选择不同的表单类型"} |
|||
</p> |
|||
</div> |
|||
|
|||
{/* 步骤映射选择 */} |
|||
{formData.stepType === 3 && ( |
|||
<div className="space-y-2"> |
|||
<Label htmlFor="mapping">步骤映射</Label> |
|||
<div className="flex items-center gap-3"> |
|||
<Select |
|||
value={formData.mapping || ""} |
|||
onValueChange={(value) => { |
|||
const processingStep = getProcessingStepByMapping(value); |
|||
setFormData({ |
|||
...formData, |
|||
mapping: value, |
|||
// 自动设置对应的表单类型
|
|||
formType: processingStep?.formType || 0 |
|||
}); |
|||
}} |
|||
disabled={isSubmitting || loadingFormTypes} |
|||
> |
|||
<SelectTrigger className="flex-1"> |
|||
<SelectValue placeholder="选择步骤映射" /> |
|||
</SelectTrigger> |
|||
<SelectContent> |
|||
{getSupportedStepMappings(formData.formType).map((mapping) => ( |
|||
<SelectItem key={mapping.value} value={mapping.name}> |
|||
<div className="flex items-center gap-2"> |
|||
<span className="font-mono text-xs">{mapping.name}</span> |
|||
<span className="text-muted-foreground">-</span> |
|||
<span>{mapping.description}</span> |
|||
</div> |
|||
</SelectItem> |
|||
))} |
|||
</SelectContent> |
|||
</Select> |
|||
{formData.mapping && ( |
|||
<div className="flex items-center justify-center w-10 h-10 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800/50 dark:to-gray-900/50"> |
|||
<div className="p-1 rounded-full bg-white/80 dark:bg-white/10 shadow-sm"> |
|||
<span className="text-xs font-mono">M</span> |
|||
</div> |
|||
</div> |
|||
)} |
|||
</div> |
|||
<p className="text-sm text-muted-foreground"> |
|||
选择对应的控制器类名,用于执行具体的操作步骤 |
|||
{formData.mapping && ( |
|||
<span className="ml-2 text-blue-600 dark:text-blue-400"> |
|||
(已选择: {formData.mapping}) |
|||
</span> |
|||
)} |
|||
</p> |
|||
</div> |
|||
)} |
|||
|
|||
<div className="space-y-2"> |
|||
<Label htmlFor="description">步骤描述</Label> |
|||
<Textarea |
|||
id="description" |
|||
value={formData.description} |
|||
onChange={e => setFormData({ ...formData, description: e.target.value })} |
|||
placeholder="请输入步骤描述" |
|||
rows={3} |
|||
disabled={isSubmitting} |
|||
/> |
|||
</div> |
|||
|
|||
<div className="flex items-center space-x-2"> |
|||
<Checkbox |
|||
id="isEnabled" |
|||
checked={formData.isEnabled ?? true} |
|||
onCheckedChange={(checked) => setFormData({ ...formData, isEnabled: checked as boolean })} |
|||
disabled={isSubmitting} |
|||
/> |
|||
<Label htmlFor="isEnabled">启用</Label> |
|||
</div> |
|||
|
|||
<Button type="submit" className="w-full" disabled={isSubmitting}> |
|||
{isSubmitting ? '提交中...' : (isEdit ? '更新测试步骤' : '创建测试步骤')} |
|||
</Button> |
|||
</form> |
|||
); |
|||
} |
|||
Loading…
Reference in new issue