Browse Source

feat: 实现测试场景任务执行过程管理功能

主要功能:
- 新增 TestScenarioTaskTree Feature,提供任务执行过程的树形结构查询
- 实现 TestScenarioTaskExecutionController 控制器,支持任务执行过程管理
- 新增步骤详情查询功能,支持通过 CaseDetailId 获取步骤执行日志
- 为 GetTestScenarioTasksQuery 添加 ExecutionStatus 查询条件过滤

技术实现:
- 遵循 CQRS 模式,使用 MediatR 进行查询处理
- 实现三层树形结构:任务 → 执行详情 → 用例详情
- 支持按场景、设备、执行状态等条件过滤
- 前端实现响应式布局的任务执行过程管理界面

文件变更:
- 新增:TestScenarioTaskTree Feature 相关文件
- 新增:TestScenarioTaskExecutionController 控制器
- 新增:前端任务执行过程管理页面和组件
- 修改:GetTestScenarioTasksQuery 添加执行状态过滤
- 更新:导航菜单和路由配置

优化内容:
- 任务列表显示优化,移除冗余字段
- 步骤日志显示优化,简化信息展示
- 性能优化,支持按需加载执行和用例详情
refactor/permission-config
root 3 months ago
parent
commit
36f6ec6fb2
  1. 35
      src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsQuery.cs
  2. 171
      src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsQueryHandler.cs
  3. 124
      src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsResponse.cs
  4. 33
      src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeQuery.cs
  5. 204
      src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeQueryHandler.cs
  6. 245
      src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeResponse.cs
  7. 5
      src/X1.Application/Features/TestScenarioTasks/Queries/GetTestScenarioTasks/GetTestScenarioTasksQuery.cs
  8. 1
      src/X1.Application/Features/TestScenarioTasks/Queries/GetTestScenarioTasks/GetTestScenarioTasksQueryHandler.cs
  9. 2
      src/X1.Domain/Repositories/TestTask/ITestScenarioTaskRepository.cs
  10. 31
      src/X1.Infrastructure/Repositories/TestTask/TestScenarioTaskRepository.cs
  11. 118
      src/X1.Presentation/Controllers/TestScenarioTaskExecutionController.cs
  12. 105
      src/X1.WebUI/src/components/ui/collapsible.tsx
  13. 3
      src/X1.WebUI/src/constants/api.ts
  14. 9
      src/X1.WebUI/src/constants/navigationMenuPresets.ts
  15. 18
      src/X1.WebUI/src/pages/navigation-menus/NavigationMenuForm_Examples.md
  16. 338
      src/X1.WebUI/src/pages/task-execution-process/StepDetailsView.tsx
  17. 199
      src/X1.WebUI/src/pages/task-execution-process/TaskExecutionProcessView.tsx
  18. 354
      src/X1.WebUI/src/pages/task-execution-process/TaskExecutionTable.tsx
  19. 279
      src/X1.WebUI/src/pages/task-execution-process/TaskExecutionTree.tsx
  20. 8
      src/X1.WebUI/src/routes/AppRouter.tsx
  21. 185
      src/X1.WebUI/src/services/testScenarioTaskExecutionService.ts
  22. 5
      src/modify.md
  23. 86
      src/modify_20250121_execution_status_filter.md
  24. 420
      src/modify_20250121_testscenariotaskexecution_feature.md
  25. 228
      src/modify_20250121_testscenariotasktree_feature.md

35
src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsQuery.cs

@ -0,0 +1,35 @@
using MediatR;
using X1.Application.Features.TestScenarioTaskTree.Queries.GetTestScenarioTaskStepDetails;
namespace X1.Application.Features.TestScenarioTaskTree.Queries.GetTestScenarioTaskStepDetails;
/// <summary>
/// 获取测试场景任务步骤详情查询
/// </summary>
public class GetTestScenarioTaskStepDetailsQuery : IRequest<X1.Domain.Common.OperationResult<GetTestScenarioTaskStepDetailsResponse>>
{
/// <summary>
/// 用例执行明细ID
/// </summary>
public string CaseDetailId { get; set; } = null!;
/// <summary>
/// 是否包含步骤日志
/// </summary>
public bool IncludeStepLogs { get; set; } = true;
/// <summary>
/// 日志类型过滤(可选)
/// </summary>
public string? LogTypeFilter { get; set; }
/// <summary>
/// 开始时间过滤(可选)
/// </summary>
public DateTime? StartTimeFilter { get; set; }
/// <summary>
/// 结束时间过滤(可选)
/// </summary>
public DateTime? EndTimeFilter { get; set; }
}

171
src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsQueryHandler.cs

@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using Microsoft.Extensions.Logging;
using X1.Domain.Common;
using X1.Domain.Common.Enums;
using X1.Domain.Entities.TestTask;
using X1.Domain.Repositories.TestTask;
namespace X1.Application.Features.TestScenarioTaskTree.Queries.GetTestScenarioTaskStepDetails;
/// <summary>
/// 获取测试场景任务步骤详情查询处理器
/// </summary>
public class GetTestScenarioTaskStepDetailsQueryHandler : IRequestHandler<GetTestScenarioTaskStepDetailsQuery, OperationResult<GetTestScenarioTaskStepDetailsResponse>>
{
private readonly ITestScenarioTaskExecutionCaseStepDetailRepository _stepDetailRepository;
private readonly ITestScenarioTaskExecutionStepLogRepository _stepLogRepository;
private readonly ILogger<GetTestScenarioTaskStepDetailsQueryHandler> _logger;
/// <summary>
/// 初始化处理器
/// </summary>
public GetTestScenarioTaskStepDetailsQueryHandler(
ITestScenarioTaskExecutionCaseStepDetailRepository stepDetailRepository,
ITestScenarioTaskExecutionStepLogRepository stepLogRepository,
ILogger<GetTestScenarioTaskStepDetailsQueryHandler> logger)
{
_stepDetailRepository = stepDetailRepository;
_stepLogRepository = stepLogRepository;
_logger = logger;
}
/// <summary>
/// 处理获取测试场景任务步骤详情查询
/// </summary>
public async Task<OperationResult<GetTestScenarioTaskStepDetailsResponse>> Handle(
GetTestScenarioTaskStepDetailsQuery request,
CancellationToken cancellationToken)
{
try
{
_logger.LogInformation("开始获取测试场景任务步骤详情,用例明细ID: {CaseDetailId}", request.CaseDetailId);
// 1. 根据CaseDetailId获取StepId列表
var stepDetails = await _stepDetailRepository.GetByCaseDetailIdAsync(request.CaseDetailId, cancellationToken);
var stepDetailList = stepDetails.ToList();
if (!stepDetailList.Any())
{
_logger.LogWarning("未找到用例明细ID为 {CaseDetailId} 的步骤详情", request.CaseDetailId);
return OperationResult<GetTestScenarioTaskStepDetailsResponse>.CreateSuccess(
new GetTestScenarioTaskStepDetailsResponse());
}
// 2. 构建步骤详情树
var stepDetailDtos = new List<TestScenarioTaskStepDetailDto>();
var totalLogCount = 0;
foreach (var stepDetail in stepDetailList)
{
var stepDetailDto = await BuildStepDetailDtoAsync(stepDetail, request, cancellationToken);
stepDetailDtos.Add(stepDetailDto);
totalLogCount += stepDetailDto.LogCount;
}
// 3. 构建响应
var response = new GetTestScenarioTaskStepDetailsResponse
{
StepDetails = stepDetailDtos,
TotalStepCount = stepDetailList.Count,
TotalLogCount = totalLogCount
};
_logger.LogInformation("获取测试场景任务步骤详情成功,步骤数量: {StepCount}, 日志数量: {LogCount}",
response.TotalStepCount, response.TotalLogCount);
return OperationResult<GetTestScenarioTaskStepDetailsResponse>.CreateSuccess(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取测试场景任务步骤详情时发生错误");
return OperationResult<GetTestScenarioTaskStepDetailsResponse>.CreateFailure(
new List<string> { "获取测试场景任务步骤详情失败" });
}
}
/// <summary>
/// 构建步骤详情DTO
/// </summary>
private async Task<TestScenarioTaskStepDetailDto> BuildStepDetailDtoAsync(
TestScenarioTaskExecutionCaseStepDetail stepDetail,
GetTestScenarioTaskStepDetailsQuery request,
CancellationToken cancellationToken)
{
var stepDetailDto = new TestScenarioTaskStepDetailDto
{
StepDetailId = stepDetail.Id,
CaseDetailId = stepDetail.CaseDetailId,
StepId = stepDetail.StepId,
StepName = stepDetail.StepName,
ExecutorId = stepDetail.ExecutorId,
Status = stepDetail.Status.ToString(),
StartTime = stepDetail.StartTime,
EndTime = stepDetail.EndTime,
Duration = stepDetail.Duration,
Loop = stepDetail.Loop,
StepLogs = new List<TestScenarioTaskStepLogDto>()
};
// 如果不需要包含步骤日志,直接返回
if (!request.IncludeStepLogs)
{
return stepDetailDto;
}
// 获取步骤日志
var stepLogs = await GetStepLogsAsync(stepDetail.Id, request, cancellationToken);
var stepLogList = stepLogs.ToList();
foreach (var stepLog in stepLogList)
{
var stepLogDto = new TestScenarioTaskStepLogDto
{
LogId = stepLog.Id,
StepDetailId = stepLog.StepDetailId,
LogType = stepLog.LogType.ToString(),
LogTypeDescription = stepLog.GetLogTypeDescription(),
Content = stepLog.Content,
CreatedTime = stepLog.CreatedTime
};
stepDetailDto.StepLogs.Add(stepLogDto);
}
stepDetailDto.LogCount = stepLogList.Count;
return stepDetailDto;
}
/// <summary>
/// 获取步骤日志
/// </summary>
private async Task<IEnumerable<TestScenarioTaskExecutionStepLog>> GetStepLogsAsync(
string stepDetailId,
GetTestScenarioTaskStepDetailsQuery request,
CancellationToken cancellationToken)
{
// 解析日志类型过滤
StepLogType? logTypeFilter = null;
if (!string.IsNullOrEmpty(request.LogTypeFilter))
{
if (Enum.TryParse<StepLogType>(request.LogTypeFilter, out var parsedLogType))
{
logTypeFilter = parsedLogType;
}
}
// 使用条件查询获取日志
var logs = await _stepLogRepository.GetByConditionsAsync(
stepDetailId: stepDetailId,
logType: logTypeFilter,
startTime: request.StartTimeFilter,
endTime: request.EndTimeFilter,
cancellationToken: cancellationToken);
return logs.OrderBy(x => x.CreatedTime);
}
}

124
src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsResponse.cs

@ -0,0 +1,124 @@
namespace X1.Application.Features.TestScenarioTaskTree.Queries.GetTestScenarioTaskStepDetails;
/// <summary>
/// 获取测试场景任务步骤详情响应
/// </summary>
public class GetTestScenarioTaskStepDetailsResponse
{
/// <summary>
/// 步骤详情列表
/// </summary>
public List<TestScenarioTaskStepDetailDto> StepDetails { get; set; } = new();
/// <summary>
/// 总步骤数量
/// </summary>
public int TotalStepCount { get; set; }
/// <summary>
/// 总日志数量
/// </summary>
public int TotalLogCount { get; set; }
}
/// <summary>
/// 测试场景任务步骤详情数据传输对象
/// </summary>
public class TestScenarioTaskStepDetailDto
{
/// <summary>
/// 步骤执行明细ID
/// </summary>
public string StepDetailId { get; set; } = null!;
/// <summary>
/// 关联用例执行明细ID
/// </summary>
public string CaseDetailId { get; set; } = null!;
/// <summary>
/// 对应步骤ID
/// </summary>
public string StepId { get; set; } = null!;
/// <summary>
/// 步骤名称
/// </summary>
public string? StepName { get; set; }
/// <summary>
/// 实际执行人/终端ID
/// </summary>
public string ExecutorId { get; set; } = null!;
/// <summary>
/// 执行状态
/// </summary>
public string Status { get; set; } = null!;
/// <summary>
/// 步骤开始时间
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// 步骤结束时间
/// </summary>
public DateTime? EndTime { get; set; }
/// <summary>
/// 执行耗时(秒,保留3位小数)
/// </summary>
public decimal Duration { get; set; }
/// <summary>
/// 循环次数
/// </summary>
public int Loop { get; set; }
/// <summary>
/// 步骤执行日志列表
/// </summary>
public List<TestScenarioTaskStepLogDto> StepLogs { get; set; } = new();
/// <summary>
/// 日志数量
/// </summary>
public int LogCount { get; set; }
}
/// <summary>
/// 测试场景任务步骤日志数据传输对象
/// </summary>
public class TestScenarioTaskStepLogDto
{
/// <summary>
/// 日志ID
/// </summary>
public string LogId { get; set; } = null!;
/// <summary>
/// 关联步骤执行明细ID
/// </summary>
public string StepDetailId { get; set; } = null!;
/// <summary>
/// 日志类型
/// </summary>
public string LogType { get; set; } = null!;
/// <summary>
/// 日志类型描述
/// </summary>
public string LogTypeDescription { get; set; } = null!;
/// <summary>
/// 日志内容或输出信息
/// </summary>
public string? Content { get; set; }
/// <summary>
/// 日志生成时间
/// </summary>
public DateTime CreatedTime { get; set; }
}

33
src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeQuery.cs

@ -0,0 +1,33 @@
using X1.Domain.Common;
using MediatR;
using System.ComponentModel.DataAnnotations;
namespace X1.Application.Features.TestScenarioTaskTree.Queries.GetTestScenarioTaskTree;
/// <summary>
/// 获取测试场景任务树查询
/// </summary>
public class GetTestScenarioTaskTreeQuery : IRequest<OperationResult<GetTestScenarioTaskTreeResponse>>
{
/// <summary>
/// 场景编码过滤
/// </summary>
[MaxLength(50)]
public string? ScenarioCode { get; set; }
/// <summary>
/// 设备编码过滤
/// </summary>
[MaxLength(50)]
public string? DeviceCode { get; set; }
/// <summary>
/// 是否包含执行详情
/// </summary>
public bool IncludeExecutionDetails { get; set; } = true;
/// <summary>
/// 是否包含用例详情
/// </summary>
public bool IncludeCaseDetails { get; set; } = true;
}

204
src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeQueryHandler.cs

@ -0,0 +1,204 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using Microsoft.Extensions.Logging;
using X1.Domain.Common;
using X1.Domain.Entities.TestTask;
using X1.Domain.Repositories.TestTask;
namespace X1.Application.Features.TestScenarioTaskTree.Queries.GetTestScenarioTaskTree;
/// <summary>
/// 获取测试场景任务树查询处理器
/// </summary>
public class GetTestScenarioTaskTreeQueryHandler : IRequestHandler<GetTestScenarioTaskTreeQuery, OperationResult<GetTestScenarioTaskTreeResponse>>
{
private readonly ITestScenarioTaskRepository _testScenarioTaskRepository;
private readonly ITestScenarioTaskExecutionDetailRepository _executionDetailRepository;
private readonly ITestScenarioTaskExecutionCaseDetailRepository _caseDetailRepository;
private readonly ILogger<GetTestScenarioTaskTreeQueryHandler> _logger;
/// <summary>
/// 初始化处理器
/// </summary>
public GetTestScenarioTaskTreeQueryHandler(
ITestScenarioTaskRepository testScenarioTaskRepository,
ITestScenarioTaskExecutionDetailRepository executionDetailRepository,
ITestScenarioTaskExecutionCaseDetailRepository caseDetailRepository,
ILogger<GetTestScenarioTaskTreeQueryHandler> logger)
{
_testScenarioTaskRepository = testScenarioTaskRepository;
_executionDetailRepository = executionDetailRepository;
_caseDetailRepository = caseDetailRepository;
_logger = logger;
}
/// <summary>
/// 处理获取测试场景任务树查询
/// </summary>
public async Task<OperationResult<GetTestScenarioTaskTreeResponse>> Handle(
GetTestScenarioTaskTreeQuery request,
CancellationToken cancellationToken)
{
try
{
_logger.LogInformation("开始获取测试场景任务树,场景编码: {ScenarioCode}, 设备编码: {DeviceCode}",
request.ScenarioCode, request.DeviceCode);
// 1. 获取任务列表
var tasks = await _testScenarioTaskRepository.GetByConditionsAsync(
scenarioCode: request.ScenarioCode,
deviceCode: request.DeviceCode,
cancellationToken: cancellationToken);
var taskList = tasks.ToList();
if (!taskList.Any())
{
_logger.LogWarning("未找到符合条件的测试场景任务");
return OperationResult<GetTestScenarioTaskTreeResponse>.CreateSuccess(
new GetTestScenarioTaskTreeResponse());
}
// 2. 构建任务树
var taskTrees = new List<TestScenarioTaskTreeDto>();
var totalExecutionCount = 0;
var totalCaseCount = 0;
foreach (var task in taskList)
{
var taskTree = await BuildTaskTreeAsync(task, request, cancellationToken);
taskTrees.Add(taskTree);
totalExecutionCount += taskTree.ExecutionCount;
totalCaseCount += taskTree.Executions.Sum(e => e.CaseCount);
}
// 3. 构建响应
var response = new GetTestScenarioTaskTreeResponse
{
TaskTrees = taskTrees,
TotalTaskCount = taskList.Count,
TotalExecutionCount = totalExecutionCount,
TotalCaseCount = totalCaseCount
};
_logger.LogInformation("获取测试场景任务树成功,任务数量: {TaskCount}, 执行数量: {ExecutionCount}, 用例数量: {CaseCount}",
response.TotalTaskCount, response.TotalExecutionCount, response.TotalCaseCount);
return OperationResult<GetTestScenarioTaskTreeResponse>.CreateSuccess(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取测试场景任务树时发生错误");
return OperationResult<GetTestScenarioTaskTreeResponse>.CreateFailure(
new List<string> { "获取测试场景任务树失败" });
}
}
/// <summary>
/// 构建单个任务树
/// </summary>
private async Task<TestScenarioTaskTreeDto> BuildTaskTreeAsync(
TestScenarioTask task,
GetTestScenarioTaskTreeQuery request,
CancellationToken cancellationToken)
{
var taskTree = new TestScenarioTaskTreeDto
{
TaskId = task.Id,
TaskCode = task.TaskCode,
TaskName = task.TaskName,
ScenarioCode = task.ScenarioCode,
DeviceCode = task.DeviceCode,
Description = task.Description,
Priority = task.Priority,
IsDisabled = task.IsDisabled,
Tags = task.Tags,
Lifecycle = task.Lifecycle.ToString(),
TemplateId = task.TemplateId,
CreatedAt = task.CreatedAt,
CreatedBy = task.CreatedBy,
UpdatedAt = task.UpdatedAt,
UpdatedBy = task.UpdatedBy,
Executions = new List<TaskExecutionTreeDto>()
};
// 如果不需要包含执行详情,直接返回
if (!request.IncludeExecutionDetails)
{
return taskTree;
}
// 获取执行详情
var executions = await _executionDetailRepository.GetByTaskIdAsync(task.Id, cancellationToken);
var executionList = executions.ToList();
foreach (var execution in executionList)
{
var executionTree = await BuildExecutionTreeAsync(execution, request, cancellationToken);
taskTree.Executions.Add(executionTree);
}
taskTree.ExecutionCount = executionList.Count;
return taskTree;
}
/// <summary>
/// 构建执行树
/// </summary>
private async Task<TaskExecutionTreeDto> BuildExecutionTreeAsync(
TestScenarioTaskExecutionDetail execution,
GetTestScenarioTaskTreeQuery request,
CancellationToken cancellationToken)
{
var executionTree = new TaskExecutionTreeDto
{
ExecutionId = execution.Id,
TaskId = execution.TaskId,
ScenarioCode = execution.ScenarioCode,
ExecutorId = execution.ExecutorId,
Status = execution.Status.ToString(),
StartTime = execution.StartTime,
EndTime = execution.EndTime,
Duration = execution.Duration,
Loop = execution.Loop,
Progress = execution.Progress,
RuntimeCode = execution.RuntimeCode,
CaseDetails = new List<TaskExecutionCaseTreeDto>()
};
// 如果不需要包含用例详情,直接返回
if (!request.IncludeCaseDetails)
{
return executionTree;
}
// 获取用例执行详情
var caseDetails = await _caseDetailRepository.GetByExecutionIdAsync(execution.Id, cancellationToken);
var caseDetailList = caseDetails.ToList();
foreach (var caseDetail in caseDetailList)
{
var caseTree = new TaskExecutionCaseTreeDto
{
CaseDetailId = caseDetail.Id,
ExecutionId = caseDetail.ExecutionId,
ScenarioCode = caseDetail.ScenarioCode,
TestCaseFlowId = caseDetail.TestCaseFlowId,
ExecutorId = caseDetail.ExecutorId,
Status = caseDetail.Status.ToString(),
StartTime = caseDetail.StartTime,
EndTime = caseDetail.EndTime,
Duration = caseDetail.Duration,
Loop = caseDetail.Loop
};
executionTree.CaseDetails.Add(caseTree);
}
executionTree.CaseCount = caseDetailList.Count;
return executionTree;
}
}

245
src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeResponse.cs

@ -0,0 +1,245 @@
namespace X1.Application.Features.TestScenarioTaskTree.Queries.GetTestScenarioTaskTree;
/// <summary>
/// 获取测试场景任务树响应
/// </summary>
public class GetTestScenarioTaskTreeResponse
{
/// <summary>
/// 任务树列表
/// </summary>
public List<TestScenarioTaskTreeDto> TaskTrees { get; set; } = new();
/// <summary>
/// 总任务数量
/// </summary>
public int TotalTaskCount { get; set; }
/// <summary>
/// 总执行数量
/// </summary>
public int TotalExecutionCount { get; set; }
/// <summary>
/// 总用例数量
/// </summary>
public int TotalCaseCount { get; set; }
}
/// <summary>
/// 测试场景任务树数据传输对象
/// </summary>
public class TestScenarioTaskTreeDto
{
/// <summary>
/// 任务ID
/// </summary>
public string TaskId { get; set; } = null!;
/// <summary>
/// 任务编码
/// </summary>
public string TaskCode { get; set; } = null!;
/// <summary>
/// 任务名称
/// </summary>
public string TaskName { get; set; } = null!;
/// <summary>
/// 绑定场景编码
/// </summary>
public string ScenarioCode { get; set; } = null!;
/// <summary>
/// 蜂窝设备编码
/// </summary>
public string DeviceCode { get; set; } = null!;
/// <summary>
/// 任务描述或备注
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 任务优先级
/// </summary>
public int Priority { get; set; }
/// <summary>
/// 是否禁用
/// </summary>
public bool IsDisabled { get; set; }
/// <summary>
/// 分类标签
/// </summary>
public string? Tags { get; set; }
/// <summary>
/// 任务生命周期
/// </summary>
public string Lifecycle { get; set; } = null!;
/// <summary>
/// 引用的任务模板ID
/// </summary>
public string? TemplateId { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 创建人
/// </summary>
public string CreatedBy { get; set; } = null!;
/// <summary>
/// 更新时间
/// </summary>
public DateTime? UpdatedAt { get; set; }
/// <summary>
/// 更新人
/// </summary>
public string? UpdatedBy { get; set; }
/// <summary>
/// 执行详情列表
/// </summary>
public List<TaskExecutionTreeDto> Executions { get; set; } = new();
/// <summary>
/// 执行数量
/// </summary>
public int ExecutionCount { get; set; }
}
/// <summary>
/// 任务执行树数据传输对象
/// </summary>
public class TaskExecutionTreeDto
{
/// <summary>
/// 执行ID
/// </summary>
public string ExecutionId { get; set; } = null!;
/// <summary>
/// 对应测试场景任务ID
/// </summary>
public string TaskId { get; set; } = null!;
/// <summary>
/// 绑定场景编码
/// </summary>
public string ScenarioCode { get; set; } = null!;
/// <summary>
/// 实际执行人/执行终端ID
/// </summary>
public string ExecutorId { get; set; } = null!;
/// <summary>
/// 执行状态
/// </summary>
public string Status { get; set; } = null!;
/// <summary>
/// 执行开始时间
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// 执行结束时间
/// </summary>
public DateTime? EndTime { get; set; }
/// <summary>
/// 执行时长(秒,保留3位小数)
/// </summary>
public decimal Duration { get; set; }
/// <summary>
/// 执行轮次/循环次数
/// </summary>
public int Loop { get; set; }
/// <summary>
/// 执行进度百分比 (0-100)
/// </summary>
public int Progress { get; set; }
/// <summary>
/// 蜂窝设备运行时编码
/// </summary>
public string? RuntimeCode { get; set; }
/// <summary>
/// 用例执行详情列表
/// </summary>
public List<TaskExecutionCaseTreeDto> CaseDetails { get; set; } = new();
/// <summary>
/// 用例数量
/// </summary>
public int CaseCount { get; set; }
}
/// <summary>
/// 任务执行用例树数据传输对象
/// </summary>
public class TaskExecutionCaseTreeDto
{
/// <summary>
/// 用例执行ID
/// </summary>
public string CaseDetailId { get; set; } = null!;
/// <summary>
/// 对应任务执行记录ID
/// </summary>
public string ExecutionId { get; set; } = null!;
/// <summary>
/// 绑定场景编码
/// </summary>
public string ScenarioCode { get; set; } = null!;
/// <summary>
/// 测试用例流程ID
/// </summary>
public string TestCaseFlowId { get; set; } = null!;
/// <summary>
/// 实际执行人/终端ID
/// </summary>
public string ExecutorId { get; set; } = null!;
/// <summary>
/// 执行状态
/// </summary>
public string Status { get; set; } = null!;
/// <summary>
/// 开始时间
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// 结束时间
/// </summary>
public DateTime? EndTime { get; set; }
/// <summary>
/// 执行耗时(秒,保留3位小数)
/// </summary>
public decimal Duration { get; set; }
/// <summary>
/// 执行轮次
/// </summary>
public int Loop { get; set; }
}

5
src/X1.Application/Features/TestScenarioTasks/Queries/GetTestScenarioTasks/GetTestScenarioTasksQuery.cs

@ -54,4 +54,9 @@ public class GetTestScenarioTasksQuery : IRequest<OperationResult<GetTestScenari
/// 是否禁用过滤
/// </summary>
public bool? IsDisabled { get; set; }
/// <summary>
/// 执行状态过滤
/// </summary>
public TaskExecutionStatus? ExecutionStatus { get; set; }
}

1
src/X1.Application/Features/TestScenarioTasks/Queries/GetTestScenarioTasks/GetTestScenarioTasksQueryHandler.cs

@ -42,6 +42,7 @@ public class GetTestScenarioTasksQueryHandler : IRequestHandler<GetTestScenarioT
lifecycle: request.Lifecycle,
priority: request.Priority,
isDisabled: request.IsDisabled,
executionStatus: request.ExecutionStatus,
searchTerm: request.SearchTerm,
pageNumber: request.PageNumber,
pageSize: request.PageSize,

2
src/X1.Domain/Repositories/TestTask/ITestScenarioTaskRepository.cs

@ -114,6 +114,7 @@ namespace X1.Domain.Repositories.TestTask
/// <param name="lifecycle">生命周期状态过滤</param>
/// <param name="priority">优先级过滤</param>
/// <param name="isDisabled">禁用状态过滤</param>
/// <param name="executionStatus">执行状态过滤</param>
/// <param name="searchTerm">搜索关键词</param>
/// <param name="pageNumber">页码(从1开始,为null时不分页)</param>
/// <param name="pageSize">每页数量(为null时不分页)</param>
@ -125,6 +126,7 @@ namespace X1.Domain.Repositories.TestTask
TaskLifecycle? lifecycle = null,
int? priority = null,
bool? isDisabled = null,
TaskExecutionStatus? executionStatus = null,
string? searchTerm = null,
int? pageNumber = null,
int? pageSize = null,

31
src/X1.Infrastructure/Repositories/TestTask/TestScenarioTaskRepository.cs

@ -153,6 +153,7 @@ namespace X1.Infrastructure.Repositories.TestTask
TaskLifecycle? lifecycle = null,
int? priority = null,
bool? isDisabled = null,
TaskExecutionStatus? executionStatus = null,
string? searchTerm = null,
int? pageNumber = null,
int? pageSize = null,
@ -254,6 +255,14 @@ namespace X1.Infrastructure.Repositories.TestTask
paramIndex++;
}
// 按执行状态过滤
if (executionStatus.HasValue)
{
baseSql += $" AND ted.\"Status\" = {{{paramIndex}}}";
parameters.Add(executionStatus.Value.ToString());
paramIndex++;
}
// 按搜索关键词过滤
if (!string.IsNullOrWhiteSpace(searchTerm))
{
@ -268,8 +277,18 @@ namespace X1.Infrastructure.Repositories.TestTask
// 如果启用了分页,先获取总数量
if (pageNumber.HasValue && pageSize.HasValue)
{
// 构建简化的 COUNT 查询,只统计主表记录数量
var countSql = @"
// 构建 COUNT 查询,如果需要按执行状态过滤则需要 JOIN 执行状态表
var countSql = executionStatus.HasValue ? @"
SELECT COUNT(DISTINCT tst.""Id"")
FROM ""tb_testscenario_task"" tst
LEFT JOIN (
SELECT DISTINCT ON (""TaskId"")
""TaskId"",
""Status""
FROM ""tb_testscenario_task_execution_detail""
ORDER BY ""TaskId"", ""StartTime"" DESC NULLS LAST
) ted ON tst.""Id"" = ted.""TaskId""
WHERE 1=1" : @"
SELECT COUNT(*)
FROM ""tb_testscenario_task"" tst
WHERE 1=1";
@ -317,6 +336,14 @@ namespace X1.Infrastructure.Repositories.TestTask
countParamIndex++;
}
// 按执行状态过滤
if (executionStatus.HasValue)
{
countSql += $" AND ted.\"Status\" = {{{countParamIndex}}}";
countParameters.Add(executionStatus.Value.ToString());
countParamIndex++;
}
// 按搜索关键词过滤
if (!string.IsNullOrWhiteSpace(searchTerm))
{

118
src/X1.Presentation/Controllers/TestScenarioTaskExecutionController.cs

@ -0,0 +1,118 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using X1.Application.Features.TestScenarioTaskTree.Queries.GetTestScenarioTaskTree;
using X1.Application.Features.TestScenarioTaskTree.Queries.GetTestScenarioTaskStepDetails;
using X1.Presentation.Abstractions;
using X1.Domain.Common;
using MediatR;
using Microsoft.Extensions.Logging;
namespace X1.Presentation.Controllers;
/// <summary>
/// 测试场景任务执行过程控制器
/// </summary>
[ApiController]
[Route("api/testscenariotaskexecution")]
[Authorize]
public class TestScenarioTaskExecutionController : ApiController
{
private readonly ILogger<TestScenarioTaskExecutionController> _logger;
/// <summary>
/// 初始化测试场景任务执行过程控制器
/// </summary>
public TestScenarioTaskExecutionController(IMediator mediator, ILogger<TestScenarioTaskExecutionController> logger)
: base(mediator)
{
_logger = logger;
}
/// <summary>
/// 获取测试场景任务执行过程
/// </summary>
/// <param name="scenarioCode">场景编码过滤</param>
/// <param name="deviceCode">设备编码过滤</param>
/// <param name="includeExecutionDetails">是否包含执行详情</param>
/// <param name="includeCaseDetails">是否包含用例详情</param>
/// <returns>测试场景任务执行过程</returns>
[HttpGet]
public async Task<OperationResult<GetTestScenarioTaskTreeResponse>> GetTestScenarioTaskExecution(
[FromQuery] string? scenarioCode = null,
[FromQuery] string? deviceCode = null,
[FromQuery] bool includeExecutionDetails = true,
[FromQuery] bool includeCaseDetails = true)
{
_logger.LogInformation("获取测试场景任务执行过程,场景编码: {ScenarioCode}, 设备编码: {DeviceCode}, 包含执行详情: {IncludeExecutionDetails}, 包含用例详情: {IncludeCaseDetails}",
scenarioCode, deviceCode, includeExecutionDetails, includeCaseDetails);
var query = new GetTestScenarioTaskTreeQuery
{
ScenarioCode = scenarioCode,
DeviceCode = deviceCode,
IncludeExecutionDetails = includeExecutionDetails,
IncludeCaseDetails = includeCaseDetails
};
var result = await mediator.Send(query);
if (!result.IsSuccess)
{
_logger.LogWarning("获取测试场景任务执行过程失败: {ErrorMessages}",
string.Join(", ", result.ErrorMessages ?? new List<string>()));
return result;
}
_logger.LogInformation("成功获取测试场景任务执行过程,任务数量: {TaskCount}, 执行数量: {ExecutionCount}, 用例数量: {CaseCount}",
result.Data?.TotalTaskCount, result.Data?.TotalExecutionCount, result.Data?.TotalCaseCount);
return result;
}
/// <summary>
/// 获取测试场景任务步骤详情
/// </summary>
/// <param name="caseDetailId">用例执行明细ID</param>
/// <param name="includeStepLogs">是否包含步骤日志</param>
/// <param name="logTypeFilter">日志类型过滤</param>
/// <param name="startTimeFilter">开始时间过滤</param>
/// <param name="endTimeFilter">结束时间过滤</param>
/// <returns>测试场景任务步骤详情</returns>
[HttpGet("step-details")]
public async Task<OperationResult<GetTestScenarioTaskStepDetailsResponse>> GetTestScenarioTaskStepDetails(
[FromQuery] string caseDetailId,
[FromQuery] bool includeStepLogs = true,
[FromQuery] string? logTypeFilter = null,
[FromQuery] DateTime? startTimeFilter = null,
[FromQuery] DateTime? endTimeFilter = null)
{
_logger.LogInformation("获取测试场景任务步骤详情,用例明细ID: {CaseDetailId}, 包含步骤日志: {IncludeStepLogs}, 日志类型过滤: {LogTypeFilter}",
caseDetailId, includeStepLogs, logTypeFilter);
if (string.IsNullOrEmpty(caseDetailId))
{
_logger.LogWarning("用例明细ID不能为空");
return OperationResult<GetTestScenarioTaskStepDetailsResponse>.CreateFailure(
new List<string> { "用例明细ID不能为空" });
}
var query = new GetTestScenarioTaskStepDetailsQuery
{
CaseDetailId = caseDetailId,
IncludeStepLogs = includeStepLogs,
LogTypeFilter = logTypeFilter,
StartTimeFilter = startTimeFilter,
EndTimeFilter = endTimeFilter
};
var result = await mediator.Send(query);
if (!result.IsSuccess)
{
_logger.LogWarning("获取测试场景任务步骤详情失败: {ErrorMessages}",
string.Join(", ", result.ErrorMessages ?? new List<string>()));
return result;
}
_logger.LogInformation("成功获取测试场景任务步骤详情,步骤数量: {StepCount}, 日志数量: {LogCount}",
result.Data?.TotalStepCount, result.Data?.TotalLogCount);
return result;
}
}

105
src/X1.WebUI/src/components/ui/collapsible.tsx

@ -0,0 +1,105 @@
import React, { createContext, useContext, useState } from 'react';
import { cn } from '@/lib/utils';
interface CollapsibleContextValue {
open: boolean;
onOpenChange: (open: boolean) => void;
}
const CollapsibleContext = createContext<CollapsibleContextValue | undefined>(undefined);
interface CollapsibleProps {
open?: boolean;
onOpenChange?: (open: boolean) => void;
children: React.ReactNode;
className?: string;
}
export const Collapsible: React.FC<CollapsibleProps> = ({
open,
onOpenChange,
children,
className
}) => {
const [internalOpen, setInternalOpen] = useState(false);
const isOpen = open !== undefined ? open : internalOpen;
const handleOpenChange = onOpenChange || setInternalOpen;
return (
<CollapsibleContext.Provider value={{ open: isOpen, onOpenChange: handleOpenChange }}>
<div className={cn(className)}>
{children}
</div>
</CollapsibleContext.Provider>
);
};
interface CollapsibleTriggerProps {
asChild?: boolean;
children: React.ReactNode;
className?: string;
}
export const CollapsibleTrigger: React.FC<CollapsibleTriggerProps> = ({
asChild,
children,
className
}) => {
const context = useContext(CollapsibleContext);
if (!context) {
throw new Error('CollapsibleTrigger must be used within a Collapsible');
}
const { open, onOpenChange } = context;
const handleClick = () => {
onOpenChange(!open);
};
if (asChild && React.isValidElement(children)) {
return React.cloneElement(children, {
onClick: handleClick,
className: cn(children.props.className, className),
});
}
return (
<button
type="button"
onClick={handleClick}
className={cn(className)}
>
{children}
</button>
);
};
interface CollapsibleContentProps {
children: React.ReactNode;
className?: string;
}
export const CollapsibleContent: React.FC<CollapsibleContentProps> = ({
children,
className
}) => {
const context = useContext(CollapsibleContext);
if (!context) {
throw new Error('CollapsibleContent must be used within a Collapsible');
}
const { open } = context;
if (!open) {
return null;
}
return (
<div className={cn(className)}>
{children}
</div>
);
};

3
src/X1.WebUI/src/constants/api.ts

@ -62,6 +62,9 @@ export const API_PATHS = {
// 测试场景任务相关
TEST_SCENARIO_TASKS: '/testscenariotasks',
// 测试场景任务执行过程相关
TEST_SCENARIO_TASK_EXECUTION: '/testscenariotaskexecution',
// 用例步骤配置相关
CASE_STEP_CONFIGS: '/casestepconfigs',

9
src/X1.WebUI/src/constants/navigationMenuPresets.ts

@ -198,6 +198,15 @@ export const subMenuPresets: NavigationMenuPreset[] = [
description: '执行和管理测试任务',
parentTitle: '任务管理'
},
{
title: '查看任务',
path: '/dashboard/tasks/execution-process',
icon: 'ClipboardList',
permissionCode: 'testscenariotaskexecution.view',
sortOrder: 6,
description: '查看任务执行过程和结果',
parentTitle: '任务管理'
},
// 结果分析下的子菜单
{

18
src/X1.WebUI/src/pages/navigation-menus/NavigationMenuForm_Examples.md

@ -355,7 +355,7 @@
父级菜单: [选择"任务管理"菜单的ID]
图标: ClipboardList
权限代码: taskexecutions.view
排序: 4
排序: 5
启用状态: ✅
系统菜单: ✅
描述: 执行和管理测试任务
@ -363,6 +363,21 @@
自动识别结果: SubMenuItem(子菜单项)
```
**查看任务**
```
菜单标题: 查看任务
菜单路径: /dashboard/tasks/execution-process
父级菜单: [选择"任务管理"菜单的ID]
图标: ClipboardList
权限代码: testscenariotaskexecution.view
排序: 6
启用状态: ✅
系统菜单: ✅
描述: 查看任务执行过程和结果
自动识别结果: SubMenuItem(子菜单项)
```
#### 3.4 结果分析下的子菜单
**功能分析**
@ -728,6 +743,7 @@
- `taskexecution`: 任务执行明细
- `taskreviews`: 任务审核
- `taskexecutions`: 任务执行
- `testscenariotaskexecution`: 测试场景任务执行过程
- `functionalanalysis`: 功能分析
- `performanceanalysis`: 性能分析
- `issueanalysis`: 问题分析

338
src/X1.WebUI/src/pages/task-execution-process/StepDetailsView.tsx

@ -0,0 +1,338 @@
import React, { useEffect, useState } from 'react';
import {
testScenarioTaskExecutionService,
TestScenarioTaskStepDetailDto,
TestScenarioTaskStepLogDto,
GetTestScenarioTaskStepDetailsRequest,
StepLogType
} from '@/services/testScenarioTaskExecutionService';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { useToast } from '@/components/ui/use-toast';
import {
Play,
CheckCircle,
XCircle,
Clock,
Search,
RefreshCw,
FileText,
AlertTriangle,
Info,
Bug,
Eye,
Settings
} from 'lucide-react';
// 状态颜色映射
const getStatusColor = (status: string) => {
switch (status) {
case 'Success':
return 'bg-green-100 text-green-800 border-green-200';
case 'Failed':
return 'bg-red-100 text-red-800 border-red-200';
case 'Running':
return 'bg-blue-100 text-blue-800 border-blue-200';
case 'Pending':
return 'bg-yellow-100 text-yellow-800 border-yellow-200';
default:
return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
// 状态图标映射
const getStatusIcon = (status: string) => {
switch (status) {
case 'Success':
return <CheckCircle className="w-4 h-4" />;
case 'Failed':
return <XCircle className="w-4 h-4" />;
case 'Running':
return <Play className="w-4 h-4" />;
case 'Pending':
return <Clock className="w-4 h-4" />;
default:
return <Clock className="w-4 h-4" />;
}
};
// 日志类型颜色映射
const getLogTypeColor = (logType: StepLogType) => {
switch (logType) {
case 'Error':
return 'bg-red-100 text-red-800 border-red-200';
case 'Success':
return 'bg-green-100 text-green-800 border-green-200';
case 'Warn':
return 'bg-yellow-100 text-yellow-800 border-yellow-200';
case 'Info':
return 'bg-blue-100 text-blue-800 border-blue-200';
case 'Debug':
return 'bg-purple-100 text-purple-800 border-purple-200';
case 'Output':
return 'bg-gray-100 text-gray-800 border-gray-200';
case 'Parameter':
return 'bg-indigo-100 text-indigo-800 border-indigo-200';
default:
return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
// 日志类型图标映射
const getLogTypeIcon = (logType: StepLogType) => {
switch (logType) {
case 'Error':
return <XCircle className="w-4 h-4" />;
case 'Success':
return <CheckCircle className="w-4 h-4" />;
case 'Warn':
return <AlertTriangle className="w-4 h-4" />;
case 'Info':
return <Info className="w-4 h-4" />;
case 'Debug':
return <Bug className="w-4 h-4" />;
case 'Output':
return <Eye className="w-4 h-4" />;
case 'Parameter':
return <Settings className="w-4 h-4" />;
default:
return <FileText className="w-4 h-4" />;
}
};
// 格式化时间
const formatTime = (time?: string) => {
if (!time) return '-';
return new Date(time).toLocaleString('zh-CN');
};
// 格式化时长
const formatDuration = (duration: number) => {
if (duration < 60) {
return `${duration.toFixed(2)}`;
} else if (duration < 3600) {
return `${(duration / 60).toFixed(2)}分钟`;
} else {
return `${(duration / 3600).toFixed(2)}小时`;
}
};
// 步骤日志组件(带步骤信息)
const StepLogItemWithStepInfo: React.FC<{
log: TestScenarioTaskStepLogDto;
stepDetail: TestScenarioTaskStepDetailDto;
}> = ({ log, stepDetail }) => {
return (
<div className="p-2 bg-muted/20 rounded border border-border">
<div className="flex items-center gap-2 mb-1">
<Badge className={getLogTypeColor(log.logType)}>
{getLogTypeIcon(log.logType)}
<span className="ml-1">{log.logTypeDescription}</span>
</Badge>
<span className="text-sm text-muted-foreground">
{formatTime(log.createdTime)}
</span>
</div>
{/* 步骤信息 */}
<div className="mb-1 p-1 bg-muted/10 rounded text-xs text-muted-foreground">
<div className="flex items-center gap-2">
<span className="font-medium">: {stepDetail.stepName || stepDetail.stepId}</span>
<Badge className={getStatusColor(stepDetail.status)}>
{getStatusIcon(stepDetail.status)}
<span className="ml-1">{stepDetail.status}</span>
</Badge>
<span>: {formatDuration(stepDetail.duration)}</span>
</div>
</div>
{/* 日志内容 */}
{log.content && (
<div className="p-1 bg-background rounded border text-sm font-mono max-h-32 overflow-y-auto">
{log.content}
</div>
)}
</div>
);
};
// 主组件接口
interface StepDetailsViewProps {
caseDetailId: string;
onClose?: () => void;
}
// 主组件
const StepDetailsView: React.FC<StepDetailsViewProps> = ({ caseDetailId, onClose }) => {
const [stepDetails, setStepDetails] = useState<TestScenarioTaskStepDetailDto[]>([]);
const [loading, setLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [logTypeFilter, setLogTypeFilter] = useState<StepLogType | 'all'>('all');
const [includeStepLogs, setIncludeStepLogs] = useState(true);
const { toast } = useToast();
const fetchStepDetails = async () => {
setLoading(true);
try {
const params: GetTestScenarioTaskStepDetailsRequest = {
caseDetailId,
includeStepLogs,
logTypeFilter: logTypeFilter === 'all' ? undefined : logTypeFilter
};
const result = await testScenarioTaskExecutionService.getTestScenarioTaskStepDetails(params);
if (result.isSuccess && result.data) {
setStepDetails(result.data.stepDetails);
toast({
title: "获取成功",
description: `获取到 ${result.data.totalStepCount} 个步骤,${result.data.totalLogCount} 条日志`,
});
} else {
const errorMessage = result.errorMessages?.join(', ') || "获取步骤详情时发生错误";
console.error('获取步骤详情失败:', errorMessage);
toast({
title: "获取失败",
description: errorMessage,
variant: "destructive",
});
}
} catch (error) {
console.error('获取步骤详情异常:', error);
toast({
title: "获取失败",
description: "网络错误,请稍后重试",
variant: "destructive",
});
} finally {
setLoading(false);
}
};
useEffect(() => {
if (caseDetailId) {
fetchStepDetails();
}
}, [caseDetailId, includeStepLogs, logTypeFilter]);
// 获取所有日志并按时间排序
const allLogs = stepDetails.flatMap(stepDetail =>
stepDetail.stepLogs.map(log => ({ log, stepDetail }))
).sort((a, b) => new Date(a.log.createdTime).getTime() - new Date(b.log.createdTime).getTime());
// 过滤日志
const filteredLogs = allLogs.filter(({ log, stepDetail }) => {
if (!searchTerm) return true;
const searchLower = searchTerm.toLowerCase();
return (
log.content?.toLowerCase().includes(searchLower) ||
log.logTypeDescription.toLowerCase().includes(searchLower) ||
stepDetail.stepName?.toLowerCase().includes(searchLower) ||
stepDetail.stepId.toLowerCase().includes(searchLower) ||
stepDetail.executorId.toLowerCase().includes(searchLower) ||
stepDetail.status.toLowerCase().includes(searchLower)
);
});
return (
<div className="h-full flex flex-col">
<div className="p-4 border-b flex-shrink-0">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold"></h3>
<p className="text-sm text-muted-foreground">
ID: {caseDetailId} | : {allLogs.length}
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={fetchStepDetails} disabled={loading}>
<RefreshCw className={`w-4 h-4 mr-1 ${loading ? 'animate-spin' : ''}`} />
</Button>
{onClose && (
<Button variant="outline" size="sm" onClick={onClose}>
</Button>
)}
</div>
</div>
{/* 搜索和过滤 */}
<div className="flex items-center gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input
placeholder="搜索日志内容..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<Select value={logTypeFilter} onValueChange={(value: StepLogType | 'all') => setLogTypeFilter(value)}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="Error"></SelectItem>
<SelectItem value="Success"></SelectItem>
<SelectItem value="Warn"></SelectItem>
<SelectItem value="Info"></SelectItem>
<SelectItem value="Debug"></SelectItem>
<SelectItem value="Output"></SelectItem>
<SelectItem value="Parameter"></SelectItem>
</SelectContent>
</Select>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={includeStepLogs}
onChange={(e) => setIncludeStepLogs(e.target.checked)}
className="rounded border-border"
/>
</label>
</div>
</div>
<div className="flex-1 overflow-y-auto p-4">
{loading ? (
<div className="flex items-center justify-center h-full">
<div className="text-muted-foreground">...</div>
</div>
) : filteredLogs.length > 0 ? (
<div className="space-y-1">
{filteredLogs.map(({ log, stepDetail }) => (
<StepLogItemWithStepInfo
key={log.logId}
log={log}
stepDetail={stepDetail}
/>
))}
</div>
) : (
<div className="flex items-center justify-center h-full">
<div className="text-muted-foreground">
{searchTerm ? '未找到匹配的日志' : '暂无日志数据'}
</div>
</div>
)}
</div>
</div>
);
};
export default StepDetailsView;

199
src/X1.WebUI/src/pages/task-execution-process/TaskExecutionProcessView.tsx

@ -0,0 +1,199 @@
import React, { useEffect, useState } from 'react';
import {
testScenarioTaskExecutionService,
TestScenarioTaskTreeDto,
TaskExecutionCaseTreeDto,
GetTestScenarioTaskExecutionRequest
} from '@/services/testScenarioTaskExecutionService';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/use-toast';
import { Card, CardContent } from '@/components/ui/card';
import TaskExecutionTree from './TaskExecutionTree';
import StepDetailsView from './StepDetailsView';
export default function TaskExecutionProcessView() {
const [taskTrees, setTaskTrees] = useState<TestScenarioTaskTreeDto[]>([]);
const [loading, setLoading] = useState(false);
const [selectedTask, setSelectedTask] = useState<TestScenarioTaskTreeDto | null>(null);
// 步骤详情相关状态
const [selectedCaseDetailId, setSelectedCaseDetailId] = useState<string>('');
// 搜索参数
const [scenarioCode, setScenarioCode] = useState('');
const [deviceCode, setDeviceCode] = useState('');
const [includeExecutionDetails, setIncludeExecutionDetails] = useState(true);
const [includeCaseDetails, setIncludeCaseDetails] = useState(true);
// Toast 提示
const { toast } = useToast();
const fetchTaskExecutionProcess = async (params: Partial<GetTestScenarioTaskExecutionRequest> = {}) => {
setLoading(true);
const queryParams: GetTestScenarioTaskExecutionRequest = {
scenarioCode: scenarioCode || undefined,
deviceCode: deviceCode || undefined,
includeExecutionDetails,
includeCaseDetails,
...params
};
try {
const result = await testScenarioTaskExecutionService.getTestScenarioTaskExecution(queryParams);
if (result.isSuccess && result.data) {
setTaskTrees(result.data.taskTrees || []);
} else {
const errorMessage = result.errorMessages?.join(', ') || "获取任务执行过程时发生错误";
console.error('获取任务执行过程失败:', errorMessage);
toast({
title: "获取失败",
description: errorMessage,
variant: "destructive",
});
}
} catch (error) {
console.error('获取任务执行过程异常:', error);
toast({
title: "获取失败",
description: "网络错误,请稍后重试",
variant: "destructive",
});
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTaskExecutionProcess();
}, []);
// 查询按钮
const handleQuery = () => {
fetchTaskExecutionProcess();
};
// 重置按钮
const handleReset = () => {
setScenarioCode('');
setDeviceCode('');
setIncludeExecutionDetails(true);
setIncludeCaseDetails(true);
fetchTaskExecutionProcess({
scenarioCode: undefined,
deviceCode: undefined,
includeExecutionDetails: true,
includeCaseDetails: true
});
};
// 处理用例点击
const handleCaseClick = (caseDetail: TaskExecutionCaseTreeDto) => {
setSelectedCaseDetailId(caseDetail.caseDetailId);
};
// 关闭步骤详情
const handleCloseStepDetails = () => {
setSelectedCaseDetailId('');
};
return (
<main className="flex-1 p-4 transition-all duration-300 ease-in-out sm:p-6">
<div className="w-full space-y-4">
{/* 搜索工具栏 */}
<Card>
<CardContent className="p-4">
<form
className="flex gap-x-8 gap-y-4 items-center flex-wrap"
onSubmit={e => {
e.preventDefault();
handleQuery();
}}
>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right" style={{ width: 80, minWidth: 80 }}>
</label>
<Input
className="flex-1 bg-background text-foreground placeholder:text-muted-foreground border border-border focus:outline-none focus:ring-0 focus:border-border transition-all"
placeholder="请输入场景编码"
value={scenarioCode}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setScenarioCode(e.target.value)}
/>
</div>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right" style={{ width: 80, minWidth: 80 }}>
</label>
<Input
className="flex-1 bg-background text-foreground placeholder:text-muted-foreground border border-border focus:outline-none focus:ring-0 focus:border-border transition-all"
placeholder="请输入设备编码"
value={deviceCode}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setDeviceCode(e.target.value)}
/>
</div>
<div className="flex flex-row items-center gap-4">
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={includeExecutionDetails}
onChange={(e) => setIncludeExecutionDetails(e.target.checked)}
className="rounded border-gray-300"
/>
</label>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={includeCaseDetails}
onChange={(e) => setIncludeCaseDetails(e.target.checked)}
className="rounded border-gray-300"
/>
</label>
</div>
<div className="flex flex-row items-center gap-2">
<Button variant="outline" onClick={handleReset}></Button>
<Button onClick={handleQuery} disabled={loading}>
{loading ? '查询中...' : '查询'}
</Button>
</div>
</form>
</CardContent>
</Card>
{/* 两列布局:任务树 + 步骤详情 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 h-[600px]">
{/* 左侧:任务树 */}
<Card className="h-full flex flex-col">
<TaskExecutionTree
taskTrees={taskTrees}
selectedTask={selectedTask}
onTaskSelect={setSelectedTask}
onCaseClick={handleCaseClick}
loading={loading}
/>
</Card>
{/* 右侧:步骤详情 */}
<Card className="h-full flex flex-col">
{selectedCaseDetailId ? (
<StepDetailsView
caseDetailId={selectedCaseDetailId}
onClose={handleCloseStepDetails}
/>
) : (
<div className="h-full flex items-center justify-center">
<div className="text-center text-muted-foreground">
<div className="text-lg mb-2"></div>
<div className="text-sm"></div>
</div>
</div>
)}
</Card>
</div>
</div>
</main>
);
}

354
src/X1.WebUI/src/pages/task-execution-process/TaskExecutionTable.tsx

@ -0,0 +1,354 @@
import React, { useState } from 'react';
import { TestScenarioTaskTreeDto, TaskExecutionTreeDto, TaskExecutionCaseTreeDto } from '@/services/testScenarioTaskExecutionService';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { ChevronDown, ChevronRight, Play, CheckCircle, XCircle, Clock, User, Calendar, Search } from 'lucide-react';
// 状态颜色映射
const getStatusColor = (status: string) => {
switch (status) {
case 'Success':
return 'bg-green-100 text-green-800 border-green-200';
case 'Failed':
return 'bg-red-100 text-red-800 border-red-200';
case 'Running':
return 'bg-blue-100 text-blue-800 border-blue-200';
case 'Pending':
return 'bg-yellow-100 text-yellow-800 border-yellow-200';
default:
return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
// 状态图标映射
const getStatusIcon = (status: string) => {
switch (status) {
case 'Success':
return <CheckCircle className="w-4 h-4" />;
case 'Failed':
return <XCircle className="w-4 h-4" />;
case 'Running':
return <Play className="w-4 h-4" />;
case 'Pending':
return <Clock className="w-4 h-4" />;
default:
return <Clock className="w-4 h-4" />;
}
};
// 格式化时间
const formatTime = (time?: string) => {
if (!time) return '-';
return new Date(time).toLocaleString('zh-CN');
};
// 格式化时长
const formatDuration = (duration: number) => {
if (duration < 60) {
return `${duration.toFixed(2)}`;
} else if (duration < 3600) {
return `${(duration / 60).toFixed(2)}分钟`;
} else {
return `${(duration / 3600).toFixed(2)}小时`;
}
};
// 表格数据类型
type TableDataType = 'executions' | 'cases';
// 执行记录行组件
const ExecutionRow: React.FC<{
execution: TaskExecutionTreeDto;
taskName: string;
taskCode: string;
}> = ({ execution, taskName, taskCode }) => {
return (
<TableRow>
<TableCell className="font-medium">{execution.executionId}</TableCell>
<TableCell>{taskName}</TableCell>
<TableCell>{taskCode}</TableCell>
<TableCell>
<Badge className={getStatusColor(execution.status)}>
{getStatusIcon(execution.status)}
<span className="ml-1">{execution.status}</span>
</Badge>
</TableCell>
<TableCell>{execution.executorId}</TableCell>
<TableCell>{execution.progress}%</TableCell>
<TableCell>{execution.loop}</TableCell>
<TableCell>{execution.caseCount}</TableCell>
<TableCell>{formatTime(execution.startTime)}</TableCell>
<TableCell>{formatTime(execution.endTime)}</TableCell>
<TableCell>{formatDuration(execution.duration)}</TableCell>
<TableCell>{execution.runtimeCode || '-'}</TableCell>
</TableRow>
);
};
// 用例记录行组件
const CaseRow: React.FC<{
caseDetail: TaskExecutionCaseTreeDto;
taskName: string;
taskCode: string;
executionId: string;
}> = ({ caseDetail, taskName, taskCode, executionId }) => {
return (
<TableRow>
<TableCell className="font-medium">{caseDetail.caseDetailId}</TableCell>
<TableCell>{taskName}</TableCell>
<TableCell>{taskCode}</TableCell>
<TableCell>{executionId}</TableCell>
<TableCell>{caseDetail.testCaseFlowId}</TableCell>
<TableCell>
<Badge className={getStatusColor(caseDetail.status)}>
{getStatusIcon(caseDetail.status)}
<span className="ml-1">{caseDetail.status}</span>
</Badge>
</TableCell>
<TableCell>{caseDetail.executorId}</TableCell>
<TableCell>{caseDetail.loop}</TableCell>
<TableCell>{formatTime(caseDetail.startTime)}</TableCell>
<TableCell>{formatTime(caseDetail.endTime)}</TableCell>
<TableCell>{formatDuration(caseDetail.duration)}</TableCell>
</TableRow>
);
};
// 表格组件主组件
interface TaskExecutionTableProps {
selectedTask: TestScenarioTaskTreeDto | null;
loading: boolean;
}
const TaskExecutionTable: React.FC<TaskExecutionTableProps> = ({
selectedTask,
loading
}) => {
const [tableType, setTableType] = useState<TableDataType>('executions');
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
// 获取执行记录数据
const getExecutionData = () => {
if (!selectedTask) return [];
let executions = selectedTask.executions;
// 状态过滤
if (statusFilter !== 'all') {
executions = executions.filter(exec => exec.status === statusFilter);
}
// 搜索过滤
if (searchTerm) {
executions = executions.filter(exec =>
exec.executionId.toLowerCase().includes(searchTerm.toLowerCase()) ||
exec.executorId.toLowerCase().includes(searchTerm.toLowerCase()) ||
(exec.runtimeCode && exec.runtimeCode.toLowerCase().includes(searchTerm.toLowerCase()))
);
}
return executions;
};
// 获取用例记录数据
const getCaseData = () => {
if (!selectedTask) return [];
let cases: Array<{ caseDetail: TaskExecutionCaseTreeDto; executionId: string }> = [];
selectedTask.executions.forEach(execution => {
execution.caseDetails.forEach(caseDetail => {
cases.push({ caseDetail, executionId: execution.executionId });
});
});
// 状态过滤
if (statusFilter !== 'all') {
cases = cases.filter(item => item.caseDetail.status === statusFilter);
}
// 搜索过滤
if (searchTerm) {
cases = cases.filter(item =>
item.caseDetail.caseDetailId.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.caseDetail.testCaseFlowId.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.caseDetail.executorId.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.executionId.toLowerCase().includes(searchTerm.toLowerCase())
);
}
return cases;
};
const executionData = getExecutionData();
const caseData = getCaseData();
return (
<div className="h-full flex flex-col">
<div className="p-4 border-b">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold"></h3>
<p className="text-sm text-gray-600">
{selectedTask ? `当前任务: ${selectedTask.taskName}` : '请选择任务查看详细信息'}
</p>
</div>
<div className="flex items-center gap-2">
<Select value={tableType} onValueChange={(value: TableDataType) => setTableType(value)}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="executions"></SelectItem>
<SelectItem value="cases"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* 搜索和过滤 */}
<div className="flex items-center gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="搜索..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="Success"></SelectItem>
<SelectItem value="Failed"></SelectItem>
<SelectItem value="Running"></SelectItem>
<SelectItem value="Pending"></SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
onClick={() => {
setSearchTerm('');
setStatusFilter('all');
}}
>
</Button>
</div>
</div>
<div className="flex-1 overflow-auto">
{loading ? (
<div className="flex items-center justify-center h-32">
<div className="text-gray-500">...</div>
</div>
) : !selectedTask ? (
<div className="flex items-center justify-center h-32">
<div className="text-gray-500"></div>
</div>
) : tableType === 'executions' ? (
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{executionData.length > 0 ? (
executionData.map((execution) => (
<ExecutionRow
key={execution.executionId}
execution={execution}
taskName={selectedTask.taskName}
taskCode={selectedTask.taskCode}
/>
))
) : (
<TableRow>
<TableCell colSpan={12} className="text-center text-gray-500">
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>ID</TableHead>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{caseData.length > 0 ? (
caseData.map((item) => (
<CaseRow
key={item.caseDetail.caseDetailId}
caseDetail={item.caseDetail}
taskName={selectedTask.taskName}
taskCode={selectedTask.taskCode}
executionId={item.executionId}
/>
))
) : (
<TableRow>
<TableCell colSpan={11} className="text-center text-gray-500">
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div>
</div>
);
};
export default TaskExecutionTable;

279
src/X1.WebUI/src/pages/task-execution-process/TaskExecutionTree.tsx

@ -0,0 +1,279 @@
import React, { useState } from 'react';
import { TestScenarioTaskTreeDto, TaskExecutionTreeDto, TaskExecutionCaseTreeDto } from '@/services/testScenarioTaskExecutionService';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { ChevronDown, ChevronRight, Play, CheckCircle, XCircle, Clock, Calendar } from 'lucide-react';
// 状态颜色映射
const getStatusColor = (status: string) => {
switch (status) {
case 'Success':
return 'bg-green-100 text-green-800 border-green-200';
case 'Failed':
return 'bg-red-100 text-red-800 border-red-200';
case 'Running':
return 'bg-blue-100 text-blue-800 border-blue-200';
case 'Pending':
return 'bg-yellow-100 text-yellow-800 border-yellow-200';
default:
return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
// 状态图标映射
const getStatusIcon = (status: string) => {
switch (status) {
case 'Success':
return <CheckCircle className="w-4 h-4" />;
case 'Failed':
return <XCircle className="w-4 h-4" />;
case 'Running':
return <Play className="w-4 h-4" />;
case 'Pending':
return <Clock className="w-4 h-4" />;
default:
return <Clock className="w-4 h-4" />;
}
};
// 格式化时间
const formatTime = (time?: string) => {
if (!time) return '-';
return new Date(time).toLocaleString('zh-CN');
};
// 格式化时长
const formatDuration = (duration: number) => {
if (duration < 60) {
return `${duration.toFixed(2)}`;
} else if (duration < 3600) {
return `${(duration / 60).toFixed(2)}分钟`;
} else {
return `${(duration / 3600).toFixed(2)}小时`;
}
};
// 用例执行详情组件
const CaseExecutionDetail: React.FC<{
caseDetail: TaskExecutionCaseTreeDto;
onCaseClick?: (caseDetail: TaskExecutionCaseTreeDto) => void;
}> = ({ caseDetail, onCaseClick }) => {
return (
<div
className="ml-4 p-2 bg-muted/20 rounded border border-border cursor-pointer hover:bg-muted/30 transition-colors"
onClick={() => onCaseClick?.(caseDetail)}
>
{/* 标题行 */}
<div className="text-sm font-medium text-foreground mb-1">
</div>
{/* 第一行:状态和基本信息 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Badge className={getStatusColor(caseDetail.status)}>
{getStatusIcon(caseDetail.status)}
<span className="ml-1">{caseDetail.status}</span>
</Badge>
<span className="text-sm text-muted-foreground">
: {caseDetail.loop} | : {formatDuration(caseDetail.duration)}
</span>
</div>
</div>
{/* 第二行:时间信息和提示 */}
<div className="mt-1 flex items-center justify-between text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
<span>: {formatTime(caseDetail.startTime)}</span>
{caseDetail.endTime && (
<>
<span className="mx-1">|</span>
<span>: {formatTime(caseDetail.endTime)}</span>
</>
)}
</div>
<span className="text-xs"></span>
</div>
</div>
);
};
// 执行详情组件
const ExecutionDetail: React.FC<{
execution: TaskExecutionTreeDto;
onCaseClick?: (caseDetail: TaskExecutionCaseTreeDto) => void;
}> = ({ execution, onCaseClick }) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger asChild>
<div className="p-2 bg-muted/30 rounded border border-border cursor-pointer hover:bg-muted/50 transition-colors">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{isOpen ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
<Badge className={getStatusColor(execution.status)}>
{getStatusIcon(execution.status)}
<span className="ml-1">{execution.status}</span>
</Badge>
</div>
<div className="text-sm text-muted-foreground">
: {execution.progress}% | : {execution.loop} | : {execution.caseCount}
</div>
</div>
<div className="mt-1 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
<span>: {formatTime(execution.startTime)}</span>
<span className="ml-4">: {formatDuration(execution.duration)}</span>
</div>
</div>
{execution.runtimeCode && (
<div className="mt-1 text-sm text-muted-foreground">
: {execution.runtimeCode}
</div>
)}
</div>
</CollapsibleTrigger>
<CollapsibleContent className="mt-1">
<div className="space-y-1">
{execution.caseDetails.map((caseDetail) => (
<CaseExecutionDetail
key={caseDetail.caseDetailId}
caseDetail={caseDetail}
onCaseClick={onCaseClick}
/>
))}
</div>
</CollapsibleContent>
</Collapsible>
);
};
// 任务树组件
const TaskTree: React.FC<{
task: TestScenarioTaskTreeDto;
isSelected: boolean;
onSelect: (task: TestScenarioTaskTreeDto) => void;
onCaseClick?: (caseDetail: TaskExecutionCaseTreeDto) => void;
}> = ({ task, isSelected, onSelect, onCaseClick }) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Card className={`mb-2 cursor-pointer transition-all ${isSelected ? 'ring-2 ring-primary bg-primary/5' : 'hover:bg-muted/50'}`}>
<CardHeader className="pb-2">
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger asChild>
<div
className="flex items-center justify-between"
onClick={() => onSelect(task)}
>
<div className="flex items-center gap-3">
{isOpen ? <ChevronDown className="w-5 h-5" /> : <ChevronRight className="w-5 h-5" />}
<CardTitle className="text-base">{task.taskName}</CardTitle>
<Badge variant="outline" className="text-xs">: {task.taskCode}</Badge>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>: {task.executionCount}</span>
<span>: {task.priority === 1 ? '高' : '普通'}</span>
</div>
</div>
</CollapsibleTrigger>
<CollapsibleContent className="mt-1">
<div className="grid grid-cols-2 gap-2 text-sm text-muted-foreground">
<div>: {task.deviceCode}</div>
<div>: {task.lifecycle}</div>
<div>: {task.createdBy}</div>
<div>: {formatTime(task.createdAt)}</div>
{task.updatedAt && <div>: {formatTime(task.updatedAt)}</div>}
</div>
{task.description && (
<div className="mt-1 text-sm text-muted-foreground">
: {task.description}
</div>
)}
{task.tags && (
<div className="mt-1">
<span className="text-sm text-muted-foreground">: </span>
<Badge variant="secondary" className="text-xs">{task.tags}</Badge>
</div>
)}
</CollapsibleContent>
</Collapsible>
</CardHeader>
<CardContent className="pt-0">
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleContent>
<div className="space-y-1">
<h4 className="font-medium text-foreground text-sm"></h4>
{task.executions.length > 0 ? (
task.executions.map((execution) => (
<ExecutionDetail
key={execution.executionId}
execution={execution}
onCaseClick={onCaseClick}
/>
))
) : (
<div className="text-sm text-muted-foreground italic"></div>
)}
</div>
</CollapsibleContent>
</Collapsible>
</CardContent>
</Card>
);
};
// 树形组件主组件
interface TaskExecutionTreeProps {
taskTrees: TestScenarioTaskTreeDto[];
selectedTask: TestScenarioTaskTreeDto | null;
onTaskSelect: (task: TestScenarioTaskTreeDto) => void;
onCaseClick?: (caseDetail: TaskExecutionCaseTreeDto) => void;
loading: boolean;
}
const TaskExecutionTree: React.FC<TaskExecutionTreeProps> = ({
taskTrees,
selectedTask,
onTaskSelect,
onCaseClick,
loading
}) => {
return (
<div className="h-full flex flex-col">
<div className="p-4 border-b flex-shrink-0">
<h3 className="text-lg font-semibold"></h3>
<p className="text-sm text-muted-foreground"></p>
</div>
<div className="flex-1 overflow-y-auto">
{loading ? (
<div className="flex items-center justify-center h-full">
<div className="text-muted-foreground">...</div>
</div>
) : taskTrees.length > 0 ? (
<div className="p-4 space-y-2">
{taskTrees.map((task) => (
<TaskTree
key={task.taskId}
task={task}
isSelected={selectedTask?.taskId === task.taskId}
onSelect={onTaskSelect}
onCaseClick={onCaseClick}
/>
))}
</div>
) : (
<div className="flex items-center justify-center h-full">
<div className="text-muted-foreground"></div>
</div>
)}
</div>
</div>
);
};
export default TaskExecutionTree;

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

@ -23,6 +23,7 @@ const TestStepsView = lazy(() => import('@/pages/teststeps/TestStepsView'));
// 任务管理页面
const TasksView = lazy(() => import('@/pages/tasks/TasksView'));
const TaskExecutionView = lazy(() => import('@/pages/taskExecution/TaskExecutionView'));
const TaskExecutionProcessView = lazy(() => import('@/pages/task-execution-process/TaskExecutionProcessView'));
// 结果分析页面
const FunctionalAnalysisView = lazy(() => import('@/pages/analysis/FunctionalAnalysisView'));
@ -184,6 +185,13 @@ export function AppRouter() {
</AnimatedContainer>
</ProtectedRoute>
} />
<Route path="execution-process" element={
<ProtectedRoute requiredPermission="testscenariotaskexecution.view">
<AnimatedContainer>
<TaskExecutionProcessView />
</AnimatedContainer>
</ProtectedRoute>
} />
</Route>
{/* 结果分析路由 */}

185
src/X1.WebUI/src/services/testScenarioTaskExecutionService.ts

@ -0,0 +1,185 @@
import { httpClient } from '@/lib/http-client';
import { OperationResult } from '@/types/auth';
import { API_PATHS } from '@/constants/api';
// ============================================================================
// 类型定义 - 与后端 TestScenarioTaskTree 相关实体保持一致
// ============================================================================
// 任务执行状态枚举 - 与后端 TaskExecutionStatus 保持一致
export type TaskExecutionStatus = 'Pending' | 'Running' | 'Success' | 'Failed';
// 步骤日志类型枚举 - 与后端 StepLogType 保持一致
export type StepLogType = 'Error' | 'Success' | 'Debug' | 'Info' | 'Warn' | 'Output' | 'Parameter' | 'Other';
// 任务执行用例树数据传输对象
export interface TaskExecutionCaseTreeDto {
caseDetailId: string;
executionId: string;
scenarioCode: string;
testCaseFlowId: string;
executorId: string;
status: TaskExecutionStatus;
startTime?: string;
endTime?: string;
duration: number;
loop: number;
}
// 任务执行树数据传输对象
export interface TaskExecutionTreeDto {
executionId: string;
taskId: string;
scenarioCode: string;
executorId: string;
status: TaskExecutionStatus;
startTime?: string;
endTime?: string;
duration: number;
loop: number;
progress: number;
runtimeCode?: string;
caseDetails: TaskExecutionCaseTreeDto[];
caseCount: number;
}
// 测试场景任务树数据传输对象
export interface TestScenarioTaskTreeDto {
taskId: string;
taskCode: string;
taskName: string;
scenarioCode: string;
deviceCode: string;
description?: string;
priority: number;
isDisabled: boolean;
tags?: string;
lifecycle: string;
templateId?: string;
createdAt: string;
createdBy: string;
updatedAt?: string;
updatedBy?: string;
executions: TaskExecutionTreeDto[];
executionCount: number;
}
// ============================================================================
// 请求/响应接口定义 - 与后端 Controller 完全对应
// ============================================================================
// 获取任务执行过程请求接口 - 对应 TestScenarioTaskExecutionController.GetTestScenarioTaskExecution 参数
export interface GetTestScenarioTaskExecutionRequest {
scenarioCode?: string;
deviceCode?: string;
includeExecutionDetails?: boolean;
includeCaseDetails?: boolean;
}
// 获取任务执行过程响应接口
export interface GetTestScenarioTaskExecutionResponse {
taskTrees: TestScenarioTaskTreeDto[];
totalTaskCount: number;
totalExecutionCount: number;
totalCaseCount: number;
}
// ============================================================================
// 步骤详情相关类型定义 - 与后端 GetTestScenarioTaskStepDetails 保持一致
// ============================================================================
// 测试场景任务步骤日志数据传输对象
export interface TestScenarioTaskStepLogDto {
logId: string;
stepDetailId: string;
logType: StepLogType;
logTypeDescription: string;
content?: string;
createdTime: string;
}
// 测试场景任务步骤详情数据传输对象
export interface TestScenarioTaskStepDetailDto {
stepDetailId: string;
caseDetailId: string;
stepId: string;
stepName?: string;
executorId: string;
status: TaskExecutionStatus;
startTime?: string;
endTime?: string;
duration: number;
loop: number;
stepLogs: TestScenarioTaskStepLogDto[];
logCount: number;
}
// 获取测试场景任务步骤详情请求接口
export interface GetTestScenarioTaskStepDetailsRequest {
caseDetailId: string;
includeStepLogs?: boolean;
logTypeFilter?: StepLogType;
startTimeFilter?: string;
endTimeFilter?: string;
}
// 获取测试场景任务步骤详情响应接口
export interface GetTestScenarioTaskStepDetailsResponse {
stepDetails: TestScenarioTaskStepDetailDto[];
totalStepCount: number;
totalLogCount: number;
}
// ============================================================================
// 测试场景任务执行过程服务类 - 完全对应 TestScenarioTaskExecutionController
// ============================================================================
class TestScenarioTaskExecutionService {
private readonly baseUrl = API_PATHS.TEST_SCENARIO_TASK_EXECUTION;
/**
*
* 对应: GET /api/testscenariotaskexecution
* 对应控制器方法: GetTestScenarioTaskExecution()
*
*/
async getTestScenarioTaskExecution(params: GetTestScenarioTaskExecutionRequest = {}): Promise<OperationResult<GetTestScenarioTaskExecutionResponse>> {
const queryParams = new URLSearchParams();
if (params.scenarioCode) queryParams.append('scenarioCode', params.scenarioCode);
if (params.deviceCode) queryParams.append('deviceCode', params.deviceCode);
if (params.includeExecutionDetails !== undefined) queryParams.append('includeExecutionDetails', params.includeExecutionDetails.toString());
if (params.includeCaseDetails !== undefined) queryParams.append('includeCaseDetails', params.includeCaseDetails.toString());
const url = queryParams.toString() ? `${this.baseUrl}?${queryParams.toString()}` : this.baseUrl;
return httpClient.get<GetTestScenarioTaskExecutionResponse>(url);
}
/**
*
* 对应: GET /api/testscenariotaskexecution/step-details
* 对应控制器方法: GetTestScenarioTaskStepDetails()
* CaseDetailId获取步骤详情和日志信息
*/
async getTestScenarioTaskStepDetails(params: GetTestScenarioTaskStepDetailsRequest): Promise<OperationResult<GetTestScenarioTaskStepDetailsResponse>> {
const queryParams = new URLSearchParams();
// 必需参数
queryParams.append('caseDetailId', params.caseDetailId);
// 可选参数
if (params.includeStepLogs !== undefined) queryParams.append('includeStepLogs', params.includeStepLogs.toString());
if (params.logTypeFilter) queryParams.append('logTypeFilter', params.logTypeFilter);
if (params.startTimeFilter) queryParams.append('startTimeFilter', params.startTimeFilter);
if (params.endTimeFilter) queryParams.append('endTimeFilter', params.endTimeFilter);
const url = `${this.baseUrl}/step-details?${queryParams.toString()}`;
return httpClient.get<GetTestScenarioTaskStepDetailsResponse>(url);
}
}
// ============================================================================
// 导出服务实例
// ============================================================================
export const testScenarioTaskExecutionService = new TestScenarioTaskExecutionService();

5
src/modify.md

@ -169841,5 +169841,8 @@ export const getRouteComponent = (routeKey: string): React.LazyExoticComponent<a
### 修改原因
根据业务需求,测试场景任务需要支持启用/禁用功能,允许管理员在不删除任务的情况下控制任务的可用性。
根据业务需求,测试场景任务需要支持启用/禁用功能,允许管理员在不删除任务的情况下控制任务的可用性。

86
src/modify_20250121_execution_status_filter.md

@ -0,0 +1,86 @@
# 2025-01-21 为 GetTestScenarioTasksQuery 添加 ExecutionStatus 查询条件
## 概述
`GetTestScenarioTasksQuery` 添加 `ExecutionStatus` 查询条件,支持按任务执行状态过滤测试场景任务列表。
## 主要变更
### 1. 更新查询模型
- **文件**: `X1.Application/Features/TestScenarioTasks/Queries/GetTestScenarioTasks/GetTestScenarioTasksQuery.cs`
- **新增属性**: `ExecutionStatus` - 执行状态过滤条件
- **类型**: `TaskExecutionStatus?` - 可空枚举类型,默认为空
- **功能**: 支持按任务执行状态(待执行、执行中、执行成功、执行失败)过滤任务列表
### 2. 更新查询处理器
- **文件**: `X1.Application/Features/TestScenarioTasks/Queries/GetTestScenarioTasks/GetTestScenarioTasksQueryHandler.cs`
- **修改**: 在调用仓储方法时传递 `executionStatus` 参数
- **功能**: 将查询条件传递给仓储层进行数据过滤
### 3. 更新仓储接口
- **文件**: `X1.Domain/Repositories/TestTask/ITestScenarioTaskRepository.cs`
- **修改**: `GetTestScenarioTasksWithExecutionStatusAsync` 方法签名
- **新增参数**: `TaskExecutionStatus? executionStatus = null`
- **功能**: 支持按执行状态过滤的仓储接口定义
### 4. 更新仓储实现
- **文件**: `X1.Infrastructure/Repositories/TestTask/TestScenarioTaskRepository.cs`
- **修改**: `GetTestScenarioTasksWithExecutionStatusAsync` 方法实现
- **SQL 查询优化**:
- 在主查询中添加执行状态过滤条件
- 在 COUNT 查询中根据是否需要执行状态过滤动态构建 JOIN 语句
- 使用 `DISTINCT` 确保 COUNT 查询的准确性
- **性能优化**: 只在需要按执行状态过滤时才 JOIN 执行状态表
## 技术实现
### 1. 查询条件添加
```csharp
/// <summary>
/// 执行状态过滤
/// </summary>
public TaskExecutionStatus? ExecutionStatus { get; set; }
```
### 2. SQL 查询优化
- **主查询**: 添加 `AND ted."Status" = {paramIndex}` 过滤条件
- **COUNT 查询**: 根据 `executionStatus` 参数动态选择是否 JOIN 执行状态表
- **参数处理**: 使用枚举的字符串表示进行 SQL 参数绑定
### 3. 性能考虑
- **条件查询**: 只在需要时才 JOIN 执行状态表,避免不必要的性能开销
- **索引优化**: 利用现有的执行状态表索引进行高效过滤
- **分页支持**: 保持原有的分页功能,确保大数据量下的查询性能
## 使用示例
```csharp
// 查询所有执行成功的任务
var query = new GetTestScenarioTasksQuery
{
ExecutionStatus = TaskExecutionStatus.Success,
PageNumber = 1,
PageSize = 10
};
// 查询所有执行失败的任务
var failedQuery = new GetTestScenarioTasksQuery
{
ExecutionStatus = TaskExecutionStatus.Failed
};
```
## 兼容性
- **向后兼容**: 新增的 `ExecutionStatus` 参数为可空类型,不影响现有调用
- **默认行为**: 当 `ExecutionStatus``null` 时,不进行执行状态过滤
- **API 兼容**: 前端 API 调用保持兼容,新参数为可选参数
## 修改的文件列表
1. `X1.Application/Features/TestScenarioTasks/Queries/GetTestScenarioTasks/GetTestScenarioTasksQuery.cs`
2. `X1.Application/Features/TestScenarioTasks/Queries/GetTestScenarioTasks/GetTestScenarioTasksQueryHandler.cs`
3. `X1.Domain/Repositories/TestTask/ITestScenarioTaskRepository.cs`
4. `X1.Infrastructure/Repositories/TestTask/TestScenarioTaskRepository.cs`
## 测试建议
1. 测试不传递 `ExecutionStatus` 参数时的默认行为
2. 测试传递不同执行状态值时的过滤效果
3. 测试与其他过滤条件组合使用时的效果
4. 测试分页功能在执行状态过滤下的正确性

420
src/modify_20250121_testscenariotaskexecution_feature.md

@ -0,0 +1,420 @@
## 2025-01-21 创建测试场景任务执行过程Feature
### 概述
为测试场景任务系统创建树形结构Feature,将三个核心表(tb_testscenario_task、tb_testscenario_task_execution_detail、tb_testscenario_task_execution_case_detail)组合生成层次化的树形结构,便于前端展示和管理。
### 主要变更
#### 1. 创建 TestScenarioTaskTree Feature
- **目录**: `X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/`
- **功能**: 提供测试场景任务执行过程查询功能
- **架构**: 遵循CQRS模式,使用MediatR进行查询处理
#### 2. 实现的功能组件
- **查询类**: `GetTestScenarioTaskTreeQuery.cs` - 定义查询参数和条件
- **响应类**: `GetTestScenarioTaskTreeResponse.cs` - 定义树形结构响应格式
- **处理器**: `GetTestScenarioTaskTreeQueryHandler.cs` - 实现树形结构构建逻辑
- **DTO类**: 包含任务树、执行树、用例树的数据传输对象
#### 3. 树形结构设计
- **第一层**: 测试场景任务(TestScenarioTaskTreeDto)
- **第二层**: 任务执行详情(TaskExecutionTreeDto)
- **第三层**: 用例执行详情(TaskExecutionCaseTreeDto)
#### 4. 查询参数支持
- **场景过滤**: `ScenarioCode` - 按场景编码过滤
- **设备过滤**: `DeviceCode` - 按设备编码过滤
- **执行详情**: `IncludeExecutionDetails` - 是否包含执行详情
- **用例详情**: `IncludeCaseDetails` - 是否包含用例详情
#### 5. 技术特点
- **性能优化**: 支持按需加载,可选择是否包含执行和用例详情
- **数据完整性**: 提供完整的统计信息(任务数量、执行数量、用例数量)
- **错误处理**: 完整的异常处理和日志记录
- **类型安全**: 强类型的DTO设计,确保数据一致性
### 技术实现
#### 1. 查询类设计
```csharp
public class GetTestScenarioTaskTreeQuery : IRequest<OperationResult<GetTestScenarioTaskTreeResponse>>
{
public string? ScenarioCode { get; set; }
public string? DeviceCode { get; set; }
public bool IncludeExecutionDetails { get; set; } = true;
public bool IncludeCaseDetails { get; set; } = true;
}
```
#### 2. 树形结构DTO
- **TestScenarioTaskTreeDto**: 任务树节点,包含任务基本信息和执行列表
- **TaskExecutionTreeDto**: 执行树节点,包含执行信息和用例列表
- **TaskExecutionCaseTreeDto**: 用例树节点,包含用例执行详情
#### 3. 处理器逻辑
- **分层构建**: 先构建任务层,再构建执行层,最后构建用例层
- **按需加载**: 根据参数决定是否加载执行和用例详情
- **统计计算**: 自动计算各层级的数量统计
#### 4. 数据关系
- **任务 → 执行**: 一对多关系,通过TaskId关联
- **执行 → 用例**: 一对多关系,通过ExecutionId关联
- **完整链路**: 任务 → 执行详情 → 用例详情
### 使用场景
1. **任务管理界面**: 展示完整的任务执行层次结构
2. **执行监控**: 实时查看任务执行状态和进度
3. **用例分析**: 分析用例执行情况和性能数据
4. **报告生成**: 生成包含完整执行链路的报告
### 文件清单
#### Application Layer
- `X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeQuery.cs`
- `X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeResponse.cs`
- `X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeQueryHandler.cs`
#### Presentation Layer
- `X1.Presentation/Controllers/TestScenarioTaskExecutionController.cs`
#### Frontend Layer
- `X1.WebUI/src/services/testScenarioTaskExecutionService.ts` - 任务执行过程服务
- `X1.WebUI/src/pages/task-execution-process/TaskExecutionProcessView.tsx` - 任务执行过程主视图组件
- `X1.WebUI/src/pages/task-execution-process/TaskExecutionTree.tsx` - 左侧树形结构组件
- `X1.WebUI/src/pages/task-execution-process/TaskExecutionTable.tsx` - 右侧表格组件
- `X1.WebUI/src/components/ui/collapsible.tsx` - 可折叠组件(新增)
- `X1.WebUI/src/constants/api.ts` - 更新API常量配置
- `X1.WebUI/src/constants/navigationMenuPresets.ts` - 更新导航菜单预设配置
- `X1.WebUI/src/pages/navigation-menus/NavigationMenuForm_Examples.md` - 更新导航菜单示例文档
- `X1.WebUI/src/routes/AppRouter.tsx` - 添加任务执行过程页面路由配置
### Controller实现
#### 1. 控制器设计
- **基类**: 继承自 `ApiController` 基类
- **路由**: `api/testscenariotaskexecution`
- **权限**: 需要授权访问 `[Authorize]`
- **日志**: 完整的操作日志记录
#### 2. API端点
- **获取任务执行过程**: `GET /api/testscenariotaskexecution` - 支持场景和设备过滤,可控制是否包含执行和用例详情
**设计说明**: 基于现有Controller的实现模式,采用单一端点设计,通过查询参数控制不同的过滤条件和数据包含选项,这与现有的TestScenarioTasksController和TestScenariosController的设计保持一致。
#### 3. 查询参数
- **scenarioCode**: 场景编码过滤
- **deviceCode**: 设备编码过滤
- **includeExecutionDetails**: 是否包含执行详情(默认true)
- **includeCaseDetails**: 是否包含用例详情(默认true)
#### 4. 技术特点
- **统一响应**: 使用 `OperationResult<T>` 统一响应格式
- **错误处理**: 完整的错误处理和用户友好的错误消息
- **日志记录**: 详细的操作日志,包括开始、成功、失败状态
- **参数验证**: 自动参数验证和类型转换
#### 5. API使用示例
```http
# 获取所有任务执行过程
GET /api/testscenariotaskexecution
# 获取指定场景的任务执行过程
GET /api/testscenariotaskexecution?scenarioCode=SCENARIO_001
# 获取指定设备的任务执行过程
GET /api/testscenariotaskexecution?deviceCode=DEVICE_001
# 获取任务执行过程概览(不包含执行详情)
GET /api/testscenariotaskexecution?scenarioCode=SCENARIO_001&includeExecutionDetails=false&includeCaseDetails=false
# 获取完整任务执行过程(包含执行和用例详情)
GET /api/testscenariotaskexecution?scenarioCode=SCENARIO_001&includeExecutionDetails=true&includeCaseDetails=true
# 获取任务执行过程(仅包含执行详情,不包含用例详情)
GET /api/testscenariotaskexecution?deviceCode=DEVICE_001&includeExecutionDetails=true&includeCaseDetails=false
```
#### 6. 响应格式示例
```json
{
"isSuccess": true,
"data": {
"taskTrees": [
{
"taskId": "TASK_001",
"taskCode": "TASK_001",
"taskName": "测试任务1",
"scenarioCode": "SCENARIO_001",
"deviceCode": "DEVICE_001",
"executionCount": 2,
"executions": [
{
"executionId": "EXEC_001",
"taskId": "TASK_001",
"status": "Success",
"caseCount": 3,
"caseDetails": [
{
"caseDetailId": "CASE_001",
"executionId": "EXEC_001",
"testCaseFlowId": "FLOW_001",
"status": "Success"
}
]
}
]
}
],
"totalTaskCount": 1,
"totalExecutionCount": 2,
"totalCaseCount": 3
},
"errorMessages": null
}
### 数据库表关系
```
tb_testscenario_task (任务表)
├── Id (主键)
├── TaskCode (任务编码)
├── TaskName (任务名称)
├── ScenarioCode (场景编码)
├── DeviceCode (设备编码)
└── ... (其他字段)
tb_testscenario_task_execution_detail (执行详情表)
├── Id (主键)
├── TaskId (外键 → tb_testscenario_task.Id)
├── ScenarioCode (场景编码)
├── ExecutorId (执行人ID)
├── Status (执行状态)
└── ... (其他字段)
tb_testscenario_task_execution_case_detail (用例详情表)
├── Id (主键)
├── ExecutionId (外键 → tb_testscenario_task_execution_detail.Id)
├── ScenarioCode (场景编码)
├── TestCaseFlowId (测试用例流程ID)
├── ExecutorId (执行人ID)
└── ... (其他字段)
```
### 树形结构示例
```
任务1 (TestScenarioTaskTreeDto)
├── 执行1 (TaskExecutionTreeDto)
│ ├── 用例1 (TaskExecutionCaseTreeDto)
│ ├── 用例2 (TaskExecutionCaseTreeDto)
│ └── 用例3 (TaskExecutionCaseTreeDto)
├── 执行2 (TaskExecutionTreeDto)
│ ├── 用例4 (TaskExecutionCaseTreeDto)
│ └── 用例5 (TaskExecutionCaseTreeDto)
└── 执行3 (TaskExecutionTreeDto)
└── 用例6 (TaskExecutionCaseTreeDto)
任务2 (TestScenarioTaskTreeDto)
└── 执行4 (TaskExecutionTreeDto)
├── 用例7 (TaskExecutionCaseTreeDto)
└── 用例8 (TaskExecutionCaseTreeDto)
```
### 性能优化
1. **按需加载**: 通过参数控制是否加载执行和用例详情
2. **批量查询**: 使用仓储的批量查询方法减少数据库访问次数
3. **内存优化**: 避免N+1查询问题,使用高效的关联查询
4. **缓存友好**: 设计支持后续缓存优化的数据结构
### 前端实现
#### 1. 服务层设计
- **服务类**: `TestScenarioTaskExecutionService` - 完全对应后端Controller
- **API路径**: `/testscenariotaskexecution` - 与后端路由保持一致
- **类型定义**: 与后端DTO完全对应的TypeScript接口
- **错误处理**: 统一的错误处理和用户友好的提示
#### 2. 视图组件设计
- **主组件**: `TaskExecutionProcessView` - 避免与现有组件重名
- **功能定位**: 专门用于查看任务执行结果和过程
- **布局设计**: 左右分栏布局,左侧树形结构,右侧详细信息表格
- **UI特点**:
- 左侧可折叠的树形结构展示任务层次
- 右侧表格展示选中任务的详细执行信息
- 状态图标和颜色区分不同执行状态
- 支持按场景和设备过滤
- 可控制是否显示执行和用例详情
#### 3. 组件拆分架构
- **TaskExecutionProcessView**: 主视图组件,负责数据获取和状态管理
- **TaskExecutionTree**: 左侧树形组件,展示任务列表和层次结构
- **TaskExecutionTable**: 右侧表格组件,展示选中任务的详细信息
- **组件通信**: 通过props传递数据和回调函数实现组件间通信
#### 4. 组件特性
- **响应式设计**: 适配不同屏幕尺寸,大屏显示左右布局,小屏堆叠显示
- **交互友好**: 左侧树形结构可折叠展开,右侧表格支持搜索和过滤
- **状态可视化**: 不同颜色和图标表示执行状态
- **实时更新**: 支持刷新和重新查询
- **性能优化**: 按需加载执行和用例详情
- **数据联动**: 左侧选择任务,右侧自动更新对应详细信息
- **主题适配**: 使用主题变量,支持明暗主题切换
- **布局优化**: 任务列表铺满容器,内部滚动条优化用户体验
- **Card布局**: 为Card组件添加flex布局确保子组件正确继承高度
#### 5. 步骤详情功能扩展
##### 5.1 新增功能概述
基于用户需求,在现有的TestScenarioTaskTree功能基础上,新增了通过CaseDetailId获取StepId,然后关联tb_testscenario_task_execution_step_log表的功能。
##### 5.2 数据流程
1. **输入**: CaseDetailId (用例执行明细ID)
2. **查询步骤**: 通过CaseDetailId从tb_testscenario_task_execution_case_step_detail表获取StepId列表
3. **关联日志**: 使用StepDetailId关联tb_testscenario_task_execution_step_log表获取步骤执行日志
4. **输出**: 包含步骤详情和日志的完整数据结构
##### 5.3 新增文件
**X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsResponse.cs**
- 定义了步骤详情和日志的DTO结构
- `GetTestScenarioTaskStepDetailsResponse`: 响应容器
- `TestScenarioTaskStepDetailDto`: 步骤详情DTO
- `TestScenarioTaskStepLogDto`: 步骤日志DTO
**X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsQuery.cs**
- 定义了查询参数
- 支持日志类型过滤、时间范围过滤等条件
**X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsQueryHandler.cs**
- 实现了查询逻辑
- 通过Repository获取步骤详情和日志数据
- 支持条件过滤和排序
##### 5.4 Controller扩展
**X1.Presentation/Controllers/TestScenarioTaskExecutionController.cs**
- 新增 `GetTestScenarioTaskStepDetails` 端点
- 路由: `GET /api/testscenariotaskexecution/step-details`
- 支持查询参数: caseDetailId, includeStepLogs, logTypeFilter, startTimeFilter, endTimeFilter
##### 5.5 技术特性
- **CQRS模式**: 遵循现有的Command Query Responsibility Segregation模式
- **MediatR**: 使用MediatR进行查询处理
- **Repository模式**: 利用现有的Repository接口和实现
- **日志记录**: 完整的日志记录和错误处理
- **参数验证**: 输入参数验证和错误处理
- **条件过滤**: 支持多种过滤条件
#### 6. 前端服务实现
##### 6.1 服务层扩展
**X1.WebUI/src/services/testScenarioTaskExecutionService.ts**
- 新增步骤日志类型枚举 `StepLogType`
- 新增步骤详情相关DTO接口:
- `TestScenarioTaskStepLogDto`: 步骤日志DTO
- `TestScenarioTaskStepDetailDto`: 步骤详情DTO
- `GetTestScenarioTaskStepDetailsRequest`: 请求接口
- `GetTestScenarioTaskStepDetailsResponse`: 响应接口
- 新增服务方法 `getTestScenarioTaskStepDetails()`: 获取步骤详情和日志
##### 6.2 组件层实现
**X1.WebUI/src/pages/task-execution-process/StepDetailsView.tsx**
- 步骤日志视图组件
- 直接显示所有步骤的日志,按时间排序
- 支持日志类型过滤和搜索功能
- 每个日志项包含对应的步骤信息
- 完整的错误处理和加载状态
**X1.WebUI/src/pages/task-execution-process/TaskExecutionTree.tsx**
- 修改用例执行详情组件,添加点击事件
- 支持用例点击查看步骤详情
- 传递用例点击回调函数
**X1.WebUI/src/pages/task-execution-process/TaskExecutionProcessView.tsx**
- 添加步骤详情状态管理
- 移除TaskExecutionTable组件,直接使用StepDetailsView
- 实现固定两列布局:任务树 + 步骤详情
- 添加用例点击处理逻辑
- 集成步骤详情视图
##### 6.3 交互流程
1. **用例点击**: 用户在任务树中点击用例执行详情
2. **状态更新**: 设置选中的用例明细ID,显示步骤日志视图
3. **数据获取**: 自动调用步骤详情API获取数据
4. **日志展示**: 在右侧面板展示所有步骤的日志,按时间排序
5. **关闭功能**: 用户可点击关闭按钮清空选择
##### 6.4 技术特性
- **响应式布局**: 支持不同屏幕尺寸的布局适配
- **状态管理**: 完整的组件状态管理
- **错误处理**: 统一的错误处理和用户提示
- **加载状态**: 友好的加载状态显示
- **搜索过滤**: 支持步骤搜索和日志类型过滤
- **主题适配**: 使用主题变量,支持明暗主题
#### 7. 技术实现
- **React Hooks**: 使用useState和useEffect管理状态
- **TypeScript**: 强类型定义确保数据安全
- **Tailwind CSS**: 现代化的样式设计,支持响应式布局
- **Lucide Icons**: 统一的图标系统
- **Toast通知**: 用户友好的操作反馈
- **组件化设计**: 模块化的组件结构,便于维护和扩展
- **状态管理**: 集中式状态管理,通过props传递数据
#### 6. 导航菜单配置
- **菜单位置**: 任务管理 → 查看任务
- **菜单路径**: `/dashboard/tasks/execution-process`
- **权限代码**: `testscenariotaskexecution.view`
- **图标**: `ClipboardList` - 表示查看和列表
- **排序**: 6 - 在任务管理子菜单中排在最后
- **描述**: 查看任务执行过程和结果
#### 7. 路由配置
- **路由路径**: `/dashboard/tasks/execution-process`
- **组件**: `TaskExecutionProcessView`
- **权限控制**: `testscenariotaskexecution.view`
- **懒加载**: 使用React.lazy进行代码分割
- **动画容器**: 使用AnimatedContainer包装
- **保护路由**: 使用ProtectedRoute进行权限验证
### 最新更新 (2025-01-21)
#### 任务列表显示优化
- **文件**: `X1.WebUI/src/pages/task-execution-process/TaskExecutionTree.tsx`
- **变更**: 移除不必要的字段显示,优化任务列表显示为更紧凑的格式
- **具体修改**:
- 移除用例流程 (`testCaseFlowId`) 显示
- 移除场景 (`scenarioCode`) 显示
- 移除执行人 (`executorId`) 显示
- 移除执行ID (`executionId`) 显示
- 优化间距和内边距,使显示更紧凑
- 减少组件间距:`space-y-2` → `space-y-1`
- 减少内边距:`p-3` → `p-2`,`p-4` → `p-2`
- 移除未使用的 `User` 图标导入
#### 步骤日志显示优化
- **文件**: `X1.WebUI/src/pages/task-execution-process/StepDetailsView.tsx`
- **变更**: 移除步骤日志信息中的执行人和ID显示,优化显示内容
- **具体修改**:
- 移除 `log.logId` 的显示
- 移除 `stepDetail.executorId` 的显示
- 简化步骤信息显示,只保留步骤名称、状态和耗时
- 优化布局,减少不必要的空白空间
- 为日志内容添加滚动条:`max-h-32 overflow-y-auto`
- 进一步紧凑化显示:减少间距和内边距
### 扩展性
1. **层级扩展**: 可以轻松添加更多层级(如步骤详情)
2. **过滤扩展**: 可以添加更多过滤条件(如时间范围、状态等)
3. **排序扩展**: 可以添加排序功能(如按时间、状态等)
4. **分页扩展**: 可以添加分页功能以处理大量数据
5. **实时更新**: 可以添加WebSocket支持实时状态更新
6. **导出功能**: 可以添加数据导出功能

228
src/modify_20250121_testscenariotasktree_feature.md

@ -0,0 +1,228 @@
## 2025-01-21 创建测试场景任务执行过程Feature
### 概述
为测试场景任务系统创建树形结构Feature,将三个核心表(tb_testscenario_task、tb_testscenario_task_execution_detail、tb_testscenario_task_execution_case_detail)组合生成层次化的树形结构,便于前端展示和管理。
### 主要变更
#### 1. 创建 TestScenarioTaskTree Feature
- **目录**: `X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/`
- **功能**: 提供测试场景任务执行过程查询功能
- **架构**: 遵循CQRS模式,使用MediatR进行查询处理
#### 2. 实现的功能组件
- **查询类**: `GetTestScenarioTaskTreeQuery.cs` - 定义查询参数和条件
- **响应类**: `GetTestScenarioTaskTreeResponse.cs` - 定义树形结构响应格式
- **处理器**: `GetTestScenarioTaskTreeQueryHandler.cs` - 实现树形结构构建逻辑
- **DTO类**: 包含任务树、执行树、用例树的数据传输对象
#### 3. 树形结构设计
- **第一层**: 测试场景任务(TestScenarioTaskTreeDto)
- **第二层**: 任务执行详情(TaskExecutionTreeDto)
- **第三层**: 用例执行详情(TaskExecutionCaseTreeDto)
#### 4. 查询参数支持
- **场景过滤**: `ScenarioCode` - 按场景编码过滤
- **设备过滤**: `DeviceCode` - 按设备编码过滤
- **执行详情**: `IncludeExecutionDetails` - 是否包含执行详情
- **用例详情**: `IncludeCaseDetails` - 是否包含用例详情
#### 5. 技术特点
- **性能优化**: 支持按需加载,可选择是否包含执行和用例详情
- **数据完整性**: 提供完整的统计信息(任务数量、执行数量、用例数量)
- **错误处理**: 完整的异常处理和日志记录
- **类型安全**: 强类型的DTO设计,确保数据一致性
### 技术实现
#### 1. 查询类设计
```csharp
public class GetTestScenarioTaskTreeQuery : IRequest<OperationResult<GetTestScenarioTaskTreeResponse>>
{
public string? ScenarioCode { get; set; }
public string? DeviceCode { get; set; }
public bool IncludeExecutionDetails { get; set; } = true;
public bool IncludeCaseDetails { get; set; } = true;
}
```
#### 2. 树形结构DTO
- **TestScenarioTaskTreeDto**: 任务树节点,包含任务基本信息和执行列表
- **TaskExecutionTreeDto**: 执行树节点,包含执行信息和用例列表
- **TaskExecutionCaseTreeDto**: 用例树节点,包含用例执行详情
#### 3. 处理器逻辑
- **分层构建**: 先构建任务层,再构建执行层,最后构建用例层
- **按需加载**: 根据参数决定是否加载执行和用例详情
- **统计计算**: 自动计算各层级的数量统计
#### 4. 数据关系
- **任务 → 执行**: 一对多关系,通过TaskId关联
- **执行 → 用例**: 一对多关系,通过ExecutionId关联
- **完整链路**: 任务 → 执行详情 → 用例详情
### 使用场景
1. **任务管理界面**: 展示完整的任务执行层次结构
2. **执行监控**: 实时查看任务执行状态和进度
3. **用例分析**: 分析用例执行情况和性能数据
4. **报告生成**: 生成包含完整执行链路的报告
### 文件清单
#### Application Layer
- `X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeQuery.cs`
- `X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeResponse.cs`
- `X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeQueryHandler.cs`
#### Presentation Layer
- `X1.Presentation/Controllers/TestScenarioTaskExecutionController.cs`
### Controller实现
#### 1. 控制器设计
- **基类**: 继承自 `ApiController` 基类
- **路由**: `api/testscenariotaskexecution`
- **权限**: 需要授权访问 `[Authorize]`
- **日志**: 完整的操作日志记录
#### 2. API端点
- **获取任务执行过程**: `GET /api/testscenariotaskexecution` - 支持场景和设备过滤,可控制是否包含执行和用例详情
**设计说明**: 基于现有Controller的实现模式,采用单一端点设计,通过查询参数控制不同的过滤条件和数据包含选项,这与现有的TestScenarioTasksController和TestScenariosController的设计保持一致。
#### 3. 查询参数
- **scenarioCode**: 场景编码过滤
- **deviceCode**: 设备编码过滤
- **includeExecutionDetails**: 是否包含执行详情(默认true)
- **includeCaseDetails**: 是否包含用例详情(默认true)
#### 4. 技术特点
- **统一响应**: 使用 `OperationResult<T>` 统一响应格式
- **错误处理**: 完整的错误处理和用户友好的错误消息
- **日志记录**: 详细的操作日志,包括开始、成功、失败状态
- **参数验证**: 自动参数验证和类型转换
#### 5. API使用示例
```http
# 获取所有任务执行过程
GET /api/testscenariotaskexecution
# 获取指定场景的任务执行过程
GET /api/testscenariotaskexecution?scenarioCode=SCENARIO_001
# 获取指定设备的任务执行过程
GET /api/testscenariotaskexecution?deviceCode=DEVICE_001
# 获取任务执行过程概览(不包含执行详情)
GET /api/testscenariotaskexecution?scenarioCode=SCENARIO_001&includeExecutionDetails=false&includeCaseDetails=false
# 获取完整任务执行过程(包含执行和用例详情)
GET /api/testscenariotaskexecution?scenarioCode=SCENARIO_001&includeExecutionDetails=true&includeCaseDetails=true
# 获取任务执行过程(仅包含执行详情,不包含用例详情)
GET /api/testscenariotaskexecution?deviceCode=DEVICE_001&includeExecutionDetails=true&includeCaseDetails=false
```
#### 6. 响应格式示例
```json
{
"isSuccess": true,
"data": {
"taskTrees": [
{
"taskId": "TASK_001",
"taskCode": "TASK_001",
"taskName": "测试任务1",
"scenarioCode": "SCENARIO_001",
"deviceCode": "DEVICE_001",
"executionCount": 2,
"executions": [
{
"executionId": "EXEC_001",
"taskId": "TASK_001",
"status": "Success",
"caseCount": 3,
"caseDetails": [
{
"caseDetailId": "CASE_001",
"executionId": "EXEC_001",
"testCaseFlowId": "FLOW_001",
"status": "Success"
}
]
}
]
}
],
"totalTaskCount": 1,
"totalExecutionCount": 2,
"totalCaseCount": 3
},
"errorMessages": null
}
### 数据库表关系
```
tb_testscenario_task (任务表)
├── Id (主键)
├── TaskCode (任务编码)
├── TaskName (任务名称)
├── ScenarioCode (场景编码)
├── DeviceCode (设备编码)
└── ... (其他字段)
tb_testscenario_task_execution_detail (执行详情表)
├── Id (主键)
├── TaskId (外键 → tb_testscenario_task.Id)
├── ScenarioCode (场景编码)
├── ExecutorId (执行人ID)
├── Status (执行状态)
└── ... (其他字段)
tb_testscenario_task_execution_case_detail (用例详情表)
├── Id (主键)
├── ExecutionId (外键 → tb_testscenario_task_execution_detail.Id)
├── ScenarioCode (场景编码)
├── TestCaseFlowId (测试用例流程ID)
├── ExecutorId (执行人ID)
└── ... (其他字段)
```
### 树形结构示例
```
任务1 (TestScenarioTaskTreeDto)
├── 执行1 (TaskExecutionTreeDto)
│ ├── 用例1 (TaskExecutionCaseTreeDto)
│ ├── 用例2 (TaskExecutionCaseTreeDto)
│ └── 用例3 (TaskExecutionCaseTreeDto)
├── 执行2 (TaskExecutionTreeDto)
│ ├── 用例4 (TaskExecutionCaseTreeDto)
│ └── 用例5 (TaskExecutionCaseTreeDto)
└── 执行3 (TaskExecutionTreeDto)
└── 用例6 (TaskExecutionCaseTreeDto)
任务2 (TestScenarioTaskTreeDto)
└── 执行4 (TaskExecutionTreeDto)
├── 用例7 (TaskExecutionCaseTreeDto)
└── 用例8 (TaskExecutionCaseTreeDto)
```
### 性能优化
1. **按需加载**: 通过参数控制是否加载执行和用例详情
2. **批量查询**: 使用仓储的批量查询方法减少数据库访问次数
3. **内存优化**: 避免N+1查询问题,使用高效的关联查询
4. **缓存友好**: 设计支持后续缓存优化的数据结构
### 扩展性
1. **层级扩展**: 可以轻松添加更多层级(如步骤详情)
2. **过滤扩展**: 可以添加更多过滤条件(如时间范围、状态等)
3. **排序扩展**: 可以添加排序功能(如按时间、状态等)
4. **分页扩展**: 可以添加分页功能以处理大量数据
Loading…
Cancel
Save