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 { /// /// DeviceConfig.xaml 的交互逻辑 /// [ObservableObject] public partial class DeviceConfig : SimpleMaskControl { private DeviceConfigModel _model; [ObservableProperty] private ObservableCollection devices = new(); [ObservableProperty] private ObservableCollection channels = new(); [ObservableProperty] private ObservableCollection slotMappings = new(); [ObservableProperty] private DeviceDefinition selectedDevice; [ObservableProperty] private string currentVersionStatus = "Draft"; [ObservableProperty] private ObservableCollection treeCategories = new(); public DeviceConfig() { DataContext = this; InitializeComponent(); } /// /// 强制刷新数据(供集成测试预置设备后调用) /// 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 { new TreeNode { Name = "全部设备", Kind = null, Children = new ObservableCollection() }, new TreeNode { Name = "电源系统", Kind = DeviceKind.PowerSupply, Children = new ObservableCollection() }, new TreeNode { Name = "CAN/LIN", Kind = DeviceKind.CanDevice, Children = new ObservableCollection() }, new TreeNode { Name = "PLC/IO", Kind = DeviceKind.PlcDevice, Children = new ObservableCollection() }, new TreeNode { Name = "温箱/DAQ", Kind = null, Children = new ObservableCollection(), FilterKinds = new[] { DeviceKind.Chamber, DeviceKind.DAQ } }, }; deviceTree.ItemsSource = TreeCategories; } #endregion #region 数据加载 private void RefreshAllData() { try { var deviceList = _model.GetAllDevices(); Devices = new ObservableCollection(deviceList); SlotMappings = new ObservableCollection(_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 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 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(chList); // V3.0 执行位置展示 tbExecutionMode.Text = dev.ExecutionMode == ExecutionModeKind.Broker ? "Broker 共享" : "Local 直连"; tbAuthorityInfo.Text = dev.ExecutionMode == ExecutionModeKind.Broker ? $"{dev.AuthorityId} · {dev.PhysicalSessionKey}" : (string.IsNullOrWhiteSpace(dev.AuthorityId) ? V3AuthorityCatalog.LocalAuthorityId : dev.AuthorityId); } 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}"); } } /// /// 编辑当前选中的设备 /// 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, ExecutionMode = SelectedDevice.ExecutionMode, AuthorityId = SelectedDevice.AuthorityId, BrokerId = SelectedDevice.BrokerId, OwnerProcessId = SelectedDevice.OwnerProcessId, PhysicalSessionKey = SelectedDevice.PhysicalSessionKey, 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}"); } } /// /// 删除当前选中的设备(同时清理相关通道与 Slot 映射) /// 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}"); } } /// /// 清空中栏设备详情展示 /// 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; tbExecutionMode.Text = string.Empty; tbAuthorityInfo.Text = string.Empty; Channels = new ObservableCollection(); } 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; } // V3.0 §05 发布门禁:执行位置绑定校验不通过则阻止发布 var gateErrors = _model.ValidatePublishGate(); if (gateErrors.Count > 0) { var detail = string.Join("\n", gateErrors.Take(8)); if (gateErrors.Count > 8) { detail += $"\n… 共 {gateErrors.Count} 项错误"; } log.Warn($"发布门禁拦截: {string.Join("; ", gateErrors)}"); MsgBoxHelper.Error(Window.GetWindow(this), $"发布门禁校验未通过:\n{detail}"); 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 } /// /// 树节点模型 /// 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 Children { get; set; } = new(); } }