Browse Source

feat: 重构测试步骤表单为抽屉模式并优化步骤配置架构

- 将 TestStepForm 改为 Drawer 方式,提升用户体验
- 创建 TestStepDrawer 组件,支持创建和编辑模式
- 提取步骤配置到独立文件 stepConfigs.ts,增强可维护性
- 优化表单类型数据加载,避免重复请求
- 完善步骤映射功能,支持自动设置和双向绑定
- 修复映射字段类型问题,使用数值类型匹配后端枚举
- 优化步骤映射选择界面,为不同步骤类型提供专门映射选项
- 更新 TestStepsView 组件,集成新的抽屉组件
- 删除旧的 TestStepForm 文件,清理冗余代码

技术特性:
- 响应式设计,支持不同屏幕尺寸
- 状态隔离,创建和编辑状态完全独立
- 生命周期管理,自动重置表单数据
- 配置化架构,便于后续扩展和维护
- 性能优化,减少不必要的网络请求
release/web-ui-v1.0.0
root 4 months ago
parent
commit
3ede04882f
  1. 64
      src/X1.Domain/Common/FormTypeStepTypeConverter.cs
  2. 30
      src/X1.Domain/Entities/TestCase/StepMapping.cs
  3. 1978
      src/X1.Infrastructure/Migrations/20250822184324_UpdateCaseStepConfigMappingFieldType.Designer.cs
  4. 188
      src/X1.Infrastructure/Migrations/20250822184324_UpdateCaseStepConfigMappingFieldType.cs
  5. 109
      src/X1.Infrastructure/Migrations/AppDbContextModelSnapshot.cs
  6. 20
      src/X1.WebUI/src/constants/stepConfigs.ts
  7. 503
      src/X1.WebUI/src/pages/teststeps/TestStepDrawer.tsx
  8. 425
      src/X1.WebUI/src/pages/teststeps/TestStepForm.tsx
  9. 97
      src/X1.WebUI/src/pages/teststeps/TestStepsView.tsx
  10. 6
      src/X1.WebUI/src/services/teststepsService.ts
  11. 59
      src/modify.md

64
src/X1.Domain/Common/FormTypeStepTypeConverter.cs

@ -55,12 +55,12 @@ public static class FormTypeStepTypeConverter
{
CaseStepType.Start => new List<FormType> { FormType.NoForm },
CaseStepType.End => new List<FormType> { FormType.NoForm },
CaseStepType.Process => new List<FormType>
{
FormType.NoForm,
FormType.DeviceRegistrationForm,
FormType.NetworkConnectivityForm,
FormType.NetworkPerformanceForm,
CaseStepType.Process => new List<FormType>
{
FormType.NoForm,
FormType.DeviceRegistrationForm,
FormType.NetworkConnectivityForm,
FormType.NetworkPerformanceForm,
FormType.VoiceCallForm
},
CaseStepType.Decision => new List<FormType> { FormType.NoForm },
@ -148,7 +148,7 @@ public static class FormTypeStepTypeConverter
public static List<EnumValueObject> GetStepMappings()
{
var stepMappings = new List<EnumValueObject>();
foreach (StepMapping mapping in Enum.GetValues(typeof(StepMapping)))
{
var fieldInfo = typeof(StepMapping).GetField(mapping.ToString());
@ -156,10 +156,10 @@ public static class FormTypeStepTypeConverter
{
var displayAttribute = fieldInfo.GetCustomAttribute<DisplayAttribute>();
var descriptionAttribute = fieldInfo.GetCustomAttribute<DescriptionAttribute>();
var name = displayAttribute?.ShortName ?? mapping.ToString();
var description = descriptionAttribute?.Description ?? mapping.ToString();
stepMappings.Add(new EnumValueObject
{
Value = (int)mapping,
@ -168,7 +168,7 @@ public static class FormTypeStepTypeConverter
});
}
}
return stepMappings;
}
@ -182,6 +182,8 @@ public static class FormTypeStepTypeConverter
return mapping switch
{
StepMapping.None => FormType.NoForm,
StepMapping.StartFlow => FormType.NoForm,
StepMapping.EndFlow => FormType.NoForm,
StepMapping.EnableFlightMode => FormType.NoForm,
StepMapping.DisableFlightMode => FormType.NoForm,
StepMapping.ImsiRegistration => FormType.DeviceRegistrationForm,
@ -189,7 +191,7 @@ public static class FormTypeStepTypeConverter
StepMapping.MtCall => FormType.VoiceCallForm,
StepMapping.HangUpCall => FormType.VoiceCallForm,
StepMapping.PingTest => FormType.NetworkConnectivityForm,
StepMapping.IperfTest => FormType.NetworkPerformanceForm,
_ => FormType.NoForm
};
@ -204,29 +206,31 @@ public static class FormTypeStepTypeConverter
{
return formType switch
{
FormType.NoForm => new List<StepMapping>
{
StepMapping.None,
StepMapping.EnableFlightMode,
StepMapping.DisableFlightMode
FormType.NoForm => new List<StepMapping>
{
StepMapping.None,
StepMapping.StartFlow,
StepMapping.EndFlow,
StepMapping.EnableFlightMode,
StepMapping.DisableFlightMode
},
FormType.DeviceRegistrationForm => new List<StepMapping>
{
StepMapping.ImsiRegistration
FormType.DeviceRegistrationForm => new List<StepMapping>
{
StepMapping.ImsiRegistration
},
FormType.VoiceCallForm => new List<StepMapping>
{
StepMapping.MoCall,
StepMapping.MtCall,
StepMapping.HangUpCall
FormType.VoiceCallForm => new List<StepMapping>
{
StepMapping.MoCall,
StepMapping.MtCall,
StepMapping.HangUpCall
},
FormType.NetworkConnectivityForm => new List<StepMapping>
{
StepMapping.PingTest
FormType.NetworkConnectivityForm => new List<StepMapping>
{
StepMapping.PingTest
},
FormType.NetworkPerformanceForm => new List<StepMapping>
{
StepMapping.IperfTest
FormType.NetworkPerformanceForm => new List<StepMapping>
{
StepMapping.IperfTest
},
_ => new List<StepMapping> { StepMapping.None }
};

30
src/X1.Domain/Entities/TestCase/StepMapping.cs

@ -15,59 +15,73 @@ public enum StepMapping
[Description("不包含任何具体操作的步骤")]
None = 0,
/// <summary>
/// 启动流程
/// </summary>
[Display(Name = "启动流程", ShortName = "StartFlowController")]
[Description("启动测试流程,初始化测试环境和参数")]
StartFlow = 1,
/// <summary>
/// 结束流程
/// </summary>
[Display(Name = "结束流程", ShortName = "EndFlowController")]
[Description("结束测试流程,清理资源并生成测试报告")]
EndFlow = 2,
/// <summary>
/// 开启飞行模式
/// </summary>
[Display(Name = "开启飞行模式", ShortName = "EnableFlightModeController")]
[Description("开启设备的飞行模式,禁用所有无线通信功能")]
EnableFlightMode = 1,
EnableFlightMode = 3,
/// <summary>
/// 关闭飞行模式
/// </summary>
[Display(Name = "关闭飞行模式", ShortName = "DisableFlightModeController")]
[Description("关闭设备的飞行模式,恢复无线通信功能")]
DisableFlightMode = 2,
DisableFlightMode = 4,
/// <summary>
/// IMSI注册
/// </summary>
[Display(Name = "IMSI注册", ShortName = "ImsiRegistrationController")]
[Description("使用IMSI进行网络注册,建立与移动网络的连接")]
ImsiRegistration = 3,
ImsiRegistration = 5,
/// <summary>
/// 主叫通话 (MoCall)
/// </summary>
[Display(Name = "主叫通话", ShortName = "MoCallController")]
[Description("发起主叫通话,作为主叫方拨打电话")]
MoCall = 4,
MoCall = 6,
/// <summary>
/// 被叫通话 (MTCall)
/// </summary>
[Display(Name = "被叫通话", ShortName = "MtCallController")]
[Description("接收被叫通话,作为被叫方接听电话")]
MtCall = 5,
MtCall = 7,
/// <summary>
/// 挂断电话
/// </summary>
[Display(Name = "挂断电话", ShortName = "HangUpCallController")]
[Description("结束当前通话,挂断电话连接")]
HangUpCall = 6,
HangUpCall = 8,
/// <summary>
/// Ping测试
/// </summary>
[Display(Name = "Ping测试", ShortName = "PingTestController")]
[Description("执行网络连通性测试,检测网络连接状态")]
PingTest = 7,
PingTest = 9,
/// <summary>
/// Iperf测试
/// </summary>
[Display(Name = "Iperf测试", ShortName = "IperfTestController")]
[Description("执行网络性能测试,测量网络带宽和延迟")]
IperfTest = 8
IperfTest = 10
}

1978
src/X1.Infrastructure/Migrations/20250822184324_UpdateCaseStepConfigMappingFieldType.Designer.cs

File diff suppressed because it is too large

188
src/X1.Infrastructure/Migrations/20250822184324_UpdateCaseStepConfigMappingFieldType.cs

@ -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");
}
}
}

109
src/X1.Infrastructure/Migrations/AppDbContextModelSnapshot.cs

@ -1443,10 +1443,8 @@ namespace X1.Infrastructure.Migrations
.HasColumnType("boolean")
.HasColumnName("isenabled");
b.Property<string>("Mapping")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)")
b.Property<int>("Mapping")
.HasColumnType("integer")
.HasColumnName("mapping");
b.Property<string>("StepName")
@ -1470,18 +1468,90 @@ namespace X1.Infrastructure.Migrations
b.HasKey("Id");
b.HasIndex("IsEnabled")
.HasDatabaseName("ix_tb_casestepconfig_isenabled");
b.HasIndex("CreatedAt");
b.HasIndex("FormType");
b.HasIndex("StepName")
.HasDatabaseName("ix_tb_casestepconfig_stepname");
b.HasIndex("IsEnabled");
b.HasIndex("StepType")
.HasDatabaseName("ix_tb_casestepconfig_steptype");
b.HasIndex("Mapping");
b.HasIndex("StepType");
b.ToTable("tb_casestepconfig", (string)null);
});
modelBuilder.Entity("X1.Domain.Entities.TestCase.ImsiRegistrationRecord", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<bool>("IsDualSim")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<string>("NodeId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Sim1CellId")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("Sim1Plmn")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<int?>("Sim1RegistrationWaitTime")
.HasColumnType("integer");
b.Property<string>("Sim2CellId")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("Sim2Plmn")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<int?>("Sim2RegistrationWaitTime")
.HasColumnType("integer");
b.Property<string>("TestCaseId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("CreatedAt")
.HasDatabaseName("IX_tb_imsi_registration_records_createdat");
b.HasIndex("NodeId")
.HasDatabaseName("IX_tb_imsi_registration_records_nodeid");
b.HasIndex("TestCaseId")
.HasDatabaseName("IX_tb_imsi_registration_records_testcaseid");
b.HasIndex("TestCaseId", "NodeId")
.IsUnique()
.HasDatabaseName("IX_tb_imsi_registration_records_testcaseid_nodeid");
b.ToTable("tb_imsi_registration_records", null, t =>
{
t.HasComment("IMSI注册记录表");
});
});
modelBuilder.Entity("X1.Domain.Entities.TestCase.TestCaseEdge", b =>
{
b.Property<string>("Id")
@ -1808,6 +1878,25 @@ namespace X1.Infrastructure.Migrations
b.Navigation("Role");
});
modelBuilder.Entity("X1.Domain.Entities.TestCase.ImsiRegistrationRecord", b =>
{
b.HasOne("X1.Domain.Entities.TestCase.TestCaseNode", "TestCaseNode")
.WithMany()
.HasForeignKey("NodeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("X1.Domain.Entities.TestCase.TestCaseFlow", "TestCase")
.WithMany()
.HasForeignKey("TestCaseId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("TestCase");
b.Navigation("TestCaseNode");
});
modelBuilder.Entity("X1.Domain.Entities.TestCase.TestCaseEdge", b =>
{
b.HasOne("X1.Domain.Entities.TestCase.TestCaseFlow", "TestCase")

20
src/X1.WebUI/src/constants/stepConfigs.ts

@ -68,7 +68,7 @@ export const PROCESSING_STEPS = [
icon: 'wifi',
label: '开启飞行',
stepName: 'EnableFlightMode',
mapping: 'EnableFlightModeController',
mapping: 3, // StepMapping.EnableFlightMode
formType: 0, // FormType.None
color: 'bg-green-50 dark:bg-green-950/30 border-green-200 dark:border-green-600/50 text-green-700 dark:text-green-300',
selectedColor: 'bg-green-100 dark:bg-green-900/50 border-green-300 dark:border-green-500 text-green-800 dark:text-green-200'
@ -77,7 +77,7 @@ export const PROCESSING_STEPS = [
icon: 'wifi-off',
label: '关闭飞行',
stepName: 'DisableFlightMode',
mapping: 'DisableFlightModeController',
mapping: 4, // StepMapping.DisableFlightMode
formType: 0, // FormType.None
color: 'bg-red-50 dark:bg-red-950/30 border-red-200 dark:border-red-600/50 text-red-700 dark:text-red-300',
selectedColor: 'bg-red-100 dark:bg-red-900/50 border-red-300 dark:border-red-500 text-red-800 dark:text-red-200'
@ -87,17 +87,17 @@ export const PROCESSING_STEPS = [
icon: 'signal',
label: 'IMSI注册',
stepName: 'ImsiRegistration',
mapping: 'ImsiRegistrationController',
mapping: 5, // StepMapping.ImsiRegistration
formType: 1, // FormType.Registration
color: 'bg-purple-50 dark:bg-purple-950/30 border-purple-200 dark:border-purple-600/50 text-purple-700 dark:text-purple-300',
selectedColor: 'bg-purple-100 dark:bg-purple-900/50 border-purple-300 dark:border-purple-500 text-purple-800 dark:text-purple-200'
selectedColor: 'bg-purple-100 dark:bg-purple-900/50 border-purple-300 dark:border-purple-505 text-purple-800 dark:text-purple-200'
},
// 通话操作
{
icon: 'phone-call',
label: 'MoCall',
stepName: 'MoCall',
mapping: 'MoCallController',
mapping: 6, // StepMapping.MoCall
formType: 4, // FormType.Call
color: 'bg-orange-50 dark:bg-orange-950/30 border-orange-200 dark:border-orange-600/50 text-orange-700 dark:text-orange-300',
selectedColor: 'bg-orange-100 dark:bg-orange-900/50 border-orange-300 dark:border-orange-500 text-orange-800 dark:text-orange-200'
@ -106,7 +106,7 @@ export const PROCESSING_STEPS = [
icon: 'phone',
label: 'MTCall',
stepName: 'MtCall',
mapping: 'MtCallController',
mapping: 7, // StepMapping.MtCall
formType: 4, // FormType.Call
color: 'bg-indigo-50 dark:bg-indigo-950/30 border-indigo-200 dark:border-indigo-600/50 text-indigo-700 dark:text-indigo-300',
selectedColor: 'bg-indigo-100 dark:bg-indigo-900/50 border-indigo-300 dark:border-indigo-500 text-indigo-800 dark:text-indigo-200'
@ -115,7 +115,7 @@ export const PROCESSING_STEPS = [
icon: 'phone-off',
label: '挂电话',
stepName: 'HangUpCall',
mapping: 'HangUpCallController',
mapping: 8, // StepMapping.HangUpCall
formType: 4, // FormType.Call
color: 'bg-gray-50 dark:bg-gray-950/30 border-gray-200 dark:border-gray-600/50 text-gray-700 dark:text-gray-300',
selectedColor: 'bg-gray-100 dark:bg-gray-900/50 border-gray-300 dark:border-gray-500 text-gray-800 dark:text-gray-200'
@ -125,7 +125,7 @@ export const PROCESSING_STEPS = [
icon: 'network',
label: 'Ping',
stepName: 'PingTest',
mapping: 'PingTestController',
mapping: 9, // StepMapping.PingTest
formType: 2, // FormType.Ping
color: 'bg-teal-50 dark:bg-teal-950/30 border-teal-200 dark:border-teal-600/50 text-teal-700 dark:text-teal-300',
selectedColor: 'bg-teal-100 dark:bg-teal-900/50 border-teal-300 dark:border-teal-500 text-teal-800 dark:text-teal-200'
@ -134,7 +134,7 @@ export const PROCESSING_STEPS = [
icon: 'activity',
label: 'Iperf',
stepName: 'IperfTest',
mapping: 'IperfTestController',
mapping: 10, // StepMapping.IperfTest
formType: 3, // FormType.Iperf
color: 'bg-cyan-50 dark:bg-cyan-950/30 border-cyan-200 dark:border-cyan-600/50 text-cyan-700 dark:text-cyan-300',
selectedColor: 'bg-cyan-100 dark:bg-cyan-900/50 border-cyan-300 dark:border-cyan-500 text-cyan-800 dark:text-cyan-200'
@ -220,7 +220,7 @@ export const getProcessingStepConfig = (stepName: string) => {
return PROCESSING_STEPS.find(ps => ps.stepName === stepName);
};
export const getProcessingStepByMapping = (mapping: string) => {
export const getProcessingStepByMapping = (mapping: number | string) => {
return PROCESSING_STEPS.find(ps => ps.mapping === mapping);
};

503
src/X1.WebUI/src/pages/teststeps/TestStepDrawer.tsx

@ -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>
);
}

425
src/X1.WebUI/src/pages/teststeps/TestStepForm.tsx

@ -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>
);
}

97
src/X1.WebUI/src/pages/teststeps/TestStepsView.tsx

@ -1,10 +1,9 @@
import React, { useState, useEffect } from 'react';
import { teststepsService, TestStep, GetAllTestStepsRequest, CreateTestStepRequest, UpdateTestStepRequest, FormTypeDto, StepTypeDto, StepMappingDto } from '@/services/teststepsService';
import TestStepsTable from './TestStepsTable';
import TestStepForm from './TestStepForm';
import TestStepDrawer from './TestStepDrawer';
import { Input } from '@/components/ui/input';
import TableToolbar, { DensityType } from '@/components/ui/TableToolbar';
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/use-toast';
@ -32,9 +31,9 @@ export default function TestStepsView() {
const [isEnabled, setIsEnabled] = useState<boolean | undefined>(undefined);
const [stepType, setStepType] = useState<number | undefined>(undefined);
// 表单对话框状态
const [open, setOpen] = useState(false);
const [editOpen, setEditOpen] = useState(false);
// 表单抽屉状态
const [drawerOpen, setDrawerOpen] = useState(false);
const [editDrawerOpen, setEditDrawerOpen] = useState(false);
const [selectedStep, setSelectedStep] = useState<TestStep | null>(null);
// 提交状态
@ -99,7 +98,7 @@ export default function TestStepsView() {
const handleEdit = (step: TestStep) => {
setSelectedStep(step);
setEditOpen(true);
setEditDrawerOpen(true);
};
const handleDelete = async (step: TestStep) => {
@ -144,7 +143,7 @@ export default function TestStepsView() {
title: "创建成功",
description: `测试步骤 "${data.stepName}" 创建成功`,
});
setOpen(false);
setDrawerOpen(false);
fetchTestSteps();
} else {
const errorMessage = result.errorMessages?.join(', ') || "创建测试步骤时发生错误";
@ -177,7 +176,7 @@ export default function TestStepsView() {
title: "更新成功",
description: `测试步骤 "${selectedStep.stepName}" 更新成功`,
});
setEditOpen(false);
setEditDrawerOpen(false);
setSelectedStep(null);
fetchTestSteps();
} else {
@ -293,24 +292,12 @@ export default function TestStepsView() {
{/* 表格整体卡片区域 */}
<div className="rounded-md border bg-background p-4">
<div className="flex items-center justify-between mb-2">
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="bg-primary text-primary-foreground hover:bg-primary/90">+ </Button>
</DialogTrigger>
<DialogContent className="bg-background">
<TestStepForm
onSubmit={(data: CreateTestStepRequest | UpdateTestStepRequest) => {
handleCreate(data as CreateTestStepRequest);
}}
isSubmitting={isSubmitting}
existingSteps={testSteps.map(step => ({ stepType: step.stepType, stepName: step.stepName }))}
formTypes={formTypes}
stepTypes={stepTypes}
stepMappings={stepMappings}
loadingFormTypes={loadingFormTypes}
/>
</DialogContent>
</Dialog>
<Button
className="bg-primary text-primary-foreground hover:bg-primary/90"
onClick={() => setDrawerOpen(true)}
>
+
</Button>
<TableToolbar
onRefresh={() => fetchTestSteps()}
onDensityChange={setDensity}
@ -334,29 +321,41 @@ export default function TestStepsView() {
</div>
</div>
<Dialog open={editOpen} onOpenChange={setEditOpen}>
<DialogContent className="bg-background">
<TestStepForm
onSubmit={(data: CreateTestStepRequest | UpdateTestStepRequest) => handleUpdate(data as UpdateTestStepRequest)}
initialData={selectedStep ? {
id: selectedStep.id,
stepName: selectedStep.stepName,
stepType: selectedStep.stepType,
description: selectedStep.description,
icon: selectedStep.icon,
mapping: selectedStep.mapping,
isEnabled: selectedStep.isEnabled,
formType: selectedStep.formType
} : undefined}
isEdit={true}
isSubmitting={isSubmitting}
formTypes={formTypes}
stepTypes={stepTypes}
stepMappings={stepMappings}
loadingFormTypes={loadingFormTypes}
/>
</DialogContent>
</Dialog>
<TestStepDrawer
open={drawerOpen}
onOpenChange={setDrawerOpen}
onSubmit={(data: CreateTestStepRequest | UpdateTestStepRequest) => {
handleCreate(data as CreateTestStepRequest);
}}
isSubmitting={isSubmitting}
existingSteps={testSteps.map(step => ({ stepType: step.stepType, stepName: step.stepName }))}
formTypes={formTypes}
stepTypes={stepTypes}
stepMappings={stepMappings}
loadingFormTypes={loadingFormTypes}
/>
<TestStepDrawer
open={editDrawerOpen}
onOpenChange={setEditDrawerOpen}
onSubmit={(data: CreateTestStepRequest | UpdateTestStepRequest) => handleUpdate(data as UpdateTestStepRequest)}
initialData={selectedStep ? {
id: selectedStep.id,
stepName: selectedStep.stepName,
stepType: selectedStep.stepType,
description: selectedStep.description,
icon: selectedStep.icon,
mapping: selectedStep.mapping,
isEnabled: selectedStep.isEnabled,
formType: selectedStep.formType
} : undefined}
isEdit={true}
isSubmitting={isSubmitting}
formTypes={formTypes}
stepTypes={stepTypes}
stepMappings={stepMappings}
loadingFormTypes={loadingFormTypes}
/>
</main>
);
}

6
src/X1.WebUI/src/services/teststepsService.ts

@ -9,7 +9,7 @@ export interface TestStep {
stepType: number;
description?: string;
icon?: string;
mapping: string;
mapping: number; // 改为枚举类型的数值
isEnabled: boolean;
formType: number; // 添加表单类型字段
createdAt: string;
@ -22,7 +22,7 @@ export interface TestStep {
export interface CreateTestStepRequest {
stepName: string;
stepType: number;
mapping: string;
mapping: number; // 改为枚举类型的数值
description?: string;
icon?: string;
isEnabled?: boolean;
@ -34,7 +34,7 @@ export interface UpdateTestStepRequest {
id: string;
description?: string;
icon?: string;
mapping: string;
mapping: number; // 改为枚举类型的数值
isEnabled?: boolean;
formType?: number; // 添加表单类型字段
}

59
src/modify.md

@ -1,5 +1,46 @@
# 修改记录
## 2025-01-22 - TestStepForm 改为 Drawer 方式
### 修改内容
1. **创建 TestStepDrawer 组件**
- 新建 `X1.WebUI/src/pages/teststeps/TestStepDrawer.tsx` 文件
- 将原来的 TestStepForm 改为 Drawer 方式
- 保持所有原有功能,包括步骤类型选择、图标选择、表单类型选择、步骤映射等
- 添加 Drawer 特有的状态管理和生命周期管理
2. **更新 TestStepsView 组件**
- 修改 `X1.WebUI/src/pages/teststeps/TestStepsView.tsx` 文件
- 将 Dialog 组件替换为 Drawer 组件
- 更新状态管理:`open` → `drawerOpen`,`editOpen` → `editDrawerOpen`
- 移除 Dialog 相关的导入和组件使用
- 添加两个 TestStepDrawer 实例:一个用于创建,一个用于编辑
3. **删除旧文件**
- 删除 `X1.WebUI/src/pages/teststeps/TestStepForm.tsx` 文件
- 清理不再使用的组件引用
### 功能特点
- **Drawer 布局**:使用右侧滑出的 Drawer 组件,提供更好的用户体验
- **状态管理**:独立的创建和编辑抽屉状态管理
- **表单重置**:抽屉打开时自动重置表单数据
- **响应式设计**:支持不同屏幕尺寸的响应式布局
- **保持功能**:所有原有功能完全保留,包括复杂的步骤类型选择和验证逻辑
### 技术特性
- **组件复用**:创建和编辑使用同一个 TestStepDrawer 组件
- **状态隔离**:创建和编辑状态完全独立,避免数据冲突
- **生命周期管理**:使用 useEffect 管理抽屉打开时的数据初始化
- **用户体验**:Drawer 方式提供更流畅的交互体验
### 修改时间
2025-01-22
### 修改原因
用户要求将 TestStepForm.tsx 改为 Drawer 方式,提供更好的用户体验和界面布局。
---
## 2025-01-22 - 步骤配置架构优化和性能提升
### 修改内容
@ -31,6 +72,24 @@
- 实现表单类型变化时自动过滤相关步骤映射
- 步骤映射变化时自动设置对应的表单类型
- 添加选择状态提示和帮助信息
- 移除旧的文本输入框,统一使用下拉选择
- 为所有步骤类型提供合适的映射选项(处理步骤显示所有相关映射,非处理步骤显示空控制器)
6. **修复映射字段类型问题**
- 将 `mapping` 字段从 `string` 类型改为 `number` 类型,匹配后端 `StepMapping` 枚举
- 更新 `CreateTestStepRequest``UpdateTestStepRequest` 接口
- 更新 `TestStep` 接口的 `mapping` 字段类型
- 修复配置文件中的映射值,使用枚举数值而非字符串
- 更新相关的工具函数和组件逻辑以支持数值类型映射
7. **优化步骤映射选择界面**
- 简化映射显示,只显示控制器名称,移除描述拼接
- 为不同步骤类型提供专门的映射选项(使用正确的枚举值):
- **开始步骤**:可选择 `StartFlowController(1)``EmptyController(0)`
- **结束步骤**:可选择 `EndFlowController(2)``EmptyController(0)`
- **处理步骤**:排除通用控制器(0,1,2),只显示具体操作控制器(3-10)
- **判断步骤**:只可选择 `EmptyController(0)`
- 更新提示信息,准确描述各步骤类型的映射用途
### 性能优化
- **减少网络请求**:表单类型映射数据只在列表页加载一次

Loading…
Cancel
Save