using CommunityToolkit.Mvvm.ComponentModel;
|
using HMI.Base;
|
using HMI.Core.Util;
|
using HMI.UI.Util;
|
using Plugin.DeviceConfig.Entity;
|
using Plugin.DeviceConfig.Model;
|
using System;
|
using System.Collections.Generic;
|
using System.Collections.ObjectModel;
|
using System.IO;
|
using System.Linq;
|
using System.Windows;
|
using System.Windows.Controls;
|
using System.Windows.Data;
|
|
namespace Plugin.DeviceConfig
|
{
|
/// <summary>
|
/// DeviceConfig.xaml 的交互逻辑
|
/// </summary>
|
[ObservableObject]
|
public partial class DeviceConfig : SimpleMaskControl
|
{
|
private DeviceConfigModel _model;
|
|
[ObservableProperty]
|
private ObservableCollection<DeviceDefinition> devices = new();
|
|
[ObservableProperty]
|
private ObservableCollection<DeviceChannel> channels = new();
|
|
[ObservableProperty]
|
private ObservableCollection<SlotDeviceMapping> slotMappings = new();
|
|
[ObservableProperty]
|
private DeviceDefinition selectedDevice;
|
|
[ObservableProperty]
|
private string currentVersionStatus = "Draft";
|
|
[ObservableProperty]
|
private ObservableCollection<TreeNode> treeCategories = new();
|
|
public DeviceConfig()
|
{
|
DataContext = this;
|
InitializeComponent();
|
}
|
|
/// <summary>
|
/// 强制刷新数据(供集成测试预置设备后调用)
|
/// </summary>
|
public void ForceRefreshForTest()
|
{
|
try { RefreshAllData(); }
|
catch (Exception ex) { log.Error("ForceRefreshForTest Error.", ex); }
|
}
|
|
public override void InitFinish()
|
{
|
base.InitFinish();
|
try
|
{
|
_model = new DeviceConfigModel();
|
var dbPath = Path.Combine(BaseFolder, "data.db");
|
_model.InitDatabase(dbPath);
|
LoadTreeCategories();
|
RefreshAllData();
|
}
|
catch (Exception ex)
|
{
|
log.Error("InitFinish Error.", ex);
|
ToastHelper.Error($"初始化失败: {ex.Message}");
|
}
|
}
|
|
#region 树分类加载
|
|
private void LoadTreeCategories()
|
{
|
TreeCategories = new ObservableCollection<TreeNode>
|
{
|
new TreeNode { Name = "全部设备", Kind = null, Children = new ObservableCollection<TreeNode>() },
|
new TreeNode { Name = "电源系统", Kind = DeviceKind.PowerSupply, Children = new ObservableCollection<TreeNode>() },
|
new TreeNode { Name = "CAN/LIN", Kind = DeviceKind.CanDevice, Children = new ObservableCollection<TreeNode>() },
|
new TreeNode { Name = "PLC/IO", Kind = DeviceKind.PlcDevice, Children = new ObservableCollection<TreeNode>() },
|
new TreeNode { Name = "温箱/DAQ", Kind = null, Children = new ObservableCollection<TreeNode>(),
|
FilterKinds = new[] { DeviceKind.Chamber, DeviceKind.DAQ } },
|
};
|
deviceTree.ItemsSource = TreeCategories;
|
}
|
|
#endregion
|
|
#region 数据加载
|
|
private void RefreshAllData()
|
{
|
try
|
{
|
var deviceList = _model.GetAllDevices();
|
Devices = new ObservableCollection<DeviceDefinition>(deviceList);
|
SlotMappings = new ObservableCollection<SlotDeviceMapping>(_model.GetSlotMappings());
|
RefreshTreeDevices();
|
LoadLatestVersionStatus();
|
}
|
catch (Exception ex)
|
{
|
log.Error("RefreshAllData Error.", ex);
|
ToastHelper.Error($"加载数据失败: {ex.Message}");
|
}
|
}
|
|
private void RefreshTreeDevices()
|
{
|
foreach (var cat in TreeCategories)
|
{
|
cat.Children.Clear();
|
IEnumerable<DeviceDefinition> filtered = Devices;
|
if (cat.Kind.HasValue)
|
{
|
filtered = Devices.Where(d => d.Kind == cat.Kind.Value);
|
}
|
else if (cat.FilterKinds != null)
|
{
|
filtered = Devices.Where(d => cat.FilterKinds.Contains(d.Kind));
|
}
|
foreach (var dev in filtered)
|
{
|
cat.Children.Add(new TreeNode { Name = $"{dev.DeviceId} ({dev.Endpoint})", Tag = dev });
|
}
|
}
|
}
|
|
private void LoadLatestVersionStatus()
|
{
|
try
|
{
|
var versions = _model.GetConfigVersions();
|
var effective = versions.FirstOrDefault(v => v.Status == VersionStatus.Effective);
|
if (effective != null)
|
{
|
CurrentVersionStatus = "Effective";
|
}
|
else if (versions.Any(v => v.Status == VersionStatus.Published))
|
{
|
CurrentVersionStatus = "Published";
|
}
|
else
|
{
|
CurrentVersionStatus = "Draft";
|
}
|
}
|
catch (Exception ex)
|
{
|
log.Error("LoadLatestVersionStatus Error.", ex);
|
}
|
}
|
|
#endregion
|
|
#region 事件处理
|
|
private void SimpleMaskControl_Loaded(object sender, RoutedEventArgs e)
|
{
|
// InitFinish 已加载数据,此处仅做一次性 UI 初始化
|
Loaded -= SimpleMaskControl_Loaded;
|
}
|
|
private void DeviceTree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
|
{
|
if (e.NewValue is TreeNode node && node.Tag is DeviceDefinition dev)
|
{
|
SelectDevice(dev);
|
}
|
}
|
|
private void SearchBox_TextChanged(object sender, TextChangedEventArgs e)
|
{
|
try
|
{
|
var keyword = searchBox.Text?.Trim() ?? string.Empty;
|
if (string.IsNullOrEmpty(keyword))
|
{
|
RefreshTreeDevices();
|
return;
|
}
|
foreach (var cat in TreeCategories)
|
{
|
cat.Children.Clear();
|
var matched = Devices.Where(d =>
|
d.DeviceId.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0 ||
|
d.Endpoint.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0 ||
|
d.DriverType.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0);
|
if (cat.Kind.HasValue)
|
matched = matched.Where(d => d.Kind == cat.Kind.Value);
|
else if (cat.FilterKinds != null)
|
matched = matched.Where(d => cat.FilterKinds.Contains(d.Kind));
|
|
foreach (var dev in matched)
|
cat.Children.Add(new TreeNode { Name = $"{dev.DeviceId} ({dev.Endpoint})", Tag = dev });
|
}
|
}
|
catch (Exception ex)
|
{
|
log.Error("SearchBox_TextChanged Error.", ex);
|
}
|
}
|
|
private void SelectDevice(DeviceDefinition dev)
|
{
|
try
|
{
|
SelectedDevice = dev;
|
tbDeviceId.Text = dev.DeviceId;
|
tbKind.Text = dev.Kind.ToString();
|
tbDriverType.Text = dev.DriverType;
|
tbTransport.Text = dev.Transport.ToString();
|
tbEndpoint.Text = dev.Endpoint;
|
tbEffectiveVersion.Text = dev.EffectiveVersion;
|
|
var chList = _model.GetChannelsByDevice(dev.DeviceId);
|
Channels = new ObservableCollection<DeviceChannel>(chList);
|
}
|
catch (Exception ex)
|
{
|
log.Error("SelectDevice Error.", ex);
|
ToastHelper.Error($"加载设备详情失败: {ex.Message}");
|
}
|
}
|
|
private void Button_Click(object sender, RoutedEventArgs e)
|
{
|
Button btn = sender as Button;
|
try
|
{
|
switch (btn?.Name)
|
{
|
case "addBtn":
|
OpenDeviceEditModal(null);
|
break;
|
case "editBtn":
|
EditSelectedDevice();
|
break;
|
case "delBtn":
|
DeleteSelectedDevice();
|
break;
|
case "batchBtn":
|
OpenBatchGenerateModal();
|
break;
|
case "importBtn":
|
ImportDevices();
|
break;
|
case "exportBtn":
|
ExportDevices();
|
break;
|
case "versionBtn":
|
OpenVersionManageModal();
|
break;
|
case "rfhBtn":
|
RefreshAllData();
|
ToastHelper.Success("刷新成功");
|
break;
|
case "saveDraftBtn":
|
SaveDraft();
|
break;
|
case "publishBtn":
|
PublishVersion();
|
break;
|
case "applyBtn":
|
ApplyVersion();
|
break;
|
}
|
}
|
catch (Exception ex)
|
{
|
log.Error("Button_Click Error.", ex);
|
ToastHelper.Error($"操作失败: {ex.Message}");
|
}
|
}
|
|
#endregion
|
|
#region 弹窗
|
|
private void OpenDeviceEditModal(DeviceDefinition device)
|
{
|
try
|
{
|
device ??= new DeviceDefinition();
|
var win = new DeviceEditModal
|
{
|
Title = string.IsNullOrEmpty(device.DeviceId) ? "新增设备" : "编辑设备",
|
Owner = Window.GetWindow(this)
|
};
|
if (win.Open(device, _model) == true)
|
{
|
RefreshAllData();
|
}
|
}
|
catch (Exception ex)
|
{
|
log.Error("OpenDeviceEditModal Error.", ex);
|
ToastHelper.Error($"打开编辑弹窗失败: {ex.Message}");
|
}
|
}
|
|
/// <summary>
|
/// 编辑当前选中的设备
|
/// </summary>
|
private void EditSelectedDevice()
|
{
|
try
|
{
|
if (SelectedDevice == null)
|
{
|
ToastHelper.Warning("请先在左侧设备树中选择一个设备");
|
return;
|
}
|
|
// 使用当前设备信息的副本避免直接修改内部数据
|
var editingDevice = new DeviceDefinition
|
{
|
DeviceId = SelectedDevice.DeviceId,
|
Kind = SelectedDevice.Kind,
|
DriverType = SelectedDevice.DriverType,
|
Transport = SelectedDevice.Transport,
|
Endpoint = SelectedDevice.Endpoint,
|
EffectiveVersion = SelectedDevice.EffectiveVersion,
|
CreatedAt = SelectedDevice.CreatedAt,
|
UpdatedAt = SelectedDevice.UpdatedAt
|
};
|
|
var win = new DeviceEditModal
|
{
|
Title = $"编辑设备 - {SelectedDevice.DeviceId}",
|
Owner = Window.GetWindow(this)
|
};
|
if (win.Open(editingDevice, _model) == true)
|
{
|
RefreshAllData();
|
ToastHelper.Success($"设备 {editingDevice.DeviceId} 已更新");
|
}
|
}
|
catch (Exception ex)
|
{
|
log.Error("EditSelectedDevice Error.", ex);
|
ToastHelper.Error($"编辑设备失败: {ex.Message}");
|
}
|
}
|
|
/// <summary>
|
/// 删除当前选中的设备(同时清理相关通道与 Slot 映射)
|
/// </summary>
|
private void DeleteSelectedDevice()
|
{
|
try
|
{
|
if (SelectedDevice == null)
|
{
|
ToastHelper.Warning("请先在左侧设备树中选择一个设备");
|
return;
|
}
|
|
var deviceId = SelectedDevice.DeviceId;
|
if (MsgBoxHelper.WarnMessage(Window.GetWindow(this),
|
$"确定要删除设备 [{deviceId}] 吗?\n该操作将同时删除相关的通道与 Slot 映射,且不可恢复。") != MessageBoxResult.OK)
|
{
|
return;
|
}
|
|
// 先删除关联的通道与 Slot 映射
|
var channels = _model.GetChannelsByDevice(deviceId);
|
foreach (var ch in channels)
|
{
|
_model.DeleteChannel(deviceId, ch.ChannelId);
|
}
|
|
var mappings = _model.GetSlotMappings();
|
foreach (var m in mappings.Where(x => x.DeviceId == deviceId))
|
{
|
_model.DeleteSlotMapping(m.SlotId, m.Capability, m.Version);
|
}
|
|
// 删除设备本身
|
_model.DeleteDevice(deviceId);
|
|
SelectedDevice = null;
|
ClearDeviceDetails();
|
RefreshAllData();
|
ToastHelper.Success($"设备 {deviceId} 已删除");
|
}
|
catch (Exception ex)
|
{
|
log.Error("DeleteSelectedDevice Error.", ex);
|
ToastHelper.Error($"删除设备失败: {ex.Message}");
|
}
|
}
|
|
/// <summary>
|
/// 清空中栏设备详情展示
|
/// </summary>
|
private void ClearDeviceDetails()
|
{
|
tbDeviceId.Text = string.Empty;
|
tbKind.Text = string.Empty;
|
tbDriverType.Text = string.Empty;
|
tbTransport.Text = string.Empty;
|
tbEndpoint.Text = string.Empty;
|
tbEffectiveVersion.Text = string.Empty;
|
Channels = new ObservableCollection<DeviceChannel>();
|
}
|
|
private void OpenBatchGenerateModal()
|
{
|
try
|
{
|
var win = new BatchGenerateModal
|
{
|
Title = "批量生成设备",
|
Owner = Window.GetWindow(this)
|
};
|
if (win.Open(_model) == true)
|
{
|
RefreshAllData();
|
}
|
}
|
catch (Exception ex)
|
{
|
log.Error("OpenBatchGenerateModal Error.", ex);
|
ToastHelper.Error($"打开批量生成弹窗失败: {ex.Message}");
|
}
|
}
|
|
private void OpenVersionManageModal()
|
{
|
try
|
{
|
var win = new VersionManageModal
|
{
|
Title = "版本管理",
|
Owner = Window.GetWindow(this)
|
};
|
if (win.Open(_model) == true)
|
{
|
RefreshAllData();
|
}
|
}
|
catch (Exception ex)
|
{
|
log.Error("OpenVersionManageModal Error.", ex);
|
ToastHelper.Error($"打开版本管理弹窗失败: {ex.Message}");
|
}
|
}
|
|
#endregion
|
|
#region 版本操作
|
|
private void SaveDraft()
|
{
|
try
|
{
|
var versionId = $"V{DateTime.Now:yyyyMMddHHmmss}";
|
var hash = _model.ComputeConfigHash();
|
var ver = new DeviceConfigVersion
|
{
|
VersionId = versionId,
|
Status = VersionStatus.Draft,
|
Hash = hash,
|
Author = Environment.UserName,
|
Timestamp = DateTime.Now,
|
DiffDescription = "保存草稿"
|
};
|
_model.SaveConfigVersion(ver);
|
CurrentVersionStatus = "Draft";
|
ToastHelper.Success($"草稿已保存: {versionId}");
|
}
|
catch (Exception ex)
|
{
|
log.Error("SaveDraft Error.", ex);
|
ToastHelper.Error($"保存草稿失败: {ex.Message}");
|
}
|
}
|
|
private void PublishVersion()
|
{
|
try
|
{
|
var versions = _model.GetConfigVersions();
|
var draft = versions.FirstOrDefault(v => v.Status == VersionStatus.Draft);
|
if (draft == null)
|
{
|
ToastHelper.Warning("没有可发布的草稿版本");
|
return;
|
}
|
var hash = _model.ComputeConfigHash();
|
draft.Hash = hash;
|
draft.Status = VersionStatus.Published;
|
draft.Timestamp = DateTime.Now;
|
_model.SaveConfigVersion(draft);
|
CurrentVersionStatus = "Published";
|
ToastHelper.Success($"版本 {draft.VersionId} 已发布");
|
}
|
catch (Exception ex)
|
{
|
log.Error("PublishVersion Error.", ex);
|
ToastHelper.Error($"发布失败: {ex.Message}");
|
}
|
}
|
|
private void ApplyVersion()
|
{
|
try
|
{
|
var versions = _model.GetConfigVersions();
|
var published = versions.FirstOrDefault(v => v.Status == VersionStatus.Published);
|
if (published == null)
|
{
|
ToastHelper.Warning("没有可应用的已发布版本");
|
return;
|
}
|
|
var effective = versions.FirstOrDefault(v => v.Status == VersionStatus.Effective);
|
if (effective != null)
|
{
|
if (MsgBoxHelper.WarnMessage(Window.GetWindow(this),
|
$"当前已有生效版本 [{effective.VersionId}],应用新版本将替换它,是否继续?") != MessageBoxResult.OK)
|
{
|
return;
|
}
|
effective.Status = VersionStatus.Published;
|
_model.SaveConfigVersion(effective);
|
}
|
|
published.Status = VersionStatus.Effective;
|
published.Timestamp = DateTime.Now;
|
_model.SaveConfigVersion(published);
|
CurrentVersionStatus = "Effective";
|
ToastHelper.Success($"版本 {published.VersionId} 已应用为生效版本");
|
}
|
catch (Exception ex)
|
{
|
log.Error("ApplyVersion Error.", ex);
|
ToastHelper.Error($"应用版本失败: {ex.Message}");
|
}
|
}
|
|
#endregion
|
|
#region 导入导出
|
|
private void ImportDevices()
|
{
|
try
|
{
|
var ofd = new Microsoft.Win32.OpenFileDialog
|
{
|
Filter = "XML文件|*.xml|JSON文件|*.json"
|
};
|
if (ofd.ShowDialog() == true)
|
{
|
// TODO: 实现导入逻辑
|
ToastHelper.Success($"已从 {Path.GetFileName(ofd.FileName)} 导入");
|
RefreshAllData();
|
}
|
}
|
catch (Exception ex)
|
{
|
log.Error("ImportDevices Error.", ex);
|
ToastHelper.Error($"导入失败: {ex.Message}");
|
}
|
}
|
|
private void ExportDevices()
|
{
|
try
|
{
|
var sfd = new Microsoft.Win32.SaveFileDialog
|
{
|
FileName = "DeviceConfig",
|
Filter = "XML文件|*.xml"
|
};
|
if (sfd.ShowDialog() == true)
|
{
|
// TODO: 实现导出逻辑
|
ToastHelper.Success("导出成功");
|
}
|
}
|
catch (Exception ex)
|
{
|
log.Error("ExportDevices Error.", ex);
|
ToastHelper.Error($"导出失败: {ex.Message}");
|
}
|
}
|
|
#endregion
|
}
|
|
/// <summary>
|
/// 树节点模型
|
/// </summary>
|
public class TreeNode
|
{
|
public string Name { get; set; } = string.Empty;
|
public DeviceKind? Kind { get; set; }
|
public DeviceKind[] FilterKinds { get; set; }
|
public object Tag { get; set; }
|
public ObservableCollection<TreeNode> Children { get; set; } = new();
|
}
|
}
|