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; } = "-";
}