版本260406

This commit is contained in:
2026-04-06 22:04:05 +08:00
parent 7dc5e73af7
commit 0b150470be
216 changed files with 98993 additions and 33 deletions

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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

View File

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

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