1668 lines
54 KiB
C#
1668 lines
54 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// 主窗口 ViewModel。
|
||
///
|
||
/// 职责:
|
||
/// - 持有页面输入参数与输出状态;
|
||
/// - 统一编排“检查连通状态”和“发起同步”两类操作;
|
||
/// - 将服务层返回结果转换为界面可直接展示的文本。
|
||
/// </summary>
|
||
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;
|
||
|
||
/// <summary>
|
||
/// 主窗口 ViewModel 构造函数。
|
||
/// </summary>
|
||
/// <param name="annotationSyncAppService">标注同步应用服务。</param>
|
||
/// <param name="ruleConfigurationAppService">规则配置应用服务。</param>
|
||
/// <param name="currentUserContext">当前用户上下文。</param>
|
||
/// <param name="cvatOptions">CVAT 配置。</param>
|
||
/// <param name="serviceProvider">服务提供程序。</param>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存规则草稿。
|
||
/// </summary>
|
||
public async Task SaveRuleDraftAsync()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(RuleProductTypeCodeText) || string.IsNullOrWhiteSpace(RuleProductTypeNameText))
|
||
{
|
||
SetError("RULE_PRODUCT_TYPE_REQUIRED", "机种编码和名称不能为空。", Array.Empty<string>());
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停用指定规则版本。
|
||
/// </summary>
|
||
public async Task DisableVersionAsync()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(RuleProductTypeCodeText))
|
||
{
|
||
SetError("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。", Array.Empty<string>());
|
||
return;
|
||
}
|
||
|
||
var versionNo = DisableVersionNoText.Trim();
|
||
if (string.IsNullOrWhiteSpace(versionNo))
|
||
{
|
||
SetError("RULE_VERSION_NO_REQUIRED", "请输入要停用的版本号。", Array.Empty<string>());
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查询指定规则版本详情。
|
||
/// </summary>
|
||
public async Task QueryVersionDetailAsync()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(RuleProductTypeCodeText))
|
||
{
|
||
SetError("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。", Array.Empty<string>());
|
||
return;
|
||
}
|
||
|
||
var versionNo = VersionDetailNoText.Trim();
|
||
if (string.IsNullOrWhiteSpace(versionNo))
|
||
{
|
||
SetError("RULE_VERSION_NO_REQUIRED", "请输入要查询详情的版本号。", Array.Empty<string>());
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分页查询规则版本列表。
|
||
/// </summary>
|
||
public async Task QueryVersionPagedListAsync()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(RuleProductTypeCodeText))
|
||
{
|
||
SetError("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。", Array.Empty<string>());
|
||
return;
|
||
}
|
||
|
||
if (!int.TryParse(VersionPageIndexText.Trim(), out var pageIndex) || pageIndex < 1)
|
||
{
|
||
SetError("RULE_PAGE_INDEX_INVALID", "页码必须是大于 0 的整数。", Array.Empty<string>());
|
||
return;
|
||
}
|
||
|
||
if (!int.TryParse(VersionPageSizeText.Trim(), out var pageSize) || pageSize < 1 || pageSize > 100)
|
||
{
|
||
SetError("RULE_PAGE_SIZE_INVALID", "每页大小必须在 1-100 之间。", Array.Empty<string>());
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 发布规则版本。
|
||
/// </summary>
|
||
public async Task PublishRuleVersionAsync()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(RuleProductTypeCodeText))
|
||
{
|
||
SetError("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。", Array.Empty<string>());
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 属性变更事件。
|
||
/// </summary>
|
||
public event PropertyChangedEventHandler? PropertyChanged;
|
||
|
||
/// <summary>
|
||
/// 本地项目 ID 文本。
|
||
/// </summary>
|
||
public string ProjectIdText
|
||
{
|
||
get => _projectIdText;
|
||
set => SetField(ref _projectIdText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 本地标注任务 ID 文本。
|
||
/// </summary>
|
||
public string AnnotationTaskIdText
|
||
{
|
||
get => _annotationTaskIdText;
|
||
set => SetField(ref _annotationTaskIdText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// CVAT 服务地址文本。
|
||
/// </summary>
|
||
public string CvatEndpointText
|
||
{
|
||
get => _cvatEndpointText;
|
||
set => SetField(ref _cvatEndpointText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// CVAT 项目 ID 文本。
|
||
/// </summary>
|
||
public string CvatProjectIdText
|
||
{
|
||
get => _cvatProjectIdText;
|
||
set => SetField(ref _cvatProjectIdText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// CVAT 任务 ID 文本。
|
||
/// </summary>
|
||
public string CvatTaskIdText
|
||
{
|
||
get => _cvatTaskIdText;
|
||
set => SetField(ref _cvatTaskIdText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 请求人文本。
|
||
/// </summary>
|
||
public string RequestedByText
|
||
{
|
||
get => _requestedByText;
|
||
set => SetField(ref _requestedByText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 当前用户ID。
|
||
/// </summary>
|
||
public Guid CurrentUserId => _currentUserContext.CurrentUserId;
|
||
|
||
/// <summary>
|
||
/// 状态栏文本。
|
||
/// </summary>
|
||
public string StatusText
|
||
{
|
||
get => _statusText;
|
||
set => SetField(ref _statusText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 结果详情文本。
|
||
/// </summary>
|
||
public string OutputText
|
||
{
|
||
get => _outputText;
|
||
set => SetField(ref _outputText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 状态文本颜色。
|
||
/// </summary>
|
||
public Brush StatusBrush
|
||
{
|
||
get => _statusBrush;
|
||
private set => SetField(ref _statusBrush, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 友好提示文本。
|
||
/// </summary>
|
||
public string FriendlyHintText
|
||
{
|
||
get => _friendlyHintText;
|
||
private set => SetField(ref _friendlyHintText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 友好提示颜色。
|
||
/// </summary>
|
||
public Brush FriendlyHintBrush
|
||
{
|
||
get => _friendlyHintBrush;
|
||
private set => SetField(ref _friendlyHintBrush, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 任务详情-TaskId。
|
||
/// </summary>
|
||
public string DetailTaskIdText
|
||
{
|
||
get => _detailTaskIdText;
|
||
private set => SetField(ref _detailTaskIdText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 任务详情-TaskName。
|
||
/// </summary>
|
||
public string DetailTaskNameText
|
||
{
|
||
get => _detailTaskNameText;
|
||
private set => SetField(ref _detailTaskNameText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 任务详情-TaskStatus。
|
||
/// </summary>
|
||
public string DetailTaskStatusText
|
||
{
|
||
get => _detailTaskStatusText;
|
||
private set => SetField(ref _detailTaskStatusText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 任务详情-ProjectId。
|
||
/// </summary>
|
||
public string DetailProjectIdText
|
||
{
|
||
get => _detailProjectIdText;
|
||
private set => SetField(ref _detailProjectIdText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 任务详情-ItemCount。
|
||
/// </summary>
|
||
public string DetailItemCountText
|
||
{
|
||
get => _detailItemCountText;
|
||
private set => SetField(ref _detailItemCountText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 任务详情-UpdatedAtUtc。
|
||
/// </summary>
|
||
public string DetailUpdatedAtText
|
||
{
|
||
get => _detailUpdatedAtText;
|
||
private set => SetField(ref _detailUpdatedAtText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 规则机种编码。
|
||
/// </summary>
|
||
public string RuleProductTypeCodeText
|
||
{
|
||
get => _ruleProductTypeCodeText;
|
||
set => SetField(ref _ruleProductTypeCodeText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 规则机种名称。
|
||
/// </summary>
|
||
public string RuleProductTypeNameText
|
||
{
|
||
get => _ruleProductTypeNameText;
|
||
set => SetField(ref _ruleProductTypeNameText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 层配置列表。
|
||
/// </summary>
|
||
public ObservableCollection<RuleLayerInputRow> RuleLayers { get; } = [];
|
||
|
||
/// <summary>
|
||
/// 部件配置列表。
|
||
/// </summary>
|
||
public ObservableCollection<RulePartInputRow> RuleParts { get; } = [];
|
||
|
||
/// <summary>
|
||
/// ROI 配置列表。
|
||
/// </summary>
|
||
public ObservableCollection<RuleRoiInputRow> RuleRois { get; } = [];
|
||
|
||
/// <summary>
|
||
/// 规则项配置列表。
|
||
/// </summary>
|
||
public ObservableCollection<RuleItemInputRow> RuleItems { get; } = [];
|
||
|
||
/// <summary>
|
||
/// 规则版本审计记录列表。
|
||
/// </summary>
|
||
public ObservableCollection<RuleVersionAuditViewRow> RuleAuditRows { get; } = [];
|
||
|
||
/// <summary>
|
||
/// 规则版本分页展示列表。
|
||
/// </summary>
|
||
public ObservableCollection<RuleVersionListViewRow> RuleVersionRows { get; } = [];
|
||
|
||
/// <summary>
|
||
/// 当前选中层。
|
||
/// </summary>
|
||
public RuleLayerInputRow? SelectedRuleLayer
|
||
{
|
||
get => _selectedRuleLayer;
|
||
set => SetField(ref _selectedRuleLayer, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 当前选中部件。
|
||
/// </summary>
|
||
public RulePartInputRow? SelectedRulePart
|
||
{
|
||
get => _selectedRulePart;
|
||
set => SetField(ref _selectedRulePart, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 当前选中 ROI。
|
||
/// </summary>
|
||
public RuleRoiInputRow? SelectedRuleRoi
|
||
{
|
||
get => _selectedRuleRoi;
|
||
set => SetField(ref _selectedRuleRoi, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 当前选中规则项。
|
||
/// </summary>
|
||
public RuleItemInputRow? SelectedRuleItem
|
||
{
|
||
get => _selectedRuleItem;
|
||
set => SetField(ref _selectedRuleItem, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 新增层。
|
||
/// </summary>
|
||
public void AddRuleLayerRow()
|
||
{
|
||
RuleLayers.Add(new RuleLayerInputRow
|
||
{
|
||
LayerNoText = (RuleLayers.Count + 1).ToString(),
|
||
LayerName = $"Layer{RuleLayers.Count + 1}"
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除选中层。
|
||
/// </summary>
|
||
public void RemoveSelectedRuleLayerRow()
|
||
{
|
||
if (SelectedRuleLayer is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
RuleLayers.Remove(SelectedRuleLayer);
|
||
SelectedRuleLayer = null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 新增部件。
|
||
/// </summary>
|
||
public void AddRulePartRow()
|
||
{
|
||
RuleParts.Add(new RulePartInputRow
|
||
{
|
||
PartCode = $"PART-{RuleParts.Count + 1:000}",
|
||
PartName = $"Part{RuleParts.Count + 1}",
|
||
LayerNoText = "1"
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除选中部件。
|
||
/// </summary>
|
||
public void RemoveSelectedRulePartRow()
|
||
{
|
||
if (SelectedRulePart is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
RuleParts.Remove(SelectedRulePart);
|
||
SelectedRulePart = null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 新增 ROI。
|
||
/// </summary>
|
||
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"
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除选中 ROI。
|
||
/// </summary>
|
||
public void RemoveSelectedRuleRoiRow()
|
||
{
|
||
if (SelectedRuleRoi is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
RuleRois.Remove(SelectedRuleRoi);
|
||
SelectedRuleRoi = null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 新增规则项。
|
||
/// </summary>
|
||
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
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除选中规则项。
|
||
/// </summary>
|
||
public void RemoveSelectedRuleItemRow()
|
||
{
|
||
if (SelectedRuleItem is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
RuleItems.Remove(SelectedRuleItem);
|
||
SelectedRuleItem = null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 最新发布版本号。
|
||
/// </summary>
|
||
public string PublishedRuleVersionText
|
||
{
|
||
get => _publishedRuleVersionText;
|
||
private set => SetField(ref _publishedRuleVersionText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 最新发布时间。
|
||
/// </summary>
|
||
public string PublishedRuleAtText
|
||
{
|
||
get => _publishedRuleAtText;
|
||
private set => SetField(ref _publishedRuleAtText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 对比源版本号。
|
||
/// </summary>
|
||
public string CompareSourceVersionText
|
||
{
|
||
get => _compareSourceVersionText;
|
||
set => SetField(ref _compareSourceVersionText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 对比目标版本号。
|
||
/// </summary>
|
||
public string CompareTargetVersionText
|
||
{
|
||
get => _compareTargetVersionText;
|
||
set => SetField(ref _compareTargetVersionText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 回滚目标版本号。
|
||
/// </summary>
|
||
public string RollbackTargetVersionText
|
||
{
|
||
get => _rollbackTargetVersionText;
|
||
set => SetField(ref _rollbackTargetVersionText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停用目标版本号。
|
||
/// </summary>
|
||
public string DisableVersionNoText
|
||
{
|
||
get => _disableVersionNoText;
|
||
set => SetField(ref _disableVersionNoText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 详情查询版本号。
|
||
/// </summary>
|
||
public string VersionDetailNoText
|
||
{
|
||
get => _versionDetailNoText;
|
||
set => SetField(ref _versionDetailNoText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分页查询页码。
|
||
/// </summary>
|
||
public string VersionPageIndexText
|
||
{
|
||
get => _versionPageIndexText;
|
||
set => SetField(ref _versionPageIndexText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分页查询每页大小。
|
||
/// </summary>
|
||
public string VersionPageSizeText
|
||
{
|
||
get => _versionPageSizeText;
|
||
set => SetField(ref _versionPageSizeText, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否处于空闲状态(用于按钮可用性绑定)。
|
||
/// </summary>
|
||
public bool IsIdle => !_isBusy;
|
||
|
||
/// <summary>
|
||
/// 执行“查询任务详情”。
|
||
/// </summary>
|
||
public async Task QueryTaskDetailAsync()
|
||
{
|
||
if (!long.TryParse(CvatTaskIdText.Trim(), out var cvatTaskId) || cvatTaskId <= 0)
|
||
{
|
||
SetError("CVAT_TASK_ID_INVALID", "CVAT 任务 ID 必须是大于 0 的整数。", Array.Empty<string>());
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行“检查连通状态”。
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行“发起同步”。
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行规则版本对比。
|
||
/// </summary>
|
||
public async Task CompareVersionsAsync()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(RuleProductTypeCodeText))
|
||
{
|
||
SetError("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。", Array.Empty<string>());
|
||
return;
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(CompareSourceVersionText) || string.IsNullOrWhiteSpace(CompareTargetVersionText))
|
||
{
|
||
SetError("RULE_VERSION_COMPARE_INPUT_REQUIRED", "请输入源版本号和目标版本号。", Array.Empty<string>());
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行规则版本回滚。
|
||
/// </summary>
|
||
public async Task RollbackVersionAsync()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(RuleProductTypeCodeText))
|
||
{
|
||
SetError("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。", Array.Empty<string>());
|
||
return;
|
||
}
|
||
|
||
var targetVersion = RollbackTargetVersionText.Trim();
|
||
if (string.IsNullOrWhiteSpace(targetVersion))
|
||
{
|
||
// 如果未指定目标版本,则回滚到最新版本的前一个版本
|
||
SetError("RULE_ROLLBACK_TARGET_REQUIRED", "请输入要回滚到的版本号。", Array.Empty<string>());
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查询最近规则版本审计记录。
|
||
/// </summary>
|
||
public async Task QueryRecentRuleAuditsAsync()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(RuleProductTypeCodeText))
|
||
{
|
||
SetError("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。", Array.Empty<string>());
|
||
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<string>());
|
||
return false;
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(CvatEndpointText))
|
||
{
|
||
SetError("ANNOTATION_ENDPOINT_REQUIRED", "CVAT 服务地址不能为空。", Array.Empty<string>());
|
||
return false;
|
||
}
|
||
|
||
if (!long.TryParse(CvatProjectIdText.Trim(), out var cvatProjectId) || cvatProjectId <= 0)
|
||
{
|
||
SetError("CVAT_PROJECT_ID_INVALID", "CVAT 项目 ID 必须是大于 0 的整数。", Array.Empty<string>());
|
||
return false;
|
||
}
|
||
|
||
if (!long.TryParse(CvatTaskIdText.Trim(), out var cvatTaskId) || cvatTaskId <= 0)
|
||
{
|
||
SetError("CVAT_TASK_ID_INVALID", "CVAT 任务 ID 必须是大于 0 的整数。", Array.Empty<string>());
|
||
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<string>());
|
||
return false;
|
||
}
|
||
|
||
if (RuleLayers.Count == 0 || RuleParts.Count == 0 || RuleRois.Count == 0 || RuleItems.Count == 0)
|
||
{
|
||
SetError("RULE_CONFIG_LIST_EMPTY", "层、部件、ROI、规则列表均至少需要一条记录。", Array.Empty<string>());
|
||
return false;
|
||
}
|
||
|
||
var layerDtos = new List<LayerConfigurationDto>();
|
||
foreach (var layer in RuleLayers)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(layer.LayerName) || !TryParsePositiveInt(layer.LayerNoText, out var layerNo))
|
||
{
|
||
SetError("RULE_LAYER_NO_INVALID", "层配置无效:层号必须为正整数且层名称不能为空。", Array.Empty<string>());
|
||
return false;
|
||
}
|
||
|
||
layerDtos.Add(new LayerConfigurationDto
|
||
{
|
||
LayerNo = layerNo,
|
||
LayerName = layer.LayerName.Trim()
|
||
});
|
||
}
|
||
|
||
var layerNoSet = layerDtos.Select(item => item.LayerNo).ToHashSet();
|
||
var partDtos = new List<PartConfigurationDto>();
|
||
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<string>());
|
||
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<RoiConfigurationDto>();
|
||
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<string>());
|
||
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<RuleItemConfigurationDto>();
|
||
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<string>());
|
||
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<string>());
|
||
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<string> 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<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||
{
|
||
if (EqualityComparer<T>.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));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 打开训练任务管理窗口。
|
||
/// </summary>
|
||
public void OpenTrainingTaskManagement()
|
||
{
|
||
try
|
||
{
|
||
// 通过依赖注入获取训练任务管理ViewModel和窗口
|
||
var viewModel = _serviceProvider.GetRequiredService<TrainingTaskManagementViewModel>();
|
||
var logger = _serviceProvider.GetRequiredService<ILogger<Views.TrainingTaskManagementWindow>>();
|
||
var window = new Views.TrainingTaskManagementWindow(logger, viewModel);
|
||
window.Show();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show($"打开训练任务管理窗口失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 规则层输入行。
|
||
/// </summary>
|
||
public sealed class RuleLayerInputRow
|
||
{
|
||
/// <summary>
|
||
/// 层号文本。
|
||
/// </summary>
|
||
public string LayerNoText { get; set; } = "1";
|
||
|
||
/// <summary>
|
||
/// 层名称。
|
||
/// </summary>
|
||
public string LayerName { get; set; } = string.Empty;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 规则部件输入行。
|
||
/// </summary>
|
||
public sealed class RulePartInputRow
|
||
{
|
||
/// <summary>
|
||
/// 部件编码。
|
||
/// </summary>
|
||
public string PartCode { get; set; } = string.Empty;
|
||
|
||
/// <summary>
|
||
/// 部件名称。
|
||
/// </summary>
|
||
public string PartName { get; set; } = string.Empty;
|
||
|
||
/// <summary>
|
||
/// 所属层号文本。
|
||
/// </summary>
|
||
public string LayerNoText { get; set; } = "1";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 规则 ROI 输入行。
|
||
/// </summary>
|
||
public sealed class RuleRoiInputRow
|
||
{
|
||
/// <summary>
|
||
/// ROI 编码。
|
||
/// </summary>
|
||
public string RoiCode { get; set; } = string.Empty;
|
||
|
||
/// <summary>
|
||
/// 部件编码。
|
||
/// </summary>
|
||
public string PartCode { get; set; } = string.Empty;
|
||
|
||
/// <summary>
|
||
/// X 坐标。
|
||
/// </summary>
|
||
public string XText { get; set; } = "0";
|
||
|
||
/// <summary>
|
||
/// Y 坐标。
|
||
/// </summary>
|
||
public string YText { get; set; } = "0";
|
||
|
||
/// <summary>
|
||
/// 宽度。
|
||
/// </summary>
|
||
public string WidthText { get; set; } = "100";
|
||
|
||
/// <summary>
|
||
/// 高度。
|
||
/// </summary>
|
||
public string HeightText { get; set; } = "100";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 规则项输入行。
|
||
/// </summary>
|
||
public sealed class RuleItemInputRow
|
||
{
|
||
/// <summary>
|
||
/// 规则编码。
|
||
/// </summary>
|
||
public string RuleCode { get; set; } = string.Empty;
|
||
|
||
/// <summary>
|
||
/// 规则名称。
|
||
/// </summary>
|
||
public string RuleName { get; set; } = string.Empty;
|
||
|
||
/// <summary>
|
||
/// 规则类型。
|
||
/// </summary>
|
||
public string RuleType { get; set; } = string.Empty;
|
||
|
||
/// <summary>
|
||
/// 规则参数 JSON。
|
||
/// </summary>
|
||
public string ParametersJson { get; set; } = "{}";
|
||
|
||
/// <summary>
|
||
/// 是否启用。
|
||
/// </summary>
|
||
public bool Enabled { get; set; } = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 规则版本审计展示行。
|
||
/// </summary>
|
||
public sealed class RuleVersionAuditViewRow
|
||
{
|
||
/// <summary>
|
||
/// 操作时间(UTC)。
|
||
/// </summary>
|
||
public string ActionAtUtcText { get; set; } = "-";
|
||
|
||
/// <summary>
|
||
/// 操作类型。
|
||
/// </summary>
|
||
public string ActionType { get; set; } = "-";
|
||
|
||
/// <summary>
|
||
/// 版本号。
|
||
/// </summary>
|
||
public string VersionNo { get; set; } = "-";
|
||
|
||
/// <summary>
|
||
/// 源版本号。
|
||
/// </summary>
|
||
public string SourceVersionNo { get; set; } = "-";
|
||
|
||
/// <summary>
|
||
/// 目标版本号。
|
||
/// </summary>
|
||
public string TargetVersionNo { get; set; } = "-";
|
||
|
||
/// <summary>
|
||
/// 操作人。
|
||
/// </summary>
|
||
public string OperatorName { get; set; } = "-";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 规则版本列表展示行。
|
||
/// </summary>
|
||
public sealed class RuleVersionListViewRow
|
||
{
|
||
/// <summary>
|
||
/// 版本号。
|
||
/// </summary>
|
||
public string VersionNo { get; set; } = "-";
|
||
|
||
/// <summary>
|
||
/// 发布时间(UTC)。
|
||
/// </summary>
|
||
public string PublishedAtUtcText { get; set; } = "-";
|
||
|
||
/// <summary>
|
||
/// 发布人。
|
||
/// </summary>
|
||
public string PublishedBy { get; set; } = "-";
|
||
|
||
/// <summary>
|
||
/// 是否已停用。
|
||
/// </summary>
|
||
public string IsDisabledText { get; set; } = "否";
|
||
|
||
/// <summary>
|
||
/// 停用时间(UTC)。
|
||
/// </summary>
|
||
public string DisabledAtUtcText { get; set; } = "-";
|
||
|
||
/// <summary>
|
||
/// 停用人。
|
||
/// </summary>
|
||
public string DisabledBy { get; set; } = "-";
|
||
}
|