Browse Source
主要功能: - 新增 TestScenarioTaskTree Feature,提供任务执行过程的树形结构查询 - 实现 TestScenarioTaskExecutionController 控制器,支持任务执行过程管理 - 新增步骤详情查询功能,支持通过 CaseDetailId 获取步骤执行日志 - 为 GetTestScenarioTasksQuery 添加 ExecutionStatus 查询条件过滤 技术实现: - 遵循 CQRS 模式,使用 MediatR 进行查询处理 - 实现三层树形结构:任务 → 执行详情 → 用例详情 - 支持按场景、设备、执行状态等条件过滤 - 前端实现响应式布局的任务执行过程管理界面 文件变更: - 新增:TestScenarioTaskTree Feature 相关文件 - 新增:TestScenarioTaskExecutionController 控制器 - 新增:前端任务执行过程管理页面和组件 - 修改:GetTestScenarioTasksQuery 添加执行状态过滤 - 更新:导航菜单和路由配置 优化内容: - 任务列表显示优化,移除冗余字段 - 步骤日志显示优化,简化信息展示 - 性能优化,支持按需加载执行和用例详情refactor/permission-config
25 changed files with 3202 additions and 4 deletions
@ -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; } |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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> |
|||
); |
|||
}; |
|||
@ -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; |
|||
@ -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> |
|||
); |
|||
} |
|||
@ -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; |
|||
@ -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; |
|||
@ -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(); |
|||