using OrpaonVision.ConfigApp.Annotation.Options; using OrpaonVision.Core.Annotation; using OrpaonVision.Core.Annotation.Contracts; using OrpaonVision.Core.Configuration; using OrpaonVision.Core.Configuration.Contracts; using OrpaonVision.Core.Enums; using OrpaonVision.ConfigApp.Views; using OrpaonVision.ConfigApp.Infrastructure.Services; using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Text.Json; using System.Windows; using System.Windows.Media; using Microsoft.Extensions.DependencyInjection; namespace OrpaonVision.ConfigApp.ViewModels; /// /// 主窗口 ViewModel。 /// /// 职责: /// - 持有页面输入参数与输出状态; /// - 统一编排“检查连通状态”和“发起同步”两类操作; /// - 将服务层返回结果转换为界面可直接展示的文本。 /// public sealed class MainWindowViewModel : INotifyPropertyChanged { private readonly IAnnotationSyncAppService _annotationSyncAppService; private readonly IRuleConfigurationAppService _ruleConfigurationAppService; private readonly CurrentUserContext _currentUserContext; private readonly IServiceProvider _serviceProvider; private string _projectIdText = string.Empty; private string _annotationTaskIdText = string.Empty; private string _cvatEndpointText = string.Empty; private string _cvatProjectIdText = string.Empty; private string _cvatTaskIdText = string.Empty; private string _requestedByText = string.Empty; private string _statusText = "等待操作"; private string _outputText = "请填写参数后执行“检查连通状态”或“发起同步”。"; private Brush _statusBrush = Brushes.DarkGreen; private string _friendlyHintText = "请输入有效参数后执行操作。"; private Brush _friendlyHintBrush = Brushes.DimGray; private string _detailTaskIdText = "-"; private string _detailTaskNameText = "-"; private string _detailTaskStatusText = "-"; private string _detailProjectIdText = "-"; private string _detailItemCountText = "-"; private string _detailUpdatedAtText = "-"; private string _ruleProductTypeCodeText = "MACHINE-A"; private string _ruleProductTypeNameText = "默认机种"; private RuleLayerInputRow? _selectedRuleLayer; private RulePartInputRow? _selectedRulePart; private RuleRoiInputRow? _selectedRuleRoi; private RuleItemInputRow? _selectedRuleItem; private string _publishedRuleVersionText = "-"; private string _publishedRuleAtText = "-"; private string _compareSourceVersionText = string.Empty; private string _compareTargetVersionText = string.Empty; private string _rollbackTargetVersionText = string.Empty; private string _disableVersionNoText = string.Empty; private string _versionDetailNoText = string.Empty; private string _versionPageIndexText = "1"; private string _versionPageSizeText = "20"; private bool _isBusy; /// /// 主窗口 ViewModel 构造函数。 /// /// 标注同步应用服务。 /// 规则配置应用服务。 /// 当前用户上下文。 /// CVAT 配置。 /// 服务提供程序。 public MainWindowViewModel( IAnnotationSyncAppService annotationSyncAppService, IRuleConfigurationAppService ruleConfigurationAppService, CurrentUserContext currentUserContext, CvatOptions cvatOptions, IServiceProvider serviceProvider) { _annotationSyncAppService = annotationSyncAppService; _ruleConfigurationAppService = ruleConfigurationAppService; _currentUserContext = currentUserContext; _serviceProvider = serviceProvider; ProjectIdText = Guid.NewGuid().ToString("D"); AnnotationTaskIdText = Guid.NewGuid().ToString("D"); CvatEndpointText = cvatOptions.ServerEndpoint; CvatProjectIdText = "1"; CvatTaskIdText = "1"; RequestedByText = Environment.UserName; AddDefaultRuleRows(); } /// /// 保存规则草稿。 /// public async Task SaveRuleDraftAsync() { if (string.IsNullOrWhiteSpace(RuleProductTypeCodeText) || string.IsNullOrWhiteSpace(RuleProductTypeNameText)) { SetError("RULE_PRODUCT_TYPE_REQUIRED", "机种编码和名称不能为空。", Array.Empty()); return; } SetBusy(true); try { StatusBrush = Brushes.DarkGreen; StatusText = "正在保存规则草稿..."; SetFriendlyHint("正在保存机种规则配置草稿,请稍候...", Brushes.SteelBlue); if (!TryBuildRuleDraft(out var draft)) { return; } var result = await _ruleConfigurationAppService.SaveDraftAsync(draft); if (!result.Succeeded) { SetError(result.Code, result.Message, result.Errors); return; } StatusBrush = Brushes.DarkGreen; StatusText = "规则草稿保存成功"; SetFriendlyHint("草稿已保存,可继续点击“发布规则版本”。", Brushes.DarkGreen); OutputText = $"Succeeded: {result.Succeeded}{Environment.NewLine}" + $"Code: {result.Code}{Environment.NewLine}" + $"Message: {result.Message}{Environment.NewLine}" + $"ProductTypeCode: {draft.ProductTypeCode}"; } catch (Exception ex) { SetError("RULE_DRAFT_UI_EXCEPTION", "保存规则草稿出现未处理异常。", new[] { ex.Message }); } finally { SetBusy(false); } } /// /// 停用指定规则版本。 /// public async Task DisableVersionAsync() { if (string.IsNullOrWhiteSpace(RuleProductTypeCodeText)) { SetError("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。", Array.Empty()); return; } var versionNo = DisableVersionNoText.Trim(); if (string.IsNullOrWhiteSpace(versionNo)) { SetError("RULE_VERSION_NO_REQUIRED", "请输入要停用的版本号。", Array.Empty()); return; } SetBusy(true); try { StatusBrush = Brushes.DarkGreen; StatusText = "正在停用规则版本..."; SetFriendlyHint("正在写入停用审计记录...", Brushes.SteelBlue); var result = await _ruleConfigurationAppService.DisableVersionAsync( RuleProductTypeCodeText.Trim(), versionNo, RequestedByText.Trim()); if (!result.Succeeded) { SetError(result.Code, result.Message, result.Errors); return; } StatusText = "规则版本停用成功"; StatusBrush = Brushes.DarkGreen; SetFriendlyHint($"版本 {versionNo} 已停用。", Brushes.DarkGreen); OutputText = $"Succeeded: {result.Succeeded}{Environment.NewLine}" + $"Code: {result.Code}{Environment.NewLine}" + $"Message: {result.Message}{Environment.NewLine}" + $"ProductTypeCode: {RuleProductTypeCodeText.Trim()}{Environment.NewLine}" + $"DisabledVersionNo: {versionNo}"; } catch (Exception ex) { SetError("RULE_VERSION_DISABLE_UI_EXCEPTION", "停用规则版本出现未处理异常。", new[] { ex.Message }); } finally { SetBusy(false); } } /// /// 查询指定规则版本详情。 /// public async Task QueryVersionDetailAsync() { if (string.IsNullOrWhiteSpace(RuleProductTypeCodeText)) { SetError("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。", Array.Empty()); return; } var versionNo = VersionDetailNoText.Trim(); if (string.IsNullOrWhiteSpace(versionNo)) { SetError("RULE_VERSION_NO_REQUIRED", "请输入要查询详情的版本号。", Array.Empty()); return; } SetBusy(true); try { StatusBrush = Brushes.DarkGreen; StatusText = "正在查询规则版本详情..."; SetFriendlyHint("正在读取版本快照与状态信息...", Brushes.SteelBlue); var result = await _ruleConfigurationAppService.GetVersionDetailAsync( RuleProductTypeCodeText.Trim(), versionNo); if (!result.Succeeded || result.Data is null) { SetError(result.Code, result.Message, result.Errors); return; } StatusText = "规则版本详情查询成功"; StatusBrush = Brushes.DarkGreen; SetFriendlyHint($"版本 {versionNo} 详情已加载。", Brushes.DarkGreen); OutputText = $"Succeeded: {result.Succeeded}{Environment.NewLine}" + $"Code: {result.Code}{Environment.NewLine}" + $"Message: {result.Message}{Environment.NewLine}" + $"ProductTypeCode: {result.Data.ProductTypeCode}{Environment.NewLine}" + $"VersionNo: {result.Data.VersionNo}{Environment.NewLine}" + $"PublishedAtUtc: {result.Data.PublishedAtUtc:O}{Environment.NewLine}" + $"PublishedBy: {result.Data.PublishedBy}{Environment.NewLine}" + $"IsDisabled: {result.Data.IsDisabled}{Environment.NewLine}" + $"DisabledAtUtc: {(result.Data.DisabledAtUtc?.ToString("O") ?? "-")}{Environment.NewLine}" + $"DisabledBy: {(string.IsNullOrWhiteSpace(result.Data.DisabledBy) ? "-" : result.Data.DisabledBy)}{Environment.NewLine}" + $"SnapshotLength: {result.Data.SnapshotJson.Length}"; } catch (Exception ex) { SetError("RULE_VERSION_DETAIL_UI_EXCEPTION", "查询规则版本详情出现未处理异常。", new[] { ex.Message }); } finally { SetBusy(false); } } /// /// 分页查询规则版本列表。 /// public async Task QueryVersionPagedListAsync() { if (string.IsNullOrWhiteSpace(RuleProductTypeCodeText)) { SetError("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。", Array.Empty()); return; } if (!int.TryParse(VersionPageIndexText.Trim(), out var pageIndex) || pageIndex < 1) { SetError("RULE_PAGE_INDEX_INVALID", "页码必须是大于 0 的整数。", Array.Empty()); return; } if (!int.TryParse(VersionPageSizeText.Trim(), out var pageSize) || pageSize < 1 || pageSize > 100) { SetError("RULE_PAGE_SIZE_INVALID", "每页大小必须在 1-100 之间。", Array.Empty()); return; } SetBusy(true); try { StatusBrush = Brushes.DarkGreen; StatusText = "正在分页查询规则版本..."; SetFriendlyHint("正在按页读取规则版本清单...", Brushes.SteelBlue); RuleVersionRows.Clear(); var result = await _ruleConfigurationAppService.GetVersionPagedListAsync( RuleProductTypeCodeText.Trim(), pageIndex, pageSize); if (!result.Succeeded || result.Data is null) { SetError(result.Code, result.Message, result.Errors); return; } foreach (var item in result.Data.Items) { RuleVersionRows.Add(new RuleVersionListViewRow { VersionNo = item.VersionNo, PublishedAtUtcText = item.PublishedAtUtc.ToString("O"), PublishedBy = string.IsNullOrWhiteSpace(item.PublishedBy) ? "-" : item.PublishedBy, IsDisabledText = item.IsDisabled ? "是" : "否", DisabledAtUtcText = item.DisabledAtUtc?.ToString("O") ?? "-", DisabledBy = string.IsNullOrWhiteSpace(item.DisabledBy) ? "-" : item.DisabledBy }); } StatusText = "规则版本分页查询成功"; StatusBrush = Brushes.DarkGreen; SetFriendlyHint($"已加载第 {result.Data.PageIndex} 页,共 {result.Data.TotalCount} 条。", Brushes.DarkGreen); OutputText = $"Succeeded: {result.Succeeded}{Environment.NewLine}" + $"Code: {result.Code}{Environment.NewLine}" + $"Message: {result.Message}{Environment.NewLine}" + $"PageIndex: {result.Data.PageIndex}{Environment.NewLine}" + $"PageSize: {result.Data.PageSize}{Environment.NewLine}" + $"TotalCount: {result.Data.TotalCount}{Environment.NewLine}" + $"TotalPages: {result.Data.TotalPages}{Environment.NewLine}" + $"ItemCount: {result.Data.Items.Count}"; } catch (Exception ex) { SetError("RULE_VERSION_PAGED_LIST_UI_EXCEPTION", "分页查询规则版本出现未处理异常。", new[] { ex.Message }); } finally { SetBusy(false); } } /// /// 发布规则版本。 /// public async Task PublishRuleVersionAsync() { if (string.IsNullOrWhiteSpace(RuleProductTypeCodeText)) { SetError("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。", Array.Empty()); return; } SetBusy(true); try { StatusBrush = Brushes.DarkGreen; StatusText = "正在发布规则版本..."; SetFriendlyHint("正在将草稿固化为版本快照,请稍候...", Brushes.SteelBlue); var result = await _ruleConfigurationAppService.PublishAsync(RuleProductTypeCodeText.Trim(), RequestedByText.Trim()); if (!result.Succeeded || result.Data is null) { SetError(result.Code, result.Message, result.Errors); return; } PublishedRuleVersionText = result.Data.VersionNo; PublishedRuleAtText = result.Data.PublishedAtUtc.ToString("O"); StatusBrush = Brushes.DarkGreen; StatusText = "规则版本发布成功"; SetFriendlyHint("已生成发布版本,可用于现场侧规则加载。", Brushes.DarkGreen); OutputText = $"Succeeded: {result.Succeeded}{Environment.NewLine}" + $"Code: {result.Code}{Environment.NewLine}" + $"Message: {result.Message}{Environment.NewLine}" + $"VersionNo: {result.Data.VersionNo}{Environment.NewLine}" + $"PublishedAtUtc: {result.Data.PublishedAtUtc:O}"; } catch (Exception ex) { SetError("RULE_PUBLISH_UI_EXCEPTION", "发布规则版本出现未处理异常。", new[] { ex.Message }); } finally { SetBusy(false); } } /// /// 属性变更事件。 /// public event PropertyChangedEventHandler? PropertyChanged; /// /// 本地项目 ID 文本。 /// public string ProjectIdText { get => _projectIdText; set => SetField(ref _projectIdText, value); } /// /// 本地标注任务 ID 文本。 /// public string AnnotationTaskIdText { get => _annotationTaskIdText; set => SetField(ref _annotationTaskIdText, value); } /// /// CVAT 服务地址文本。 /// public string CvatEndpointText { get => _cvatEndpointText; set => SetField(ref _cvatEndpointText, value); } /// /// CVAT 项目 ID 文本。 /// public string CvatProjectIdText { get => _cvatProjectIdText; set => SetField(ref _cvatProjectIdText, value); } /// /// CVAT 任务 ID 文本。 /// public string CvatTaskIdText { get => _cvatTaskIdText; set => SetField(ref _cvatTaskIdText, value); } /// /// 请求人文本。 /// public string RequestedByText { get => _requestedByText; set => SetField(ref _requestedByText, value); } /// /// 当前用户ID。 /// public Guid CurrentUserId => _currentUserContext.CurrentUserId; /// /// 状态栏文本。 /// public string StatusText { get => _statusText; set => SetField(ref _statusText, value); } /// /// 结果详情文本。 /// public string OutputText { get => _outputText; set => SetField(ref _outputText, value); } /// /// 状态文本颜色。 /// public Brush StatusBrush { get => _statusBrush; private set => SetField(ref _statusBrush, value); } /// /// 友好提示文本。 /// public string FriendlyHintText { get => _friendlyHintText; private set => SetField(ref _friendlyHintText, value); } /// /// 友好提示颜色。 /// public Brush FriendlyHintBrush { get => _friendlyHintBrush; private set => SetField(ref _friendlyHintBrush, value); } /// /// 任务详情-TaskId。 /// public string DetailTaskIdText { get => _detailTaskIdText; private set => SetField(ref _detailTaskIdText, value); } /// /// 任务详情-TaskName。 /// public string DetailTaskNameText { get => _detailTaskNameText; private set => SetField(ref _detailTaskNameText, value); } /// /// 任务详情-TaskStatus。 /// public string DetailTaskStatusText { get => _detailTaskStatusText; private set => SetField(ref _detailTaskStatusText, value); } /// /// 任务详情-ProjectId。 /// public string DetailProjectIdText { get => _detailProjectIdText; private set => SetField(ref _detailProjectIdText, value); } /// /// 任务详情-ItemCount。 /// public string DetailItemCountText { get => _detailItemCountText; private set => SetField(ref _detailItemCountText, value); } /// /// 任务详情-UpdatedAtUtc。 /// public string DetailUpdatedAtText { get => _detailUpdatedAtText; private set => SetField(ref _detailUpdatedAtText, value); } /// /// 规则机种编码。 /// public string RuleProductTypeCodeText { get => _ruleProductTypeCodeText; set => SetField(ref _ruleProductTypeCodeText, value); } /// /// 规则机种名称。 /// public string RuleProductTypeNameText { get => _ruleProductTypeNameText; set => SetField(ref _ruleProductTypeNameText, value); } /// /// 层配置列表。 /// public ObservableCollection RuleLayers { get; } = []; /// /// 部件配置列表。 /// public ObservableCollection RuleParts { get; } = []; /// /// ROI 配置列表。 /// public ObservableCollection RuleRois { get; } = []; /// /// 规则项配置列表。 /// public ObservableCollection RuleItems { get; } = []; /// /// 规则版本审计记录列表。 /// public ObservableCollection RuleAuditRows { get; } = []; /// /// 规则版本分页展示列表。 /// public ObservableCollection RuleVersionRows { get; } = []; /// /// 当前选中层。 /// public RuleLayerInputRow? SelectedRuleLayer { get => _selectedRuleLayer; set => SetField(ref _selectedRuleLayer, value); } /// /// 当前选中部件。 /// public RulePartInputRow? SelectedRulePart { get => _selectedRulePart; set => SetField(ref _selectedRulePart, value); } /// /// 当前选中 ROI。 /// public RuleRoiInputRow? SelectedRuleRoi { get => _selectedRuleRoi; set => SetField(ref _selectedRuleRoi, value); } /// /// 当前选中规则项。 /// public RuleItemInputRow? SelectedRuleItem { get => _selectedRuleItem; set => SetField(ref _selectedRuleItem, value); } /// /// 新增层。 /// public void AddRuleLayerRow() { RuleLayers.Add(new RuleLayerInputRow { LayerNoText = (RuleLayers.Count + 1).ToString(), LayerName = $"Layer{RuleLayers.Count + 1}" }); } /// /// 删除选中层。 /// public void RemoveSelectedRuleLayerRow() { if (SelectedRuleLayer is null) { return; } RuleLayers.Remove(SelectedRuleLayer); SelectedRuleLayer = null; } /// /// 新增部件。 /// public void AddRulePartRow() { RuleParts.Add(new RulePartInputRow { PartCode = $"PART-{RuleParts.Count + 1:000}", PartName = $"Part{RuleParts.Count + 1}", LayerNoText = "1" }); } /// /// 删除选中部件。 /// public void RemoveSelectedRulePartRow() { if (SelectedRulePart is null) { return; } RuleParts.Remove(SelectedRulePart); SelectedRulePart = null; } /// /// 新增 ROI。 /// public void AddRuleRoiRow() { RuleRois.Add(new RuleRoiInputRow { RoiCode = $"ROI-{RuleRois.Count + 1:000}", PartCode = RuleParts.FirstOrDefault()?.PartCode ?? "PART-001", XText = "0", YText = "0", WidthText = "100", HeightText = "100" }); } /// /// 删除选中 ROI。 /// public void RemoveSelectedRuleRoiRow() { if (SelectedRuleRoi is null) { return; } RuleRois.Remove(SelectedRuleRoi); SelectedRuleRoi = null; } /// /// 新增规则项。 /// public void AddRuleItemRow() { RuleItems.Add(new RuleItemInputRow { RuleCode = $"RULE-{RuleItems.Count + 1:000}", RuleName = $"Rule{RuleItems.Count + 1}", RuleType = "ConfidenceThreshold", ParametersJson = "{\"minConfidence\":0.95}", Enabled = true }); } /// /// 删除选中规则项。 /// public void RemoveSelectedRuleItemRow() { if (SelectedRuleItem is null) { return; } RuleItems.Remove(SelectedRuleItem); SelectedRuleItem = null; } /// /// 最新发布版本号。 /// public string PublishedRuleVersionText { get => _publishedRuleVersionText; private set => SetField(ref _publishedRuleVersionText, value); } /// /// 最新发布时间。 /// public string PublishedRuleAtText { get => _publishedRuleAtText; private set => SetField(ref _publishedRuleAtText, value); } /// /// 对比源版本号。 /// public string CompareSourceVersionText { get => _compareSourceVersionText; set => SetField(ref _compareSourceVersionText, value); } /// /// 对比目标版本号。 /// public string CompareTargetVersionText { get => _compareTargetVersionText; set => SetField(ref _compareTargetVersionText, value); } /// /// 回滚目标版本号。 /// public string RollbackTargetVersionText { get => _rollbackTargetVersionText; set => SetField(ref _rollbackTargetVersionText, value); } /// /// 停用目标版本号。 /// public string DisableVersionNoText { get => _disableVersionNoText; set => SetField(ref _disableVersionNoText, value); } /// /// 详情查询版本号。 /// public string VersionDetailNoText { get => _versionDetailNoText; set => SetField(ref _versionDetailNoText, value); } /// /// 分页查询页码。 /// public string VersionPageIndexText { get => _versionPageIndexText; set => SetField(ref _versionPageIndexText, value); } /// /// 分页查询每页大小。 /// public string VersionPageSizeText { get => _versionPageSizeText; set => SetField(ref _versionPageSizeText, value); } /// /// 是否处于空闲状态(用于按钮可用性绑定)。 /// public bool IsIdle => !_isBusy; /// /// 执行“查询任务详情”。 /// public async Task QueryTaskDetailAsync() { if (!long.TryParse(CvatTaskIdText.Trim(), out var cvatTaskId) || cvatTaskId <= 0) { SetError("CVAT_TASK_ID_INVALID", "CVAT 任务 ID 必须是大于 0 的整数。", Array.Empty()); return; } SetBusy(true); try { StatusBrush = Brushes.DarkGreen; StatusText = "正在查询 CVAT 任务详情..."; SetFriendlyHint("正在查询任务详情,请稍候...", Brushes.SteelBlue); var result = await _annotationSyncAppService.GetTaskDetailAsync(cvatTaskId); if (!result.Succeeded || result.Data is null) { SetError(result.Code, result.Message, result.Errors); return; } StatusBrush = Brushes.DarkGreen; StatusText = $"任务详情查询完成:{result.Message}"; SetFriendlyHint("任务详情已更新,可直接核对任务状态、项目归属和数量。", Brushes.DarkGreen); ApplyTaskDetail(result.Data); OutputText = $"Succeeded: {result.Succeeded}{Environment.NewLine}" + $"Code: {result.Code}{Environment.NewLine}" + $"Message: {result.Message}{Environment.NewLine}" + $"TaskId: {result.Data.CvatTaskId}{Environment.NewLine}" + $"TaskName: {result.Data.TaskName}{Environment.NewLine}" + $"TaskStatus: {result.Data.TaskStatus}{Environment.NewLine}" + $"ProjectId: {result.Data.CvatProjectId}{Environment.NewLine}" + $"ItemCount: {result.Data.ItemCount}{Environment.NewLine}" + $"UpdatedAtUtc: {result.Data.UpdatedAtUtc:O}"; } catch (Exception ex) { SetError("UI_UNHANDLED_EXCEPTION", "查询任务详情出现未处理异常。", new[] { ex.Message }); } finally { SetBusy(false); } } /// /// 执行“检查连通状态”。 /// public async Task CheckStatusAsync() { if (!TryParseProjectId(out var projectId)) { return; } SetBusy(true); try { StatusBrush = Brushes.DarkGreen; StatusText = "正在检查 CVAT 连通状态..."; SetFriendlyHint("正在检查服务连通性与版本信息...", Brushes.SteelBlue); var result = await _annotationSyncAppService.GetSyncStatusAsync(projectId); if (!result.Succeeded || result.Data is null) { SetError(result.Code, result.Message, result.Errors); return; } StatusBrush = Brushes.DarkGreen; StatusText = $"状态检查完成:{result.Message}"; SetFriendlyHint("连通性检查完成,可继续执行任务详情查询或发起同步。", Brushes.DarkGreen); OutputText = $"Succeeded: {result.Succeeded}{Environment.NewLine}" + $"Code: {result.Code}{Environment.NewLine}" + $"Message: {result.Message}{Environment.NewLine}" + $"SyncStatus: {result.Data.SyncStatus}{Environment.NewLine}" + $"Progress: {result.Data.ProgressPercent}{Environment.NewLine}" + $"LastSyncedAtUtc: {result.Data.LastSyncedAtUtc:O}{Environment.NewLine}" + $"LastErrorMessage: {result.Data.LastErrorMessage}"; } catch (Exception ex) { SetError("UI_UNHANDLED_EXCEPTION", "状态检查出现未处理异常。", new[] { ex.Message }); } finally { SetBusy(false); } } /// /// 执行“发起同步”。 /// public async Task SyncAsync() { if (!TryBuildSyncCommand(out var command)) { return; } SetBusy(true); try { StatusBrush = Brushes.DarkGreen; StatusText = "正在发起 CVAT 同步..."; SetFriendlyHint("正在校验任务与项目关系并提交同步请求...", Brushes.SteelBlue); var result = await _annotationSyncAppService.SyncProjectAsync(command); if (!result.Succeeded) { SetError(result.Code, result.Message, result.Errors); return; } StatusBrush = Brushes.DarkGreen; StatusText = "同步请求成功"; SetFriendlyHint("同步请求已受理,请稍后再次检查状态。", Brushes.DarkGreen); OutputText = $"Succeeded: {result.Succeeded}{Environment.NewLine}" + $"Code: {result.Code}{Environment.NewLine}" + $"Message: {result.Message}"; } catch (Exception ex) { SetError("UI_UNHANDLED_EXCEPTION", "同步请求出现未处理异常。", new[] { ex.Message }); } finally { SetBusy(false); } } /// /// 执行规则版本对比。 /// public async Task CompareVersionsAsync() { if (string.IsNullOrWhiteSpace(RuleProductTypeCodeText)) { SetError("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。", Array.Empty()); return; } if (string.IsNullOrWhiteSpace(CompareSourceVersionText) || string.IsNullOrWhiteSpace(CompareTargetVersionText)) { SetError("RULE_VERSION_COMPARE_INPUT_REQUIRED", "请输入源版本号和目标版本号。", Array.Empty()); return; } SetBusy(true); try { StatusBrush = Brushes.DarkGreen; StatusText = "正在对比规则版本..."; SetFriendlyHint("正在获取版本快照并进行对比分析...", Brushes.SteelBlue); var result = await _ruleConfigurationAppService.CompareVersionsAsync( RuleProductTypeCodeText.Trim(), CompareSourceVersionText.Trim(), CompareTargetVersionText.Trim()); if (!result.Succeeded || result.Data is null) { SetError(result.Code, result.Message, result.Errors); return; } StatusBrush = Brushes.DarkGreen; StatusText = "版本对比完成"; var friendlyHint = result.Data.IsSame ? "版本内容完全一致。" : "版本内容存在差异,请查看详细日志。"; SetFriendlyHint(friendlyHint, result.Data.IsSame ? Brushes.DarkGreen : Brushes.Orange); OutputText = $"Succeeded: {result.Succeeded}{Environment.NewLine}" + $"Code: {result.Code}{Environment.NewLine}" + $"Message: {result.Message}{Environment.NewLine}" + $"ProductTypeCode: {result.Data.ProductTypeCode}{Environment.NewLine}" + $"SourceVersionNo: {result.Data.SourceVersionNo}{Environment.NewLine}" + $"TargetVersionNo: {result.Data.TargetVersionNo}{Environment.NewLine}" + $"IsSame: {result.Data.IsSame}{Environment.NewLine}" + $"Summary: {result.Data.Summary}{Environment.NewLine}" + $"SourceSnapshotLength: {result.Data.SourceSnapshotJson.Length}{Environment.NewLine}" + $"TargetSnapshotLength: {result.Data.TargetSnapshotJson.Length}"; } catch (Exception ex) { SetError("RULE_VERSION_COMPARE_UI_EXCEPTION", "版本对比出现未处理异常。", new[] { ex.Message }); } finally { SetBusy(false); } } /// /// 执行规则版本回滚。 /// public async Task RollbackVersionAsync() { if (string.IsNullOrWhiteSpace(RuleProductTypeCodeText)) { SetError("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。", Array.Empty()); return; } var targetVersion = RollbackTargetVersionText.Trim(); if (string.IsNullOrWhiteSpace(targetVersion)) { // 如果未指定目标版本,则回滚到最新版本的前一个版本 SetError("RULE_ROLLBACK_TARGET_REQUIRED", "请输入要回滚到的版本号。", Array.Empty()); return; } SetBusy(true); try { StatusBrush = Brushes.DarkGreen; StatusText = "正在回滚规则版本..."; SetFriendlyHint("正在创建回滚版本并复制目标快照...", Brushes.SteelBlue); var result = await _ruleConfigurationAppService.RollbackToVersionAsync( RuleProductTypeCodeText.Trim(), targetVersion, RequestedByText.Trim()); if (!result.Succeeded || result.Data is null) { SetError(result.Code, result.Message, result.Errors); return; } // 更新界面显示的版本信息 PublishedRuleVersionText = result.Data.VersionNo; PublishedRuleAtText = result.Data.PublishedAtUtc.ToString("O"); StatusBrush = Brushes.DarkGreen; StatusText = "版本回滚成功"; SetFriendlyHint($"已成功回滚到版本 {targetVersion},新版本号为 {result.Data.VersionNo}。", Brushes.DarkGreen); OutputText = $"Succeeded: {result.Succeeded}{Environment.NewLine}" + $"Code: {result.Code}{Environment.NewLine}" + $"Message: {result.Message}{Environment.NewLine}" + $"ProductTypeCode: {result.Data.ProductTypeCode}{Environment.NewLine}" + $"NewVersionNo: {result.Data.VersionNo}{Environment.NewLine}" + $"TargetVersionNo: {targetVersion}{Environment.NewLine}" + $"PublishedAtUtc: {result.Data.PublishedAtUtc:O}{Environment.NewLine}" + $"PublishedBy: {result.Data.PublishedBy}"; } catch (Exception ex) { SetError("RULE_ROLLBACK_UI_EXCEPTION", "版本回滚出现未处理异常。", new[] { ex.Message }); } finally { SetBusy(false); } } /// /// 查询最近规则版本审计记录。 /// public async Task QueryRecentRuleAuditsAsync() { if (string.IsNullOrWhiteSpace(RuleProductTypeCodeText)) { SetError("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。", Array.Empty()); return; } SetBusy(true); try { StatusBrush = Brushes.DarkGreen; StatusText = "正在查询规则版本审计..."; SetFriendlyHint("正在读取最近发布/回滚审计记录...", Brushes.SteelBlue); RuleAuditRows.Clear(); var result = await _ruleConfigurationAppService.GetRecentAuditsAsync(RuleProductTypeCodeText.Trim(), 20); if (!result.Succeeded || result.Data is null) { SetError(result.Code, result.Message, result.Errors); return; } StatusBrush = Brushes.DarkGreen; StatusText = "规则版本审计查询成功"; SetFriendlyHint($"已获取 {result.Data.Count} 条审计记录。", Brushes.DarkGreen); foreach (var audit in result.Data) { RuleAuditRows.Add(new RuleVersionAuditViewRow { ActionAtUtcText = audit.ActionAtUtc.ToString("O"), ActionType = string.IsNullOrWhiteSpace(audit.ActionType) ? "-" : audit.ActionType, VersionNo = string.IsNullOrWhiteSpace(audit.VersionNo) ? "-" : audit.VersionNo, SourceVersionNo = string.IsNullOrWhiteSpace(audit.SourceVersionNo) ? "-" : audit.SourceVersionNo, TargetVersionNo = string.IsNullOrWhiteSpace(audit.TargetVersionNo) ? "-" : audit.TargetVersionNo, OperatorName = string.IsNullOrWhiteSpace(audit.OperatorName) ? "-" : audit.OperatorName }); } OutputText = $"Succeeded: {result.Succeeded}{Environment.NewLine}" + $"Code: {result.Code}{Environment.NewLine}" + $"Message: {result.Message}{Environment.NewLine}" + $"Count: {result.Data.Count}"; } catch (Exception ex) { SetError("RULE_AUDIT_QUERY_UI_EXCEPTION", "查询规则版本审计出现未处理异常。", new[] { ex.Message }); } finally { SetBusy(false); } } private bool TryBuildSyncCommand(out SyncAnnotationProjectCommand command) { command = new SyncAnnotationProjectCommand(); if (!TryParseProjectId(out var projectId)) { return false; } if (!Guid.TryParse(AnnotationTaskIdText.Trim(), out var annotationTaskId) || annotationTaskId == Guid.Empty) { SetError("ANNOTATION_TASK_ID_INVALID", "本地标注任务 ID 不是有效 GUID。", Array.Empty()); return false; } if (string.IsNullOrWhiteSpace(CvatEndpointText)) { SetError("ANNOTATION_ENDPOINT_REQUIRED", "CVAT 服务地址不能为空。", Array.Empty()); return false; } if (!long.TryParse(CvatProjectIdText.Trim(), out var cvatProjectId) || cvatProjectId <= 0) { SetError("CVAT_PROJECT_ID_INVALID", "CVAT 项目 ID 必须是大于 0 的整数。", Array.Empty()); return false; } if (!long.TryParse(CvatTaskIdText.Trim(), out var cvatTaskId) || cvatTaskId <= 0) { SetError("CVAT_TASK_ID_INVALID", "CVAT 任务 ID 必须是大于 0 的整数。", Array.Empty()); return false; } command = new SyncAnnotationProjectCommand { Platform = AnnotationPlatformEnum.Cvat, ProjectId = projectId, AnnotationTaskId = annotationTaskId, CvatServerEndpoint = CvatEndpointText.Trim(), CvatProjectId = cvatProjectId, CvatTaskId = cvatTaskId, RequestedBy = RequestedByText.Trim() }; return true; } private bool TryBuildRuleDraft(out RuleConfigurationDraftDto draft) { draft = new RuleConfigurationDraftDto(); var productTypeCode = RuleProductTypeCodeText.Trim(); var productTypeName = RuleProductTypeNameText.Trim(); if (string.IsNullOrWhiteSpace(productTypeCode) || string.IsNullOrWhiteSpace(productTypeName)) { SetError("RULE_CONFIG_REQUIRED_FIELDS", "规则配置字段存在空值。", Array.Empty()); return false; } if (RuleLayers.Count == 0 || RuleParts.Count == 0 || RuleRois.Count == 0 || RuleItems.Count == 0) { SetError("RULE_CONFIG_LIST_EMPTY", "层、部件、ROI、规则列表均至少需要一条记录。", Array.Empty()); return false; } var layerDtos = new List(); foreach (var layer in RuleLayers) { if (string.IsNullOrWhiteSpace(layer.LayerName) || !TryParsePositiveInt(layer.LayerNoText, out var layerNo)) { SetError("RULE_LAYER_NO_INVALID", "层配置无效:层号必须为正整数且层名称不能为空。", Array.Empty()); return false; } layerDtos.Add(new LayerConfigurationDto { LayerNo = layerNo, LayerName = layer.LayerName.Trim() }); } var layerNoSet = layerDtos.Select(item => item.LayerNo).ToHashSet(); var partDtos = new List(); foreach (var part in RuleParts) { if (string.IsNullOrWhiteSpace(part.PartCode) || string.IsNullOrWhiteSpace(part.PartName) || !TryParsePositiveInt(part.LayerNoText, out var partLayerNo) || !layerNoSet.Contains(partLayerNo)) { SetError("RULE_PART_LAYER_NO_INVALID", "部件配置无效:请检查编码、名称和层号映射。", Array.Empty()); return false; } partDtos.Add(new PartConfigurationDto { PartCode = part.PartCode.Trim(), PartName = part.PartName.Trim(), LayerNo = partLayerNo }); } var partCodeSet = partDtos.Select(item => item.PartCode).ToHashSet(StringComparer.OrdinalIgnoreCase); var roiDtos = new List(); foreach (var roi in RuleRois) { if (string.IsNullOrWhiteSpace(roi.RoiCode) || string.IsNullOrWhiteSpace(roi.PartCode) || !partCodeSet.Contains(roi.PartCode.Trim()) || !int.TryParse(roi.XText.Trim(), out var roiX) || roiX < 0 || !int.TryParse(roi.YText.Trim(), out var roiY) || roiY < 0 || !TryParsePositiveInt(roi.WidthText, out var roiWidth) || !TryParsePositiveInt(roi.HeightText, out var roiHeight)) { SetError("RULE_ROI_INVALID", "ROI 配置无效:请检查编码、部件映射和坐标尺寸。", Array.Empty()); return false; } roiDtos.Add(new RoiConfigurationDto { RoiCode = roi.RoiCode.Trim(), PartCode = roi.PartCode.Trim(), X = roiX, Y = roiY, Width = roiWidth, Height = roiHeight }); } var ruleDtos = new List(); foreach (var rule in RuleItems) { if (string.IsNullOrWhiteSpace(rule.RuleCode) || string.IsNullOrWhiteSpace(rule.RuleName) || string.IsNullOrWhiteSpace(rule.RuleType) || string.IsNullOrWhiteSpace(rule.ParametersJson)) { SetError("RULE_CONFIG_REQUIRED_FIELDS", "规则项配置存在空值。", Array.Empty()); return false; } try { _ = JsonDocument.Parse(rule.ParametersJson); } catch (JsonException ex) { SetError("RULE_PARAMETERS_JSON_INVALID", "规则参数 JSON 格式不正确。", new[] { ex.Message }); return false; } ruleDtos.Add(new RuleItemConfigurationDto { RuleCode = rule.RuleCode.Trim(), RuleName = rule.RuleName.Trim(), RuleType = rule.RuleType.Trim(), ParametersJson = rule.ParametersJson.Trim(), Enabled = rule.Enabled }); } draft = new RuleConfigurationDraftDto { ProductTypeCode = productTypeCode, ProductTypeName = productTypeName, UpdatedBy = RequestedByText.Trim(), Layers = layerDtos, Parts = partDtos, Rois = roiDtos, Rules = ruleDtos }; return true; } private bool TryParseProjectId(out Guid projectId) { if (!Guid.TryParse(ProjectIdText.Trim(), out projectId) || projectId == Guid.Empty) { SetError("PROJECT_ID_INVALID", "本地项目 ID 不是有效 GUID。", Array.Empty()); return false; } return true; } private static bool TryParsePositiveInt(string text, out int value) { return int.TryParse(text.Trim(), out value) && value > 0; } private void AddDefaultRuleRows() { RuleLayers.Add(new RuleLayerInputRow { LayerNoText = "1", LayerName = "TopLayer" }); RuleParts.Add(new RulePartInputRow { PartCode = "PART-001", PartName = "DefaultPart", LayerNoText = "1" }); RuleRois.Add(new RuleRoiInputRow { RoiCode = "ROI-001", PartCode = "PART-001", XText = "100", YText = "100", WidthText = "200", HeightText = "150" }); RuleItems.Add(new RuleItemInputRow { RuleCode = "RULE-001", RuleName = "ConfidenceThreshold", RuleType = "ConfidenceThreshold", ParametersJson = "{\"minConfidence\":0.95}", Enabled = true }); } private void SetBusy(bool isBusy) { _isBusy = isBusy; OnPropertyChanged(nameof(IsIdle)); } private void SetError(string code, string message, IReadOnlyCollection errors) { StatusBrush = Brushes.IndianRed; StatusText = $"操作失败:{message}"; var (friendlyHint, hintBrush) = BuildFriendlyHint(code); SetFriendlyHint(friendlyHint, hintBrush); OutputText = $"Succeeded: False{Environment.NewLine}" + $"Code: {code}{Environment.NewLine}" + $"Message: {message}{Environment.NewLine}" + $"Errors: {string.Join("; ", errors)}"; } private void ApplyTaskDetail(AnnotationTaskDetailDto detail) { DetailTaskIdText = detail.CvatTaskId.ToString(); DetailTaskNameText = string.IsNullOrWhiteSpace(detail.TaskName) ? "-" : detail.TaskName; DetailTaskStatusText = string.IsNullOrWhiteSpace(detail.TaskStatus) ? "-" : detail.TaskStatus; DetailProjectIdText = detail.CvatProjectId?.ToString() ?? "-"; DetailItemCountText = detail.ItemCount.ToString(); DetailUpdatedAtText = detail.UpdatedAtUtc?.ToString("O") ?? "-"; } private void SetFriendlyHint(string hint, Brush brush) { FriendlyHintText = hint; FriendlyHintBrush = brush; } private static (string Hint, Brush Brush) BuildFriendlyHint(string code) { return code switch { "ANNOTATION_CVAT_AUTH_FAILED" => ("认证失败:请检查 appsettings 中的 CVAT Token 是否正确。", Brushes.IndianRed), "ANNOTATION_CVAT_UNREACHABLE" => ("连接失败:请确认 CVAT 服务地址、端口和网络连通性。", Brushes.IndianRed), "ANNOTATION_CVAT_TASK_NOT_FOUND" => ("任务不存在:请核对 CVAT 任务 ID。", Brushes.OrangeRed), "ANNOTATION_CVAT_PROJECT_MISMATCH" => ("项目不一致:请核对 CVAT 项目 ID 与任务归属。", Brushes.OrangeRed), "ANNOTATION_SYNC_TIMEOUT" => ("请求超时:建议稍后重试或提高超时时间。", Brushes.OrangeRed), "RULE_CONFIG_REQUIRED_FIELDS" => ("规则配置字段缺失:请完整填写机种、层、部件、ROI、规则信息。", Brushes.OrangeRed), "RULE_CONFIG_LIST_EMPTY" => ("规则配置列表为空:请至少添加一条层、部件、ROI 和规则。", Brushes.OrangeRed), "RULE_LAYER_NO_INVALID" or "RULE_PART_LAYER_NO_INVALID" => ("层号无效:请填写大于 0 的整数。", Brushes.OrangeRed), "RULE_ROI_INVALID" => ("ROI 参数无效:请检查坐标与宽高。", Brushes.OrangeRed), "RULE_PARAMETERS_JSON_INVALID" => ("规则参数 JSON 无效:请检查格式。", Brushes.OrangeRed), "PROJECT_ID_INVALID" or "ANNOTATION_TASK_ID_INVALID" or "CVAT_TASK_ID_INVALID" or "CVAT_PROJECT_ID_INVALID" => ("输入参数无效:请检查 GUID 与数字字段格式。", Brushes.OrangeRed), _ => ("执行失败:请根据详细日志定位原因并重试。", Brushes.IndianRed) }; } private bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) { if (EqualityComparer.Default.Equals(field, value)) { return false; } field = value; OnPropertyChanged(propertyName); return true; } private void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } /// /// 打开训练任务管理窗口。 /// public void OpenTrainingTaskManagement() { try { // 通过依赖注入获取训练任务管理ViewModel和窗口 var viewModel = _serviceProvider.GetRequiredService(); var logger = _serviceProvider.GetRequiredService>(); var window = new Views.TrainingTaskManagementWindow(logger, viewModel); window.Show(); } catch (Exception ex) { MessageBox.Show($"打开训练任务管理窗口失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); } } } /// /// 规则层输入行。 /// public sealed class RuleLayerInputRow { /// /// 层号文本。 /// public string LayerNoText { get; set; } = "1"; /// /// 层名称。 /// public string LayerName { get; set; } = string.Empty; } /// /// 规则部件输入行。 /// public sealed class RulePartInputRow { /// /// 部件编码。 /// public string PartCode { get; set; } = string.Empty; /// /// 部件名称。 /// public string PartName { get; set; } = string.Empty; /// /// 所属层号文本。 /// public string LayerNoText { get; set; } = "1"; } /// /// 规则 ROI 输入行。 /// public sealed class RuleRoiInputRow { /// /// ROI 编码。 /// public string RoiCode { get; set; } = string.Empty; /// /// 部件编码。 /// public string PartCode { get; set; } = string.Empty; /// /// X 坐标。 /// public string XText { get; set; } = "0"; /// /// Y 坐标。 /// public string YText { get; set; } = "0"; /// /// 宽度。 /// public string WidthText { get; set; } = "100"; /// /// 高度。 /// public string HeightText { get; set; } = "100"; } /// /// 规则项输入行。 /// public sealed class RuleItemInputRow { /// /// 规则编码。 /// public string RuleCode { get; set; } = string.Empty; /// /// 规则名称。 /// public string RuleName { get; set; } = string.Empty; /// /// 规则类型。 /// public string RuleType { get; set; } = string.Empty; /// /// 规则参数 JSON。 /// public string ParametersJson { get; set; } = "{}"; /// /// 是否启用。 /// public bool Enabled { get; set; } = true; } /// /// 规则版本审计展示行。 /// public sealed class RuleVersionAuditViewRow { /// /// 操作时间(UTC)。 /// public string ActionAtUtcText { get; set; } = "-"; /// /// 操作类型。 /// public string ActionType { get; set; } = "-"; /// /// 版本号。 /// public string VersionNo { get; set; } = "-"; /// /// 源版本号。 /// public string SourceVersionNo { get; set; } = "-"; /// /// 目标版本号。 /// public string TargetVersionNo { get; set; } = "-"; /// /// 操作人。 /// public string OperatorName { get; set; } = "-"; } /// /// 规则版本列表展示行。 /// public sealed class RuleVersionListViewRow { /// /// 版本号。 /// public string VersionNo { get; set; } = "-"; /// /// 发布时间(UTC)。 /// public string PublishedAtUtcText { get; set; } = "-"; /// /// 发布人。 /// public string PublishedBy { get; set; } = "-"; /// /// 是否已停用。 /// public string IsDisabledText { get; set; } = "否"; /// /// 停用时间(UTC)。 /// public string DisabledAtUtcText { get; set; } = "-"; /// /// 停用人。 /// public string DisabledBy { get; set; } = "-"; }