版本260406
This commit is contained in:
1667
OrpaonVision.ConfigApp/ViewModels/MainWindowViewModel.cs
Normal file
1667
OrpaonVision.ConfigApp/ViewModels/MainWindowViewModel.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,272 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Media;
|
||||
using OrpaonVision.Core.Production;
|
||||
using OrpaonVision.Core.Production.Contracts;
|
||||
using OrpaonVision.Core.Production.Contracts.Queries;
|
||||
|
||||
namespace OrpaonVision.ConfigApp.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// 生产监控ViewModel。
|
||||
/// </summary>
|
||||
public sealed class ProductionMonitoringViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private readonly IProductionAppService _productionAppService;
|
||||
|
||||
private string _statusText = "准备就绪。";
|
||||
private Brush _statusBrush = Brushes.DarkGreen;
|
||||
private string _outputText = string.Empty;
|
||||
private RealtimeProductionStatusDto? _realtimeStatus;
|
||||
private ProductionStatisticsDto? _statistics;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数。
|
||||
/// </summary>
|
||||
public ProductionMonitoringViewModel(IProductionAppService productionAppService)
|
||||
{
|
||||
_productionAppService = productionAppService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 状态文本。
|
||||
/// </summary>
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
private set => SetProperty(ref _statusText, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 状态画刷。
|
||||
/// </summary>
|
||||
public Brush StatusBrush
|
||||
{
|
||||
get => _statusBrush;
|
||||
private set => SetProperty(ref _statusBrush, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 输出文本。
|
||||
/// </summary>
|
||||
public string OutputText
|
||||
{
|
||||
get => _outputText;
|
||||
private set => SetProperty(ref _outputText, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实时生产状态。
|
||||
/// </summary>
|
||||
public RealtimeProductionStatusDto? RealtimeStatus
|
||||
{
|
||||
get => _realtimeStatus;
|
||||
private set => SetProperty(ref _realtimeStatus, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生产统计数据。
|
||||
/// </summary>
|
||||
public ProductionStatisticsDto? Statistics
|
||||
{
|
||||
get => _statistics;
|
||||
private set => SetProperty(ref _statistics, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取实时生产状态。
|
||||
/// </summary>
|
||||
public async Task GetRealtimeStatusAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusBrush = Brushes.DarkGreen;
|
||||
StatusText = "正在获取实时生产状态...";
|
||||
|
||||
// 这里演示如何调用生产服务获取实时状态
|
||||
var workstationId = Guid.NewGuid(); // 实际应该从配置或选择获取
|
||||
var result = await _productionAppService.GetRealtimeProductionStatusAsync(workstationId);
|
||||
|
||||
if (result.Succeeded && result.Data != null)
|
||||
{
|
||||
RealtimeStatus = result.Data;
|
||||
StatusText = "实时状态获取成功";
|
||||
OutputText = $"工位:{result.Data.WorkstationName}\n" +
|
||||
$"状态:{result.Data.CurrentStatus}\n" +
|
||||
$"今日良率:{result.Data.TodayYieldRate:P2}\n" +
|
||||
$"平均节拍:{result.Data.AverageCycleTimeSeconds:F1}秒";
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "获取实时状态失败";
|
||||
OutputText = $"错误:{result.Message}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "获取实时状态异常";
|
||||
OutputText = $"异常:{ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取生产统计数据。
|
||||
/// </summary>
|
||||
public async Task GetProductionStatisticsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusBrush = Brushes.DarkGreen;
|
||||
StatusText = "正在获取生产统计数据...";
|
||||
|
||||
// 这里演示如何调用生产服务获取统计数据
|
||||
var query = new OrpaonVision.Core.Production.Contracts.Queries.ProductionStatisticsQueryDto
|
||||
{
|
||||
StartTimeUtc = DateTime.UtcNow.AddDays(-7), // 最近7天
|
||||
EndTimeUtc = DateTime.UtcNow,
|
||||
IncludeDetailedStatistics = true,
|
||||
Granularity = "Day"
|
||||
};
|
||||
|
||||
var result = await _productionAppService.GetProductionStatisticsAsync(query);
|
||||
|
||||
if (result.Succeeded && result.Data != null)
|
||||
{
|
||||
Statistics = result.Data;
|
||||
StatusText = "统计数据获取成功";
|
||||
OutputText = $"统计时间:{result.Data.StartTimeUtc:yyyy-MM-dd} 至 {result.Data.EndTimeUtc:yyyy-MM-dd}\n" +
|
||||
$"总产品:{result.Data.TotalProducts}\n" +
|
||||
$"良率:{result.Data.YieldRate:P2}\n" +
|
||||
$"平均节拍:{result.Data.AverageCycleTimeSeconds:F1}秒";
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "获取统计数据失败";
|
||||
OutputText = $"错误:{result.Message}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "获取统计数据异常";
|
||||
OutputText = $"异常:{ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出生产数据。
|
||||
/// </summary>
|
||||
public async Task ExportProductionDataAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusBrush = Brushes.DarkGreen;
|
||||
StatusText = "正在导出生产数据...";
|
||||
|
||||
// 这里演示如何调用生产服务导出数据
|
||||
var query = new OrpaonVision.Core.Production.Contracts.Queries.ProductSessionQueryDto
|
||||
{
|
||||
StartedAtUtcStart = DateTime.UtcNow.AddDays(-7),
|
||||
StartedAtUtcEnd = DateTime.UtcNow,
|
||||
PageIndex = 1,
|
||||
PageSize = 1000
|
||||
};
|
||||
|
||||
var result = await _productionAppService.ExportProductionDataAsync(query, "Excel");
|
||||
|
||||
if (result.Succeeded && result.Data != null)
|
||||
{
|
||||
StatusText = "生产数据导出成功";
|
||||
OutputText = $"导出文件:{result.Data.FileName}\n" +
|
||||
$"文件大小:{result.Data.FileSizeBytes / 1024.0:F1} KB\n" +
|
||||
$"导出格式:{result.Data.Format}\n" +
|
||||
$"校验和:{result.Data.Checksum}";
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "导出生产数据失败";
|
||||
OutputText = $"错误:{result.Message}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "导出生产数据异常";
|
||||
OutputText = $"异常:{ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取质量分析报告。
|
||||
/// </summary>
|
||||
public async Task GetQualityAnalysisReportAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusBrush = Brushes.DarkGreen;
|
||||
StatusText = "正在生成质量分析报告...";
|
||||
|
||||
var query = new OrpaonVision.Core.Production.Contracts.Queries.ProductionStatisticsQueryDto
|
||||
{
|
||||
StartTimeUtc = DateTime.UtcNow.AddDays(-30), // 最近30天
|
||||
EndTimeUtc = DateTime.UtcNow,
|
||||
IncludeDetailedStatistics = true,
|
||||
GroupByLayer = true,
|
||||
GroupByNgType = true,
|
||||
GroupByOperator = true
|
||||
};
|
||||
|
||||
var result = await _productionAppService.GetQualityAnalysisReportAsync(query);
|
||||
|
||||
if (result.Succeeded && result.Data != null)
|
||||
{
|
||||
StatusText = "质量分析报告生成成功";
|
||||
var report = result.Data;
|
||||
OutputText = $"报告时间:{report.StartTimeUtc:yyyy-MM-dd} 至 {report.EndTimeUtc:yyyy-MM-dd}\n" +
|
||||
$"总体良率:{report.OverallMetrics.YieldRate:P2}\n" +
|
||||
$"质量评分:{report.OverallMetrics.QualityScore:F1}/100\n" +
|
||||
$"质量等级:{report.OverallMetrics.QualityGrade}\n" +
|
||||
$"改进建议数:{report.ImprovementSuggestions.Count}";
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "生成质量分析报告失败";
|
||||
OutputText = $"错误:{result.Message}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "生成质量分析报告异常";
|
||||
OutputText = $"异常:{ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新数据。
|
||||
/// </summary>
|
||||
public async Task RefreshDataAsync()
|
||||
{
|
||||
await GetRealtimeStatusAsync();
|
||||
await GetProductionStatisticsAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private void SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OrpaonVision.Core.Abstractions;
|
||||
using OrpaonVision.Model.Production;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
|
||||
namespace OrpaonVision.ConfigApp.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// 产品会话管理ViewModel。
|
||||
/// </summary>
|
||||
public sealed class ProductionSessionManagementViewModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
private readonly IProductionSessionService _productionSessionService;
|
||||
private readonly ILogger<ProductionSessionManagementViewModel> _logger;
|
||||
|
||||
private string _productTypeCode = string.Empty;
|
||||
private string _stationId = string.Empty;
|
||||
private string _operatorId = string.Empty;
|
||||
private ProductionSessionStatus? _status;
|
||||
private ProductionSessionResult? _result;
|
||||
private DateTime? _startTimeUtc;
|
||||
private DateTime? _endTimeUtc;
|
||||
private string _statusText = "就绪";
|
||||
private ObservableCollection<ProductionSessionModel> _sessions = new();
|
||||
private ProductionSessionStatistics? _statistics;
|
||||
private int _currentPage = 1;
|
||||
private int _pageSize = 20;
|
||||
private int _totalPages = 1;
|
||||
private int _totalCount = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数。
|
||||
/// </summary>
|
||||
public ProductionSessionManagementViewModel(
|
||||
IProductionSessionService productionSessionService,
|
||||
ILogger<ProductionSessionManagementViewModel> logger)
|
||||
{
|
||||
_productionSessionService = productionSessionService;
|
||||
_logger = logger;
|
||||
|
||||
// 设置默认时间范围为最近7天
|
||||
_endTimeUtc = DateTime.UtcNow;
|
||||
_startTimeUtc = _endTimeUtc.Value.AddDays(-7);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 产品类型编码。
|
||||
/// </summary>
|
||||
public string ProductTypeCode
|
||||
{
|
||||
get => _productTypeCode;
|
||||
set => SetProperty(ref _productTypeCode, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 工位ID。
|
||||
/// </summary>
|
||||
public string StationId
|
||||
{
|
||||
get => _stationId;
|
||||
set => SetProperty(ref _stationId, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 操作员ID。
|
||||
/// </summary>
|
||||
public string OperatorId
|
||||
{
|
||||
get => _operatorId;
|
||||
set => SetProperty(ref _operatorId, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 会话状态。
|
||||
/// </summary>
|
||||
public ProductionSessionStatus? Status
|
||||
{
|
||||
get => _status;
|
||||
set => SetProperty(ref _status, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 会话结果。
|
||||
/// </summary>
|
||||
public ProductionSessionResult? Result
|
||||
{
|
||||
get => _result;
|
||||
set => SetProperty(ref _result, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime? StartTimeUtc
|
||||
{
|
||||
get => _startTimeUtc;
|
||||
set => SetProperty(ref _startTimeUtc, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime? EndTimeUtc
|
||||
{
|
||||
get => _endTimeUtc;
|
||||
set => SetProperty(ref _endTimeUtc, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 状态文本。
|
||||
/// </summary>
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
private set => SetProperty(ref _statusText, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 会话列表。
|
||||
/// </summary>
|
||||
public ObservableCollection<ProductionSessionModel> Sessions
|
||||
{
|
||||
get => _sessions;
|
||||
private set => SetProperty(ref _sessions, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统计信息。
|
||||
/// </summary>
|
||||
public ProductionSessionStatistics? Statistics
|
||||
{
|
||||
get => _statistics;
|
||||
private set => SetProperty(ref _statistics, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前页码。
|
||||
/// </summary>
|
||||
public int CurrentPage
|
||||
{
|
||||
get => _currentPage;
|
||||
private set => SetProperty(ref _currentPage, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 每页大小。
|
||||
/// </summary>
|
||||
public int PageSize
|
||||
{
|
||||
get => _pageSize;
|
||||
set => SetProperty(ref _pageSize, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 总页数。
|
||||
/// </summary>
|
||||
public int TotalPages
|
||||
{
|
||||
get => _totalPages;
|
||||
private set => SetProperty(ref _totalPages, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 总记录数。
|
||||
/// </summary>
|
||||
public int TotalCount
|
||||
{
|
||||
get => _totalCount;
|
||||
private set => SetProperty(ref _totalCount, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 查询会话列表。
|
||||
/// </summary>
|
||||
public async Task QuerySessionsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusText = "正在查询会话列表...";
|
||||
|
||||
var request = new GetProductionSessionsRequest
|
||||
{
|
||||
ProductTypeCode = string.IsNullOrEmpty(ProductTypeCode) ? null : ProductTypeCode,
|
||||
StationId = string.IsNullOrEmpty(StationId) ? null : StationId,
|
||||
OperatorId = string.IsNullOrEmpty(OperatorId) ? null : OperatorId,
|
||||
Status = Status,
|
||||
Result = Result,
|
||||
StartTimeUtc = StartTimeUtc,
|
||||
EndTimeUtc = EndTimeUtc,
|
||||
PageIndex = CurrentPage,
|
||||
PageSize = PageSize
|
||||
};
|
||||
|
||||
var result = await Task.Run(() => _productionSessionService.GetSessions(request));
|
||||
|
||||
if (!result.Succeeded || result.Data == null)
|
||||
{
|
||||
StatusText = $"查询失败: {result.Message}";
|
||||
MessageBox.Show(result.Message, "查询失败", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
Sessions.Clear();
|
||||
foreach (var session in result.Data.Items)
|
||||
{
|
||||
Sessions.Add(session);
|
||||
}
|
||||
|
||||
TotalCount = result.Data.TotalCount;
|
||||
TotalPages = (int)Math.Ceiling((double)TotalCount / PageSize);
|
||||
|
||||
StatusText = $"查询完成,共 {TotalCount} 条记录";
|
||||
_logger.LogInformation("查询会话列表成功,记录数: {Count}", TotalCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"查询失败: {ex.Message}";
|
||||
_logger.LogError(ex, "查询会话列表失败");
|
||||
MessageBox.Show($"查询失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置筛选条件。
|
||||
/// </summary>
|
||||
public void ResetFilters()
|
||||
{
|
||||
ProductTypeCode = string.Empty;
|
||||
StationId = string.Empty;
|
||||
OperatorId = string.Empty;
|
||||
Status = null;
|
||||
Result = null;
|
||||
StartTimeUtc = DateTime.UtcNow.AddDays(-7);
|
||||
EndTimeUtc = DateTime.UtcNow;
|
||||
CurrentPage = 1;
|
||||
|
||||
_logger.LogInformation("筛选条件已重置");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载统计信息。
|
||||
/// </summary>
|
||||
public async Task LoadStatisticsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusText = "正在加载统计信息...";
|
||||
|
||||
var request = new GetProductionSessionStatisticsRequest
|
||||
{
|
||||
ProductTypeCode = string.IsNullOrEmpty(ProductTypeCode) ? null : ProductTypeCode,
|
||||
StationId = string.IsNullOrEmpty(StationId) ? null : StationId,
|
||||
OperatorId = string.IsNullOrEmpty(OperatorId) ? null : OperatorId,
|
||||
StartTimeUtc = StartTimeUtc ?? DateTime.UtcNow.AddDays(-7),
|
||||
EndTimeUtc = EndTimeUtc ?? DateTime.UtcNow
|
||||
};
|
||||
|
||||
var result = await Task.Run(() => _productionSessionService.GetStatistics(request));
|
||||
|
||||
if (!result.Succeeded || result.Data == null)
|
||||
{
|
||||
StatusText = $"加载统计信息失败: {result.Message}";
|
||||
MessageBox.Show(result.Message, "加载失败", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
Statistics = result.Data;
|
||||
StatusText = "统计信息加载完成";
|
||||
|
||||
_logger.LogInformation("统计信息加载完成,总会话数: {TotalSessions}, 合格率: {PassRate:P1}",
|
||||
Statistics.TotalSessions, Statistics.PassRate);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"加载统计信息失败: {ex.Message}";
|
||||
_logger.LogError(ex, "加载统计信息失败");
|
||||
MessageBox.Show($"加载统计信息失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出会话记录。
|
||||
/// </summary>
|
||||
public async Task ExportSessionsAsync(string filePath, ExportFormat format)
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusText = "正在导出会话记录...";
|
||||
|
||||
var request = new ExportProductionSessionsRequest
|
||||
{
|
||||
ProductTypeCode = string.IsNullOrEmpty(ProductTypeCode) ? null : ProductTypeCode,
|
||||
StationId = string.IsNullOrEmpty(StationId) ? null : StationId,
|
||||
OperatorId = string.IsNullOrEmpty(OperatorId) ? null : OperatorId,
|
||||
Status = Status,
|
||||
Result = Result,
|
||||
StartTimeUtc = StartTimeUtc,
|
||||
EndTimeUtc = EndTimeUtc,
|
||||
Format = format
|
||||
};
|
||||
|
||||
var result = await Task.Run(() => _productionSessionService.ExportSessions(request));
|
||||
|
||||
if (!result.Succeeded || result.Data == null)
|
||||
{
|
||||
StatusText = $"导出失败: {result.Message}";
|
||||
MessageBox.Show(result.Message, "导出失败", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
await File.WriteAllBytesAsync(filePath, result.Data);
|
||||
StatusText = $"导出成功: {filePath}";
|
||||
|
||||
_logger.LogInformation("会话记录导出成功,文件: {FilePath}, 格式: {Format}", filePath, format);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"导出失败: {ex.Message}";
|
||||
_logger.LogError(ex, "导出会话记录失败");
|
||||
MessageBox.Show($"导出失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查看会话详情。
|
||||
/// </summary>
|
||||
public void ViewSessionDetail(ProductionSessionModel session)
|
||||
{
|
||||
try
|
||||
{
|
||||
var detail = $@"会话详情
|
||||
|
||||
会话ID: {session.SessionId}
|
||||
产品类型: {session.ProductTypeCode} - {session.ProductTypeName}
|
||||
工位: {session.StationId} - {session.StationName}
|
||||
操作员: {session.OperatorId} - {session.OperatorName}
|
||||
班次: {session.ShiftId} - {session.ShiftName}
|
||||
|
||||
时间信息:
|
||||
开始时间: {session.StartedAtUtc:yyyy-MM-dd HH:mm:ss} UTC
|
||||
结束时间: {(session.EndedAtUtc?.ToString("yyyy-MM-dd HH:mm:ss") ?? "进行中")} UTC
|
||||
|
||||
状态信息:
|
||||
状态: {session.Status}
|
||||
结果: {session.Result}
|
||||
进度: {session.CurrentLayer}/{session.TotalLayers}
|
||||
|
||||
其他信息:
|
||||
NG原因: {session.NgReason ?? "无"}
|
||||
备注: {session.Remark ?? "无"}
|
||||
创建时间: {session.CreatedAtUtc:yyyy-MM-dd HH:mm:ss} UTC
|
||||
更新时间: {session.UpdatedAtUtc:yyyy-MM-dd HH:mm:ss} UTC";
|
||||
|
||||
MessageBox.Show(detail, $"会话详情 - {session.SessionId:N}", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
|
||||
_logger.LogInformation("查看会话详情,会话ID: {SessionId}", session.SessionId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "查看会话详情失败");
|
||||
MessageBox.Show($"查看详情失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 跳转到上一页。
|
||||
/// </summary>
|
||||
public async Task GoToPreviousPageAsync()
|
||||
{
|
||||
if (CurrentPage > 1)
|
||||
{
|
||||
CurrentPage--;
|
||||
await QuerySessionsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 跳转到下一页。
|
||||
/// </summary>
|
||||
public async Task GoToNextPageAsync()
|
||||
{
|
||||
if (CurrentPage < TotalPages)
|
||||
{
|
||||
CurrentPage++;
|
||||
await QuerySessionsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源。
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Sessions?.Clear();
|
||||
Statistics = null;
|
||||
_logger.LogInformation("产品会话管理ViewModel已释放");
|
||||
}
|
||||
|
||||
private void SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
271
OrpaonVision.ConfigApp/ViewModels/TrainingManagementViewModel.cs
Normal file
271
OrpaonVision.ConfigApp/ViewModels/TrainingManagementViewModel.cs
Normal file
@@ -0,0 +1,271 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Media;
|
||||
using OrpaonVision.Core.Training;
|
||||
using OrpaonVision.Core.Training.Contracts.Commands;
|
||||
using OrpaonVision.Core.Training.Contracts;
|
||||
|
||||
namespace OrpaonVision.ConfigApp.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// 训练管理ViewModel。
|
||||
/// </summary>
|
||||
public sealed class TrainingManagementViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private readonly IDatasetAppService _datasetAppService;
|
||||
private readonly ITrainingJobAppService _trainingJobAppService;
|
||||
private readonly IModelPackageAppService _modelPackageAppService;
|
||||
private readonly IModelVersionAppService _modelVersionAppService;
|
||||
|
||||
private string _statusText = "准备就绪。";
|
||||
private Brush _statusBrush = Brushes.DarkGreen;
|
||||
private string _outputText = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数。
|
||||
/// </summary>
|
||||
public TrainingManagementViewModel(
|
||||
IDatasetAppService datasetAppService,
|
||||
ITrainingJobAppService trainingJobAppService,
|
||||
IModelPackageAppService modelPackageAppService,
|
||||
IModelVersionAppService modelVersionAppService)
|
||||
{
|
||||
_datasetAppService = datasetAppService;
|
||||
_trainingJobAppService = trainingJobAppService;
|
||||
_modelPackageAppService = modelPackageAppService;
|
||||
_modelVersionAppService = modelVersionAppService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 状态文本。
|
||||
/// </summary>
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
private set => SetProperty(ref _statusText, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 状态画刷。
|
||||
/// </summary>
|
||||
public Brush StatusBrush
|
||||
{
|
||||
get => _statusBrush;
|
||||
private set => SetProperty(ref _statusBrush, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 输出文本。
|
||||
/// </summary>
|
||||
public string OutputText
|
||||
{
|
||||
get => _outputText;
|
||||
private set => SetProperty(ref _outputText, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建示例数据集。
|
||||
/// </summary>
|
||||
public async Task BuildSampleDatasetAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusBrush = Brushes.DarkGreen;
|
||||
StatusText = "正在构建示例数据集...";
|
||||
|
||||
// 这里演示如何调用数据集构建服务
|
||||
// 实际实现需要具体的标注任务ID和机种ID
|
||||
var command = new OrpaonVision.Core.Training.Contracts.Commands.BuildDatasetCommand
|
||||
{
|
||||
Name = "示例数据集",
|
||||
Description = "用于演示的数据集",
|
||||
ProductTypeId = Guid.NewGuid(), // 实际应该从UI获取
|
||||
AnnotationTaskIds = [Guid.NewGuid()], // 实际应该从UI获取
|
||||
CreatedBy = "DemoUser"
|
||||
};
|
||||
|
||||
// var result = await _datasetAppService.BuildDatasetAsync(command);
|
||||
// 这里为了演示,直接返回成功
|
||||
StatusText = "示例数据集构建完成(演示)";
|
||||
OutputText = $"数据集构建命令已准备:{command.Name} - {command.Description}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "数据集构建失败";
|
||||
OutputText = $"错误:{ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提交示例训练任务。
|
||||
/// </summary>
|
||||
public async Task SubmitSampleTrainingJobAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusBrush = Brushes.DarkGreen;
|
||||
StatusText = "正在提交训练任务...";
|
||||
|
||||
// 这里演示如何调用训练任务服务
|
||||
var command = new OrpaonVision.Core.Training.Contracts.Commands.SubmitTrainingJobCommand
|
||||
{
|
||||
Name = "示例训练任务",
|
||||
Description = "用于演示的训练任务",
|
||||
DatasetVersionId = Guid.NewGuid(), // 实际应该从UI获取
|
||||
AlgorithmType = "YOLOv8",
|
||||
TotalEpochs = 50,
|
||||
BatchSize = 16,
|
||||
LearningRate = 0.001m,
|
||||
CreatedBy = "DemoUser"
|
||||
};
|
||||
|
||||
// var result = await _trainingJobAppService.SubmitAsync(command);
|
||||
// 这里为了演示,直接返回成功
|
||||
StatusText = "训练任务提交完成(演示)";
|
||||
OutputText = $"训练任务命令已准备:{command.Name} - {command.AlgorithmType} - {command.TotalEpochs}轮";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "训练任务提交失败";
|
||||
OutputText = $"错误:{ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建示例模型包。
|
||||
/// </summary>
|
||||
public async Task BuildSampleModelPackageAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusBrush = Brushes.DarkGreen;
|
||||
StatusText = "正在构建模型包...";
|
||||
|
||||
var command = new OrpaonVision.Core.Training.Contracts.Commands.BuildModelPackageCommand
|
||||
{
|
||||
Name = "示例模型包",
|
||||
Description = "用于演示的模型包",
|
||||
ModelVersionId = Guid.NewGuid(),
|
||||
TrainingJobId = Guid.NewGuid(),
|
||||
ProductTypeId = Guid.NewGuid(),
|
||||
ModelFilePath = "/models/yolov8_best.onnx",
|
||||
LabelMappingPath = "/models/labels.json",
|
||||
ConfigPath = "/models/config.json",
|
||||
Accuracy = 0.95m,
|
||||
Recall = 0.92m,
|
||||
F1Score = 0.93m,
|
||||
ValidationLoss = 0.15m,
|
||||
CompatibilityVersion = "1.0.0",
|
||||
CreatedBy = "DemoUser"
|
||||
};
|
||||
|
||||
var result = await _modelPackageAppService.BuildPackageAsync(command);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
StatusBrush = Brushes.DarkGreen;
|
||||
StatusText = "模型包构建成功";
|
||||
OutputText = $"模型包构建成功:ID={result.Data} - 准确率{command.Accuracy:P2} - F1分数{command.F1Score:P2}";
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "模型包构建失败";
|
||||
OutputText = $"错误:{result.Message}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "模型包构建失败";
|
||||
OutputText = $"错误:{ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出示例模型包。
|
||||
/// </summary>
|
||||
public async Task ExportSampleModelPackageAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusBrush = Brushes.DarkGreen;
|
||||
StatusText = "正在导出模型包...";
|
||||
|
||||
var command = new OrpaonVision.Core.Training.Contracts.Commands.ExportModelPackageCommand
|
||||
{
|
||||
ModelPackageId = Guid.NewGuid(),
|
||||
ExportFormat = "ZIP",
|
||||
IncludeSourceCode = false,
|
||||
IncludeTestData = true,
|
||||
Operator = "DemoUser"
|
||||
};
|
||||
|
||||
var result = await _modelPackageAppService.ExportPackageAsync(command);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
StatusBrush = Brushes.DarkGreen;
|
||||
StatusText = "模型包导出成功";
|
||||
OutputText = $"模型包导出成功:{result.Data.FileName} - 大小{result.Data.FileSizeBytes:N0} 字节 - 校验和{result.Data.Checksum}";
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "模型包导出失败";
|
||||
OutputText = $"错误:{result.Message}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "模型包导出失败";
|
||||
OutputText = $"错误:{ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批准示例模型版本。
|
||||
/// </summary>
|
||||
public async Task ApproveSampleModelVersionAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusBrush = Brushes.DarkGreen;
|
||||
StatusText = "正在批准模型版本...";
|
||||
|
||||
var command = new OrpaonVision.Core.Training.Contracts.Commands.ApproveModelVersionCommand
|
||||
{
|
||||
ModelVersionId = Guid.NewGuid(),
|
||||
ApprovalComment = "模型性能满足要求,批准发布。",
|
||||
ApprovedBy = "DemoUser",
|
||||
ApprovalLevel = "Standard",
|
||||
ForceApprove = false
|
||||
};
|
||||
|
||||
// var result = await _modelVersionAppService.MarkAsApprovedAsync(command);
|
||||
StatusText = "模型版本批准完成(演示)";
|
||||
OutputText = $"模型版本批准命令已准备:批准者{command.ApprovedBy} - 级别{command.ApprovalLevel}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "模型版本批准失败";
|
||||
OutputText = $"错误:{ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private void SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OrpaonVision.Core.Results;
|
||||
using OrpaonVision.Model.Training;
|
||||
using OrpaonVision.ConfigApp.Infrastructure.Services;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace OrpaonVision.ConfigApp.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// 训练任务管理视图模型。
|
||||
/// </summary>
|
||||
public partial class TrainingTaskManagementViewModel : ObservableObject
|
||||
{
|
||||
private readonly ILogger<TrainingTaskManagementViewModel> _logger;
|
||||
private readonly ITrainingTaskService _trainingTaskService;
|
||||
private readonly DispatcherTimer _refreshTimer;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<TrainingTaskModel> _tasks = new();
|
||||
|
||||
[ObservableProperty]
|
||||
private TrainingTaskModel? _selectedTask;
|
||||
|
||||
[ObservableProperty]
|
||||
private TrainingTaskStatus? _selectedStatus;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _searchKeyword = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _currentPageIndex = 1;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _pageSize = 20;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _totalCount;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _totalPages;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isLoading;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isRefreshing;
|
||||
|
||||
[ObservableProperty]
|
||||
private TrainingTaskStatistics _statistics = new();
|
||||
|
||||
[ObservableProperty]
|
||||
private string _statusMessage = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数。
|
||||
/// </summary>
|
||||
public TrainingTaskManagementViewModel(
|
||||
ILogger<TrainingTaskManagementViewModel> logger,
|
||||
ITrainingTaskService trainingTaskService)
|
||||
{
|
||||
_logger = logger;
|
||||
_trainingTaskService = trainingTaskService;
|
||||
|
||||
// 初始化刷新定时器
|
||||
_refreshTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(30) // 30秒刷新一次
|
||||
};
|
||||
_refreshTimer.Tick += async (sender, e) => await RefreshDataAsync();
|
||||
|
||||
// 初始化状态选项
|
||||
StatusOptions = new ObservableCollection<TrainingTaskStatus?>
|
||||
{
|
||||
null, // 全部
|
||||
TrainingTaskStatus.Draft,
|
||||
TrainingTaskStatus.Pending,
|
||||
TrainingTaskStatus.Running,
|
||||
TrainingTaskStatus.Paused,
|
||||
TrainingTaskStatus.Completed,
|
||||
TrainingTaskStatus.Failed,
|
||||
TrainingTaskStatus.Cancelled
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
_ = Task.Run(async () => await LoadDataAsync());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 状态选项。
|
||||
/// </summary>
|
||||
public ObservableCollection<TrainingTaskStatus?> StatusOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否有选中的任务。
|
||||
/// </summary>
|
||||
public bool HasSelectedTask => SelectedTask != null;
|
||||
|
||||
/// <summary>
|
||||
/// 选中任务是否可以启动。
|
||||
/// </summary>
|
||||
public bool CanStartTask => SelectedTask != null &&
|
||||
(SelectedTask.Status == TrainingTaskStatus.Pending || SelectedTask.Status == TrainingTaskStatus.Paused);
|
||||
|
||||
/// <summary>
|
||||
/// 选中任务是否可以暂停。
|
||||
/// </summary>
|
||||
public bool CanPauseTask => SelectedTask != null && SelectedTask.Status == TrainingTaskStatus.Running;
|
||||
|
||||
/// <summary>
|
||||
/// 选中任务是否可以停止。
|
||||
/// </summary>
|
||||
public bool CanStopTask => SelectedTask != null &&
|
||||
(SelectedTask.Status == TrainingTaskStatus.Running || SelectedTask.Status == TrainingTaskStatus.Paused);
|
||||
|
||||
/// <summary>
|
||||
/// 选中任务是否可以取消。
|
||||
/// </summary>
|
||||
public bool CanCancelTask => SelectedTask != null &&
|
||||
SelectedTask.Status != TrainingTaskStatus.Completed &&
|
||||
SelectedTask.Status != TrainingTaskStatus.Cancelled;
|
||||
|
||||
/// <summary>
|
||||
/// 选中任务是否可以重新启动。
|
||||
/// </summary>
|
||||
public bool CanRestartTask => SelectedTask != null &&
|
||||
(SelectedTask.Status == TrainingTaskStatus.Failed || SelectedTask.Status == TrainingTaskStatus.Cancelled);
|
||||
|
||||
/// <summary>
|
||||
/// 选中任务是否可以删除。
|
||||
/// </summary>
|
||||
public bool CanDeleteTask => SelectedTask != null && SelectedTask.Status != TrainingTaskStatus.Running;
|
||||
|
||||
/// <summary>
|
||||
/// 刷新数据命令。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private async Task RefreshDataAsync()
|
||||
{
|
||||
if (IsRefreshing) return;
|
||||
|
||||
try
|
||||
{
|
||||
IsRefreshing = true;
|
||||
StatusMessage = "正在刷新数据...";
|
||||
|
||||
await LoadDataAsync();
|
||||
await LoadStatisticsAsync();
|
||||
|
||||
StatusMessage = $"数据已更新 - {DateTime.Now:HH:mm:ss}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "刷新数据失败");
|
||||
StatusMessage = "刷新数据失败";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 搜索命令。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
public async Task SearchAsync()
|
||||
{
|
||||
CurrentPageIndex = 1;
|
||||
await LoadTasksAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 页面变化命令。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private async Task PageChangedAsync(int pageIndex)
|
||||
{
|
||||
CurrentPageIndex = pageIndex;
|
||||
await LoadTasksAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动任务命令。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private async Task StartTaskAsync()
|
||||
{
|
||||
if (SelectedTask == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
StatusMessage = "正在启动任务...";
|
||||
|
||||
var result = await _trainingTaskService.StartTask(SelectedTask.Id, "当前用户");
|
||||
if (result.Succeeded)
|
||||
{
|
||||
StatusMessage = "任务启动成功";
|
||||
await LoadTasksAsync();
|
||||
await LoadStatisticsAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusMessage = $"任务启动失败: {result.Message}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "启动任务失败");
|
||||
StatusMessage = "任务启动失败";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暂停任务命令。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private async Task PauseTaskAsync()
|
||||
{
|
||||
if (SelectedTask == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
StatusMessage = "正在暂停任务...";
|
||||
|
||||
var result = await _trainingTaskService.PauseTask(SelectedTask.Id, "当前用户");
|
||||
if (result.Succeeded)
|
||||
{
|
||||
StatusMessage = "任务暂停成功";
|
||||
await LoadTasksAsync();
|
||||
await LoadStatisticsAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusMessage = $"任务暂停失败: {result.Message}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "暂停任务失败");
|
||||
StatusMessage = "任务暂停失败";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止任务命令。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private async Task StopTaskAsync()
|
||||
{
|
||||
if (SelectedTask == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
StatusMessage = "正在停止任务...";
|
||||
|
||||
var result = await _trainingTaskService.StopTask(SelectedTask.Id, "当前用户");
|
||||
if (result.Succeeded)
|
||||
{
|
||||
StatusMessage = "任务停止成功";
|
||||
await LoadTasksAsync();
|
||||
await LoadStatisticsAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusMessage = $"任务停止失败: {result.Message}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "停止任务失败");
|
||||
StatusMessage = "任务停止失败";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消任务命令。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private async Task CancelTaskAsync()
|
||||
{
|
||||
if (SelectedTask == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
StatusMessage = "正在取消任务...";
|
||||
|
||||
var result = await _trainingTaskService.CancelTask(SelectedTask.Id, "当前用户");
|
||||
if (result.Succeeded)
|
||||
{
|
||||
StatusMessage = "任务取消成功";
|
||||
await LoadTasksAsync();
|
||||
await LoadStatisticsAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusMessage = $"任务取消失败: {result.Message}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "取消任务失败");
|
||||
StatusMessage = "任务取消失败";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重新启动任务命令。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private async Task RestartTaskAsync()
|
||||
{
|
||||
if (SelectedTask == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
StatusMessage = "正在重新启动任务...";
|
||||
|
||||
var result = await _trainingTaskService.RestartTask(SelectedTask.Id, "当前用户");
|
||||
if (result.Succeeded)
|
||||
{
|
||||
StatusMessage = "任务重新启动成功";
|
||||
await LoadTasksAsync();
|
||||
await LoadStatisticsAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusMessage = $"任务重新启动失败: {result.Message}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "重新启动任务失败");
|
||||
StatusMessage = "任务重新启动失败";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除任务命令。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private async Task DeleteTaskAsync()
|
||||
{
|
||||
if (SelectedTask == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
StatusMessage = "正在删除任务...";
|
||||
|
||||
var result = await _trainingTaskService.DeleteTask(SelectedTask.Id);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
StatusMessage = "任务删除成功";
|
||||
SelectedTask = null;
|
||||
await LoadTasksAsync();
|
||||
await LoadStatisticsAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusMessage = $"任务删除失败: {result.Message}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "删除任务失败");
|
||||
StatusMessage = "任务删除失败";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查看任务详情命令。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private async Task ViewTaskDetailsAsync()
|
||||
{
|
||||
if (SelectedTask == null) return;
|
||||
|
||||
// TODO: 实现查看任务详情功能
|
||||
StatusMessage = $"查看任务详情: {SelectedTask.Name}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载数据。
|
||||
/// </summary>
|
||||
private async Task LoadDataAsync()
|
||||
{
|
||||
await Task.WhenAll(LoadTasksAsync(), LoadStatisticsAsync());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载任务列表。
|
||||
/// </summary>
|
||||
private async Task LoadTasksAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _trainingTaskService.GetTaskPagedList(
|
||||
CurrentPageIndex,
|
||||
PageSize,
|
||||
SelectedStatus,
|
||||
string.IsNullOrWhiteSpace(SearchKeyword) ? null : SearchKeyword);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var tasks = result.Data.Items.ToList();
|
||||
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
Tasks.Clear();
|
||||
foreach (var task in tasks)
|
||||
{
|
||||
Tasks.Add(task);
|
||||
}
|
||||
TotalCount = result.Data.TotalCount;
|
||||
TotalPages = result.Data.TotalPages;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("加载任务列表失败: {Message}", result.Message);
|
||||
StatusMessage = "加载任务列表失败";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "加载任务列表异常");
|
||||
StatusMessage = "加载任务列表异常";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载统计信息。
|
||||
/// </summary>
|
||||
private async Task LoadStatisticsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _trainingTaskService.GetTaskStatistics();
|
||||
if (result.Succeeded)
|
||||
{
|
||||
Statistics = result.Data;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("加载统计信息失败: {Message}", result.Message);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "加载统计信息异常");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 选中任务变化时更新命令状态。
|
||||
/// </summary>
|
||||
partial void OnSelectedTaskChanged(TrainingTaskModel? value)
|
||||
{
|
||||
OnPropertyChanged(nameof(HasSelectedTask));
|
||||
OnPropertyChanged(nameof(CanStartTask));
|
||||
OnPropertyChanged(nameof(CanPauseTask));
|
||||
OnPropertyChanged(nameof(CanStopTask));
|
||||
OnPropertyChanged(nameof(CanCancelTask));
|
||||
OnPropertyChanged(nameof(CanRestartTask));
|
||||
OnPropertyChanged(nameof(CanDeleteTask));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动自动刷新。
|
||||
/// </summary>
|
||||
public void StartAutoRefresh()
|
||||
{
|
||||
_refreshTimer.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止自动刷新。
|
||||
/// </summary>
|
||||
public void StopAutoRefresh()
|
||||
{
|
||||
_refreshTimer.Stop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源。
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_refreshTimer?.Stop();
|
||||
_refreshTimer?.Stop();
|
||||
}
|
||||
}
|
||||
393
OrpaonVision.ConfigApp/ViewModels/UserManagementViewModel.cs
Normal file
393
OrpaonVision.ConfigApp/ViewModels/UserManagementViewModel.cs
Normal file
@@ -0,0 +1,393 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Media;
|
||||
using OrpaonVision.Core.Security;
|
||||
using OrpaonVision.Core.Security.Contracts;
|
||||
using OrpaonVision.Core.Security.Contracts.Commands;
|
||||
using OrpaonVision.Core.Security.Contracts.Queries;
|
||||
|
||||
namespace OrpaonVision.ConfigApp.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// 用户管理ViewModel。
|
||||
/// </summary>
|
||||
public sealed class UserManagementViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private readonly IUserAppService _userAppService;
|
||||
|
||||
private string _statusText = "准备就绪。";
|
||||
private Brush _statusBrush = Brushes.DarkGreen;
|
||||
private string _outputText = string.Empty;
|
||||
private UserDetailDto? _selectedUser;
|
||||
private string _searchKeyword = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数。
|
||||
/// </summary>
|
||||
public UserManagementViewModel(IUserAppService userAppService)
|
||||
{
|
||||
_userAppService = userAppService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 状态文本。
|
||||
/// </summary>
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
private set => SetProperty(ref _statusText, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 状态画刷。
|
||||
/// </summary>
|
||||
public Brush StatusBrush
|
||||
{
|
||||
get => _statusBrush;
|
||||
private set => SetProperty(ref _statusBrush, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 输出文本。
|
||||
/// </summary>
|
||||
public string OutputText
|
||||
{
|
||||
get => _outputText;
|
||||
private set => SetProperty(ref _outputText, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 选中的用户。
|
||||
/// </summary>
|
||||
public UserDetailDto? SelectedUser
|
||||
{
|
||||
get => _selectedUser;
|
||||
private set => SetProperty(ref _selectedUser, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 搜索关键词。
|
||||
/// </summary>
|
||||
public string SearchKeyword
|
||||
{
|
||||
get => _searchKeyword;
|
||||
set => SetProperty(ref _searchKeyword, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询用户详情。
|
||||
/// </summary>
|
||||
public async Task QueryUserDetailAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusBrush = Brushes.DarkGreen;
|
||||
StatusText = "正在查询用户详情...";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(SearchKeyword))
|
||||
{
|
||||
StatusBrush = Brushes.Orange;
|
||||
StatusText = "请输入用户ID或用户名";
|
||||
return;
|
||||
}
|
||||
|
||||
// 这里演示如何查询用户详情
|
||||
// 实际实现需要根据SearchKeyword判断是用户ID还是用户名
|
||||
var userId = Guid.TryParse(SearchKeyword, out var parsedId) ? parsedId : Guid.NewGuid();
|
||||
var result = await _userAppService.GetDetailAsync(userId);
|
||||
|
||||
if (result.Succeeded && result.Data != null)
|
||||
{
|
||||
SelectedUser = result.Data;
|
||||
StatusText = "用户详情查询成功";
|
||||
OutputText = $"用户名:{result.Data.UserName}\n" +
|
||||
$"显示名称:{result.Data.DisplayName}\n" +
|
||||
$"邮箱:{result.Data.Email}\n" +
|
||||
$"状态:{result.Data.Status}\n" +
|
||||
$"角色数:{result.Data.Roles.Count}\n" +
|
||||
$"权限数:{result.Data.Permissions.Count}\n" +
|
||||
$"最后登录:{result.Data.LastLoginAtUtc:yyyy-MM-dd HH:mm}";
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "查询用户详情失败";
|
||||
OutputText = $"错误:{result.Message}";
|
||||
SelectedUser = null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "查询用户详情异常";
|
||||
OutputText = $"异常:{ex.Message}";
|
||||
SelectedUser = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建新用户。
|
||||
/// </summary>
|
||||
public async Task CreateUserAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusBrush = Brushes.DarkGreen;
|
||||
StatusText = "正在创建新用户...";
|
||||
|
||||
// 这里演示如何创建用户
|
||||
var command = new OrpaonVision.Core.Security.Contracts.Commands.CreateUserCommand
|
||||
{
|
||||
UserName = "newuser_" + DateTime.Now.Ticks,
|
||||
DisplayName = "新用户",
|
||||
Email = "newuser@example.com",
|
||||
Password = "TempPassword123!",
|
||||
Roles = ["Operator"],
|
||||
Permissions = ["production.view", "training.view"],
|
||||
CreatedBy = "Admin"
|
||||
};
|
||||
|
||||
var result = await _userAppService.CreateAsync(command);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
StatusText = "用户创建成功";
|
||||
OutputText = $"用户ID:{result.Data}\n" +
|
||||
$"用户名:{command.UserName}\n" +
|
||||
$"显示名称:{command.DisplayName}\n" +
|
||||
$"角色:{string.Join(", ", command.Roles)}\n" +
|
||||
$"请及时修改初始密码";
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "创建用户失败";
|
||||
OutputText = $"错误:{result.Message}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "创建用户异常";
|
||||
OutputText = $"异常:{ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新用户信息。
|
||||
/// </summary>
|
||||
public async Task UpdateUserAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusBrush = Brushes.DarkGreen;
|
||||
StatusText = "正在更新用户信息...";
|
||||
|
||||
if (SelectedUser == null)
|
||||
{
|
||||
StatusBrush = Brushes.Orange;
|
||||
StatusText = "请先选择要更新的用户";
|
||||
return;
|
||||
}
|
||||
|
||||
// 这里演示如何更新用户信息
|
||||
var command = new OrpaonVision.Core.Security.Contracts.Commands.UpdateUserCommand
|
||||
{
|
||||
UserId = SelectedUser.UserId,
|
||||
DisplayName = SelectedUser.DisplayName + " (已更新)",
|
||||
Email = SelectedUser.Email,
|
||||
IsActive = SelectedUser.IsActive,
|
||||
Roles = SelectedUser.Roles.ToList(),
|
||||
UpdatedBy = "Admin"
|
||||
};
|
||||
|
||||
var result = await _userAppService.UpdateAsync(command);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
StatusText = "用户信息更新成功";
|
||||
OutputText = $"用户ID:{command.UserId}\n" +
|
||||
$"显示名称:{command.DisplayName}\n" +
|
||||
$"状态:{(command.IsActive ? "激活" : "停用")}\n" +
|
||||
$"角色:{string.Join(", ", command.Roles)}";
|
||||
|
||||
// 重新查询用户详情以更新显示
|
||||
await QueryUserDetailAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "更新用户信息失败";
|
||||
OutputText = $"错误:{result.Message}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "更新用户信息异常";
|
||||
OutputText = $"异常:{ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除用户。
|
||||
/// </summary>
|
||||
public async Task DeleteUserAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusBrush = Brushes.DarkGreen;
|
||||
StatusText = "正在删除用户...";
|
||||
|
||||
if (SelectedUser == null)
|
||||
{
|
||||
StatusBrush = Brushes.Orange;
|
||||
StatusText = "请先选择要删除的用户";
|
||||
return;
|
||||
}
|
||||
|
||||
// 这里演示如何删除用户
|
||||
var command = new OrpaonVision.Core.Security.Contracts.Commands.DeleteUserCommand
|
||||
{
|
||||
UserId = SelectedUser.UserId,
|
||||
DeletedBy = "Admin"
|
||||
};
|
||||
|
||||
var result = await _userAppService.DeleteAsync(command);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
StatusText = "用户删除成功";
|
||||
OutputText = $"已删除用户:{SelectedUser.UserName} ({SelectedUser.DisplayName})\n" +
|
||||
$"用户ID:{command.UserId}\n" +
|
||||
$"操作时间:{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}";
|
||||
|
||||
SelectedUser = null;
|
||||
SearchKeyword = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "删除用户失败";
|
||||
OutputText = $"错误:{result.Message}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "删除用户异常";
|
||||
OutputText = $"异常:{ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置用户密码。
|
||||
/// </summary>
|
||||
public async Task ResetUserPasswordAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusBrush = Brushes.DarkGreen;
|
||||
StatusText = "正在重置用户密码...";
|
||||
|
||||
if (SelectedUser == null)
|
||||
{
|
||||
StatusBrush = Brushes.Orange;
|
||||
StatusText = "请先选择要重置密码的用户";
|
||||
return;
|
||||
}
|
||||
|
||||
// 这里演示如何重置用户密码
|
||||
var command = new OrpaonVision.Core.Security.Contracts.Commands.ResetUserPasswordCommand
|
||||
{
|
||||
UserId = SelectedUser.UserId,
|
||||
NewPassword = "NewTempPassword123!",
|
||||
ForceChangeOnNextLogin = true,
|
||||
ResetBy = "Admin"
|
||||
};
|
||||
|
||||
var result = await _userAppService.ResetPasswordAsync(command);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
StatusText = "密码重置成功";
|
||||
OutputText = $"用户:{SelectedUser.UserName}\n" +
|
||||
$"新密码:{command.NewPassword}\n" +
|
||||
$"下次登录必须修改:{(command.ForceChangeOnNextLogin ? "是" : "否")}\n" +
|
||||
$"请及时通知用户";
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "重置密码失败";
|
||||
OutputText = $"错误:{result.Message}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "重置密码异常";
|
||||
OutputText = $"异常:{ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询用户列表。
|
||||
/// </summary>
|
||||
public async Task QueryUserListAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusBrush = Brushes.DarkGreen;
|
||||
StatusText = "正在查询用户列表...";
|
||||
|
||||
// 这里演示如何分页查询用户列表
|
||||
var query = new OrpaonVision.Core.Security.Contracts.Queries.UserQueryDto
|
||||
{
|
||||
Keyword = SearchKeyword,
|
||||
PageIndex = 1,
|
||||
PageSize = 20,
|
||||
SortField = "UserName",
|
||||
SortDirection = "ASC"
|
||||
};
|
||||
|
||||
var result = await _userAppService.GetPagedListAsync(query);
|
||||
|
||||
if (result.Succeeded && result.Data != null)
|
||||
{
|
||||
StatusText = "用户列表查询成功";
|
||||
OutputText = $"查询条件:{query.Keyword ?? "全部"}\n" +
|
||||
$"用户总数:{result.Data.TotalCount}\n" +
|
||||
$"当前页:{result.Data.PageIndex}/{result.Data.TotalPages}\n" +
|
||||
$"本页记录:{result.Data.Items.Count}\n" +
|
||||
$"查询时间:{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}";
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "查询用户列表失败";
|
||||
OutputText = $"错误:{result.Message}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusBrush = Brushes.OrangeRed;
|
||||
StatusText = "查询用户列表异常";
|
||||
OutputText = $"异常:{ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private void SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user