feat: CrazyCoder 新增3个数据工具 - 数据建模/代码生成器(DataModelingWindow) - Redis 管理器(RedisManagerWindow) - 跨库数据同步(DataSyncWindow) 每个工具 = 独立 Window + ViewModel 架构,使用 CommunityToolkit.Mvvm。 已注册到 MainViewModel 菜单列表。 注意:本次提交仅含本次新增的 3 个工具文件, 其他变更(IoControl/ModbusRtu/ModbusTcp/SerialPort/XCoderAv)由单独提交处理。大石头 authored at 2026-07-14 02:29:50
diff --git a/CrazyCoder/CrazyCoder.csproj b/CrazyCoder/CrazyCoder.csproj
index 725a910..f6e0fb9 100644
--- a/CrazyCoder/CrazyCoder.csproj
+++ b/CrazyCoder/CrazyCoder.csproj
@@ -45,6 +45,8 @@
<PackageReference Include="System.IO.Ports" Version="10.0.7" />
<PackageReference Include="System.Speech" Version="10.0.7" />
<PackageReference Include="System.Management" Version="10.0.7" />
+ <PackageReference Include="NewLife.IoT" Version="2.7.2026.501" />
+ <PackageReference Include="NewLife.Modbus" Version="2.0.2025.701" />
<PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
diff --git a/CrazyCoder/Models/RedisConfig.cs b/CrazyCoder/Models/RedisConfig.cs
new file mode 100644
index 0000000..7bade8f
--- /dev/null
+++ b/CrazyCoder/Models/RedisConfig.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace CrazyCoder.Models
+{
+ /// <summary>Redis 服务器节点配置</summary>
+ public class RedisConfig
+ {
+ /// <summary>节点名称</summary>
+ public String Name { get; set; } = "";
+
+ /// <summary>服务器地址</summary>
+ public String Server { get; set; } = "127.0.0.1";
+
+ /// <summary>端口</summary>
+ public Int32 Port { get; set; } = 6379;
+
+ /// <summary>用户名</summary>
+ public String Username { get; set; } = "";
+
+ /// <summary>密码</summary>
+ public String Password { get; set; } = "";
+ }
+}
diff --git a/CrazyCoder/Models/SyncTableModel.cs b/CrazyCoder/Models/SyncTableModel.cs
new file mode 100644
index 0000000..1d0e4f7
--- /dev/null
+++ b/CrazyCoder/Models/SyncTableModel.cs
@@ -0,0 +1,36 @@
+using System.ComponentModel;
+
+namespace CrazyCoder.Models
+{
+ /// <summary>同步表模型</summary>
+ public class SyncTableModel
+ {
+ /// <summary>表名</summary>
+ [DisplayName("名称")]
+ public String Name { get; set; } = "";
+
+ /// <summary>显示名</summary>
+ [DisplayName("昵称")]
+ public String DisplayName { get; set; } = "";
+
+ /// <summary>启用同步</summary>
+ [DisplayName("同步")]
+ public Boolean EnableSync { get; set; } = true;
+
+ /// <summary>源表行数</summary>
+ [DisplayName("源行数")]
+ public Int32 SourceCount { get; set; } = -1;
+
+ /// <summary>目标表行数</summary>
+ [DisplayName("目标行数")]
+ public Int32 TargetCount { get; set; } = -1;
+
+ /// <summary>已同步行数</summary>
+ [DisplayName("已同步")]
+ public Int32 SyncCount { get; set; }
+
+ /// <summary>备注</summary>
+ [DisplayName("备注")]
+ public String Description { get; set; } = "";
+ }
+}
diff --git a/CrazyCoder/ViewModels/DataModelingViewModel.cs b/CrazyCoder/ViewModels/DataModelingViewModel.cs
new file mode 100644
index 0000000..57a0fe4
--- /dev/null
+++ b/CrazyCoder/ViewModels/DataModelingViewModel.cs
@@ -0,0 +1,269 @@
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.Windows;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using NewLife;
+using NewLife.Log;
+using NewLife.Reflection;
+using XCode;
+using XCode.Code;
+using XCode.DataAccessLayer;
+
+namespace CrazyCoder.ViewModels;
+
+/// <summary>数据建模/代码生成器 ViewModel</summary>
+public partial class DataModelingViewModel : ObservableObject
+{
+ #region 属性
+
+ /// <summary>当前连接名</summary>
+ [ObservableProperty]
+ private String _connName = "";
+
+ /// <summary>是否已连接</summary>
+ [ObservableProperty]
+ private Boolean _isConnected;
+
+ /// <summary>是否包含视图</summary>
+ [ObservableProperty]
+ private Boolean _includeView = true;
+
+ /// <summary>数据表列表</summary>
+ public ObservableCollection<IDataTable> Tables { get; } = [];
+
+ /// <summary>选中的数据表</summary>
+ [ObservableProperty]
+ private IDataTable _selectedTable;
+
+ /// <summary>命名空间</summary>
+ [ObservableProperty]
+ private String _nameSpace = "";
+
+ /// <summary>实体连接名</summary>
+ [ObservableProperty]
+ private String _entityConnName = "";
+
+ /// <summary>输出路径</summary>
+ [ObservableProperty]
+ private String _outputPath = "";
+
+ /// <summary>实体基类</summary>
+ [ObservableProperty]
+ private String _baseClass = "Entity";
+
+ /// <summary>使用中文文件名</summary>
+ [ObservableProperty]
+ private Boolean _useCNFileName;
+
+ /// <summary>生成泛型实体类</summary>
+ [ObservableProperty]
+ private Boolean _renderGenEntity;
+
+ /// <summary>连接按钮文本</summary>
+ public String ConnectButtonText => IsConnected ? "断开" : "连接";
+
+ partial void OnIsConnectedChanged(Boolean value)
+ {
+ OnPropertyChanged(nameof(ConnectButtonText));
+ }
+
+ /// <summary>状态文本</summary>
+ [ObservableProperty]
+ private String _status = "就绪";
+
+ /// <summary>所有可用连接</summary>
+ public ObservableCollection<String> Connections { get; } = [];
+
+ #endregion
+
+ #region 构造
+
+ /// <summary>实例化数据建模 ViewModel</summary>
+ public DataModelingViewModel()
+ {
+ LoadConnections();
+
+ if (Connections.Count > 0)
+ ConnName = Connections[0];
+ }
+
+ private void LoadConnections()
+ {
+ Connections.Clear();
+ foreach (var item in DAL.ConnStrs.Keys.OrderBy(e => e))
+ {
+ Connections.Add(item);
+ }
+ }
+
+ #endregion
+
+ #region 命令
+
+ /// <summary>连接/断开数据库</summary>
+ [RelayCommand]
+ private async Task Connect()
+ {
+ if (IsConnected)
+ {
+ Tables.Clear();
+ IsConnected = false;
+ Status = "已断开";
+ return;
+ }
+
+ if (ConnName.IsNullOrEmpty()) return;
+
+ try
+ {
+ Status = "正在连接...";
+
+ // 确保连接字符串可用
+ var dal = DAL.Create(ConnName);
+ _ = dal.ConnStr;
+
+ // 异步加载表
+ await Task.Run(() => LoadTables());
+
+ IsConnected = true;
+ Status = $"已连接 {ConnName},共 {Tables.Count} 个表";
+
+ if (NameSpace.IsNullOrEmpty()) NameSpace = ConnName;
+ if (EntityConnName.IsNullOrEmpty()) EntityConnName = ConnName;
+ if (OutputPath.IsNullOrEmpty()) OutputPath = ConnName;
+ }
+ catch (Exception ex)
+ {
+ Status = $"连接失败:{ex.Message}";
+ XTrace.WriteException(ex);
+ }
+ }
+
+ /// <summary>刷新数据表</summary>
+ [RelayCommand]
+ private async Task RefreshTables()
+ {
+ if (ConnName.IsNullOrEmpty()) return;
+
+ Status = "正在刷新表...";
+ await Task.Run(() => LoadTables());
+ Status = $"已刷新,共 {Tables.Count} 个表";
+ }
+
+ private void LoadTables()
+ {
+ var dal = DAL.Create(ConnName);
+ var list = dal.Tables;
+
+ if (!IncludeView)
+ list = list.Where(t => !t.IsView).ToList();
+
+ // 过滤系统表
+ list = list.Where(t => t.Name != "dtproperties" && t.Name != "sysconstraints" && t.Name != "syssegments").ToList();
+
+ // 排序
+ list = [.. list.OrderBy(t => t.Name)];
+
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ Tables.Clear();
+ foreach (var table in list)
+ {
+ Tables.Add(table);
+ }
+ });
+ }
+
+ /// <summary>生成选中表的代码</summary>
+ [RelayCommand]
+ private async Task GenerateTable()
+ {
+ if (SelectedTable == null) return;
+
+ await Task.Run(() =>
+ {
+ try
+ {
+ var option = new EntityBuilderOption
+ {
+ BaseClass = BaseClass,
+ ConnName = EntityConnName,
+ Namespace = NameSpace,
+ Output = OutputPath,
+ };
+
+ var sw = Stopwatch.StartNew();
+ var rs = EntityBuilder.BuildTables([SelectedTable], option);
+ sw.Stop();
+
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ Status = $"生成 {SelectedTable.Name} 完成!耗时:{sw.Elapsed}";
+ });
+ }
+ catch (Exception ex)
+ {
+ XTrace.WriteException(ex);
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ Status = $"生成失败:{ex.Message}";
+ });
+ }
+ });
+ }
+
+ /// <summary>生成所有表的代码</summary>
+ [RelayCommand]
+ private async Task GenerateAll()
+ {
+ if (Tables.Count == 0) return;
+
+ var tables = Tables.ToList();
+ await Task.Run(() =>
+ {
+ try
+ {
+ var option = new EntityBuilderOption
+ {
+ BaseClass = BaseClass,
+ ConnName = EntityConnName,
+ Namespace = NameSpace,
+ Output = OutputPath,
+ };
+
+ var sw = Stopwatch.StartNew();
+ var rs = EntityBuilder.BuildTables(tables, option);
+ sw.Stop();
+
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ Status = $"生成 {tables.Count} 个类完成!耗时:{sw.Elapsed}";
+ });
+ }
+ catch (Exception ex)
+ {
+ XTrace.WriteException(ex);
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ Status = $"生成失败:{ex.Message}";
+ });
+ }
+ });
+ }
+
+ /// <summary>打开输出目录</summary>
+ [RelayCommand]
+ private void OpenOutputDir()
+ {
+ var dir = OutputPath.GetFullPath();
+ if (!Directory.Exists(dir))
+ dir = AppDomain.CurrentDomain.BaseDirectory;
+
+ Process.Start("explorer.exe", "\"" + dir + "\"");
+ }
+
+ #endregion
+}
diff --git a/CrazyCoder/ViewModels/DataSyncViewModel.cs b/CrazyCoder/ViewModels/DataSyncViewModel.cs
new file mode 100644
index 0000000..fb7380a
--- /dev/null
+++ b/CrazyCoder/ViewModels/DataSyncViewModel.cs
@@ -0,0 +1,347 @@
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Windows;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using CrazyCoder.Models;
+using NewLife;
+using NewLife.Log;
+using XCode;
+using XCode.DataAccessLayer;
+
+namespace CrazyCoder.ViewModels;
+
+/// <summary>跨库数据同步 ViewModel</summary>
+public partial class DataSyncViewModel : ObservableObject
+{
+ #region 属性
+
+ /// <summary>所有可用连接</summary>
+ public ObservableCollection<String> Connections { get; } = [];
+
+ /// <summary>目标连接列表(排除源连接)</summary>
+ public ObservableCollection<String> TargetConnections { get; } = [];
+
+ /// <summary>选中的源连接</summary>
+ [ObservableProperty]
+ private String _sourceConn = "";
+
+ /// <summary>选中的目标连接</summary>
+ [ObservableProperty]
+ private String _targetConn = "";
+
+ /// <summary>源数据库是否已连接</summary>
+ [ObservableProperty]
+ private Boolean _isSourceConnected;
+
+ /// <summary>目标数据库是否已连接</summary>
+ [ObservableProperty]
+ private Boolean _isTargetConnected;
+
+ /// <summary>同步表集合</summary>
+ public ObservableCollection<SyncTableModel> Tables { get; } = [];
+
+ /// <summary>是否同步架构</summary>
+ [ObservableProperty]
+ private Boolean _syncSchema = true;
+
+ /// <summary>是否忽略错误</summary>
+ [ObservableProperty]
+ private Boolean _ignoreError;
+
+ /// <summary>是否正在同步中</summary>
+ [ObservableProperty]
+ private Boolean _isSyncing;
+
+ /// <summary>同步进度文本</summary>
+ [ObservableProperty]
+ private String _progressText = "";
+
+ /// <summary>状态文本</summary>
+ [ObservableProperty]
+ private String _status = "就绪";
+
+ /// <summary>源 DAL</summary>
+ private DAL _sourceDal;
+
+ /// <summary>源数据表集合</summary>
+ private IList<IDataTable> _sourceTables;
+
+ #endregion
+
+ #region 构造
+
+ /// <summary>实例化数据同步 ViewModel</summary>
+ public DataSyncViewModel()
+ {
+ LoadConnections();
+ }
+
+ private void LoadConnections()
+ {
+ Connections.Clear();
+ foreach (var item in DAL.ConnStrs.Keys.OrderBy(e => e))
+ {
+ Connections.Add(item);
+ }
+ }
+
+ #endregion
+
+ #region 命令
+
+ /// <summary>连接源数据库</summary>
+ [RelayCommand]
+ private async Task ConnectSource()
+ {
+ if (SourceConn.IsNullOrEmpty()) return;
+
+ try
+ {
+ Status = "正在连接源数据库...";
+ _sourceDal = DAL.Create(SourceConn);
+
+ await Task.Run(() =>
+ {
+ _sourceTables = _sourceDal.Tables;
+ });
+
+ if (_sourceTables == null) return;
+
+ // 填充表列表
+ Tables.Clear();
+ foreach (var tb in _sourceTables.OrderBy(e => e.TableName))
+ {
+ Tables.Add(new SyncTableModel
+ {
+ Name = tb.TableName,
+ DisplayName = tb.DisplayName,
+ EnableSync = true,
+ });
+ }
+
+ // 加载行数
+ await Task.Run(() => LoadRowCounts());
+
+ // 更新目标列表
+ TargetConnections.Clear();
+ foreach (var item in Connections.Where(e => e != SourceConn))
+ {
+ TargetConnections.Add(item);
+ }
+
+ IsSourceConnected = true;
+ Status = $"已连接源 {SourceConn},共 {Tables.Count} 个表";
+ }
+ catch (Exception ex)
+ {
+ Status = $"连接失败:{ex.Message}";
+ XTrace.WriteException(ex);
+ }
+ }
+
+ /// <summary>断开源数据库</summary>
+ [RelayCommand]
+ private void DisconnectSource()
+ {
+ _sourceDal = null;
+ _sourceTables = null;
+ Tables.Clear();
+ TargetConnections.Clear();
+ IsSourceConnected = false;
+ IsTargetConnected = false;
+ Status = "已断开源数据库";
+ }
+
+ /// <summary>连接目标数据库</summary>
+ [RelayCommand]
+ private async Task ConnectTarget()
+ {
+ if (TargetConn.IsNullOrEmpty()) return;
+
+ try
+ {
+ Status = "正在连接目标数据库...";
+ var dal = DAL.Create(TargetConn);
+
+ await Task.Run(() =>
+ {
+ var tables = dal.Tables;
+ if (tables == null) return;
+
+ foreach (var item in Tables)
+ {
+ var tb = tables.FirstOrDefault(e => e.TableName.EqualIgnoreCase(item.Name));
+ if (tb != null)
+ {
+ var sb = new SelectBuilder { Table = item.Name };
+ item.TargetCount = dal.SelectCount(sb);
+ }
+ }
+ });
+
+ IsTargetConnected = true;
+ Status = $"已连接目标 {TargetConn}";
+ }
+ catch (Exception ex)
+ {
+ Status = $"连接目标失败:{ex.Message}";
+ XTrace.WriteException(ex);
+ }
+ }
+
+ /// <summary>断开目标数据库</summary>
+ [RelayCommand]
+ private void DisconnectTarget()
+ {
+ IsTargetConnected = false;
+ Status = "已断开目标数据库";
+ }
+
+ /// <summary>加载源表行数</summary>
+ private void LoadRowCounts()
+ {
+ if (_sourceDal == null || _sourceTables == null) return;
+
+ foreach (var item in Tables)
+ {
+ var sb = new SelectBuilder { Table = item.Name };
+ item.SourceCount = _sourceDal.SelectCount(sb);
+ }
+ }
+
+ /// <summary>开始同步数据</summary>
+ [RelayCommand]
+ private async Task SyncData()
+ {
+ if (_sourceDal == null || TargetConn.IsNullOrEmpty()) return;
+
+ var syncTables = Tables.Where(e => e.EnableSync).ToList();
+ if (syncTables.Count == 0)
+ {
+ Status = "没有选中任何需要同步的表";
+ return;
+ }
+
+ IsSyncing = true;
+ Status = "正在同步...";
+
+ try
+ {
+ await Task.Run(() =>
+ {
+ var dal = DAL.Create(TargetConn);
+ var targetTables = dal.Tables ?? new List<IDataTable>();
+
+ _sourceDal.Db.ShowSQL = false;
+ _sourceDal.Session.ShowSQL = false;
+ dal.Db.ShowSQL = false;
+ dal.Session.ShowSQL = false;
+
+ var total = syncTables.Count;
+ var index = 0;
+
+ foreach (var item in syncTables)
+ {
+ index++;
+ ProgressText = $"[{index}/{total}] {item.Name}";
+
+ try
+ {
+ if (!SyncSchema && !targetTables.Any(e => e.TableName.EqualIgnoreCase(item.Name)))
+ {
+ item.Description = "目标表不存在,跳过!";
+ }
+ else
+ {
+ var tb = _sourceTables?.FirstOrDefault(e => e.TableName == item.Name);
+ if (tb != null)
+ {
+ var rs = _sourceDal.Sync(tb, TargetConn, SyncSchema);
+ item.SyncCount = rs;
+ item.Description = "成功!";
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ item.Description = ex.Message;
+ XTrace.WriteException(ex);
+
+ if (!IgnoreError) throw;
+ }
+
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ // 刷新 UI
+ var _ = Tables;
+ });
+ }
+
+ _sourceDal.Session.ShowSQL = true;
+ _sourceDal.Db.ShowSQL = true;
+ dal.Session.ShowSQL = true;
+ dal.Db.ShowSQL = true;
+
+ ProgressText = "";
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ Status = $"同步完成!共处理 {total} 个表";
+ });
+ });
+ }
+ catch (Exception ex)
+ {
+ Status = $"同步失败:{ex.Message}";
+ XTrace.WriteException(ex);
+ }
+ finally
+ {
+ IsSyncing = false;
+ }
+ }
+
+ /// <summary>全选</summary>
+ [RelayCommand]
+ private void SelectAll()
+ {
+ foreach (var item in Tables)
+ {
+ item.EnableSync = true;
+ }
+ }
+
+ /// <summary>反选</summary>
+ [RelayCommand]
+ private void InvertSelection()
+ {
+ foreach (var item in Tables)
+ {
+ item.EnableSync = !item.EnableSync;
+ }
+ }
+
+ /// <summary>选择源多出(源有目标无)的表</summary>
+ [RelayCommand]
+ private async Task SelectDifferent()
+ {
+ if (TargetConn.IsNullOrEmpty()) return;
+
+ await Task.Run(() =>
+ {
+ var dal = DAL.Create(TargetConn);
+ var tables = dal.Tables;
+ if (tables == null) return;
+
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ foreach (var item in Tables)
+ {
+ item.EnableSync = !tables.Any(e => e.TableName.EqualIgnoreCase(item.Name));
+ }
+ });
+ });
+ }
+
+ #endregion
+}
diff --git a/CrazyCoder/ViewModels/IoControlViewModel.cs b/CrazyCoder/ViewModels/IoControlViewModel.cs
new file mode 100644
index 0000000..87c1c7a
--- /dev/null
+++ b/CrazyCoder/ViewModels/IoControlViewModel.cs
@@ -0,0 +1,491 @@
+#nullable enable
+
+using System.Collections.ObjectModel;
+using System.IO.Ports;
+using System.Windows;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using NewLife;
+using NewLife.Log;
+using NewLife.Serial.Protocols;
+
+namespace CrazyCoder.ViewModels;
+
+/// <summary>I/O 控制面板 ViewModel</summary>
+public partial class IoControlViewModel : ObservableObject, IDisposable
+{
+ #region 属性
+ private ModbusRtu? _modbus;
+
+ /// <summary>端口名称列表</summary>
+ public ObservableCollection<String> PortNames { get; } = [];
+
+ /// <summary>波特率列表</summary>
+ public ObservableCollection<Int32> BaudRates { get; } = [1200, 2400, 4800, 9600, 14400, 19200, 38400, 56000, 57600, 115200, 128000, 194000, 256000, 512000, 1024000, 2048000];
+
+ /// <summary>校验位列表</summary>
+ public ObservableCollection<String> ParityOptions { get; } = ["None", "Odd", "Even", "Mark", "Space"];
+
+ /// <summary>停止位列表</summary>
+ public ObservableCollection<String> StopBitsOptions { get; } = ["One", "Two", "OnePointFive"];
+
+ /// <summary>数据位列表</summary>
+ public ObservableCollection<Int32> DataBitsOptions { get; } = [5, 6, 7, 8];
+
+ /// <summary>端口名称</summary>
+ [ObservableProperty]
+ private String _portName = "COM1";
+
+ /// <summary>波特率</summary>
+ [ObservableProperty]
+ private Int32 _baudRate = 115200;
+
+ /// <summary>校验位索引</summary>
+ [ObservableProperty]
+ private Int32 _parityIndex;
+
+ /// <summary>数据位</summary>
+ [ObservableProperty]
+ private Int32 _dataBits = 8;
+
+ /// <summary>停止位索引</summary>
+ [ObservableProperty]
+ private Int32 _stopBitsIndex;
+
+ /// <summary>从站地址</summary>
+ [ObservableProperty]
+ private Byte _slaveAddress = 1;
+
+ /// <summary>是否已连接</summary>
+ [ObservableProperty]
+ private Boolean _isConnected;
+
+ /// <summary>连接按钮文本</summary>
+ [ObservableProperty]
+ private String _connectButtonText = "连接";
+
+ /// <summary>输出 1 状态</summary>
+ [ObservableProperty]
+ private Boolean _output1;
+
+ /// <summary>输出 2 状态</summary>
+ [ObservableProperty]
+ private Boolean _output2;
+
+ /// <summary>输出 3 状态</summary>
+ [ObservableProperty]
+ private Boolean _output3;
+
+ /// <summary>输出 4 状态</summary>
+ [ObservableProperty]
+ private Boolean _output4;
+
+ /// <summary>输出 5 状态</summary>
+ [ObservableProperty]
+ private Boolean _output5;
+
+ /// <summary>输出 6 状态</summary>
+ [ObservableProperty]
+ private Boolean _output6;
+
+ /// <summary>输出 7 状态</summary>
+ [ObservableProperty]
+ private Boolean _output7;
+
+ /// <summary>输出 8 状态</summary>
+ [ObservableProperty]
+ private Boolean _output8;
+
+ /// <summary>输入 1 状态</summary>
+ [ObservableProperty]
+ private Boolean _input1;
+
+ /// <summary>输入 2 状态</summary>
+ [ObservableProperty]
+ private Boolean _input2;
+
+ /// <summary>输入 3 状态</summary>
+ [ObservableProperty]
+ private Boolean _input3;
+
+ /// <summary>输入 4 状态</summary>
+ [ObservableProperty]
+ private Boolean _input4;
+
+ /// <summary>输入 5 状态</summary>
+ [ObservableProperty]
+ private Boolean _input5;
+
+ /// <summary>输入 6 状态</summary>
+ [ObservableProperty]
+ private Boolean _input6;
+
+ /// <summary>输入 7 状态</summary>
+ [ObservableProperty]
+ private Boolean _input7;
+
+ /// <summary>输入 8 状态</summary>
+ [ObservableProperty]
+ private Boolean _input8;
+
+ /// <summary>延迟(毫秒)</summary>
+ [ObservableProperty]
+ private Int32 _delay = 100;
+
+ /// <summary>产品型号</summary>
+ [ObservableProperty]
+ private String _productType = "";
+
+ /// <summary>固件版本</summary>
+ [ObservableProperty]
+ private String _firmwareVersion = "";
+
+ /// <summary>日志回调</summary>
+ public Action<String>? OnLog { get; set; }
+ #endregion
+
+ #region 构造
+ /// <summary>实例化 I/O 控制面板 ViewModel</summary>
+ public IoControlViewModel()
+ {
+ RefreshPorts();
+ }
+ #endregion
+
+ #region 方法
+ /// <summary>刷新串口列表</summary>
+ [RelayCommand]
+ private void RefreshPorts()
+ {
+ PortNames.Clear();
+ foreach (var name in SerialPort.GetPortNames())
+ {
+ PortNames.Add(name);
+ }
+ if (PortNames.Count > 0) PortName = PortNames[0];
+ }
+
+ private ModbusRtu CreateModbus()
+ {
+ var parity = ParityIndex switch
+ {
+ 1 => Parity.Odd,
+ 2 => Parity.Even,
+ 3 => Parity.Mark,
+ 4 => Parity.Space,
+ _ => Parity.None,
+ };
+ var stopBits = StopBitsIndex switch
+ {
+ 1 => StopBits.Two,
+ 2 => StopBits.OnePointFive,
+ _ => StopBits.One,
+ };
+
+ return new ModbusRtu
+ {
+ PortName = PortName,
+ Baudrate = BaudRate,
+ DataBits = DataBits,
+ Parity = parity,
+ StopBits = stopBits,
+ Log = new IoControlLog(this),
+ };
+ }
+ #endregion
+
+ #region 连接/断开
+ /// <summary>切换连接/断开</summary>
+ [RelayCommand]
+ private void ToggleConnect()
+ {
+ if (IsConnected)
+ Disconnect();
+ else
+ Connect();
+ }
+
+ private void Connect()
+ {
+ try
+ {
+ _modbus?.Dispose();
+ _modbus = CreateModbus();
+ _modbus.Open();
+
+ IsConnected = true;
+ ConnectButtonText = "断开";
+
+ WriteLog($"已连接 {PortName},{BaudRate}/{DataBits}/{ParityOptions[ParityIndex]}/{StopBitsOptions[StopBitsIndex]}");
+
+ // 读取设备信息
+ ReadDeviceInfo();
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"连接失败:{ex.Message}");
+ MessageBox.Show($"连接失败:{ex.Message}", "错误");
+ }
+ }
+
+ private void Disconnect()
+ {
+ if (_modbus != null)
+ {
+ try { _modbus.Dispose(); } catch { }
+ _modbus = null;
+ }
+
+ IsConnected = false;
+ ConnectButtonText = "连接";
+
+ WriteLog("已断开连接");
+ }
+ #endregion
+
+ #region I/O 操作
+ /// <summary>读取设备信息</summary>
+ [RelayCommand]
+ private void ReadDeviceInfo()
+ {
+ var mb = _modbus;
+ if (mb == null) return;
+
+ try
+ {
+ var data = mb.ReadRegister(SlaveAddress, 0x1000, 14);
+ if (data == null || data.Length < 8)
+ {
+ WriteLog("读取设备信息失败:无返回数据");
+ return;
+ }
+
+ // 每个寄存器2字节,大端序
+ var buf = new Byte[data.Length * 2];
+ for (var i = 0; i < data.Length; i++)
+ {
+ buf[i * 2] = (Byte)(data[i] >> 8);
+ buf[i * 2 + 1] = (Byte)(data[i] & 0xFF);
+ }
+
+ ProductType = System.Text.Encoding.ASCII.GetString(buf, 0, 4);
+ FirmwareVersion = System.Text.Encoding.ASCII.GetString(buf, 4, 4);
+ WriteLog($"设备信息:型号={ProductType} 版本={FirmwareVersion}");
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"读取设备信息失败:{ex.Message}");
+ }
+ }
+
+ /// <summary>打开指定输出</summary>
+ [RelayCommand]
+ private void TurnOn(String index)
+ {
+ var mb = _modbus;
+ if (mb == null) return;
+
+ var addr = index.ToInt() - 1;
+ try
+ {
+ if (Delay > 0)
+ mb.WriteRegisters(SlaveAddress, (UInt16)(0x0003 + addr * 5), [0x0004, (UInt16)(Delay / 100)]);
+ else
+ mb.WriteCoil(SlaveAddress, (UInt16)addr, 0xFF00);
+
+ WriteLog($"输出 {index} 已打开 (地址={addr})");
+ UpdateOutputProperty(index, true);
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"打开输出 {index} 失败:{ex.Message}");
+ }
+ }
+
+ /// <summary>关闭指定输出</summary>
+ [RelayCommand]
+ private void TurnOff(String index)
+ {
+ var mb = _modbus;
+ if (mb == null) return;
+
+ var addr = index.ToInt() - 1;
+ try
+ {
+ if (Delay > 0)
+ mb.WriteRegisters(SlaveAddress, (UInt16)(0x0003 + addr * 5), [0x0002, (UInt16)(Delay / 100)]);
+ else
+ mb.WriteCoil(SlaveAddress, (UInt16)addr, 0);
+
+ WriteLog($"输出 {index} 已关闭 (地址={addr})");
+ UpdateOutputProperty(index, false);
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"关闭输出 {index} 失败:{ex.Message}");
+ }
+ }
+
+ /// <summary>打开所有输出</summary>
+ [RelayCommand]
+ private void TurnOnAll()
+ {
+ var mb = _modbus;
+ if (mb == null) return;
+
+ try
+ {
+ mb.WriteCoils(SlaveAddress, 0, [0xFF00, 0xFF00, 0xFF00, 0xFF00, 0xFF00, 0xFF00, 0xFF00, 0xFF00]);
+ WriteLog("所有输出已打开");
+ for (var i = 1; i <= 8; i++) UpdateOutputProperty(i.ToString(), true);
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"打开所有输出失败:{ex.Message}");
+ }
+ }
+
+ /// <summary>关闭所有输出</summary>
+ [RelayCommand]
+ private void TurnOffAll()
+ {
+ var mb = _modbus;
+ if (mb == null) return;
+
+ try
+ {
+ mb.WriteCoils(SlaveAddress, 0, [0, 0, 0, 0, 0, 0, 0, 0]);
+ WriteLog("所有输出已关闭");
+ for (var i = 1; i <= 8; i++) UpdateOutputProperty(i.ToString(), false);
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"关闭所有输出失败:{ex.Message}");
+ }
+ }
+
+ /// <summary>读取所有输入</summary>
+ [RelayCommand]
+ private void ReadInputs()
+ {
+ var mb = _modbus;
+ if (mb == null) return;
+
+ try
+ {
+ var data = mb.ReadDiscrete(SlaveAddress, 0x0100, 8);
+ if (data == null || data.Length == 0)
+ {
+ WriteLog("读取输入失败:无返回数据");
+ return;
+ }
+
+ for (var i = 0; i < Math.Min(8, data.Length); i++)
+ {
+ UpdateInputProperty((i + 1).ToString(), data[i]);
+ }
+ var bits = String.Join(" ", data.Select(e => e ? "1" : "0"));
+ WriteLog($"输入已读取:{bits}");
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"读取输入失败:{ex.Message}");
+ }
+ }
+
+ /// <summary>读取所有输出状态</summary>
+ [RelayCommand]
+ private void ReadOutputs()
+ {
+ var mb = _modbus;
+ if (mb == null) return;
+
+ try
+ {
+ var data = mb.ReadCoil(SlaveAddress, 0, 8);
+ if (data == null || data.Length == 0)
+ {
+ WriteLog("读取输出失败:无返回数据");
+ return;
+ }
+
+ for (var i = 0; i < Math.Min(8, data.Length); i++)
+ {
+ UpdateOutputProperty((i + 1).ToString(), data[i]);
+ }
+ var bits = String.Join(" ", data.Select(e => e ? "1" : "0"));
+ WriteLog($"输出已读取:{bits}");
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"读取输出失败:{ex.Message}");
+ }
+ }
+
+ private void UpdateOutputProperty(String index, Boolean value)
+ {
+ switch (index)
+ {
+ case "1": Output1 = value; break;
+ case "2": Output2 = value; break;
+ case "3": Output3 = value; break;
+ case "4": Output4 = value; break;
+ case "5": Output5 = value; break;
+ case "6": Output6 = value; break;
+ case "7": Output7 = value; break;
+ case "8": Output8 = value; break;
+ }
+ }
+
+ private void UpdateInputProperty(String index, Boolean value)
+ {
+ switch (index)
+ {
+ case "1": Input1 = value; break;
+ case "2": Input2 = value; break;
+ case "3": Input3 = value; break;
+ case "4": Input4 = value; break;
+ case "5": Input5 = value; break;
+ case "6": Input6 = value; break;
+ case "7": Input7 = value; break;
+ case "8": Input8 = value; break;
+ }
+ }
+ #endregion
+
+ #region 清空日志
+ /// <summary>清空日志</summary>
+ [RelayCommand]
+ private void ClearLog()
+ {
+ OnLog?.Invoke("__CLEAR__");
+ }
+ #endregion
+
+ #region 日志
+ private void WriteLog(String msg)
+ {
+ OnLog?.Invoke(msg);
+ }
+
+ private class IoControlLog : Logger
+ {
+ private readonly IoControlViewModel _vm;
+ public IoControlLog(IoControlViewModel vm) => _vm = vm;
+ protected override void OnWrite(LogLevel level, String format, params Object?[] args)
+ {
+ var msg = args is { Length: > 0 } ? String.Format(format, args) : format;
+ _vm.WriteLog(msg);
+ }
+ }
+ #endregion
+
+ #region IDisposable
+ /// <summary>释放资源</summary>
+ public void Dispose()
+ {
+ Disconnect();
+ GC.SuppressFinalize(this);
+ }
+ #endregion
+}
diff --git a/CrazyCoder/ViewModels/MainViewModel.cs b/CrazyCoder/ViewModels/MainViewModel.cs
index cd454f2..541bdb3 100644
--- a/CrazyCoder/ViewModels/MainViewModel.cs
+++ b/CrazyCoder/ViewModels/MainViewModel.cs
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Input;
using CrazyCoder.Models;
using CrazyCoder.Views;
using NewLife.Reflection;
+// DataModeling, RedisManager, DataSync 窗口已在 Menus 中注册 Type
namespace CrazyCoder.ViewModels
{
@@ -14,10 +15,15 @@ namespace CrazyCoder.ViewModels
{
Menus =
[
- new MenuModel() { IconFont = "\xe635", Title = "数据建模", BackColor = "#218868" },
+ new MenuModel() { IconFont = "\xe635", Title = "数据建模", BackColor = "#218868", Type = typeof(DataModelingWindow) },
+ new MenuModel() { IconFont = "\xe6b6", Title = "Redis 管理器", BackColor = "#EE3B3B", Type = typeof(RedisManagerWindow) },
+ new MenuModel() { IconFont = "\xe6e1", Title = "跨库同步", BackColor = "#218868", Type = typeof(DataSyncWindow) },
new MenuModel() { IconFont = "\xe6b6", Title = "网络工具", BackColor = "#EE3B3B", Type = typeof(NetworkWindow) },
new MenuModel() { IconFont = "\xe6e1", Title = "RPC工具", BackColor = "#218868" },
- new MenuModel() { IconFont = "\xe614", Title = "串口工具", BackColor = "#EE3B3B" },
+ new MenuModel() { IconFont = "\xe614", Title = "串口工具", BackColor = "#EE3B3B", Type = typeof(SerialPortWindow) },
+ new MenuModel() { IconFont = "\xe614", Title = "Modbus RTU", BackColor = "#EE3B3B", Type = typeof(ModbusRtuWindow) },
+ new MenuModel() { IconFont = "\xe6b6", Title = "Modbus TCP", BackColor = "#EE3B3B", Type = typeof(ModbusTcpWindow) },
+ new MenuModel() { IconFont = "\xe755", Title = "I/O 控制", BackColor = "#218868", Type = typeof(IoControlWindow) },
new MenuModel() { IconFont = "\xe755", Title = "地图接口", BackColor = "#218868" },
new MenuModel() { IconFont = "\xe635", Title = "正则表达式", BackColor = "#218868", Type = typeof(RegexWindow) },
new MenuModel() { IconFont = "\xe6b6", Title = "图标水印", BackColor = "#EE3B3B", Type = typeof(IconToolWindow) },
diff --git a/CrazyCoder/ViewModels/ModbusRtuViewModel.cs b/CrazyCoder/ViewModels/ModbusRtuViewModel.cs
new file mode 100644
index 0000000..a43e8f9
--- /dev/null
+++ b/CrazyCoder/ViewModels/ModbusRtuViewModel.cs
@@ -0,0 +1,369 @@
+#nullable enable
+
+using System.Collections.ObjectModel;
+using System.IO.Ports;
+using System.Text;
+using System.Windows;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using NewLife;
+using NewLife.Log;
+using NewLife.Serial.Protocols;
+
+namespace CrazyCoder.ViewModels;
+
+/// <summary>Modbus RTU 工具 ViewModel</summary>
+public partial class ModbusRtuViewModel : ObservableObject, IDisposable
+{
+ #region 属性
+ private ModbusRtu? _modbus;
+
+ /// <summary>端口名称列表</summary>
+ public ObservableCollection<String> PortNames { get; } = [];
+
+ /// <summary>波特率列表</summary>
+ public ObservableCollection<Int32> BaudRates { get; } = [1200, 2400, 4800, 9600, 14400, 19200, 38400, 56000, 57600, 115200, 128000, 194000, 256000, 512000, 1024000, 2048000];
+
+ /// <summary>校验位列表</summary>
+ public ObservableCollection<String> ParityOptions { get; } = ["None", "Odd", "Even", "Mark", "Space"];
+
+ /// <summary>停止位列表</summary>
+ public ObservableCollection<String> StopBitsOptions { get; } = ["One", "Two", "OnePointFive"];
+
+ /// <summary>数据位列表</summary>
+ public ObservableCollection<Int32> DataBitsOptions { get; } = [5, 6, 7, 8];
+
+ /// <summary>功能码列表</summary>
+ public ObservableCollection<String> FunctionCodes { get; } =
+ [
+ "01 读线圈",
+ "02 读离散量输入",
+ "03 读保持寄存器",
+ "04 读输入寄存器",
+ "05 写单线圈",
+ "06 写单寄存器",
+ "0F 写多线圈",
+ "10 写多寄存器",
+ ];
+
+ /// <summary>端口名称</summary>
+ [ObservableProperty]
+ private String _portName = "COM1";
+
+ /// <summary>波特率</summary>
+ [ObservableProperty]
+ private Int32 _baudRate = 115200;
+
+ /// <summary>校验位索引</summary>
+ [ObservableProperty]
+ private Int32 _parityIndex;
+
+ /// <summary>数据位</summary>
+ [ObservableProperty]
+ private Int32 _dataBits = 8;
+
+ /// <summary>停止位索引</summary>
+ [ObservableProperty]
+ private Int32 _stopBitsIndex;
+
+ /// <summary>从站地址</summary>
+ [ObservableProperty]
+ private Byte _slaveAddress = 1;
+
+ /// <summary>功能码索引</summary>
+ [ObservableProperty]
+ private Int32 _functionCodeIndex;
+
+ /// <summary>起始地址</summary>
+ [ObservableProperty]
+ private UInt16 _startAddress;
+
+ /// <summary>读取数量</summary>
+ [ObservableProperty]
+ private UInt16 _readCount = 10;
+
+ /// <summary>写入值</summary>
+ [ObservableProperty]
+ private String _writeValue = "0";
+
+ /// <summary>是否已连接</summary>
+ [ObservableProperty]
+ private Boolean _isConnected;
+
+ /// <summary>连接按钮文本</summary>
+ [ObservableProperty]
+ private String _connectButtonText = "连接";
+
+ /// <summary>读取结果(十六进制)</summary>
+ [ObservableProperty]
+ private String _readResult = "";
+
+ /// <summary>日志回调</summary>
+ public Action<String>? OnLog { get; set; }
+ #endregion
+
+ #region 构造
+ /// <summary>实例化 Modbus RTU 工具 ViewModel</summary>
+ public ModbusRtuViewModel()
+ {
+ RefreshPorts();
+ }
+ #endregion
+
+ #region 方法
+ /// <summary>刷新串口列表</summary>
+ [RelayCommand]
+ private void RefreshPorts()
+ {
+ PortNames.Clear();
+ foreach (var name in SerialPort.GetPortNames())
+ {
+ PortNames.Add(name);
+ }
+ if (PortNames.Count > 0) PortName = PortNames[0];
+ }
+
+ private ModbusRtu CreateModbus()
+ {
+ var parity = ParityIndex switch
+ {
+ 1 => Parity.Odd,
+ 2 => Parity.Even,
+ 3 => Parity.Mark,
+ 4 => Parity.Space,
+ _ => Parity.None,
+ };
+ var stopBits = StopBitsIndex switch
+ {
+ 1 => StopBits.Two,
+ 2 => StopBits.OnePointFive,
+ _ => StopBits.One,
+ };
+
+ return new ModbusRtu
+ {
+ PortName = PortName,
+ Baudrate = BaudRate,
+ DataBits = DataBits,
+ Parity = parity,
+ StopBits = stopBits,
+ Log = new ModbusLog(this),
+ };
+ }
+ #endregion
+
+ #region 连接/断开
+ /// <summary>切换连接/断开</summary>
+ [RelayCommand]
+ private void ToggleConnect()
+ {
+ if (IsConnected)
+ Disconnect();
+ else
+ Connect();
+ }
+
+ private void Connect()
+ {
+ try
+ {
+ _modbus?.Dispose();
+ _modbus = CreateModbus();
+ _modbus.Open();
+
+ IsConnected = true;
+ ConnectButtonText = "断开";
+
+ WriteLog($"Modbus RTU 已连接 {PortName},{BaudRate}/{DataBits}/{ParityOptions[ParityIndex]}/{StopBitsOptions[StopBitsIndex]}");
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"连接失败:{ex.Message}");
+ MessageBox.Show($"连接失败:{ex.Message}", "错误");
+ }
+ }
+
+ private void Disconnect()
+ {
+ if (_modbus != null)
+ {
+ try { _modbus.Dispose(); } catch { }
+ _modbus = null;
+ }
+
+ IsConnected = false;
+ ConnectButtonText = "连接";
+
+ WriteLog("Modbus RTU 已断开");
+ }
+ #endregion
+
+ #region 读写操作
+ /// <summary>执行读写操作</summary>
+ [RelayCommand]
+ private void Execute()
+ {
+ var mb = _modbus;
+ if (mb == null)
+ {
+ WriteLog("未连接");
+ return;
+ }
+
+ var host = SlaveAddress;
+ var address = StartAddress;
+ var code = FunctionCodeIndex;
+
+ try
+ {
+ switch (code)
+ {
+ case 0: // 01 读线圈
+ {
+ var count = ReadCount;
+ var data = mb.ReadCoil(host, address, count);
+ if (data != null)
+ {
+ var bits = String.Join(" ", data.Select(e => e ? "1" : "0"));
+ WriteLog($"读线圈 [{data.Length}位] {bits}");
+ ReadResult = bits;
+ }
+ else
+ {
+ WriteLog("读线圈:无返回数据");
+ ReadResult = "无返回数据";
+ }
+ break;
+ }
+ case 1: // 02 读离散量输入
+ {
+ var count = ReadCount;
+ var data = mb.ReadDiscrete(host, address, count);
+ if (data != null)
+ {
+ var bits = String.Join(" ", data.Select(e => e ? "1" : "0"));
+ WriteLog($"读离散量输入 [{data.Length}位] {bits}");
+ ReadResult = bits;
+ }
+ else
+ {
+ WriteLog("读离散量输入:无返回数据");
+ ReadResult = "无返回数据";
+ }
+ break;
+ }
+ case 2: // 03 读保持寄存器
+ {
+ var count = ReadCount;
+ var data = mb.ReadRegister(host, address, count);
+ if (data != null)
+ {
+ var hex = String.Join(", ", data);
+ WriteLog($"读保持寄存器 [{data.Length}个] {hex}");
+ ReadResult = hex;
+ }
+ else
+ {
+ WriteLog("读保持寄存器:无返回数据");
+ ReadResult = "无返回数据";
+ }
+ break;
+ }
+ case 3: // 04 读输入寄存器
+ {
+ var count = ReadCount;
+ var data = mb.ReadInput(host, address, count);
+ if (data != null)
+ {
+ var hex = String.Join(", ", data);
+ WriteLog($"读输入寄存器 [{data.Length}个] {hex}");
+ ReadResult = hex;
+ }
+ else
+ {
+ WriteLog("读输入寄存器:无返回数据");
+ ReadResult = "无返回数据";
+ }
+ break;
+ }
+ case 4: // 05 写单线圈
+ {
+ var value = (UInt16)(WriteValue.ToInt() > 0 ? 0xFF00 : 0x0000);
+ mb.WriteCoil(host, address, value);
+ WriteLog($"写单线圈 地址={address} 值=0x{value:X4}");
+ ReadResult = $"写入成功:地址={address} 值=0x{value:X4}";
+ break;
+ }
+ case 5: // 06 写单寄存器
+ {
+ var value = (UInt16)WriteValue.ToInt();
+ mb.WriteRegister(host, address, value);
+ WriteLog($"写单寄存器 地址={address} 值=0x{value:X4}");
+ ReadResult = $"写入成功:地址={address} 值=0x{value:X4}";
+ break;
+ }
+ case 6: // 0F 写多线圈
+ {
+ var count = ReadCount;
+ var values = new UInt16[count];
+ for (var i = 0; i < count; i++) values[i] = 0xFF00;
+ mb.WriteCoils(host, address, values);
+ WriteLog($"写多线圈 地址={address} 数量={count}");
+ ReadResult = $"写入成功:地址={address} 数量={count}";
+ break;
+ }
+ case 7: // 10 写多寄存器
+ {
+ var count = ReadCount;
+ var values = new UInt16[count];
+ for (var i = 0; i < count; i++) values[i] = (UInt16)(WriteValue.ToInt() + i);
+ mb.WriteRegisters(host, address, values);
+ WriteLog($"写多寄存器 地址={address} 数量={count}");
+ ReadResult = $"写入成功:地址={address} 数量={count}";
+ break;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"操作失败:{ex.Message}");
+ ReadResult = $"操作失败:{ex.Message}";
+ }
+ }
+
+
+ /// <summary>清空日志</summary>
+ [RelayCommand]
+ private void ClearLog()
+ {
+ OnLog?.Invoke("__CLEAR__");
+ }
+ #endregion
+
+ #region 日志
+ private void WriteLog(String msg)
+ {
+ OnLog?.Invoke(msg);
+ }
+
+ private class ModbusLog : Logger
+ {
+ private readonly ModbusRtuViewModel _vm;
+ public ModbusLog(ModbusRtuViewModel vm) => _vm = vm;
+ protected override void OnWrite(LogLevel level, String format, params Object?[] args)
+ {
+ var msg = args is { Length: > 0 } ? String.Format(format, args) : format;
+ _vm.WriteLog(msg);
+ }
+ }
+ #endregion
+
+ #region IDisposable
+ /// <summary>释放资源</summary>
+ public void Dispose()
+ {
+ Disconnect();
+ GC.SuppressFinalize(this);
+ }
+ #endregion
+}
diff --git a/CrazyCoder/ViewModels/ModbusTcpViewModel.cs b/CrazyCoder/ViewModels/ModbusTcpViewModel.cs
new file mode 100644
index 0000000..9710952
--- /dev/null
+++ b/CrazyCoder/ViewModels/ModbusTcpViewModel.cs
@@ -0,0 +1,578 @@
+#nullable enable
+
+using System.Collections.ObjectModel;
+using System.Windows;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using NewLife;
+using NewLife.Buffers;
+using NewLife.Data;
+using NewLife.IoT.Protocols;
+using NewLife.Log;
+using NewLife.Net;
+using NewLife.Net.Handlers;
+using NewLife.Security;
+using NewLife.Threading;
+
+namespace CrazyCoder.ViewModels;
+
+/// <summary>Modbus TCP 主站/从站模式</summary>
+public enum ModbusTcpMode
+{
+ /// <summary>主站(Master)</summary>
+ Master,
+
+ /// <summary>从站(Slave)</summary>
+ Slave
+}
+
+/// <summary>寄存器单元</summary>
+public class RegisterUnit
+{
+ /// <summary>寄存器地址</summary>
+ public Int32 Address { get; set; }
+
+ /// <summary>寄存器数值</summary>
+ public UInt16 Value { get; set; }
+
+ /// <summary>十六进制显示</summary>
+ public String Hex => Value.GetBytes(false).ToHex();
+}
+
+/// <summary>线圈单元</summary>
+public class CoilUnit
+{
+ /// <summary>线圈地址</summary>
+ public Int32 Address { get; set; }
+
+ /// <summary>线圈数值</summary>
+ public Byte Value { get; set; }
+}
+
+/// <summary>Modbus TCP 工具 ViewModel</summary>
+public partial class ModbusTcpViewModel : ObservableObject, IDisposable
+{
+ #region 属性
+ private ModbusTcp? _modbus;
+ private NetServer? _server;
+ private TimerX? _timer;
+ private List<RegisterUnit> _regs = [];
+ private List<CoilUnit> _coils = [];
+
+ /// <summary>功能码列表</summary>
+ public ObservableCollection<String> FuncCodeOptions { get; } =
+ [
+ "01 读线圈",
+ "02 读离散量输入",
+ "03 读保持寄存器",
+ "04 读输入寄存器",
+ "05 写单线圈",
+ "06 写单寄存器",
+ "0F 写多线圈",
+ "10 写多寄存器",
+ ];
+
+ /// <summary>工作模式列表</summary>
+ public ObservableCollection<String> ModeOptions { get; } = ["主站(Master)", "从站(Slave)"];
+
+ /// <summary>寄存器数据模式列表</summary>
+ public ObservableCollection<String> DataModeOptions { get; } = ["0x0000", "0x7777", "0xFFFF", "递增", "静态随机", "动态随机"];
+
+ /// <summary>工作模式(0=Master, 1=Slave)</summary>
+ [ObservableProperty]
+ private Int32 _selectedMode;
+
+ /// <summary>服务器地址</summary>
+ [ObservableProperty]
+ private String _serverAddress = "127.0.0.1";
+
+ /// <summary>端口</summary>
+ [ObservableProperty]
+ private Int32 _port = 502;
+
+ /// <summary>从站地址</summary>
+ [ObservableProperty]
+ private Byte _slaveAddress = 1;
+
+ /// <summary>功能码索引</summary>
+ [ObservableProperty]
+ private Int32 _functionCodeIndex;
+
+ /// <summary>起始地址</summary>
+ [ObservableProperty]
+ private UInt16 _startAddress;
+
+ /// <summary>读取数量</summary>
+ [ObservableProperty]
+ private UInt16 _readCount = 10;
+
+ /// <summary>写入值</summary>
+ [ObservableProperty]
+ private String _writeValue = "0";
+
+ /// <summary>是否已连接</summary>
+ [ObservableProperty]
+ private Boolean _isConnected;
+
+ /// <summary>连接按钮文本</summary>
+ [ObservableProperty]
+ private String _connectButtonText = "打开";
+
+ /// <summary>读取结果</summary>
+ [ObservableProperty]
+ private String _readResult = "";
+
+ /// <summary>从站数据模式索引</summary>
+ [ObservableProperty]
+ private Int32 _dataModeIndex;
+
+ /// <summary>从站数据地址</summary>
+ [ObservableProperty]
+ private UInt16 _slaveDataAddress;
+
+ /// <summary>从站数据数量</summary>
+ [ObservableProperty]
+ private UInt16 _slaveDataCount = 100;
+
+ /// <summary>寄存器列表(从站模式)</summary>
+ public ObservableCollection<RegisterUnit> Registers { get; } = [];
+
+ /// <summary>线圈列表(从站模式)</summary>
+ public ObservableCollection<CoilUnit> Coils { get; } = [];
+
+ /// <summary>是否从站模式</summary>
+ public Boolean IsSlaveMode => SelectedMode == 1;
+
+ /// <summary>是否主站模式</summary>
+ public Boolean IsMasterMode => SelectedMode == 0;
+
+ /// <summary>日志回调</summary>
+ public Action<String>? OnLog { get; set; }
+ #endregion
+
+ #region 连接/断开
+ /// <summary>切换连接/断开</summary>
+ [RelayCommand]
+ private void ToggleConnect()
+ {
+ if (IsConnected)
+ Disconnect();
+ else
+ Connect();
+ }
+
+ private void Connect()
+ {
+ try
+ {
+ if (SelectedMode == 0)
+ ConnectMaster();
+ else
+ ConnectSlave();
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"连接失败:{ex.Message}");
+ MessageBox.Show($"连接失败:{ex.Message}", "错误");
+ }
+ }
+
+ private void ConnectMaster()
+ {
+ _modbus?.Dispose();
+
+ var mb = new ModbusTcp
+ {
+ Server = $"{ServerAddress}:{Port}",
+ Log = new ModbusTcpLog(this),
+ };
+ mb.Open();
+
+ _modbus = mb;
+ IsConnected = true;
+ ConnectButtonText = "断开";
+
+ WriteLog($"Modbus TCP 主站已连接 {ServerAddress}:{Port}");
+ }
+
+ private void ConnectSlave()
+ {
+ _server?.Dispose();
+
+ var svr = new NetServer(Port)
+ {
+ Log = new ModbusTcpLog(this),
+ SessionLog = new ModbusTcpLog(this),
+ };
+ svr.Add(new LengthFieldCodec { Offset = 4, Size = -2 });
+ svr.Received += OnSlaveReceived;
+ svr.Start();
+
+ _server = svr;
+ _regs = [];
+ _coils = [];
+ Registers.Clear();
+ Coils.Clear();
+
+ RefreshSlaveData();
+
+ IsConnected = true;
+ ConnectButtonText = "停止";
+
+ WriteLog($"Modbus TCP 从站已启动,监听端口 {Port}");
+ }
+
+ private void Disconnect()
+ {
+ if (_modbus != null)
+ {
+ try { _modbus.Dispose(); } catch { }
+ _modbus = null;
+ }
+
+ if (_server != null)
+ {
+ try { _server.Dispose(); } catch { }
+ _server = null;
+ }
+
+ _timer.TryDispose();
+ _timer = null;
+
+ IsConnected = false;
+ ConnectButtonText = "打开";
+
+ WriteLog("连接已关闭");
+ }
+ #endregion
+
+ #region 主站操作
+ /// <summary>执行读写操作</summary>
+ [RelayCommand]
+ private void Execute()
+ {
+ var mb = _modbus;
+ if (mb == null || SelectedMode != 0)
+ {
+ WriteLog("主站未连接");
+ return;
+ }
+
+ var host = SlaveAddress;
+ var address = StartAddress;
+ var code = FunctionCodeIndex;
+
+ try
+ {
+ switch (code)
+ {
+ case 0: // 01 读线圈
+ {
+ var count = ReadCount;
+ var data = mb.ReadCoil(host, address, count);
+ if (data != null)
+ {
+ var bits = String.Join(" ", data.Select(e => e ? "1" : "0"));
+ WriteLog($"读线圈 [{data.Length}位] {bits}");
+ ReadResult = bits;
+ }
+ else
+ {
+ WriteLog("读线圈:无返回数据");
+ ReadResult = "无返回数据";
+ }
+ break;
+ }
+ case 1: // 02 读离散量输入
+ {
+ var count = ReadCount;
+ var data = mb.ReadDiscrete(host, address, count);
+ if (data != null)
+ {
+ var bits = String.Join(" ", data.Select(e => e ? "1" : "0"));
+ WriteLog($"读离散量输入 [{data.Length}位] {bits}");
+ ReadResult = bits;
+ }
+ else
+ {
+ WriteLog("读离散量输入:无返回数据");
+ ReadResult = "无返回数据";
+ }
+ break;
+ }
+ case 2: // 03 读保持寄存器
+ {
+ var count = ReadCount;
+ var data = mb.ReadRegister(host, address, count);
+ if (data != null)
+ {
+ var hex = String.Join(", ", data);
+ WriteLog($"读保持寄存器 [{data.Length}个] {hex}");
+ ReadResult = hex;
+ }
+ else
+ {
+ WriteLog("读保持寄存器:无返回数据");
+ ReadResult = "无返回数据";
+ }
+ break;
+ }
+ case 3: // 04 读输入寄存器
+ {
+ var count = ReadCount;
+ var data = mb.ReadInput(host, address, count);
+ if (data != null)
+ {
+ var hex = String.Join(", ", data);
+ WriteLog($"读输入寄存器 [{data.Length}个] {hex}");
+ ReadResult = hex;
+ }
+ else
+ {
+ WriteLog("读输入寄存器:无返回数据");
+ ReadResult = "无返回数据";
+ }
+ break;
+ }
+ case 4: // 05 写单线圈
+ {
+ var value = (UInt16)(WriteValue.ToInt() > 0 ? 0xFF00 : 0x0000);
+ mb.WriteCoil(host, address, value);
+ WriteLog($"写单线圈 地址={address} 值=0x{value:X4}");
+ ReadResult = $"写入成功:地址={address} 值=0x{value:X4}";
+ break;
+ }
+ case 5: // 06 写单寄存器
+ {
+ var value = (UInt16)WriteValue.ToInt();
+ mb.WriteRegister(host, address, value);
+ WriteLog($"写单寄存器 地址={address} 值=0x{value:X4}");
+ ReadResult = $"写入成功:地址={address} 值=0x{value:X4}";
+ break;
+ }
+ case 6: // 0F 写多线圈
+ {
+ var count = ReadCount;
+ var values = new UInt16[count];
+ for (var i = 0; i < count; i++) values[i] = 0xFF00;
+ mb.WriteCoils(host, address, values);
+ WriteLog($"写多线圈 地址={address} 数量={count}");
+ ReadResult = $"写入成功:地址={address} 数量={count}";
+ break;
+ }
+ case 7: // 10 写多寄存器
+ {
+ var count = ReadCount;
+ var values = new UInt16[count];
+ for (var i = 0; i < count; i++) values[i] = (UInt16)(WriteValue.ToInt() + i);
+ mb.WriteRegisters(host, address, values);
+ WriteLog($"写多寄存器 地址={address} 数量={count}");
+ ReadResult = $"写入成功:地址={address} 数量={count}";
+ break;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"操作失败:{ex.Message}");
+ ReadResult = $"操作失败:{ex.Message}";
+ }
+ }
+
+ private void ShowResult(String title, Byte[]? data)
+ {
+ if (data == null || data.Length == 0)
+ {
+ WriteLog($"{title}:无返回数据");
+ ReadResult = "无返回数据";
+ return;
+ }
+
+ var hex = data.ToHex(" ", 0, data.Length);
+ WriteLog($"{title} [{data.Length}字节] {hex}");
+ ReadResult = hex;
+ }
+ #endregion
+
+ #region 从站处理
+ private void RefreshSlaveData()
+ {
+ var addr = SlaveDataAddress;
+ var count = SlaveDataCount;
+ var modeIndex = DataModeIndex;
+
+ Registers.Clear();
+ Coils.Clear();
+
+ // 从站使用寄存器模式(默认)
+ for (var i = 0; i < count; i++)
+ {
+ var value = modeIndex switch
+ {
+ 0 => (UInt16)0x0000,
+ 1 => (UInt16)0x7777,
+ 2 => (UInt16)0xFFFF,
+ 3 => (UInt16)i,
+ _ => (UInt16)Rand.Next(UInt16.MaxValue),
+ };
+ Registers.Add(new RegisterUnit { Address = addr + i, Value = value });
+ }
+
+ if (modeIndex == 5) // 动态随机
+ {
+ _timer = new TimerX(DoRefreshData, null, 1_000, 1_000) { Async = true };
+ }
+
+ WriteLog($"从站数据已初始化:{count} 个寄存器");
+ }
+
+ private void DoRefreshData(Object? state)
+ {
+ foreach (var reg in Registers)
+ {
+ var val = reg.Value;
+ if (val == 0)
+ val = (UInt16)Rand.Next(UInt16.MaxValue);
+ else
+ {
+ var x = (Rand.Next(75) - 30) / 100.0;
+ val = (UInt16)(val * (1 + x));
+ }
+ reg.Value = val;
+ }
+ }
+
+ /// <summary>刷新从站数据</summary>
+ [RelayCommand]
+ private void RefreshData()
+ {
+ if (!IsConnected || SelectedMode != 1) return;
+ RefreshSlaveData();
+ }
+
+ private void OnSlaveReceived(Object? sender, ReceivedEventArgs e)
+ {
+ var session = sender as NetSession;
+ if (session == null) return;
+
+ var pk = e.Packet is ArrayPacket ap ? ap : new ArrayPacket(e.Packet.ReadBytes());
+ try
+ {
+ var spanReader = new NewLife.Buffers.SpanReader(pk);
+ var msg = new ModbusIpMessage();
+ msg.Read(ref spanReader);
+ if (msg == null) return;
+
+ session.Log?.Info("<= {0}", msg);
+
+ var addrMsg = msg.GetAddress();
+ var addrAddress = addrMsg;
+ var rs = msg.CreateReply();
+
+ switch (msg.Code)
+ {
+ case FunctionCodes.ReadCoil:
+ case FunctionCodes.ReadDiscrete:
+ // Return coils data
+ {
+ var regCount = msg.Payload.ReadBytes(2, 2).ToUInt16(0, false);
+ var bytesCount = (Int32)Math.Ceiling(regCount / 8.0);
+ var bits = new Byte[1 + bytesCount];
+ bits[0] = (Byte)bytesCount;
+ for (var i = 0; i < bytesCount; i++)
+ {
+ var b = 0;
+ var max = regCount - i * 8;
+ if (max > 8) max = 8;
+ for (var j = 0; j < max; j++)
+ {
+ var idx = addrAddress + i * 8 + j;
+ var coil = Coils.FirstOrDefault(e => e.Address == idx);
+ if (coil != null && coil.Value > 0)
+ b |= 1 << j;
+ }
+ bits[1 + i] = (Byte)b;
+ }
+ rs.Payload = new ArrayPacket(bits);
+ }
+ break;
+
+ case FunctionCodes.ReadRegister:
+ case FunctionCodes.ReadInput:
+ {
+ var regCount = msg.Payload.ReadBytes(2, 2).ToUInt16(0, false);
+ var regs = Registers.Where(e => e.Address >= addrAddress && e.Address < addrAddress + regCount).ToList();
+ if (regs.Count > 0)
+ {
+ var buf2 = regs.SelectMany(e => e.Value.GetBytes(false)).ToArray();
+ rs.Payload = new ArrayPacket(new Byte[] { (Byte)buf2.Length }.Concat(buf2).ToArray());
+ }
+ }
+ break;
+
+ case FunctionCodes.WriteCoil:
+ break;
+
+ case FunctionCodes.WriteRegister:
+ {
+ var value = msg.Payload.ReadBytes(2, 2).ToUInt16(0, false);
+ var reg = Registers.FirstOrDefault(e => e.Address == addrAddress);
+ if (reg != null)
+ {
+ reg.Value = value;
+ Application.Current?.Dispatcher.Invoke(() => { });
+ }
+ }
+ break;
+
+ case FunctionCodes.WriteCoils:
+ case FunctionCodes.WriteRegisters:
+ break;
+ }
+
+ session.Log?.Info("=> {0}", rs);
+ session.Send(rs.ToPacket());
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"从站处理异常:{ex.Message}");
+ }
+ }
+ #endregion
+
+ #region 清空日志
+ /// <summary>清空日志</summary>
+ [RelayCommand]
+ private void ClearLog()
+ {
+ OnLog?.Invoke("__CLEAR__");
+ }
+ #endregion
+
+ #region 日志
+ private void WriteLog(String msg)
+ {
+ OnLog?.Invoke(msg);
+ }
+
+ private class ModbusTcpLog : Logger
+ {
+ private readonly ModbusTcpViewModel _vm;
+ public ModbusTcpLog(ModbusTcpViewModel vm) => _vm = vm;
+ protected override void OnWrite(LogLevel level, String format, params Object?[] args)
+ {
+ var msg = args is { Length: > 0 } ? String.Format(format, args) : format;
+ _vm.WriteLog(msg);
+ }
+ }
+ #endregion
+
+ #region IDisposable
+ /// <summary>释放资源</summary>
+ public void Dispose()
+ {
+ Disconnect();
+ GC.SuppressFinalize(this);
+ }
+ #endregion
+}
diff --git a/CrazyCoder/ViewModels/RedisManagerViewModel.cs b/CrazyCoder/ViewModels/RedisManagerViewModel.cs
new file mode 100644
index 0000000..ca295af
--- /dev/null
+++ b/CrazyCoder/ViewModels/RedisManagerViewModel.cs
@@ -0,0 +1,517 @@
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Windows;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using CrazyCoder.Models;
+using NewLife;
+using NewLife.Caching;
+using NewLife.Log;
+using NewLife.Serialization;
+
+namespace CrazyCoder.ViewModels;
+
+/// <summary>Redis 管理器 ViewModel</summary>
+public partial class RedisManagerViewModel : ObservableObject
+{
+ #region 属性
+
+ /// <summary>Redis 节点配置列表</summary>
+ public ObservableCollection<RedisConfig> Nodes { get; } = [];
+
+ /// <summary>树形节点数据</summary>
+ public ObservableCollection<RedisTreeNode> TreeNodes { get; } = [];
+
+ /// <summary>Key 搜索模式</summary>
+ [ObservableProperty]
+ private String _searchPattern = "*";
+
+ /// <summary>Key 值内容</summary>
+ [ObservableProperty]
+ private String _keyValue = "";
+
+ /// <summary>选中的 Key</summary>
+ [ObservableProperty]
+ private String _selectedKey = "";
+
+ /// <summary>状态文本</summary>
+ [ObservableProperty]
+ private String _status = "就绪";
+
+ /// <summary>配置数据路径</summary>
+ private readonly String _configFile;
+
+ #endregion
+
+ #region 构造
+
+ /// <summary>实例化 Redis 管理器 ViewModel</summary>
+ public RedisManagerViewModel()
+ {
+ _configFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config", "RedisNodes.json");
+
+ LoadConfig();
+ }
+
+ #endregion
+
+ #region 配置持久化
+
+ private void LoadConfig()
+ {
+ try
+ {
+ var file = _configFile.GetFullPath();
+ if (File.Exists(file))
+ {
+ var json = File.ReadAllText(file);
+ var list = JsonHelper.Convert<IList<RedisConfig>>(json);
+ if (list != null)
+ {
+ Nodes.Clear();
+ foreach (var node in list)
+ {
+ Nodes.Add(node);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ XTrace.WriteException(ex);
+ }
+
+ BuildTree();
+ }
+
+ private void SaveConfig()
+ {
+ try
+ {
+ var json = Nodes.ToJson(true);
+ var file = _configFile.GetFullPath();
+ file.EnsureDirectory(true);
+ File.WriteAllText(file, json);
+ }
+ catch (Exception ex)
+ {
+ XTrace.WriteException(ex);
+ }
+ }
+
+ #endregion
+
+ #region 树形结构
+
+ /// <summary>构建树节点</summary>
+ private void BuildTree()
+ {
+ TreeNodes.Clear();
+
+ foreach (var cfg in Nodes)
+ {
+ var node = new RedisTreeNode
+ {
+ Title = cfg.Name,
+ Tag = cfg,
+ IsExpanded = false,
+ };
+ TreeNodes.Add(node);
+ }
+ }
+
+ /// <summary>展开 Redis 服务器节点,显示 db0~db15</summary>
+ private void ExpandServer(RedisTreeNode serverNode, RedisConfig cfg)
+ {
+ try
+ {
+ var rds = new FullRedis
+ {
+ Name = cfg.Name,
+ Server = $"{cfg.Server}:{cfg.Port}",
+ Password = cfg.Password,
+ UserName = cfg.Username,
+ };
+
+ serverNode.Children.Clear();
+ for (var i = 0; i < 16; i++)
+ {
+ var sub = rds.CreateSub(i) as FullRedis;
+ if (sub != null)
+ {
+ var count = sub.Count;
+ var dbNode = new RedisTreeNode
+ {
+ Title = $"db{i}({count})",
+ Tag = sub,
+ IsExpanded = false,
+ };
+ serverNode.Children.Add(dbNode);
+ }
+ }
+ serverNode.IsExpanded = true;
+ }
+ catch (Exception ex)
+ {
+ Status = $"连接失败:{ex.Message}";
+ XTrace.WriteException(ex);
+ }
+ }
+
+ /// <summary>展开数据库节点,显示 Key 列表</summary>
+ private void ExpandDatabase(RedisTreeNode dbNode, FullRedis redis)
+ {
+ try
+ {
+ var list = redis.Search(SearchPattern, 0, 100).ToList();
+
+ dbNode.Children.Clear();
+ foreach (var key in list)
+ {
+ var keyNode = new RedisTreeNode
+ {
+ Title = key,
+ Tag = key,
+ IsExpanded = false,
+ };
+ dbNode.Children.Add(keyNode);
+ }
+ dbNode.IsExpanded = true;
+ Status = $"共 {list.Count} 个 Key";
+ }
+ catch (Exception ex)
+ {
+ Status = $"搜索失败:{ex.Message}";
+ XTrace.WriteException(ex);
+ }
+ }
+
+ /// <summary>加载 Key 的值</summary>
+ private void LoadKeyValue(FullRedis redis, String key)
+ {
+ try
+ {
+ var value = redis.Get<String>(key);
+ KeyValue = value ?? "";
+ Status = $"Key: {key}";
+ }
+ catch (Exception ex)
+ {
+ Status = $"读取失败:{ex.Message}";
+ XTrace.WriteException(ex);
+ }
+ }
+
+ /// <summary>处理节点双击</summary>
+ public void HandleNodeDoubleClick(RedisTreeNode node)
+ {
+ if (node == null) return;
+
+ if (node.Tag is RedisConfig cfg)
+ {
+ ExpandServer(node, cfg);
+ }
+ else if (node.Tag is FullRedis redis)
+ {
+ ExpandDatabase(node, redis);
+ }
+ else if (node.Tag is String key)
+ {
+ if (node.Parent?.Tag is FullRedis parentRedis)
+ LoadKeyValue(parentRedis, key);
+ }
+ }
+
+ #endregion
+
+ #region 命令
+
+ /// <summary>添加 Redis 节点</summary>
+ [RelayCommand]
+ private void AddNode()
+ {
+ var cfg = new RedisConfig
+ {
+ Name = "NewNode",
+ Server = "127.0.0.1",
+ Port = 6379,
+ };
+
+ // 简单编辑对话框(使用 WPF 内置方式)
+ var result = ShowEditDialog(cfg, "添加Redis节点");
+ if (result == true)
+ {
+ Nodes.Add(cfg);
+ BuildTree();
+ SaveConfig();
+ Status = $"已添加节点:{cfg.Name}";
+ }
+ }
+
+ /// <summary>编辑选中的 Redis 节点</summary>
+ [RelayCommand]
+ private void EditNode()
+ {
+ var node = FindSelectedConfigNode();
+ if (node == null) return;
+
+ var cfg = node.Tag as RedisConfig;
+ if (cfg == null) return;
+
+ // 复制一份以便取消
+ var copy = new RedisConfig
+ {
+ Name = cfg.Name,
+ Server = cfg.Server,
+ Port = cfg.Port,
+ Username = cfg.Username,
+ Password = cfg.Password,
+ };
+
+ var result = ShowEditDialog(copy, "编辑Redis节点");
+ if (result == true)
+ {
+ cfg.Name = copy.Name;
+ cfg.Server = copy.Server;
+ cfg.Port = copy.Port;
+ cfg.Username = copy.Username;
+ cfg.Password = copy.Password;
+
+ BuildTree();
+ SaveConfig();
+ Status = $"已更新节点:{cfg.Name}";
+ }
+ }
+
+ /// <summary>删除选中的 Redis 节点</summary>
+ [RelayCommand]
+ private void DeleteNode()
+ {
+ var node = FindSelectedConfigNode();
+ if (node == null) return;
+
+ var cfg = node.Tag as RedisConfig;
+ if (cfg == null) return;
+
+ var result = MessageBox.Show($"确定删除节点「{cfg.Name}」吗?", "确认删除",
+ MessageBoxButton.YesNo, MessageBoxImage.Question);
+
+ if (result == MessageBoxResult.Yes)
+ {
+ Nodes.Remove(cfg);
+ BuildTree();
+ SaveConfig();
+ Status = $"已删除节点:{cfg.Name}";
+ }
+ }
+
+ /// <summary>搜索 Key</summary>
+ [RelayCommand]
+ private void SearchKeys()
+ {
+ // 在当前展开的数据库节点上搜索
+ var dbNode = FindSelectedDbNode();
+ if (dbNode?.Tag is FullRedis redis)
+ {
+ ExpandDatabase(dbNode, redis);
+ }
+ }
+
+ private RedisTreeNode FindSelectedConfigNode()
+ {
+ // 遍历 TreeNodes 查找选中项
+ foreach (var node in TreeNodes)
+ {
+ if (node.IsSelected) return node;
+ // 检查子节点
+ foreach (var child in GetAllNodes(node))
+ {
+ if (child.IsSelected) return child;
+ }
+ }
+ return null;
+ }
+
+ private RedisTreeNode FindSelectedDbNode()
+ {
+ foreach (var node in TreeNodes)
+ {
+ foreach (var child in node.Children)
+ {
+ if (child.IsSelected) return child;
+ // 也可能选中了 key 节点,取父节点
+ if (child.Children.Any(c => c.IsSelected)) return child;
+ }
+ }
+ return null;
+ }
+
+ private IEnumerable<RedisTreeNode> GetAllNodes(RedisTreeNode root)
+ {
+ foreach (var child in root.Children)
+ {
+ yield return child;
+ foreach (var grandChild in GetAllNodes(child))
+ {
+ yield return grandChild;
+ }
+ }
+ }
+
+ #endregion
+
+ #region 编辑对话框
+
+ /// <summary>显示编辑对话框</summary>
+ private Boolean? ShowEditDialog(RedisConfig cfg, String title)
+ {
+ // 使用简化的 WPF 输入对话框
+ var window = new Window
+ {
+ Title = title,
+ Width = 420,
+ Height = 320,
+ WindowStartupLocation = WindowStartupLocation.CenterScreen,
+ ResizeMode = ResizeMode.NoResize,
+ };
+
+ var stack = new System.Windows.Controls.StackPanel { Margin = new Thickness(12) };
+
+ AddTextField(stack, "名称:", cfg, nameof(cfg.Name));
+ AddTextField(stack, "服务器:", cfg, nameof(cfg.Server));
+ AddTextField(stack, "端口:", cfg, nameof(cfg.Port));
+ AddTextField(stack, "用户名:", cfg, nameof(cfg.Username));
+ AddPasswordField(stack, "密码:", cfg, nameof(cfg.Password));
+
+ var btnPanel = new System.Windows.Controls.StackPanel
+ {
+ Orientation = System.Windows.Controls.Orientation.Horizontal,
+ HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
+ Margin = new Thickness(0, 12, 0, 0),
+ };
+
+ var btnOk = new System.Windows.Controls.Button
+ {
+ Content = "确定",
+ Width = 80,
+ Height = 30,
+ Margin = new Thickness(0, 0, 8, 0),
+ IsDefault = true,
+ };
+ var btnCancel = new System.Windows.Controls.Button
+ {
+ Content = "取消",
+ Width = 80,
+ Height = 30,
+ IsCancel = true,
+ };
+
+ btnOk.Click += (s, e) => window.DialogResult = true;
+ btnPanel.Children.Add(btnOk);
+ btnPanel.Children.Add(btnCancel);
+ stack.Children.Add(btnPanel);
+
+ window.Content = stack;
+ return window.ShowDialog();
+ }
+
+ private static void AddTextField(System.Windows.Controls.StackPanel stack, String label, Object dataContext, String propertyName)
+ {
+ var panel = new System.Windows.Controls.WrapPanel { Margin = new Thickness(0, 4, 0, 4) };
+ var lbl = new System.Windows.Controls.Label
+ {
+ Content = label,
+ Width = 60,
+ VerticalAlignment = System.Windows.VerticalAlignment.Center,
+ };
+ var txt = new System.Windows.Controls.TextBox
+ {
+ Width = 300,
+ VerticalAlignment = System.Windows.VerticalAlignment.Center,
+ DataContext = dataContext,
+ };
+
+ var prop = dataContext.GetType().GetProperty(propertyName);
+ if (prop != null)
+ {
+ txt.Text = prop.GetValue(dataContext)?.ToString() ?? "";
+ txt.TextChanged += (s, e) =>
+ {
+ var value = txt.Text;
+ if (prop.PropertyType == typeof(Int32))
+ {
+ if (Int32.TryParse(value, out var n))
+ prop.SetValue(dataContext, n);
+ }
+ else
+ {
+ prop.SetValue(dataContext, value);
+ }
+ };
+ }
+
+ panel.Children.Add(lbl);
+ panel.Children.Add(txt);
+ stack.Children.Add(panel);
+ }
+
+ private static void AddPasswordField(System.Windows.Controls.StackPanel stack, String label, Object dataContext, String propertyName)
+ {
+ var panel = new System.Windows.Controls.WrapPanel { Margin = new Thickness(0, 4, 0, 4) };
+ var lbl = new System.Windows.Controls.Label
+ {
+ Content = label,
+ Width = 60,
+ VerticalAlignment = System.Windows.VerticalAlignment.Center,
+ };
+ var txt = new System.Windows.Controls.PasswordBox
+ {
+ Width = 300,
+ VerticalAlignment = System.Windows.VerticalAlignment.Center,
+ };
+
+ var prop = dataContext.GetType().GetProperty(propertyName);
+ if (prop != null)
+ {
+ txt.Password = prop.GetValue(dataContext)?.ToString() ?? "";
+ txt.PasswordChanged += (s, e) => prop.SetValue(dataContext, txt.Password);
+ }
+
+ panel.Children.Add(lbl);
+ panel.Children.Add(txt);
+ stack.Children.Add(panel);
+ }
+
+ #endregion
+}
+
+/// <summary>Redis 树节点</summary>
+public partial class RedisTreeNode : ObservableObject
+{
+ /// <summary>节点标题</summary>
+ [ObservableProperty]
+ private String _title = "";
+
+ /// <summary>关联数据</summary>
+ public Object Tag { get; set; }
+
+ /// <summary>是否展开</summary>
+ [ObservableProperty]
+ private Boolean _isExpanded;
+
+ /// <summary>是否选中</summary>
+ [ObservableProperty]
+ private Boolean _isSelected;
+
+ /// <summary>父节点</summary>
+ public RedisTreeNode Parent { get; set; }
+
+ /// <summary>子节点集合</summary>
+ public ObservableCollection<RedisTreeNode> Children { get; } = [];
+
+ /// <summary>层级</summary>
+ public Int32 Level => Parent?.Level + 1 ?? 0;
+
+ /// <summary>缩进</summary>
+ public Thickness Indent => new Thickness(Level * 16, 0, 0, 0);
+}
diff --git a/CrazyCoder/ViewModels/SerialPortViewModel.cs b/CrazyCoder/ViewModels/SerialPortViewModel.cs
new file mode 100644
index 0000000..b630495
--- /dev/null
+++ b/CrazyCoder/ViewModels/SerialPortViewModel.cs
@@ -0,0 +1,314 @@
+#nullable enable
+
+using System.Collections.ObjectModel;
+using System.IO.Ports;
+using System.Text;
+using System.Windows;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using NewLife;
+using NewLife.Threading;
+
+namespace CrazyCoder.ViewModels;
+
+/// <summary>串口调试工具 ViewModel</summary>
+public partial class SerialPortViewModel : ObservableObject, IDisposable
+{
+ #region 属性
+ private SerialPort? _port;
+ private TimerX? _timer;
+
+ /// <summary>端口名称列表</summary>
+ public ObservableCollection<String> PortNames { get; } = [];
+
+ /// <summary>波特率列表</summary>
+ public ObservableCollection<Int32> BaudRates { get; } = [1200, 2400, 4800, 9600, 14400, 19200, 38400, 56000, 57600, 115200, 128000, 194000, 256000, 512000, 1024000, 2048000];
+
+ /// <summary>校验位列表</summary>
+ public ObservableCollection<String> ParityOptions { get; } = ["None", "Odd", "Even", "Mark", "Space"];
+
+ /// <summary>停止位列表</summary>
+ public ObservableCollection<String> StopBitsOptions { get; } = ["One", "Two", "OnePointFive"];
+
+ /// <summary>数据位列表</summary>
+ public ObservableCollection<Int32> DataBitsOptions { get; } = [5, 6, 7, 8];
+
+ /// <summary>端口名称</summary>
+ [ObservableProperty]
+ private String _portName = "COM1";
+
+ /// <summary>波特率</summary>
+ [ObservableProperty]
+ private Int32 _baudRate = 115200;
+
+ /// <summary>校验位索引</summary>
+ [ObservableProperty]
+ private Int32 _parityIndex;
+
+ /// <summary>数据位</summary>
+ [ObservableProperty]
+ private Int32 _dataBits = 8;
+
+ /// <summary>停止位索引</summary>
+ [ObservableProperty]
+ private Int32 _stopBitsIndex;
+
+ /// <summary>十六进制显示</summary>
+ [ObservableProperty]
+ private Boolean _hexDisplay;
+
+ /// <summary>十六进制发送</summary>
+ [ObservableProperty]
+ private Boolean _hexSend;
+
+ /// <summary>是否已连接</summary>
+ [ObservableProperty]
+ private Boolean _isConnected;
+
+ /// <summary>连接按钮文本</summary>
+ [ObservableProperty]
+ private String _connectButtonText = "打开";
+
+ /// <summary>发送文本</summary>
+ [ObservableProperty]
+ private String _sendText = "";
+
+ /// <summary>已发送字节数</summary>
+ [ObservableProperty]
+ private Int64 _bytesSent;
+
+ /// <summary>已接收字节数</summary>
+ [ObservableProperty]
+ private Int64 _bytesReceived;
+
+ /// <summary>日志回调,由 View 绑定到 RichTextBox</summary>
+ public Action<String>? OnLog { get; set; }
+ #endregion
+
+ #region 构造
+ /// <summary>实例化串口调试工具 ViewModel</summary>
+ public SerialPortViewModel()
+ {
+ RefreshPorts();
+ }
+ #endregion
+
+ #region 方法
+ /// <summary>刷新串口列表</summary>
+ [RelayCommand]
+ private void RefreshPorts()
+ {
+ PortNames.Clear();
+ foreach (var name in SerialPort.GetPortNames())
+ {
+ PortNames.Add(name);
+ }
+ if (PortNames.Count > 0) PortName = PortNames[0];
+ }
+ #endregion
+
+ #region 连接/断开
+ /// <summary>切换连接/断开</summary>
+ [RelayCommand]
+ private void ToggleConnect()
+ {
+ if (IsConnected)
+ Disconnect();
+ else
+ Connect();
+ }
+
+ private void Connect()
+ {
+ try
+ {
+ var parity = ParityIndex switch
+ {
+ 1 => Parity.Odd,
+ 2 => Parity.Even,
+ 3 => Parity.Mark,
+ 4 => Parity.Space,
+ _ => Parity.None,
+ };
+ var stopBits = StopBitsIndex switch
+ {
+ 1 => StopBits.Two,
+ 2 => StopBits.OnePointFive,
+ _ => StopBits.One,
+ };
+
+ var port = new SerialPort(PortName, BaudRate, parity, DataBits, stopBits);
+ port.DataReceived += OnDataReceived;
+ port.ErrorReceived += OnErrorReceived;
+ port.Open();
+
+ _port = port;
+
+ IsConnected = true;
+ ConnectButtonText = "关闭";
+
+ WriteLog($"串口 {PortName} 已打开,{BaudRate}/{DataBits}/{ParityOptions[ParityIndex]}/{StopBitsOptions[StopBitsIndex]}");
+
+ BytesSent = 0;
+ BytesReceived = 0;
+
+ _timer = new TimerX(OnTimer, null, 1000, 1000) { Async = true };
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"打开失败:{ex.Message}");
+ MessageBox.Show($"打开串口失败:{ex.Message}", "错误");
+ }
+ }
+
+ private void Disconnect()
+ {
+ _timer.TryDispose();
+ _timer = null;
+
+ if (_port != null)
+ {
+ try
+ {
+ if (_port.IsOpen) _port.Close();
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"关闭异常:{ex.Message}");
+ }
+ _port.Dispose();
+ _port = null;
+ }
+
+ IsConnected = false;
+ ConnectButtonText = "打开";
+
+ WriteLog($"串口 {PortName} 已关闭");
+ }
+
+ private void OnTimer(Object? state)
+ {
+ var port = _port;
+ if (port == null || !port.IsOpen)
+ {
+ if (IsConnected)
+ {
+ WriteLog("串口已断开");
+ Disconnect();
+ }
+ return;
+ }
+
+ // 更新计数
+ // SerialPort doesn't have built-in byte counters, we track via events
+ }
+
+ private void OnDataReceived(Object sender, SerialDataReceivedEventArgs e)
+ {
+ var port = _port;
+ if (port == null) return;
+
+ try
+ {
+ var count = port.BytesToRead;
+ if (count <= 0) return;
+
+ var buf = new Byte[count];
+ var n = port.Read(buf, 0, count);
+ if (n <= 0) return;
+
+ BytesReceived += n;
+
+ var hex = buf.ToHex(" ", 0, n);
+ var str = Encoding.UTF8.GetString(buf, 0, n);
+
+ if (HexDisplay)
+ WriteLog($"[接收] [{n}字节] {hex}");
+ else
+ WriteLog($"[接收] [{n}字节] {str}");
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"接收异常:{ex.Message}");
+ }
+ }
+
+ private void OnErrorReceived(Object sender, SerialErrorReceivedEventArgs e)
+ {
+ WriteLog($"串口错误:{e.EventType}");
+ }
+ #endregion
+
+ #region 发送
+ /// <summary>发送数据</summary>
+ [RelayCommand]
+ private void Send()
+ {
+ var port = _port;
+ if (port == null || !port.IsOpen)
+ {
+ WriteLog("串口未打开");
+ return;
+ }
+
+ var str = SendText;
+ if (String.IsNullOrEmpty(str))
+ {
+ MessageBox.Show("发送内容不能为空!", "提示");
+ return;
+ }
+
+ try
+ {
+ if (HexSend)
+ {
+ // 十六进制发送
+ var hex = str.Replace(" ", "").Replace("-", "");
+ if (hex.Length % 2 != 0) hex = "0" + hex;
+ var buf = hex.ToHex();
+ if (buf.Length > 0)
+ {
+ port.Write(buf, 0, buf.Length);
+ BytesSent += buf.Length;
+ WriteLog($"[发送] [{buf.Length}字节] {buf.ToHex(" ", 0, buf.Length)}");
+ }
+ }
+ else
+ {
+ // 文本发送
+ var data = Encoding.UTF8.GetBytes(str);
+ port.Write(data, 0, data.Length);
+ BytesSent += data.Length;
+ WriteLog($"[发送] [{data.Length}字节] {str}");
+ }
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"发送失败:{ex.Message}");
+ }
+ }
+
+ /// <summary>清空日志</summary>
+ [RelayCommand]
+ private void ClearLog()
+ {
+ OnLog?.Invoke("__CLEAR__");
+ }
+ #endregion
+
+ #region 日志
+ private void WriteLog(String msg)
+ {
+ OnLog?.Invoke(msg);
+ }
+ #endregion
+
+ #region IDisposable
+ /// <summary>释放资源</summary>
+ public void Dispose()
+ {
+ Disconnect();
+ GC.SuppressFinalize(this);
+ }
+ #endregion
+}
diff --git a/CrazyCoder/Views/DataModelingWindow.xaml b/CrazyCoder/Views/DataModelingWindow.xaml
new file mode 100644
index 0000000..8faebf7
--- /dev/null
+++ b/CrazyCoder/Views/DataModelingWindow.xaml
@@ -0,0 +1,172 @@
+<Window x:Class="CrazyCoder.Views.DataModelingWindow"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:xcode="clr-namespace:XCode.DataAccessLayer;assembly=NewLife.XCode"
+ mc:Ignorable="d"
+ Title="数据建模" Height="700" Width="1100" WindowStartupLocation="CenterScreen">
+ <Window.Resources>
+ <Style x:Key="GroupBorder" TargetType="Border">
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="Margin" Value="0,0,0,6"/>
+ </Style>
+
+ <Style x:Key="LabelText" TargetType="TextBlock">
+ <Setter Property="Margin" Value="4,0,4,0"/>
+ <Setter Property="VerticalAlignment" Value="Center"/>
+ </Style>
+
+ <Style x:Key="ActionButton" TargetType="Button">
+ <Setter Property="Height" Value="30"/>
+ <Setter Property="Margin" Value="4,0"/>
+ <Setter Property="Padding" Value="12,0"/>
+ </Style>
+ </Window.Resources>
+
+ <Grid Margin="8">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <!-- 第1行:连接区 -->
+ <Border Grid.Row="0" Style="{StaticResource GroupBorder}" Background="#FFF5E6" Padding="8">
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Text="连接:" Style="{StaticResource LabelText}" FontWeight="Bold"/>
+ <ComboBox Grid.Column="1" ItemsSource="{Binding Connections}" Text="{Binding ConnName, UpdateSourceTrigger=PropertyChanged}"
+ Margin="4,0" Height="28" IsEditable="True"/>
+
+ <Button Grid.Column="2" Content="{Binding ConnectButtonText}" Command="{Binding ConnectCommand}"
+ Style="{StaticResource ActionButton}" MinWidth="80"/>
+ </Grid>
+ </Border>
+
+ <!-- 第2行:表选择和生成按钮 -->
+ <Border Grid.Row="1" Style="{StaticResource GroupBorder}" Background="#E8FFE8" Padding="8"
+ IsEnabled="{Binding IsConnected}">
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Text="数据表:" Style="{StaticResource LabelText}" FontWeight="Bold"/>
+ <ComboBox Grid.Column="1" ItemsSource="{Binding Tables}" SelectedItem="{Binding SelectedTable}"
+ Margin="4,0" Height="28" DisplayMemberPath="Name">
+ <ComboBox.ItemTemplate>
+ <DataTemplate>
+ <TextBlock Text="{Binding Name}"/>
+ </DataTemplate>
+ </ComboBox.ItemTemplate>
+ </ComboBox>
+
+ <CheckBox Grid.Column="2" Content="包含视图" IsChecked="{Binding IncludeView}" Margin="8,0" VerticalAlignment="Center"/>
+
+ <Button Grid.Column="3" Content="刷新" Command="{Binding RefreshTablesCommand}" Style="{StaticResource ActionButton}"/>
+ <Button Grid.Column="4" Content="生成选中" Command="{Binding GenerateTableCommand}" Style="{StaticResource ActionButton}"/>
+ <Button Grid.Column="5" Content="生成所有" Command="{Binding GenerateAllCommand}" Style="{StaticResource ActionButton}"/>
+ </Grid>
+ </Border>
+
+ <!-- 第3行:表列表 -->
+ <Border Grid.Row="2" Style="{StaticResource GroupBorder}" IsEnabled="{Binding IsConnected}">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="表结构列表" FontWeight="Bold" Background="#F0F0F0" Padding="6,3"/>
+ <ListView Grid.Row="1" ItemsSource="{Binding Tables}" SelectedItem="{Binding SelectedTable}"
+ BorderThickness="0">
+ <ListView.View>
+ <GridView>
+ <GridViewColumn Header="表名" Width="200" DisplayMemberBinding="{Binding Name}"/>
+ <GridViewColumn Header="描述" Width="200" DisplayMemberBinding="{Binding Description}"/>
+ <GridViewColumn Header="类型" Width="100" DisplayMemberBinding="{Binding DbType}"/>
+ <GridViewColumn Header="行数" Width="80" DisplayMemberBinding="{Binding Rows}"/>
+ </GridView>
+ </ListView.View>
+ </ListView>
+ </Grid>
+ </Border>
+
+ <!-- 第4行:配置区 -->
+ <Border Grid.Row="3" Style="{StaticResource GroupBorder}" Padding="8"
+ IsEnabled="{Binding IsConnected}">
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <StackPanel Grid.Column="0" Margin="4,0">
+ <TextBlock Text="命名空间" FontWeight="Bold"/>
+ <TextBox Text="{Binding NameSpace, UpdateSourceTrigger=PropertyChanged}" Height="26" Margin="0,2,0,0"/>
+ </StackPanel>
+
+ <StackPanel Grid.Column="1" Margin="4,0">
+ <TextBlock Text="连接名" FontWeight="Bold"/>
+ <TextBox Text="{Binding EntityConnName, UpdateSourceTrigger=PropertyChanged}" Height="26" Margin="0,2,0,0"/>
+ </StackPanel>
+
+ <StackPanel Grid.Column="2" Margin="4,0">
+ <TextBlock Text="基类" FontWeight="Bold"/>
+ <TextBox Text="{Binding BaseClass, UpdateSourceTrigger=PropertyChanged}" Height="26" Margin="0,2,0,0"/>
+ </StackPanel>
+
+ <StackPanel Grid.Column="3" Margin="4,0">
+ <TextBlock Text="输出路径" FontWeight="Bold"/>
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ <TextBox Text="{Binding OutputPath, UpdateSourceTrigger=PropertyChanged}" Height="26" Margin="0,2,0,0"/>
+ <Button Grid.Column="1" Content="..." Command="{Binding OpenOutputDirCommand}" Height="26" Width="30" Margin="2,2,0,0"/>
+ </Grid>
+ </StackPanel>
+
+ <StackPanel Grid.Column="4" Margin="8,0,0,0" VerticalAlignment="Center">
+ <CheckBox Content="中文文件名" IsChecked="{Binding UseCNFileName}" Margin="0,2"/>
+ <CheckBox Content="泛型实体" IsChecked="{Binding RenderGenEntity}" Margin="0,2"/>
+ </StackPanel>
+ </Grid>
+ </Border>
+
+ <!-- 第5行:状态栏 -->
+ <StatusBar Grid.Row="4" Margin="0,2,0,0">
+ <StatusBar.ItemsPanel>
+ <ItemsPanelTemplate>
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ </Grid>
+ </ItemsPanelTemplate>
+ </StatusBar.ItemsPanel>
+ <StatusBarItem>
+ <TextBlock Text="{Binding Status}"/>
+ </StatusBarItem>
+ </StatusBar>
+ </Grid>
+</Window>
diff --git a/CrazyCoder/Views/DataModelingWindow.xaml.cs b/CrazyCoder/Views/DataModelingWindow.xaml.cs
new file mode 100644
index 0000000..f3b0655
--- /dev/null
+++ b/CrazyCoder/Views/DataModelingWindow.xaml.cs
@@ -0,0 +1,20 @@
+using System.Windows;
+using CrazyCoder.ViewModels;
+
+namespace CrazyCoder.Views;
+
+/// <summary>数据建模工具窗口</summary>
+public partial class DataModelingWindow : Window
+{
+ /// <summary>ViewModel</summary>
+ public DataModelingViewModel ViewModel { get; }
+
+ /// <summary>实例化数据建模工具窗口</summary>
+ public DataModelingWindow()
+ {
+ InitializeComponent();
+
+ ViewModel = new DataModelingViewModel();
+ DataContext = ViewModel;
+ }
+}
diff --git a/CrazyCoder/Views/DataSyncWindow.xaml b/CrazyCoder/Views/DataSyncWindow.xaml
new file mode 100644
index 0000000..ab4aaae
--- /dev/null
+++ b/CrazyCoder/Views/DataSyncWindow.xaml
@@ -0,0 +1,155 @@
+<Window x:Class="CrazyCoder.Views.DataSyncWindow"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d"
+ Title="跨库数据同步" Height="700" Width="1100" WindowStartupLocation="CenterScreen">
+ <Window.Resources>
+ <Style x:Key="GroupBorder" TargetType="Border">
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="Margin" Value="0,0,0,6"/>
+ </Style>
+
+ <Style x:Key="LabelText" TargetType="TextBlock">
+ <Setter Property="Margin" Value="4,0,4,0"/>
+ <Setter Property="VerticalAlignment" Value="Center"/>
+ </Style>
+
+ <Style x:Key="ActionButton" TargetType="Button">
+ <Setter Property="Height" Value="30"/>
+ <Setter Property="Margin" Value="4,0"/>
+ <Setter Property="Padding" Value="12,0"/>
+ </Style>
+ </Window.Resources>
+
+ <Grid Margin="8">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <!-- 第1行:源数据库连接 -->
+ <Border Grid.Row="0" Style="{StaticResource GroupBorder}" Background="#FFF5E6" Padding="8">
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Text="源数据库:" Style="{StaticResource LabelText}" FontWeight="Bold"/>
+ <ComboBox Grid.Column="1" ItemsSource="{Binding Connections}" Text="{Binding SourceConn, UpdateSourceTrigger=PropertyChanged}"
+ Margin="4,0" Height="28" IsEditable="True"/>
+
+ <Button Grid.Column="2" Content="连接" Command="{Binding ConnectSourceCommand}" Style="{StaticResource ActionButton}" MinWidth="80"/>
+ <Button Grid.Column="3" Content="断开" Command="{Binding DisconnectSourceCommand}" Style="{StaticResource ActionButton}" MinWidth="80"
+ IsEnabled="{Binding IsSourceConnected}"/>
+ </Grid>
+ </Border>
+
+ <!-- 第2行:目标数据库连接 -->
+ <Border Grid.Row="1" Style="{StaticResource GroupBorder}" Background="#E8FFE8" Padding="8"
+ IsEnabled="{Binding IsSourceConnected}">
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Text="目标数据库:" Style="{StaticResource LabelText}" FontWeight="Bold"/>
+ <ComboBox Grid.Column="1" ItemsSource="{Binding TargetConnections}" Text="{Binding TargetConn, UpdateSourceTrigger=PropertyChanged}"
+ Margin="4,0" Height="28" IsEditable="True"/>
+
+ <Button Grid.Column="2" Content="连接" Command="{Binding ConnectTargetCommand}" Style="{StaticResource ActionButton}" MinWidth="80"/>
+ <Button Grid.Column="3" Content="断开" Command="{Binding DisconnectTargetCommand}" Style="{StaticResource ActionButton}" MinWidth="80"
+ IsEnabled="{Binding IsTargetConnected}"/>
+ </Grid>
+ </Border>
+
+ <!-- 第3行:表列表 -->
+ <Border Grid.Row="2" Style="{StaticResource GroupBorder}">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ <TextBlock Text="数据表列表" FontWeight="Bold" Background="#F0F0F0" Padding="6,3"/>
+ <StackPanel Grid.Column="1" Orientation="Horizontal" Background="#F0F0F0">
+ <Button Content="全选" Command="{Binding SelectAllCommand}" Height="24" Width="60" Margin="2"/>
+ <Button Content="反选" Command="{Binding InvertSelectionCommand}" Height="24" Width="60" Margin="2"/>
+ <Button Content="选差异" Command="{Binding SelectDifferentCommand}" Height="24" Width="60" Margin="2"/>
+ </StackPanel>
+ </Grid>
+ <ListView Grid.Row="1" ItemsSource="{Binding Tables}" BorderThickness="0">
+ <ListView.View>
+ <GridView>
+ <GridViewColumn Header="同步" Width="50">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <CheckBox IsChecked="{Binding EnableSync}" HorizontalAlignment="Center"/>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Header="表名" Width="150" DisplayMemberBinding="{Binding Name}"/>
+ <GridViewColumn Header="描述" Width="150" DisplayMemberBinding="{Binding DisplayName}"/>
+ <GridViewColumn Header="源行数" Width="80" DisplayMemberBinding="{Binding SourceCount}"/>
+ <GridViewColumn Header="目标行数" Width="80" DisplayMemberBinding="{Binding TargetCount}"/>
+ <GridViewColumn Header="已同步" Width="80" DisplayMemberBinding="{Binding SyncCount}"/>
+ <GridViewColumn Header="备注" Width="*" DisplayMemberBinding="{Binding Description}"/>
+ </GridView>
+ </ListView.View>
+ </ListView>
+ </Grid>
+ </Border>
+
+ <!-- 第4行:同步设置 -->
+ <Border Grid.Row="3" Style="{StaticResource GroupBorder}" Padding="8"
+ IsEnabled="{Binding IsTargetConnected}">
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <CheckBox Grid.Column="0" Content="同步架构" IsChecked="{Binding SyncSchema}" Margin="4,0" VerticalAlignment="Center"/>
+ <CheckBox Grid.Column="1" Content="忽略错误" IsChecked="{Binding IgnoreError}" Margin="4,0" VerticalAlignment="Center"/>
+ <TextBlock Grid.Column="2" Text="{Binding ProgressText}" VerticalAlignment="Center" Margin="8,0"/>
+ <Button Grid.Column="4" Content="开始同步" Command="{Binding SyncDataCommand}"
+ Style="{StaticResource ActionButton}" MinWidth="100"
+ IsEnabled="{Binding IsTargetConnected}"/>
+ </Grid>
+ </Border>
+
+ <!-- 第5行:状态栏 -->
+ <StatusBar Grid.Row="4" Margin="0,2,0,0">
+ <StatusBar.ItemsPanel>
+ <ItemsPanelTemplate>
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+ </Grid>
+ </ItemsPanelTemplate>
+ </StatusBar.ItemsPanel>
+ <StatusBarItem>
+ <TextBlock Text="{Binding Status}"/>
+ </StatusBarItem>
+ </StatusBar>
+ </Grid>
+</Window>
diff --git a/CrazyCoder/Views/DataSyncWindow.xaml.cs b/CrazyCoder/Views/DataSyncWindow.xaml.cs
new file mode 100644
index 0000000..caaeff8
--- /dev/null
+++ b/CrazyCoder/Views/DataSyncWindow.xaml.cs
@@ -0,0 +1,20 @@
+using System.Windows;
+using CrazyCoder.ViewModels;
+
+namespace CrazyCoder.Views;
+
+/// <summary>跨库数据同步窗口</summary>
+public partial class DataSyncWindow : Window
+{
+ /// <summary>ViewModel</summary>
+ public DataSyncViewModel ViewModel { get; }
+
+ /// <summary>实例化跨库数据同步窗口</summary>
+ public DataSyncWindow()
+ {
+ InitializeComponent();
+
+ ViewModel = new DataSyncViewModel();
+ DataContext = ViewModel;
+ }
+}
diff --git a/CrazyCoder/Views/IoControlWindow.xaml b/CrazyCoder/Views/IoControlWindow.xaml
new file mode 100644
index 0000000..5cab05c
--- /dev/null
+++ b/CrazyCoder/Views/IoControlWindow.xaml
@@ -0,0 +1,368 @@
+<Window x:Class="CrazyCoder.Views.IoControlWindow"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d"
+ Title="I/O 控制面板" Height="650" Width="900" WindowStartupLocation="CenterScreen">
+ <Window.Resources>
+ <Style x:Key="GroupBorder" TargetType="Border">
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="Margin" Value="0,0,0,6"/>
+ </Style>
+
+ <Style x:Key="SectionTitle" TargetType="TextBlock">
+ <Setter Property="FontWeight" Value="Bold"/>
+ <Setter Property="Margin" Value="4,3"/>
+ <Setter Property="FontSize" Value="13"/>
+ </Style>
+
+ <Style x:Key="ConfigLabel" TargetType="TextBlock">
+ <Setter Property="VerticalAlignment" Value="Center"/>
+ <Setter Property="Margin" Value="4,2"/>
+ </Style>
+
+ <Style x:Key="ConfigComboBox" TargetType="ComboBox">
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Height" Value="26"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ </Style>
+
+ <Style x:Key="ConfigTextBox" TargetType="TextBox">
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Height" Value="26"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ </Style>
+
+ <Style x:Key="ActionButton" TargetType="Button">
+ <Setter Property="Height" Value="30"/>
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Padding" Value="8,0"/>
+ </Style>
+
+ <Style x:Key="BigButton" TargetType="Button">
+ <Setter Property="Height" Value="40"/>
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="FontSize" Value="14"/>
+ <Setter Property="FontWeight" Value="Bold"/>
+ </Style>
+
+ <Style x:Key="PortButton" TargetType="Button">
+ <Setter Property="Width" Value="80"/>
+ <Setter Property="Height" Value="40"/>
+ <Setter Property="Margin" Value="4"/>
+ <Setter Property="FontSize" Value="14"/>
+ <Setter Property="FontWeight" Value="Bold"/>
+ </Style>
+ </Window.Resources>
+
+ <Grid Margin="6">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="280"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- ============ 左侧:配置面板 ============ -->
+ <ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto" Margin="0,0,4,0">
+ <StackPanel>
+ <!-- 串口配置 -->
+ <Border Style="{StaticResource GroupBorder}">
+ <StackPanel>
+ <TextBlock Text="串口配置" Style="{StaticResource SectionTitle}"/>
+
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="50"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="端口" Style="{StaticResource ConfigLabel}"/>
+ <ComboBox Grid.Row="0" Grid.Column="1"
+ ItemsSource="{Binding PortNames}"
+ SelectedItem="{Binding PortName, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigComboBox}"/>
+ <Button Grid.Row="0" Grid.Column="2" Content="刷新" Command="{Binding RefreshPortsCommand}"
+ Style="{StaticResource ActionButton}" Width="50"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="波特率" Style="{StaticResource ConfigLabel}"/>
+ <ComboBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"
+ ItemsSource="{Binding BaudRates}"
+ SelectedItem="{Binding BaudRate, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigComboBox}"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="校验位" Style="{StaticResource ConfigLabel}"/>
+ <ComboBox Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2"
+ ItemsSource="{Binding ParityOptions}"
+ SelectedIndex="{Binding ParityIndex, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigComboBox}"/>
+
+ <TextBlock Grid.Row="3" Grid.Column="0" Text="数据位" Style="{StaticResource ConfigLabel}"/>
+ <ComboBox Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2"
+ ItemsSource="{Binding DataBitsOptions}"
+ SelectedItem="{Binding DataBits, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigComboBox}"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="0" Text="停止位" Style="{StaticResource ConfigLabel}"/>
+ <ComboBox Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2"
+ ItemsSource="{Binding StopBitsOptions}"
+ SelectedIndex="{Binding StopBitsIndex, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigComboBox}"/>
+
+ <TextBlock Grid.Row="5" Grid.Column="0" Text="站号" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="2"
+ Text="{Binding SlaveAddress, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+
+ <Button Grid.Row="6" Grid.Column="0" Grid.ColumnSpan="3"
+ Content="{Binding ConnectButtonText}"
+ Command="{Binding ToggleConnectCommand}"
+ Style="{StaticResource BigButton}" Foreground="White"/>
+ </Grid>
+ </StackPanel>
+ </Border>
+
+ <!-- 设备信息 -->
+ <Border Style="{StaticResource GroupBorder}">
+ <StackPanel>
+ <TextBlock Text="设备信息" Style="{StaticResource SectionTitle}"/>
+ <TextBlock Margin="4,2">
+ <Run FontWeight="Bold">型号:</Run>
+ <Run Text="{Binding ProductType, Mode=OneWay}"/>
+ </TextBlock>
+ <TextBlock Margin="4,2">
+ <Run FontWeight="Bold">版本:</Run>
+ <Run Text="{Binding FirmwareVersion, Mode=OneWay}"/>
+ </TextBlock>
+ <Button Content="读取设备信息" Command="{Binding ReadDeviceInfoCommand}"
+ Style="{StaticResource ActionButton}" Margin="4,2"/>
+ </StackPanel>
+ </Border>
+
+ <!-- 批量操作 -->
+ <Border Style="{StaticResource GroupBorder}">
+ <StackPanel>
+ <TextBlock Text="批量操作" Style="{StaticResource SectionTitle}"/>
+ <Button Content="读取输入" Command="{Binding ReadInputsCommand}"
+ Style="{StaticResource ActionButton}" Margin="4,2"/>
+ <Button Content="读取输出" Command="{Binding ReadOutputsCommand}"
+ Style="{StaticResource ActionButton}" Margin="4,2"/>
+ <Button Content="全部打开" Command="{Binding TurnOnAllCommand}"
+ Style="{StaticResource ActionButton}" Margin="4,2" Foreground="Green"/>
+ <Button Content="全部关闭" Command="{Binding TurnOffAllCommand}"
+ Style="{StaticResource ActionButton}" Margin="4,2" Foreground="Red"/>
+ <WrapPanel Margin="4,2">
+ <TextBlock Text="延迟(ms)" VerticalAlignment="Center" Margin="0,0,4,0"/>
+ <TextBox Text="{Binding Delay, UpdateSourceTrigger=PropertyChanged}" Width="60"
+ Style="{StaticResource ConfigTextBox}"/>
+ </WrapPanel>
+ </StackPanel>
+ </Border>
+ </StackPanel>
+ </ScrollViewer>
+
+ <!-- ============ 右侧:I/O 面板 + 日志 ============ -->
+ <Grid Grid.Column="1">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <!-- 输出面板 -->
+ <Border Grid.Row="0" Style="{StaticResource GroupBorder}" Padding="4">
+ <StackPanel>
+ <TextBlock Text="输出端口(Output)" Style="{StaticResource SectionTitle}"/>
+ <WrapPanel>
+ <Border BorderBrush="#4CAF50" BorderThickness="1" CornerRadius="4" Margin="4" Padding="4" Width="90">
+ <StackPanel>
+ <TextBlock Text="输出 1" HorizontalAlignment="Center" FontSize="11"/>
+ <Ellipse Width="20" Height="20" Fill="{Binding Output1, Converter={x:Null}}" HorizontalAlignment="Center" Margin="0,2"/>
+ <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
+ <Button Content="开" Command="{Binding TurnOnCommand}" CommandParameter="1"
+ Width="30" Height="24" FontSize="11" Margin="1"/>
+ <Button Content="关" Command="{Binding TurnOffCommand}" CommandParameter="1"
+ Width="30" Height="24" FontSize="11" Margin="1"/>
+ </StackPanel>
+ </StackPanel>
+ </Border>
+ <Border BorderBrush="#4CAF50" BorderThickness="1" CornerRadius="4" Margin="4" Padding="4" Width="90">
+ <StackPanel>
+ <TextBlock Text="输出 2" HorizontalAlignment="Center" FontSize="11"/>
+ <Ellipse Width="20" Height="20" Fill="{Binding Output2, Converter={x:Null}}" HorizontalAlignment="Center" Margin="0,2"/>
+ <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
+ <Button Content="开" Command="{Binding TurnOnCommand}" CommandParameter="2"
+ Width="30" Height="24" FontSize="11" Margin="1"/>
+ <Button Content="关" Command="{Binding TurnOffCommand}" CommandParameter="2"
+ Width="30" Height="24" FontSize="11" Margin="1"/>
+ </StackPanel>
+ </StackPanel>
+ </Border>
+ <Border BorderBrush="#4CAF50" BorderThickness="1" CornerRadius="4" Margin="4" Padding="4" Width="90">
+ <StackPanel>
+ <TextBlock Text="输出 3" HorizontalAlignment="Center" FontSize="11"/>
+ <Ellipse Width="20" Height="20" Fill="{Binding Output3, Converter={x:Null}}" HorizontalAlignment="Center" Margin="0,2"/>
+ <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
+ <Button Content="开" Command="{Binding TurnOnCommand}" CommandParameter="3"
+ Width="30" Height="24" FontSize="11" Margin="1"/>
+ <Button Content="关" Command="{Binding TurnOffCommand}" CommandParameter="3"
+ Width="30" Height="24" FontSize="11" Margin="1"/>
+ </StackPanel>
+ </StackPanel>
+ </Border>
+ <Border BorderBrush="#4CAF50" BorderThickness="1" CornerRadius="4" Margin="4" Padding="4" Width="90">
+ <StackPanel>
+ <TextBlock Text="输出 4" HorizontalAlignment="Center" FontSize="11"/>
+ <Ellipse Width="20" Height="20" Fill="{Binding Output4, Converter={x:Null}}" HorizontalAlignment="Center" Margin="0,2"/>
+ <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
+ <Button Content="开" Command="{Binding TurnOnCommand}" CommandParameter="4"
+ Width="30" Height="24" FontSize="11" Margin="1"/>
+ <Button Content="关" Command="{Binding TurnOffCommand}" CommandParameter="4"
+ Width="30" Height="24" FontSize="11" Margin="1"/>
+ </StackPanel>
+ </StackPanel>
+ </Border>
+ <Border BorderBrush="#4CAF50" BorderThickness="1" CornerRadius="4" Margin="4" Padding="4" Width="90">
+ <StackPanel>
+ <TextBlock Text="输出 5" HorizontalAlignment="Center" FontSize="11"/>
+ <Ellipse Width="20" Height="20" Fill="{Binding Output5, Converter={x:Null}}" HorizontalAlignment="Center" Margin="0,2"/>
+ <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
+ <Button Content="开" Command="{Binding TurnOnCommand}" CommandParameter="5"
+ Width="30" Height="24" FontSize="11" Margin="1"/>
+ <Button Content="关" Command="{Binding TurnOffCommand}" CommandParameter="5"
+ Width="30" Height="24" FontSize="11" Margin="1"/>
+ </StackPanel>
+ </StackPanel>
+ </Border>
+ <Border BorderBrush="#4CAF50" BorderThickness="1" CornerRadius="4" Margin="4" Padding="4" Width="90">
+ <StackPanel>
+ <TextBlock Text="输出 6" HorizontalAlignment="Center" FontSize="11"/>
+ <Ellipse Width="20" Height="20" Fill="{Binding Output6, Converter={x:Null}}" HorizontalAlignment="Center" Margin="0,2"/>
+ <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
+ <Button Content="开" Command="{Binding TurnOnCommand}" CommandParameter="6"
+ Width="30" Height="24" FontSize="11" Margin="1"/>
+ <Button Content="关" Command="{Binding TurnOffCommand}" CommandParameter="6"
+ Width="30" Height="24" FontSize="11" Margin="1"/>
+ </StackPanel>
+ </StackPanel>
+ </Border>
+ <Border BorderBrush="#4CAF50" BorderThickness="1" CornerRadius="4" Margin="4" Padding="4" Width="90">
+ <StackPanel>
+ <TextBlock Text="输出 7" HorizontalAlignment="Center" FontSize="11"/>
+ <Ellipse Width="20" Height="20" Fill="{Binding Output7, Converter={x:Null}}" HorizontalAlignment="Center" Margin="0,2"/>
+ <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
+ <Button Content="开" Command="{Binding TurnOnCommand}" CommandParameter="7"
+ Width="30" Height="24" FontSize="11" Margin="1"/>
+ <Button Content="关" Command="{Binding TurnOffCommand}" CommandParameter="7"
+ Width="30" Height="24" FontSize="11" Margin="1"/>
+ </StackPanel>
+ </StackPanel>
+ </Border>
+ <Border BorderBrush="#4CAF50" BorderThickness="1" CornerRadius="4" Margin="4" Padding="4" Width="90">
+ <StackPanel>
+ <TextBlock Text="输出 8" HorizontalAlignment="Center" FontSize="11"/>
+ <Ellipse Width="20" Height="20" Fill="{Binding Output8, Converter={x:Null}}" HorizontalAlignment="Center" Margin="0,2"/>
+ <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
+ <Button Content="开" Command="{Binding TurnOnCommand}" CommandParameter="8"
+ Width="30" Height="24" FontSize="11" Margin="1"/>
+ <Button Content="关" Command="{Binding TurnOffCommand}" CommandParameter="8"
+ Width="30" Height="24" FontSize="11" Margin="1"/>
+ </StackPanel>
+ </StackPanel>
+ </Border>
+ </WrapPanel>
+ </StackPanel>
+ </Border>
+
+ <!-- 输入面板 -->
+ <Border Grid.Row="1" Style="{StaticResource GroupBorder}" Padding="4">
+ <StackPanel>
+ <TextBlock Text="输入端口(Input)" Style="{StaticResource SectionTitle}"/>
+ <WrapPanel>
+ <Border BorderBrush="#2196F3" BorderThickness="1" CornerRadius="4" Margin="4" Padding="4" Width="90">
+ <StackPanel>
+ <TextBlock Text="输入 1" HorizontalAlignment="Center" FontSize="11"/>
+ <Ellipse Width="20" Height="20" Fill="{Binding Input1, Converter={x:Null}}" HorizontalAlignment="Center" Margin="0,2"/>
+ </StackPanel>
+ </Border>
+ <Border BorderBrush="#2196F3" BorderThickness="1" CornerRadius="4" Margin="4" Padding="4" Width="90">
+ <StackPanel>
+ <TextBlock Text="输入 2" HorizontalAlignment="Center" FontSize="11"/>
+ <Ellipse Width="20" Height="20" Fill="{Binding Input2, Converter={x:Null}}" HorizontalAlignment="Center" Margin="0,2"/>
+ </StackPanel>
+ </Border>
+ <Border BorderBrush="#2196F3" BorderThickness="1" CornerRadius="4" Margin="4" Padding="4" Width="90">
+ <StackPanel>
+ <TextBlock Text="输入 3" HorizontalAlignment="Center" FontSize="11"/>
+ <Ellipse Width="20" Height="20" Fill="{Binding Input3, Converter={x:Null}}" HorizontalAlignment="Center" Margin="0,2"/>
+ </StackPanel>
+ </Border>
+ <Border BorderBrush="#2196F3" BorderThickness="1" CornerRadius="4" Margin="4" Padding="4" Width="90">
+ <StackPanel>
+ <TextBlock Text="输入 4" HorizontalAlignment="Center" FontSize="11"/>
+ <Ellipse Width="20" Height="20" Fill="{Binding Input4, Converter={x:Null}}" HorizontalAlignment="Center" Margin="0,2"/>
+ </StackPanel>
+ </Border>
+ <Border BorderBrush="#2196F3" BorderThickness="1" CornerRadius="4" Margin="4" Padding="4" Width="90">
+ <StackPanel>
+ <TextBlock Text="输入 5" HorizontalAlignment="Center" FontSize="11"/>
+ <Ellipse Width="20" Height="20" Fill="{Binding Input5, Converter={x:Null}}" HorizontalAlignment="Center" Margin="0,2"/>
+ </StackPanel>
+ </Border>
+ <Border BorderBrush="#2196F3" BorderThickness="1" CornerRadius="4" Margin="4" Padding="4" Width="90">
+ <StackPanel>
+ <TextBlock Text="输入 6" HorizontalAlignment="Center" FontSize="11"/>
+ <Ellipse Width="20" Height="20" Fill="{Binding Input6, Converter={x:Null}}" HorizontalAlignment="Center" Margin="0,2"/>
+ </StackPanel>
+ </Border>
+ <Border BorderBrush="#2196F3" BorderThickness="1" CornerRadius="4" Margin="4" Padding="4" Width="90">
+ <StackPanel>
+ <TextBlock Text="输入 7" HorizontalAlignment="Center" FontSize="11"/>
+ <Ellipse Width="20" Height="20" Fill="{Binding Input7, Converter={x:Null}}" HorizontalAlignment="Center" Margin="0,2"/>
+ </StackPanel>
+ </Border>
+ <Border BorderBrush="#2196F3" BorderThickness="1" CornerRadius="4" Margin="4" Padding="4" Width="90">
+ <StackPanel>
+ <TextBlock Text="输入 8" HorizontalAlignment="Center" FontSize="11"/>
+ <Ellipse Width="20" Height="20" Fill="{Binding Input8, Converter={x:Null}}" HorizontalAlignment="Center" Margin="0,2"/>
+ </StackPanel>
+ </Border>
+ </WrapPanel>
+ </StackPanel>
+ </Border>
+
+ <!-- 日志 -->
+ <Grid Grid.Row="2">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <Border Grid.Row="0" Style="{StaticResource GroupBorder}" Background="#FFF5E6">
+ <Grid>
+ <TextBlock Text="日志" Style="{StaticResource SectionTitle}" VerticalAlignment="Center"/>
+ <Button HorizontalAlignment="Right" Content="清空" Command="{Binding ClearLogCommand}"
+ Style="{StaticResource ActionButton}" Margin="0,2,4,2"/>
+ </Grid>
+ </Border>
+
+ <Border Grid.Row="1" Style="{StaticResource GroupBorder}">
+ <RichTextBox x:Name="txtLog" IsReadOnly="True" VerticalScrollBarVisibility="Auto"
+ FontFamily="Consolas" FontSize="12" Background="#1E1E1E" Foreground="#D4D4D4"/>
+ </Border>
+ </Grid>
+ </Grid>
+ </Grid>
+</Window>
diff --git a/CrazyCoder/Views/IoControlWindow.xaml.cs b/CrazyCoder/Views/IoControlWindow.xaml.cs
new file mode 100644
index 0000000..eb09bea
--- /dev/null
+++ b/CrazyCoder/Views/IoControlWindow.xaml.cs
@@ -0,0 +1,55 @@
+using System.Windows;
+using System.Windows.Documents;
+using System.Windows.Media;
+using CrazyCoder.ViewModels;
+
+using static System.Windows.Media.Brushes;
+
+namespace CrazyCoder.Views;
+
+/// <summary>I/O 控制面板窗口</summary>
+public partial class IoControlWindow : Window
+{
+ /// <summary>ViewModel</summary>
+ public IoControlViewModel ViewModel { get; }
+
+ /// <summary>实例化 I/O 控制面板窗口</summary>
+ public IoControlWindow()
+ {
+ InitializeComponent();
+
+ ViewModel = new IoControlViewModel();
+ DataContext = ViewModel;
+
+ ViewModel.OnLog += AppendLog;
+ }
+
+ private void AppendLog(String msg)
+ {
+ if (!Dispatcher.CheckAccess())
+ {
+ Dispatcher.Invoke(() => AppendLog(msg));
+ return;
+ }
+
+ if (msg == "__CLEAR__")
+ {
+ txtLog.Document.Blocks.Clear();
+ return;
+ }
+
+ var brush = new SolidColorBrush(Color.FromRgb(0xD4, 0xD4, 0xD4));
+ if (msg.Contains("失败") || msg.Contains("错误") || msg.Contains("异常"))
+ brush = Brushes.Red;
+
+ var run = new Run(msg) { Foreground = brush };
+ var paragraph = new Paragraph(run)
+ {
+ Margin = new Thickness(0),
+ Padding = new Thickness(0)
+ };
+
+ txtLog.Document.Blocks.Add(paragraph);
+ txtLog.ScrollToEnd();
+ }
+}
diff --git a/CrazyCoder/Views/ModbusRtuWindow.xaml b/CrazyCoder/Views/ModbusRtuWindow.xaml
new file mode 100644
index 0000000..6a16742
--- /dev/null
+++ b/CrazyCoder/Views/ModbusRtuWindow.xaml
@@ -0,0 +1,196 @@
+<Window x:Class="CrazyCoder.Views.ModbusRtuWindow"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d"
+ Title="Modbus RTU 工具" Height="700" Width="1000" WindowStartupLocation="CenterScreen">
+ <Window.Resources>
+ <Style x:Key="GroupBorder" TargetType="Border">
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="Margin" Value="0,0,0,6"/>
+ </Style>
+
+ <Style x:Key="SectionTitle" TargetType="TextBlock">
+ <Setter Property="FontWeight" Value="Bold"/>
+ <Setter Property="Margin" Value="4,3"/>
+ <Setter Property="FontSize" Value="13"/>
+ </Style>
+
+ <Style x:Key="ConfigLabel" TargetType="TextBlock">
+ <Setter Property="VerticalAlignment" Value="Center"/>
+ <Setter Property="Margin" Value="4,2"/>
+ </Style>
+
+ <Style x:Key="ConfigComboBox" TargetType="ComboBox">
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Height" Value="26"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ </Style>
+
+ <Style x:Key="ConfigTextBox" TargetType="TextBox">
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Height" Value="26"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ </Style>
+
+ <Style x:Key="ActionButton" TargetType="Button">
+ <Setter Property="Height" Value="30"/>
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Padding" Value="8,0"/>
+ </Style>
+
+ <Style x:Key="BigButton" TargetType="Button">
+ <Setter Property="Height" Value="40"/>
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="FontSize" Value="14"/>
+ <Setter Property="FontWeight" Value="Bold"/>
+ </Style>
+ </Window.Resources>
+
+ <Grid Margin="6">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="300"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- ============ 左侧:配置面板 ============ -->
+ <ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto" Margin="0,0,4,0">
+ <StackPanel>
+ <!-- 串口配置 -->
+ <Border Style="{StaticResource GroupBorder}">
+ <StackPanel>
+ <TextBlock Text="串口配置" Style="{StaticResource SectionTitle}"/>
+
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="50"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="端口" Style="{StaticResource ConfigLabel}"/>
+ <ComboBox Grid.Row="0" Grid.Column="1"
+ ItemsSource="{Binding PortNames}"
+ SelectedItem="{Binding PortName, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigComboBox}"/>
+ <Button Grid.Row="0" Grid.Column="2" Content="刷新" Command="{Binding RefreshPortsCommand}"
+ Style="{StaticResource ActionButton}" Width="50"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="波特率" Style="{StaticResource ConfigLabel}"/>
+ <ComboBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"
+ ItemsSource="{Binding BaudRates}"
+ SelectedItem="{Binding BaudRate, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigComboBox}"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="校验位" Style="{StaticResource ConfigLabel}"/>
+ <ComboBox Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2"
+ ItemsSource="{Binding ParityOptions}"
+ SelectedIndex="{Binding ParityIndex, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigComboBox}"/>
+
+ <TextBlock Grid.Row="3" Grid.Column="0" Text="数据位" Style="{StaticResource ConfigLabel}"/>
+ <ComboBox Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2"
+ ItemsSource="{Binding DataBitsOptions}"
+ SelectedItem="{Binding DataBits, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigComboBox}"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="0" Text="停止位" Style="{StaticResource ConfigLabel}"/>
+ <ComboBox Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2"
+ ItemsSource="{Binding StopBitsOptions}"
+ SelectedIndex="{Binding StopBitsIndex, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigComboBox}"/>
+
+ <Button Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="3"
+ Content="{Binding ConnectButtonText}"
+ Command="{Binding ToggleConnectCommand}"
+ Style="{StaticResource BigButton}" Foreground="White"/>
+ </Grid>
+ </StackPanel>
+ </Border>
+
+ <!-- Modbus 操作 -->
+ <Border Style="{StaticResource GroupBorder}">
+ <StackPanel>
+ <TextBlock Text="Modbus 操作" Style="{StaticResource SectionTitle}"/>
+
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="50"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="站号" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding SlaveAddress, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="功能" Style="{StaticResource ConfigLabel}"/>
+ <ComboBox Grid.Row="1" Grid.Column="1"
+ ItemsSource="{Binding FuncCodeOptions}"
+ SelectedIndex="{Binding FunctionCodeIndex, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigComboBox}"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="地址" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding StartAddress, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+
+ <TextBlock Grid.Row="3" Grid.Column="0" Text="数量" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding ReadCount, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="0" Text="值" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="4" Grid.Column="1" Text="{Binding WriteValue, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+ </Grid>
+
+ <Button Content="执行" Command="{Binding ExecuteCommand}"
+ Style="{StaticResource BigButton}" Foreground="White" Background="#218868"/>
+
+ <TextBox Text="{Binding ReadResult, Mode=OneWay}" IsReadOnly="True"
+ Style="{StaticResource ConfigTextBox}" Height="60"
+ AcceptsReturn="True" TextWrapping="Wrap" Margin="2,4,2,2"/>
+ </StackPanel>
+ </Border>
+ </StackPanel>
+ </ScrollViewer>
+
+ <!-- ============ 右侧:日志显示 ============ -->
+ <Grid Grid.Column="1">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <Border Grid.Row="0" Style="{StaticResource GroupBorder}" Background="#FFF5E6">
+ <Grid>
+ <TextBlock Text="日志" Style="{StaticResource SectionTitle}" VerticalAlignment="Center"/>
+ <Button HorizontalAlignment="Right" Content="清空" Command="{Binding ClearLogCommand}"
+ Style="{StaticResource ActionButton}" Margin="0,2,4,2"/>
+ </Grid>
+ </Border>
+
+ <Border Grid.Row="1" Style="{StaticResource GroupBorder}">
+ <RichTextBox x:Name="txtLog" IsReadOnly="True" VerticalScrollBarVisibility="Auto"
+ FontFamily="Consolas" FontSize="12" Background="#1E1E1E" Foreground="#D4D4D4"/>
+ </Border>
+ </Grid>
+ </Grid>
+</Window>
diff --git a/CrazyCoder/Views/ModbusRtuWindow.xaml.cs b/CrazyCoder/Views/ModbusRtuWindow.xaml.cs
new file mode 100644
index 0000000..e2acb2c
--- /dev/null
+++ b/CrazyCoder/Views/ModbusRtuWindow.xaml.cs
@@ -0,0 +1,55 @@
+using System.Windows;
+using System.Windows.Documents;
+using System.Windows.Media;
+using CrazyCoder.ViewModels;
+
+using static System.Windows.Media.Brushes;
+
+namespace CrazyCoder.Views;
+
+/// <summary>Modbus RTU 工具窗口</summary>
+public partial class ModbusRtuWindow : Window
+{
+ /// <summary>ViewModel</summary>
+ public ModbusRtuViewModel ViewModel { get; }
+
+ /// <summary>实例化 Modbus RTU 工具窗口</summary>
+ public ModbusRtuWindow()
+ {
+ InitializeComponent();
+
+ ViewModel = new ModbusRtuViewModel();
+ DataContext = ViewModel;
+
+ ViewModel.OnLog += AppendLog;
+ }
+
+ private void AppendLog(String msg)
+ {
+ if (!Dispatcher.CheckAccess())
+ {
+ Dispatcher.Invoke(() => AppendLog(msg));
+ return;
+ }
+
+ if (msg == "__CLEAR__")
+ {
+ txtLog.Document.Blocks.Clear();
+ return;
+ }
+
+ var brush = new SolidColorBrush(Color.FromRgb(0xD4, 0xD4, 0xD4));
+ if (msg.Contains("失败") || msg.Contains("错误") || msg.Contains("异常"))
+ brush = Brushes.Red;
+
+ var run = new Run(msg) { Foreground = brush };
+ var paragraph = new Paragraph(run)
+ {
+ Margin = new Thickness(0),
+ Padding = new Thickness(0)
+ };
+
+ txtLog.Document.Blocks.Add(paragraph);
+ txtLog.ScrollToEnd();
+ }
+}
diff --git a/CrazyCoder/Views/ModbusTcpWindow.xaml b/CrazyCoder/Views/ModbusTcpWindow.xaml
new file mode 100644
index 0000000..c614ca8
--- /dev/null
+++ b/CrazyCoder/Views/ModbusTcpWindow.xaml
@@ -0,0 +1,247 @@
+<Window x:Class="CrazyCoder.Views.ModbusTcpWindow"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d"
+ Title="Modbus TCP 工具" Height="750" Width="1100" WindowStartupLocation="CenterScreen">
+ <Window.Resources>
+ <Style x:Key="GroupBorder" TargetType="Border">
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="Margin" Value="0,0,0,6"/>
+ </Style>
+
+ <Style x:Key="SectionTitle" TargetType="TextBlock">
+ <Setter Property="FontWeight" Value="Bold"/>
+ <Setter Property="Margin" Value="4,3"/>
+ <Setter Property="FontSize" Value="13"/>
+ </Style>
+
+ <Style x:Key="ConfigLabel" TargetType="TextBlock">
+ <Setter Property="VerticalAlignment" Value="Center"/>
+ <Setter Property="Margin" Value="4,2"/>
+ </Style>
+
+ <Style x:Key="ConfigComboBox" TargetType="ComboBox">
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Height" Value="26"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ </Style>
+
+ <Style x:Key="ConfigTextBox" TargetType="TextBox">
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Height" Value="26"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ </Style>
+
+ <Style x:Key="ActionButton" TargetType="Button">
+ <Setter Property="Height" Value="30"/>
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Padding" Value="8,0"/>
+ </Style>
+
+ <Style x:Key="BigButton" TargetType="Button">
+ <Setter Property="Height" Value="40"/>
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="FontSize" Value="14"/>
+ <Setter Property="FontWeight" Value="Bold"/>
+ </Style>
+ </Window.Resources>
+
+ <Grid Margin="6">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="320"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- ============ 左侧:配置面板 ============ -->
+ <ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto" Margin="0,0,4,0">
+ <StackPanel>
+ <!-- 连接配置 -->
+ <Border Style="{StaticResource GroupBorder}">
+ <StackPanel>
+ <TextBlock Text="连接配置" Style="{StaticResource SectionTitle}"/>
+
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="50"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="模式" Style="{StaticResource ConfigLabel}"/>
+ <ComboBox Grid.Row="0" Grid.Column="1"
+ ItemsSource="{Binding ModeOptions}"
+ SelectedIndex="{Binding SelectedMode, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigComboBox}"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="地址" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding ServerAddress, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="端口" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Port, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+
+ <Button Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2"
+ Content="{Binding ConnectButtonText}"
+ Command="{Binding ToggleConnectCommand}"
+ Style="{StaticResource BigButton}" Foreground="White"/>
+ </Grid>
+ </StackPanel>
+ </Border>
+
+ <!-- Modbus 操作(主站模式) -->
+ <Border Style="{StaticResource GroupBorder}">
+ <StackPanel>
+ <TextBlock Text="Modbus 操作" Style="{StaticResource SectionTitle}"/>
+
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="50"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="站号" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding SlaveAddress, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="功能" Style="{StaticResource ConfigLabel}"/>
+ <ComboBox Grid.Row="1" Grid.Column="1"
+ ItemsSource="{Binding FuncCodeOptions}"
+ SelectedIndex="{Binding FunctionCodeIndex, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigComboBox}"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="地址" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding StartAddress, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+
+ <TextBlock Grid.Row="3" Grid.Column="0" Text="数量" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding ReadCount, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="0" Text="值" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="4" Grid.Column="1" Text="{Binding WriteValue, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+ </Grid>
+
+ <Button Content="执行" Command="{Binding ExecuteCommand}"
+ Style="{StaticResource BigButton}" Foreground="White" Background="#218868"/>
+
+ <TextBox Text="{Binding ReadResult, Mode=OneWay}" IsReadOnly="True"
+ Style="{StaticResource ConfigTextBox}" Height="60"
+ AcceptsReturn="True" TextWrapping="Wrap" Margin="2,4,2,2"/>
+ </StackPanel>
+ </Border>
+
+ <!-- 从站配置 -->
+ <Border Style="{StaticResource GroupBorder}">
+ <StackPanel>
+ <TextBlock Text="从站配置" Style="{StaticResource SectionTitle}"/>
+
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="50"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="地址" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding SlaveDataAddress, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="数量" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding SlaveDataCount, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="模式" Style="{StaticResource ConfigLabel}"/>
+ <ComboBox Grid.Row="2" Grid.Column="1"
+ ItemsSource="{Binding DataModeOptions}"
+ SelectedIndex="{Binding DataModeIndex, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigComboBox}"/>
+ <Button Grid.Row="2" Grid.Column="2" Content="刷新" Command="{Binding RefreshDataCommand}"
+ Style="{StaticResource ActionButton}" Width="50"/>
+ </Grid>
+ </StackPanel>
+ </Border>
+ </StackPanel>
+ </ScrollViewer>
+
+ <!-- ============ 右侧:日志 + 从站数据 ============ -->
+ <Grid Grid.Column="1">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="*"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="2*"/>
+ </Grid.RowDefinitions>
+
+ <!-- 日志 -->
+ <Grid Grid.Row="0">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <Border Grid.Row="0" Style="{StaticResource GroupBorder}" Background="#FFF5E6">
+ <Grid>
+ <TextBlock Text="日志" Style="{StaticResource SectionTitle}" VerticalAlignment="Center"/>
+ <Button HorizontalAlignment="Right" Content="清空" Command="{Binding ClearLogCommand}"
+ Style="{StaticResource ActionButton}" Margin="0,2,4,2"/>
+ </Grid>
+ </Border>
+
+ <Border Grid.Row="1" Style="{StaticResource GroupBorder}">
+ <RichTextBox x:Name="txtLog" IsReadOnly="True" VerticalScrollBarVisibility="Auto"
+ FontFamily="Consolas" FontSize="12" Background="#1E1E1E" Foreground="#D4D4D4"/>
+ </Border>
+ </Grid>
+
+ <!-- 分隔 -->
+ <GridSplitter Grid.Row="1" Height="4" HorizontalAlignment="Stretch" Background="#D0D0D0"/>
+
+ <!-- 从站数据表格 -->
+ <Grid Grid.Row="2">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <Border Grid.Row="0" Style="{StaticResource GroupBorder}" Background="#E8F5E9">
+ <TextBlock Text="从站数据(寄存器列表)" Style="{StaticResource SectionTitle}"/>
+ </Border>
+
+ <Border Grid.Row="1" Style="{StaticResource GroupBorder}">
+ <ListView x:Name="lvRegisters" ItemsSource="{Binding Registers}" HorizontalContentAlignment="Stretch">
+ <ListView.View>
+ <GridView>
+ <GridViewColumn Header="地址" Width="100" DisplayMemberBinding="{Binding Address}"/>
+ <GridViewColumn Header="数值(十进制)" Width="150" DisplayMemberBinding="{Binding Value}"/>
+ <GridViewColumn Header="数值(十六进制)" Width="150" DisplayMemberBinding="{Binding Hex}"/>
+ </GridView>
+ </ListView.View>
+ </ListView>
+ </Border>
+ </Grid>
+ </Grid>
+ </Grid>
+</Window>
diff --git a/CrazyCoder/Views/ModbusTcpWindow.xaml.cs b/CrazyCoder/Views/ModbusTcpWindow.xaml.cs
new file mode 100644
index 0000000..7cc3518
--- /dev/null
+++ b/CrazyCoder/Views/ModbusTcpWindow.xaml.cs
@@ -0,0 +1,55 @@
+using System.Windows;
+using System.Windows.Documents;
+using System.Windows.Media;
+using CrazyCoder.ViewModels;
+
+using static System.Windows.Media.Brushes;
+
+namespace CrazyCoder.Views;
+
+/// <summary>Modbus TCP 工具窗口</summary>
+public partial class ModbusTcpWindow : Window
+{
+ /// <summary>ViewModel</summary>
+ public ModbusTcpViewModel ViewModel { get; }
+
+ /// <summary>实例化 Modbus TCP 工具窗口</summary>
+ public ModbusTcpWindow()
+ {
+ InitializeComponent();
+
+ ViewModel = new ModbusTcpViewModel();
+ DataContext = ViewModel;
+
+ ViewModel.OnLog += AppendLog;
+ }
+
+ private void AppendLog(String msg)
+ {
+ if (!Dispatcher.CheckAccess())
+ {
+ Dispatcher.Invoke(() => AppendLog(msg));
+ return;
+ }
+
+ if (msg == "__CLEAR__")
+ {
+ txtLog.Document.Blocks.Clear();
+ return;
+ }
+
+ var brush = new SolidColorBrush(Color.FromRgb(0xD4, 0xD4, 0xD4));
+ if (msg.Contains("失败") || msg.Contains("错误") || msg.Contains("异常"))
+ brush = Brushes.Red;
+
+ var run = new Run(msg) { Foreground = brush };
+ var paragraph = new Paragraph(run)
+ {
+ Margin = new Thickness(0),
+ Padding = new Thickness(0)
+ };
+
+ txtLog.Document.Blocks.Add(paragraph);
+ txtLog.ScrollToEnd();
+ }
+}
diff --git a/CrazyCoder/Views/RedisManagerWindow.xaml b/CrazyCoder/Views/RedisManagerWindow.xaml
new file mode 100644
index 0000000..4ccd3c0
--- /dev/null
+++ b/CrazyCoder/Views/RedisManagerWindow.xaml
@@ -0,0 +1,106 @@
+<Window x:Class="CrazyCoder.Views.RedisManagerWindow"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:vm="clr-namespace:CrazyCoder.ViewModels"
+ mc:Ignorable="d"
+ Title="Redis 管理器" Height="700" Width="1100" WindowStartupLocation="CenterScreen">
+ <Window.Resources>
+ <Style x:Key="GroupBorder" TargetType="Border">
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="Margin" Value="0,0,4,0"/>
+ </Style>
+
+ <HierarchicalDataTemplate DataType="{x:Type vm:RedisTreeNode}" ItemsSource="{Binding Children}">
+ <StackPanel Orientation="Horizontal" Margin="{Binding Indent}">
+ <TextBlock Text="{Binding Title}" Margin="4,2"/>
+ </StackPanel>
+ </HierarchicalDataTemplate>
+ </Window.Resources>
+
+ <Grid Margin="8">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="280"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- 左侧:树形节点 -->
+ <Border Grid.Column="0" Style="{StaticResource GroupBorder}">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <TextBlock Text="Redis 服务器" FontWeight="Bold" Background="#F0F0F0" Padding="6,3"/>
+
+ <!-- 搜索框 -->
+ <Grid Grid.Row="1" Margin="4">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ <TextBox Text="{Binding SearchPattern, UpdateSourceTrigger=PropertyChanged}" Height="26"/>
+ <Button Grid.Column="1" Content="搜索" Command="{Binding SearchKeysCommand}" Height="26" Width="50" Margin="4,0,0,0"/>
+ </Grid>
+
+ <!-- 树 -->
+ <TreeView x:Name="treeView" Grid.Row="2" ItemsSource="{Binding TreeNodes}" BorderThickness="0"
+ SelectedItemChanged="OnTreeSelectedItemChanged"
+ MouseDoubleClick="OnTreeMouseDoubleClick">
+ <TreeView.ItemTemplate>
+ <HierarchicalDataTemplate ItemsSource="{Binding Children}">
+ <TextBlock Text="{Binding Title}" Margin="4,2"/>
+ </HierarchicalDataTemplate>
+ </TreeView.ItemTemplate>
+ </TreeView>
+
+ <!-- 底部操作按钮 -->
+ <StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,4">
+ <Button Content="添加节点" Command="{Binding AddNodeCommand}" Width="80" Height="28" Margin="2"/>
+ <Button Content="编辑节点" Command="{Binding EditNodeCommand}" Width="80" Height="28" Margin="2"/>
+ <Button Content="删除节点" Command="{Binding DeleteNodeCommand}" Width="80" Height="28" Margin="2"/>
+ </StackPanel>
+ </Grid>
+ </Border>
+
+ <!-- 右侧:Key 值显示 -->
+ <Border Grid.Column="1" BorderBrush="#D0D0D0" BorderThickness="1" Margin="4,0,0,0">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <TextBlock Text="Key 值" FontWeight="Bold" Background="#F0F0F0" Padding="6,3"/>
+
+ <TextBox Grid.Row="1" Text="{Binding KeyValue, Mode=OneWay}"
+ FontFamily="Consolas" FontSize="13"
+ IsReadOnly="True"
+ VerticalScrollBarVisibility="Auto"
+ HorizontalScrollBarVisibility="Auto"
+ BorderThickness="0"
+ TextWrapping="Wrap"/>
+ </Grid>
+ </Border>
+
+ <!-- 状态栏 -->
+ <StatusBar Grid.ColumnSpan="2" VerticalAlignment="Bottom">
+ <StatusBar.ItemsPanel>
+ <ItemsPanelTemplate>
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+ </Grid>
+ </ItemsPanelTemplate>
+ </StatusBar.ItemsPanel>
+ <StatusBarItem>
+ <TextBlock Text="{Binding Status}"/>
+ </StatusBarItem>
+ </StatusBar>
+ </Grid>
+</Window>
diff --git a/CrazyCoder/Views/RedisManagerWindow.xaml.cs b/CrazyCoder/Views/RedisManagerWindow.xaml.cs
new file mode 100644
index 0000000..06c7d95
--- /dev/null
+++ b/CrazyCoder/Views/RedisManagerWindow.xaml.cs
@@ -0,0 +1,41 @@
+using System.Windows;
+using System.Windows.Controls;
+using CrazyCoder.ViewModels;
+
+namespace CrazyCoder.Views;
+
+/// <summary>Redis 管理器窗口</summary>
+public partial class RedisManagerWindow : Window
+{
+ /// <summary>ViewModel</summary>
+ public RedisManagerViewModel ViewModel { get; }
+
+ /// <summary>实例化 Redis 管理器窗口</summary>
+ public RedisManagerWindow()
+ {
+ InitializeComponent();
+
+ ViewModel = new RedisManagerViewModel();
+ DataContext = ViewModel;
+ }
+
+ private void OnTreeSelectedItemChanged(Object sender, RoutedPropertyChangedEventArgs<Object> e)
+ {
+ if (e.NewValue is RedisTreeNode node)
+ {
+ node.IsSelected = true;
+ }
+ if (e.OldValue is RedisTreeNode oldNode)
+ {
+ oldNode.IsSelected = false;
+ }
+ }
+
+ private void OnTreeMouseDoubleClick(Object sender, System.Windows.Input.MouseButtonEventArgs e)
+ {
+ if (treeView.SelectedItem is RedisTreeNode node)
+ {
+ ViewModel.HandleNodeDoubleClick(node);
+ }
+ }
+}
diff --git a/CrazyCoder/Views/SerialPortWindow.xaml b/CrazyCoder/Views/SerialPortWindow.xaml
new file mode 100644
index 0000000..0601203
--- /dev/null
+++ b/CrazyCoder/Views/SerialPortWindow.xaml
@@ -0,0 +1,179 @@
+<Window x:Class="CrazyCoder.Views.SerialPortWindow"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d"
+ Title="串口调试工具" Height="700" Width="1000" WindowStartupLocation="CenterScreen">
+ <Window.Resources>
+ <Style x:Key="GroupBorder" TargetType="Border">
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="Margin" Value="0,0,0,6"/>
+ </Style>
+
+ <Style x:Key="SectionTitle" TargetType="TextBlock">
+ <Setter Property="FontWeight" Value="Bold"/>
+ <Setter Property="Margin" Value="4,3"/>
+ <Setter Property="FontSize" Value="13"/>
+ </Style>
+
+ <Style x:Key="ConfigLabel" TargetType="TextBlock">
+ <Setter Property="VerticalAlignment" Value="Center"/>
+ <Setter Property="Margin" Value="4,2"/>
+ </Style>
+
+ <Style x:Key="ConfigTextBox" TargetType="TextBox">
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Height" Value="26"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ </Style>
+
+ <Style x:Key="ConfigComboBox" TargetType="ComboBox">
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Height" Value="26"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ </Style>
+
+ <Style x:Key="ActionButton" TargetType="Button">
+ <Setter Property="Height" Value="30"/>
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Padding" Value="8,0"/>
+ </Style>
+
+ <Style x:Key="SendButton" TargetType="Button">
+ <Setter Property="Height" Value="50"/>
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="FontSize" Value="14"/>
+ <Setter Property="FontWeight" Value="Bold"/>
+ </Style>
+ </Window.Resources>
+
+ <Grid Margin="6">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="280"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- ============ 左侧:配置面板 ============ -->
+ <ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto" Margin="0,0,4,0">
+ <StackPanel>
+ <!-- 串口配置 -->
+ <Border Style="{StaticResource GroupBorder}">
+ <StackPanel>
+ <TextBlock Text="串口配置" Style="{StaticResource SectionTitle}"/>
+
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="50"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="端口" Style="{StaticResource ConfigLabel}"/>
+ <ComboBox Grid.Row="0" Grid.Column="1"
+ ItemsSource="{Binding PortNames}"
+ SelectedItem="{Binding PortName, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigComboBox}"/>
+ <Button Grid.Row="0" Grid.Column="2" Content="刷新" Command="{Binding RefreshPortsCommand}"
+ Style="{StaticResource ActionButton}" Width="50"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="波特率" Style="{StaticResource ConfigLabel}"/>
+ <ComboBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"
+ ItemsSource="{Binding BaudRates}"
+ SelectedItem="{Binding BaudRate, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigComboBox}"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="校验位" Style="{StaticResource ConfigLabel}"/>
+ <ComboBox Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2"
+ ItemsSource="{Binding ParityOptions}"
+ SelectedIndex="{Binding ParityIndex, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigComboBox}"/>
+
+ <TextBlock Grid.Row="3" Grid.Column="0" Text="数据位" Style="{StaticResource ConfigLabel}"/>
+ <ComboBox Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2"
+ ItemsSource="{Binding DataBitsOptions}"
+ SelectedItem="{Binding DataBits, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigComboBox}"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="0" Text="停止位" Style="{StaticResource ConfigLabel}"/>
+ <ComboBox Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2"
+ ItemsSource="{Binding StopBitsOptions}"
+ SelectedIndex="{Binding StopBitsIndex, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigComboBox}"/>
+
+ <Button Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="3"
+ Content="{Binding ConnectButtonText}"
+ Command="{Binding ToggleConnectCommand}"
+ Style="{StaticResource SendButton}" Foreground="White"
+ Background="{Binding IsConnected, Converter={x:Null}}"/>
+ </Grid>
+ </StackPanel>
+ </Border>
+
+ <!-- 发送配置 -->
+ <Border Style="{StaticResource GroupBorder}">
+ <StackPanel>
+ <TextBlock Text="发送配置" Style="{StaticResource SectionTitle}"/>
+
+ <CheckBox Content="十六进制发送" IsChecked="{Binding HexSend}" Margin="4,2"/>
+
+ <TextBox Text="{Binding SendText, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}" Height="80"
+ AcceptsReturn="True" TextWrapping="Wrap"/>
+
+ <Button Content="发送" Command="{Binding SendCommand}"
+ Style="{StaticResource SendButton}" Foreground="White"
+ Background="#218868"/>
+ </StackPanel>
+ </Border>
+
+ <!-- 统计信息 -->
+ <Border Style="{StaticResource GroupBorder}">
+ <StackPanel>
+ <TextBlock Text="统计" Style="{StaticResource SectionTitle}"/>
+ <TextBlock Margin="4,2">
+ <Run FontWeight="Bold">已发送:</Run>
+ <Run Text="{Binding BytesSent}"/> 字节
+ </TextBlock>
+ <TextBlock Margin="4,2">
+ <Run FontWeight="Bold">已接收:</Run>
+ <Run Text="{Binding BytesReceived}"/> 字节
+ </TextBlock>
+ </StackPanel>
+ </Border>
+ </StackPanel>
+ </ScrollViewer>
+
+ <!-- ============ 右侧:日志显示 ============ -->
+ <Grid Grid.Column="1">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <Border Grid.Row="0" Style="{StaticResource GroupBorder}" Background="#FFF5E6">
+ <Grid>
+ <TextBlock Text="日志" Style="{StaticResource SectionTitle}" VerticalAlignment="Center"/>
+ <Button HorizontalAlignment="Right" Content="清空" Command="{Binding ClearLogCommand}"
+ Style="{StaticResource ActionButton}" Margin="0,2,4,2"/>
+ </Grid>
+ </Border>
+
+ <Border Grid.Row="1" Style="{StaticResource GroupBorder}">
+ <RichTextBox x:Name="txtLog" IsReadOnly="True" VerticalScrollBarVisibility="Auto"
+ FontFamily="Consolas" FontSize="12" Background="#1E1E1E" Foreground="#D4D4D4"/>
+ </Border>
+ </Grid>
+ </Grid>
+</Window>
diff --git a/CrazyCoder/Views/SerialPortWindow.xaml.cs b/CrazyCoder/Views/SerialPortWindow.xaml.cs
new file mode 100644
index 0000000..277d852
--- /dev/null
+++ b/CrazyCoder/Views/SerialPortWindow.xaml.cs
@@ -0,0 +1,59 @@
+using System.Windows;
+using System.Windows.Documents;
+using System.Windows.Media;
+using CrazyCoder.ViewModels;
+
+using static System.Windows.Media.Brushes;
+
+namespace CrazyCoder.Views;
+
+/// <summary>串口调试工具窗口</summary>
+public partial class SerialPortWindow : Window
+{
+ /// <summary>ViewModel</summary>
+ public SerialPortViewModel ViewModel { get; }
+
+ /// <summary>实例化串口调试工具窗口</summary>
+ public SerialPortWindow()
+ {
+ InitializeComponent();
+
+ ViewModel = new SerialPortViewModel();
+ DataContext = ViewModel;
+
+ ViewModel.OnLog += AppendLog;
+ }
+
+ private void AppendLog(String msg)
+ {
+ if (!Dispatcher.CheckAccess())
+ {
+ Dispatcher.Invoke(() => AppendLog(msg));
+ return;
+ }
+
+ if (msg == "__CLEAR__")
+ {
+ txtLog.Document.Blocks.Clear();
+ return;
+ }
+
+ var brush = new SolidColorBrush(Color.FromRgb(0xD4, 0xD4, 0xD4));
+ if (msg.StartsWith("[发送]"))
+ brush = Brushes.LimeGreen;
+ else if (msg.StartsWith("[接收]"))
+ brush = Brushes.Orange;
+ else if (msg.Contains("错误") || msg.Contains("失败") || msg.Contains("异常"))
+ brush = Brushes.Red;
+
+ var run = new Run(msg) { Foreground = brush };
+ var paragraph = new Paragraph(run)
+ {
+ Margin = new Thickness(0),
+ Padding = new Thickness(0)
+ };
+
+ txtLog.Document.Blocks.Add(paragraph);
+ txtLog.ScrollToEnd();
+ }
+}
diff --git a/XCoderAv/App.xaml b/XCoderAv/App.axaml
similarity index 66%
rename from XCoderAv/App.xaml
rename to XCoderAv/App.axaml
index bacd61f..72abfab 100644
--- a/XCoderAv/App.xaml
+++ b/XCoderAv/App.axaml
@@ -2,7 +2,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="XCoderAv.App">
<Application.Styles>
- <StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
- <StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
+ <FluentTheme />
</Application.Styles>
</Application>
diff --git a/XCoderAv/App.axaml.cs b/XCoderAv/App.axaml.cs
new file mode 100644
index 0000000..3ae29da
--- /dev/null
+++ b/XCoderAv/App.axaml.cs
@@ -0,0 +1,7 @@
+using Avalonia;
+
+namespace XCoderAv;
+
+public partial class App : Application
+{
+}
diff --git a/XCoderAv/Common/XConfig.cs b/XCoderAv/Common/XConfig.cs
new file mode 100644
index 0000000..8d8b04d
--- /dev/null
+++ b/XCoderAv/Common/XConfig.cs
@@ -0,0 +1,80 @@
+using System.ComponentModel;
+using NewLife;
+using NewLife.Configuration;
+
+namespace XCoderAv;
+
+/// <summary>应用配置</summary>
+[Config("XCoderAv")]
+public class XConfig : Config<XConfig>
+{
+ #region 属性
+ /// <summary>标题</summary>
+ [Description("标题")]
+ public String Title { get; set; } = "";
+
+ /// <summary>宽度</summary>
+ [Description("宽度")]
+ public Int32 Width { get; set; }
+
+ /// <summary>高度</summary>
+ [Description("高度")]
+ public Int32 Height { get; set; }
+
+ /// <summary>顶部</summary>
+ [Description("顶部")]
+ public Int32 Top { get; set; }
+
+ /// <summary>左边</summary>
+ [Description("左边")]
+ public Int32 Left { get; set; }
+
+ /// <summary>日志着色</summary>
+ [Description("日志着色")]
+ public Boolean ColorLog { get; set; } = true;
+
+ /// <summary>语音提示。默认true</summary>
+ [Description("语音提示。默认true")]
+ public Boolean SpeechTip { get; set; } = true;
+
+ /// <summary>证书</summary>
+ [Description("证书")]
+ public String? Code { get; set; }
+
+ /// <summary>密钥</summary>
+ [Description("密钥")]
+ public String? Secret { get; set; }
+
+ /// <summary>服务地址端口。默认为空,子网内自动发现</summary>
+ [Description("服务地址端口。默认为空,子网内自动发现")]
+ public String Server { get; set; } = "";
+
+ /// <summary>更新通道。默认Release</summary>
+ [Description("更新通道。默认Release")]
+ public String Channel { get; set; } = "Release";
+
+ /// <summary>更新服务器</summary>
+ [Description("更新服务器")]
+ public String UpdateServer { get; set; } = "";
+
+ /// <summary>最后更新时间</summary>
+ [DisplayName("最后更新时间")]
+ public DateTime LastUpdate { get; set; }
+
+ /// <summary>最后一个使用的工具</summary>
+ [DisplayName("最后一个使用的工具")]
+ public String LastTool { get; set; } = "";
+ #endregion
+
+ #region 加载/保存
+ protected override void OnLoaded()
+ {
+ if (UpdateServer.IsNullOrEmpty() || UpdateServer.EqualIgnoreCase("http://x.newlifex.com/"))
+ UpdateServer = NewLife.Setting.Current.PluginServer;
+
+ if (Server.IsNullOrEmpty()) Server = "http://s.newlifex.com:6600";
+
+ base.OnLoaded();
+ }
+ #endregion
+}
\ No newline at end of file
diff --git a/XCoderAv/MainWindow.axaml b/XCoderAv/MainWindow.axaml
new file mode 100644
index 0000000..07f94c3
--- /dev/null
+++ b/XCoderAv/MainWindow.axaml
@@ -0,0 +1,60 @@
+<Window xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:vm="clr-namespace:XCoderAv.ViewModels"
+ xmlns:views="clr-namespace:XCoderAv.Views"
+ mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="600"
+ x:Class="XCoderAv.MainWindow"
+ Title="新生命码神工具(跨平台)" Width="1000" Height="600"
+ WindowStartupLocation="CenterScreen">
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="250"/>
+ <ColumnDefinition Width="4"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- 左侧导航 -->
+ <Border Grid.Column="0" Background="#F5F5F5">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="80"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <!-- 标题 -->
+ <Border Grid.Row="0" Margin="20 0" BorderBrush="#E0E0E0" BorderThickness="0 0 0 1">
+ <StackPanel VerticalAlignment="Center">
+ <TextBlock Text="新生命码神工具" FontSize="18" FontWeight="Light" HorizontalAlignment="Center"/>
+ <TextBlock Text="跨平台版" FontSize="11" Foreground="#888" HorizontalAlignment="Center"/>
+ </StackPanel>
+ </Border>
+
+ <!-- 菜单列表 -->
+ <ListBox Grid.Row="1" ItemsSource="{Binding Menus}" SelectedItem="{Binding SelectedMenu}"
+ BorderThickness="0" Background="Transparent"
+ Margin="0 10 0 0">
+ <ListBox.ItemTemplate>
+ <DataTemplate>
+ <Border Padding="15 10" Margin="5 2" CornerRadius="4">
+ <StackPanel Orientation="Horizontal" Spacing="10">
+ <TextBlock Text="{Binding IconFont}" FontSize="18" Foreground="{Binding BackColor}"/>
+ <TextBlock Text="{Binding Title}" FontSize="14" VerticalAlignment="Center"/>
+ </StackPanel>
+ </Border>
+ </DataTemplate>
+ </ListBox.ItemTemplate>
+ </ListBox>
+ </Grid>
+ </Border>
+
+ <!-- 分隔线 -->
+ <GridSplitter Grid.Column="1" Width="4" Background="#E0E0E0" HorizontalAlignment="Stretch"/>
+
+ <!-- 右侧内容区 -->
+ <Border Grid.Column="2" Padding="20">
+ <ContentControl Content="{Binding CurrentView}" />
+ </Border>
+ </Grid>
+</Window>
diff --git a/XCoderAv/MainWindow.axaml.cs b/XCoderAv/MainWindow.axaml.cs
new file mode 100644
index 0000000..c717806
--- /dev/null
+++ b/XCoderAv/MainWindow.axaml.cs
@@ -0,0 +1,14 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using XCoderAv.ViewModels;
+
+namespace XCoderAv;
+
+public partial class MainWindow : Window
+{
+ public MainWindow()
+ {
+ InitializeComponent();
+ DataContext = new MainViewModel();
+ }
+}
diff --git a/XCoderAv/Models/IconHelper.cs b/XCoderAv/Models/IconHelper.cs
new file mode 100644
index 0000000..9f8a65b
--- /dev/null
+++ b/XCoderAv/Models/IconHelper.cs
@@ -0,0 +1,144 @@
+using Avalonia;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using NewLife;
+
+namespace XCoderAv.Models;
+
+/// <summary>ICO 图标文件辅助类(跨平台版,基于 Avalonia)</summary>
+public class IconHelper
+{
+ #region 静态方法
+ /// <summary>转换源图片到 ICO 文件流</summary>
+ /// <param name="bmp">源图片</param>
+ /// <param name="des">目标流</param>
+ /// <param name="sizes">尺寸数组</param>
+ /// <param name="bits">位深数组</param>
+ public static void Convert(Bitmap bmp, Stream des, Int32[] sizes, Int32[] bits)
+ {
+ var ico = new IconHelper();
+ foreach (var bit in bits)
+ {
+ foreach (var item in sizes)
+ {
+ ico.AddPng(bmp, item, bit);
+ }
+ }
+ ico.Sort();
+ ico.Save(des);
+ }
+
+ /// <summary>转换源图片到 ICO 文件</summary>
+ /// <param name="srcFile">源图片路径</param>
+ /// <param name="desFile">目标 ICO 文件路径</param>
+ /// <param name="sizes">尺寸数组</param>
+ /// <param name="bits">位深数组</param>
+ public static void Convert(String srcFile, String desFile, Int32[] sizes, Int32[] bits)
+ {
+ using var bmp = new Bitmap(srcFile);
+ using var fs = File.Create(desFile);
+ Convert(bmp, fs, sizes, bits);
+ }
+ #endregion
+
+ #region 属性
+ private readonly List<IconItem> _items = [];
+ #endregion
+
+ #region 方法
+ private void AddPng(Bitmap bmp, Int32 size, Int32 bit)
+ {
+ // 缩放图片到目标尺寸
+ var resized = ResizeBitmap(bmp, size, size);
+
+ var ms = new MemoryStream();
+ resized.Save(ms);
+ resized.Dispose();
+
+ var item = new IconItem
+ {
+ Data = ms.ToArray(),
+ Size = (UInt32)ms.Length,
+ BitCount = (UInt16)bit,
+ Width = (Byte)(size >= 256 ? 0 : size),
+ Height = (Byte)(size >= 256 ? 0 : size)
+ };
+
+ _items.Add(item);
+ ResetOffset();
+ }
+
+ /// <summary>缩放 Bitmap</summary>
+ private static Bitmap ResizeBitmap(Bitmap source, Int32 width, Int32 height)
+ {
+ // 使用 Avalonia 的 CreateScaledBitmap
+ return source.CreateScaledBitmap(new PixelSize(width, height), BitmapInterpolationMode.HighQuality);
+ }
+
+ private void Sort()
+ {
+ _items.Sort((a, b) =>
+ {
+ var cmp = (a.Width == 0 ? 256 : a.Width).CompareTo(b.Width == 0 ? 256 : b.Width);
+ if (cmp != 0) return -cmp;
+ return -a.BitCount.CompareTo(b.BitCount);
+ });
+
+ ResetOffset();
+ }
+
+ private void Save(Stream stream)
+ {
+ var writer = new BinaryWriter(stream);
+ writer.Write((UInt16)0); // Reserved
+ writer.Write((UInt16)1); // ICO = 1
+ writer.Write((UInt16)_items.Count);
+
+ foreach (var item in _items)
+ {
+ item.Save(writer);
+ }
+ foreach (var item in _items)
+ {
+ writer.Write(item.Data);
+ }
+ }
+
+ private void ResetOffset()
+ {
+ var idx = (UInt32)(6 + _items.Count * 16);
+ foreach (var item in _items)
+ {
+ item.Offset = idx;
+ idx += item.Size;
+ }
+ }
+ #endregion
+
+ #region 内部类
+ private class IconItem
+ {
+ public Byte Width { get; set; } = 16;
+ public Byte Height { get; set; } = 16;
+ public Byte ColorCount { get; set; }
+ public Byte Reserved { get; set; }
+ public UInt16 Planes { get; set; } = 1;
+ public UInt16 BitCount { get; set; } = 32;
+ public UInt32 Size { get; set; }
+ public UInt32 Offset { get; set; }
+ public Byte[] Data { get; set; } = [];
+
+ public void Save(BinaryWriter writer)
+ {
+ writer.Write(Width);
+ writer.Write(Height);
+ writer.Write(ColorCount);
+ writer.Write(Reserved);
+ writer.Write(Planes);
+ writer.Write(BitCount);
+ writer.Write(Size);
+ writer.Write(Offset);
+ }
+ }
+ #endregion
+}
diff --git a/XCoderAv/Models/MenuModel.cs b/XCoderAv/Models/MenuModel.cs
new file mode 100644
index 0000000..44f44e3
--- /dev/null
+++ b/XCoderAv/Models/MenuModel.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace XCoderAv.Models;
+
+/// <summary>菜单项模型</summary>
+public class MenuModel
+{
+ /// <summary>图标字体编码</summary>
+ public String IconFont { get; set; } = "";
+
+ /// <summary>标题</summary>
+ public String Title { get; set; } = "";
+
+ /// <summary>背景颜色</summary>
+ public String BackColor { get; set; } = "#888";
+
+ /// <summary>关联视图类型</summary>
+ public Type? ViewType { get; set; }
+}
\ No newline at end of file
diff --git a/XCoderAv/Models/RegexCaptureItem.cs b/XCoderAv/Models/RegexCaptureItem.cs
new file mode 100644
index 0000000..1bff322
--- /dev/null
+++ b/XCoderAv/Models/RegexCaptureItem.cs
@@ -0,0 +1,25 @@
+using System.Text.RegularExpressions;
+
+namespace XCoderAv.Models;
+
+/// <summary>正则捕获结果项</summary>
+public class RegexCaptureItem
+{
+ /// <summary>序号</summary>
+ public Int32 Index { get; set; }
+
+ /// <summary>值</summary>
+ public String Value { get; set; } = "";
+
+ /// <summary>位置</summary>
+ public Int32 Position { get; set; }
+
+ /// <summary>长度</summary>
+ public Int32 Length { get; set; }
+
+ /// <summary>位置描述</summary>
+ public String Location => $"({Position},{Length})";
+
+ /// <summary>原始 Capture 对象</summary>
+ public Capture? Capture { get; set; }
+}
diff --git a/XCoderAv/Models/RegexGroupItem.cs b/XCoderAv/Models/RegexGroupItem.cs
new file mode 100644
index 0000000..90f04c4
--- /dev/null
+++ b/XCoderAv/Models/RegexGroupItem.cs
@@ -0,0 +1,28 @@
+using System.Text.RegularExpressions;
+
+namespace XCoderAv.Models;
+
+/// <summary>正则分组结果项</summary>
+public class RegexGroupItem
+{
+ /// <summary>序号</summary>
+ public Int32 Index { get; set; }
+
+ /// <summary>分组名称</summary>
+ public String Name { get; set; } = "";
+
+ /// <summary>值</summary>
+ public String Value { get; set; } = "";
+
+ /// <summary>位置</summary>
+ public Int32 Position { get; set; }
+
+ /// <summary>长度</summary>
+ public Int32 Length { get; set; }
+
+ /// <summary>位置描述</summary>
+ public String Location => $"({Position},{Length})";
+
+ /// <summary>原始 Group 对象</summary>
+ public Group? Group { get; set; }
+}
diff --git a/XCoderAv/Models/RegexMatchItem.cs b/XCoderAv/Models/RegexMatchItem.cs
new file mode 100644
index 0000000..a56019d
--- /dev/null
+++ b/XCoderAv/Models/RegexMatchItem.cs
@@ -0,0 +1,28 @@
+using System.Text.RegularExpressions;
+
+namespace XCoderAv.Models;
+
+/// <summary>正则匹配结果项</summary>
+public class RegexMatchItem
+{
+ /// <summary>序号</summary>
+ public Int32 Index { get; set; }
+
+ /// <summary>匹配值</summary>
+ public String Value { get; set; } = "";
+
+ /// <summary>行号</summary>
+ public Int32 Line { get; set; }
+
+ /// <summary>位置</summary>
+ public Int32 Position { get; set; }
+
+ /// <summary>长度</summary>
+ public Int32 Length { get; set; }
+
+ /// <summary>位置描述</summary>
+ public String Location => $"({Line},{Position},{Length})";
+
+ /// <summary>原始 Match 对象</summary>
+ public Match? Match { get; set; }
+}
diff --git a/XCoderAv/Program.cs b/XCoderAv/Program.cs
index cea434d..a05ebbc 100644
--- a/XCoderAv/Program.cs
+++ b/XCoderAv/Program.cs
@@ -1,27 +1,67 @@
-using System;
+using System.Text;
using Avalonia;
-using Avalonia.Logging.Serilog;
+using NewLife;
+using NewLife.Log;
+using Stardust;
-namespace XCoderAv
+namespace XCoderAv;
+
+class Program
{
- class Program
+ public static void Main(String[] args)
{
- // Initialization code. Don't use any Avalonia, third-party APIs or any
- // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
- // yet and stuff might break.
- public static void Main(string[] args) => BuildAvaloniaApp().Start(AppMain, args);
-
- // Avalonia configuration, don't remove; also used by visual designer.
- public static AppBuilder BuildAvaloniaApp()
- => AppBuilder.Configure<App>()
- .UsePlatformDetect()
- .LogToDebug();
-
- // Your application's entry point. Here you can initialize your MVVM framework, DI
- // container, etc.
- private static void AppMain(Application app, string[] args)
+ Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+ MachineInfo.RegisterAsync();
+
+ StartClient();
+
+ var set = XConfig.Current;
+ StringHelper.EnableSpeechTip = set.SpeechTip;
+
+ if (set.IsNew)
{
- app.Run(new MainWindow());
+ try { "学无先后达者为师,欢迎使用新生命码神工具!".SpeechTip(); } catch { }
}
+
+ BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
+ }
+
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure<App>()
+ .UsePlatformDetect()
+ .WithInterFont()
+ .LogToTrace();
+
+ static StarClient? _Client;
+ private static void StartClient()
+ {
+ var set = XConfig.Current;
+ var server = set.Server;
+ if (server.IsNullOrEmpty()) return;
+
+ XTrace.WriteLine("初始化服务端地址:{0}", server);
+
+ var client = new StarClient(server)
+ {
+ Code = set.Code,
+ Secret = set.Secret,
+ Log = XTrace.Log,
+ };
+
+ client.OnLogined += (s, e) =>
+ {
+ if (client.Logined && !client.Code.IsNullOrEmpty())
+ {
+ set.Code = client.Code;
+ set.Secret = client.Secret;
+ set.Save();
+ }
+ };
+
+ client.Open();
+
+ NewLife.Model.Host.RegisterExit(() => client.Logout("ApplicationExit"));
+
+ _Client = client;
}
}
diff --git a/XCoderAv/ViewModels/FolderStatViewModel.cs b/XCoderAv/ViewModels/FolderStatViewModel.cs
new file mode 100644
index 0000000..9accb8c
--- /dev/null
+++ b/XCoderAv/ViewModels/FolderStatViewModel.cs
@@ -0,0 +1,259 @@
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using NewLife.Caching;
+
+namespace XCoderAv.ViewModels;
+
+/// <summary>文件夹大小统计 ViewModel</summary>
+public partial class FolderStatViewModel : ObservableObject
+{
+ #region 属性
+ private readonly ICache _cache = new MemoryCache();
+
+ /// <summary>目录根节点</summary>
+ public ObservableCollection<FolderItem> Roots { get; } = [];
+
+ /// <summary>日志文本</summary>
+ [ObservableProperty]
+ private String _logText = "";
+
+ /// <summary>日志回调</summary>
+ public Action<String>? OnLog { get; set; }
+ #endregion
+
+ #region 构造
+ /// <summary>实例化文件夹大小统计 ViewModel</summary>
+ public FolderStatViewModel()
+ {
+ LoadDrives();
+ }
+
+ private void LoadDrives()
+ {
+ foreach (var item in DriveInfo.GetDrives())
+ {
+ if (item.DriveType == DriveType.Fixed)
+ {
+ var root = new FolderItem
+ {
+ Name = item.Name,
+ FullPath = item.RootDirectory.FullName,
+ Size = item.TotalSize,
+ SizeText = FormatSize(item.TotalSize),
+ IsDirectory = true,
+ IsExpanded = false
+ };
+ // 添加占位子节点,展开时触发加载
+ root.Children.Add(new FolderItem { IsPlaceholder = true });
+ Roots.Add(root);
+ }
+ }
+ }
+ #endregion
+
+ #region 目录树操作
+ /// <summary>展开目录</summary>
+ public async Task ExpandFolder(FolderItem item)
+ {
+ if (!item.IsDirectory) return;
+ if (item.Children.Count == 1 && item.Children[0].IsPlaceholder)
+ {
+ await LoadChildren(item);
+ }
+ }
+
+ /// <summary>折叠目录</summary>
+ public void CollapseFolder(FolderItem item)
+ {
+ if (!item.IsDirectory) return;
+ item.Children.Clear();
+ item.Children.Add(new FolderItem { IsPlaceholder = true });
+ }
+
+ private async Task LoadChildren(FolderItem parent)
+ {
+ parent.Children.Clear();
+
+ var di = new DirectoryInfo(parent.FullPath);
+ if (!di.Exists) return;
+
+ WriteLog($"展开目录 {parent.FullPath}");
+
+ var list = new List<FileSystemInfo>();
+ try
+ {
+ list.AddRange(di.GetDirectories());
+ }
+ catch { }
+ try
+ {
+ list.AddRange(di.GetFiles());
+ }
+ catch { }
+
+ var max = 0;
+ foreach (var item in list)
+ {
+ max = Math.Max(max, StrLen(item.Name));
+ }
+ max++;
+
+ foreach (var item in list)
+ {
+ var len = max;
+ len -= (StrLen(item.Name) - item.Name.Length);
+ Int64 size;
+
+ if (item is FileInfo fi)
+ size = fi.Length;
+ else
+ size = -1;
+
+ var child = new FolderItem
+ {
+ Name = item.Name,
+ FullPath = item.FullName,
+ Size = size,
+ SizeText = FormatSize(size),
+ IsDirectory = item is DirectoryInfo,
+ DisplayName = String.Format("{0,-" + len + "} {1,10}", item.Name, FormatSize(size)),
+ BackgroundColor = GetSizeColor(size)
+ };
+
+ if (item is DirectoryInfo)
+ {
+ // 占位子节点,展开时继续加载
+ child.Children.Add(new FolderItem { IsPlaceholder = true });
+
+ // 异步统计文件夹大小
+ var captured = child;
+ _ = Task.Run(() => CalculateSize(captured));
+ }
+
+ parent.Children.Add(child);
+ }
+ }
+
+ private void CalculateSize(FolderItem node)
+ {
+ if (!node.IsDirectory) return;
+
+ var di = new DirectoryInfo(node.FullPath);
+ try
+ {
+ var size = FolderSize(di);
+ node.Size = size;
+ node.SizeText = FormatSize(size);
+ node.BackgroundColor = GetSizeColor(size);
+ node.DisplayName = $"{node.Name,-20} {FormatSize(size),10}";
+ }
+ catch (Exception ex)
+ {
+ WriteLog(ex.ToString());
+ }
+ }
+
+ private Int64 FolderSize(DirectoryInfo di)
+ {
+ if (_cache.TryGetValue<Int64>(di.FullName, out var v)) return v;
+
+ Int64 size = 0;
+ try
+ {
+ foreach (var item in di.GetFiles())
+ {
+ size += item.Length;
+ }
+ foreach (var item in di.GetDirectories())
+ {
+ size += FolderSize(item);
+ }
+ }
+ catch { }
+
+ // 缓存30秒,避免重复遍历
+ _cache.Set(di.FullName, size, 30);
+
+ return size;
+ }
+ #endregion
+
+ #region 辅助方法
+ /// <summary>格式化文件大小</summary>
+ public static String FormatSize(Int64 size)
+ {
+ if (size < 1024) return size + " Byte";
+ var ds = (Double)size / 1024;
+ if (ds < 1024) return ds.ToString("N2") + " K";
+ ds /= 1024;
+ if (ds < 1024) return ds.ToString("N2") + " M";
+ ds /= 1024;
+ if (ds < 1024) return ds.ToString("N2") + " G";
+ ds /= 1024;
+ if (ds < 1024) return ds.ToString("N2") + " T";
+ return "∞";
+ }
+
+ /// <summary>计算字符串显示宽度(中文字符算2个宽度)</summary>
+ private static Int32 StrLen(String str) => (str.Length + Encoding.UTF8.GetByteCount(str)) / 2;
+
+ /// <summary>根据大小获取背景色</summary>
+ private static String GetSizeColor(Int64 size)
+ {
+ if (size > 1024L * 1024 * 1024) return "#FFFF00";
+ if (size > 100L * 1024 * 1024) return "#7CFC00";
+ if (size > 1024L * 1024) return "#ADD8E6";
+ if (size > 1024L) return "#FFE4E1";
+ return "#FFFFFF";
+ }
+
+ /// <summary>写入日志</summary>
+ public void WriteLog(String msg)
+ {
+ OnLog?.Invoke(msg);
+ }
+ #endregion
+}
+
+/// <summary>文件夹项</summary>
+public partial class FolderItem : ObservableObject
+{
+ /// <summary>显示名称</summary>
+ [ObservableProperty]
+ private String _displayName = "";
+
+ /// <summary>名称</summary>
+ public String Name { get; set; } = "";
+
+ /// <summary>完整路径</summary>
+ public String FullPath { get; set; } = "";
+
+ /// <summary>大小</summary>
+ [ObservableProperty]
+ private Int64 _size;
+
+ /// <summary>大小文本</summary>
+ [ObservableProperty]
+ private String _sizeText = "";
+
+ /// <summary>是否为目录</summary>
+ public Boolean IsDirectory { get; set; }
+
+ /// <summary>是否已展开</summary>
+ [ObservableProperty]
+ private Boolean _isExpanded;
+
+ /// <summary>是否占位节点</summary>
+ public Boolean IsPlaceholder { get; set; }
+
+ /// <summary>背景颜色</summary>
+ [ObservableProperty]
+ private String _backgroundColor = "#FFFFFF";
+
+ /// <summary>子节点</summary>
+ public ObservableCollection<FolderItem> Children { get; } = [];
+}
diff --git a/XCoderAv/ViewModels/GpsViewModel.cs b/XCoderAv/ViewModels/GpsViewModel.cs
new file mode 100644
index 0000000..3e3025c
--- /dev/null
+++ b/XCoderAv/ViewModels/GpsViewModel.cs
@@ -0,0 +1,87 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using NewLife;
+
+namespace XCoderAv.ViewModels;
+
+/// <summary>GPS 辅助工具 ViewModel</summary>
+public partial class GpsViewModel : ObservableObject
+{
+ #region 属性
+ /// <summary>HEX 纬度(8字符)</summary>
+ [ObservableProperty]
+ private String _hexLat = "";
+
+ /// <summary>HEX 经度(8字符)</summary>
+ [ObservableProperty]
+ private String _hexLong = "";
+
+ /// <summary>HEX 合并输入(16字符)</summary>
+ [ObservableProperty]
+ private String _hexCombined = "";
+
+ /// <summary>转换后纬度</summary>
+ [ObservableProperty]
+ private String _latitude = "";
+
+ /// <summary>转换后经度</summary>
+ [ObservableProperty]
+ private String _longitude = "";
+
+ /// <summary>合并坐标</summary>
+ [ObservableProperty]
+ private String _latLong = "";
+ #endregion
+
+ #region 方法
+ /// <summary>分离转换</summary>
+ [RelayCommand]
+ private void Convert()
+ {
+ var lat = HexLat?.Trim();
+ var lng = HexLong?.Trim();
+
+ if (lat.IsNullOrEmpty() || lng.IsNullOrEmpty()) return;
+
+ var vLat = BitConverter.ToSingle(lat.ToHex(), 0);
+ var vLng = BitConverter.ToSingle(lng.ToHex(), 0);
+
+ Latitude = vLat.ToString();
+ Longitude = vLng.ToString();
+ LatLong = $"{vLat},{vLng}";
+ }
+
+ /// <summary>合并转换</summary>
+ [RelayCommand]
+ private void ConvertCombined()
+ {
+ var temp = HexCombined?.Trim();
+ if (temp.IsNullOrEmpty() || temp.Length < 16) return;
+
+ var lat = temp[..8];
+ var lng = temp[8..];
+
+ HexLat = lat;
+ HexLong = lng;
+
+ var vLat = BitConverter.ToSingle(lat.ToHex(), 0);
+ var vLng = BitConverter.ToSingle(lng.ToHex(), 0);
+
+ Latitude = vLat.ToString();
+ Longitude = vLng.ToString();
+ LatLong = $"{vLat},{vLng}";
+ }
+
+ /// <summary>清空</summary>
+ [RelayCommand]
+ private void Clear()
+ {
+ HexLat = "";
+ HexLong = "";
+ HexCombined = "";
+ Latitude = "";
+ Longitude = "";
+ LatLong = "";
+ }
+ #endregion
+}
diff --git a/XCoderAv/ViewModels/IconToolViewModel.cs b/XCoderAv/ViewModels/IconToolViewModel.cs
new file mode 100644
index 0000000..06b4afd
--- /dev/null
+++ b/XCoderAv/ViewModels/IconToolViewModel.cs
@@ -0,0 +1,279 @@
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using NewLife;
+using XCoderAv.Models;
+
+namespace XCoderAv.ViewModels;
+
+/// <summary>图标水印处理工具 ViewModel(跨平台版,基于 Avalonia + Skia)</summary>
+public partial class IconToolViewModel : ObservableObject
+{
+ #region 属性
+ /// <summary>源图片路径</summary>
+ [ObservableProperty]
+ private String _sourceFile = "";
+
+ /// <summary>源图片</summary>
+ [ObservableProperty]
+ private Bitmap? _sourceImage;
+
+ /// <summary>预览图片</summary>
+ [ObservableProperty]
+ private Bitmap? _previewImage;
+
+ /// <summary>水印文字</summary>
+ [ObservableProperty]
+ private String _watermarkText = "";
+
+ /// <summary>字体名称</summary>
+ [ObservableProperty]
+ private String _fontName = "Arial";
+
+ /// <summary>字体大小</summary>
+ [ObservableProperty]
+ private Int32 _fontSize = 96;
+
+ /// <summary>水印 X 位置</summary>
+ [ObservableProperty]
+ private Int32 _watermarkX;
+
+ /// <summary>水印 Y 位置</summary>
+ [ObservableProperty]
+ private Int32 _watermarkY;
+
+ /// <summary>水印颜色</summary>
+ [ObservableProperty]
+ private String _watermarkColor = "#FF0000";
+
+ /// <summary>状态文本</summary>
+ [ObservableProperty]
+ private String _status = "就绪";
+
+ /// <summary>可用字体列表</summary>
+ public ObservableCollection<String> FontNames { get; } = [];
+
+ /// <summary>ICO 大小选项</summary>
+ public ObservableCollection<IconSizeOption> IconSizes { get; } =
+ [
+ new() { Name = "16x16", Size = 16 },
+ new() { Name = "24x24", Size = 24 },
+ new() { Name = "32x32", Size = 32, IsSelected = true },
+ new() { Name = "48x48", Size = 48, IsSelected = true },
+ new() { Name = "64x64", Size = 64 },
+ new() { Name = "96x96", Size = 96 },
+ new() { Name = "128x128", Size = 128 },
+ new() { Name = "256x256", Size = 256 },
+ ];
+
+ private Bitmap? _originalBitmap;
+ #endregion
+
+ #region 构造
+ /// <summary>实例化图标水印处理工具 ViewModel</summary>
+ public IconToolViewModel()
+ {
+ // 加载系统字体
+ foreach (var ft in FontManager.Current.SystemFonts)
+ {
+ FontNames.Add(ft.Name);
+ }
+ }
+ #endregion
+
+ #region 命令
+ /// <summary>加载图片</summary>
+ [RelayCommand]
+ private async Task LoadImage()
+ {
+ var window = GetParentWindow();
+ if (window == null) return;
+
+ var dialog = new OpenFileDialog
+ {
+ Filters =
+ [
+ new FileDialogFilter { Name = "图片文件", Extensions = ["png", "jpg", "jpeg", "bmp", "gif", "ico"] },
+ new FileDialogFilter { Name = "所有文件", Extensions = ["*"] }
+ ],
+ Title = "选择图片",
+ AllowMultiple = false
+ };
+
+ var result = await dialog.ShowAsync(window);
+ if (result == null || result.Length == 0) return;
+
+ LoadFromFile(result[0]);
+ }
+
+ /// <summary>从文件加载图片</summary>
+ public void LoadFromFile(String fileName)
+ {
+ SourceFile = fileName;
+
+ _originalBitmap?.Dispose();
+ _originalBitmap = new Bitmap(fileName);
+
+ SourceImage = _originalBitmap;
+
+ // 自动设置水印位置到右下角
+ WatermarkX = _originalBitmap.PixelSize.Width - 150;
+ WatermarkY = _originalBitmap.PixelSize.Height - 50;
+
+ MakeWater();
+ }
+
+ /// <summary>应用水印</summary>
+ [RelayCommand]
+ private void MakeWater()
+ {
+ if (_originalBitmap == null) return;
+
+ var bmp = MakeWatermarkImage();
+ PreviewImage = bmp;
+ }
+
+ /// <summary>保存图片</summary>
+ [RelayCommand]
+ private async Task SaveImage()
+ {
+ if (_originalBitmap == null) return;
+
+ var window = GetParentWindow();
+ if (window == null) return;
+
+ var dialog = new SaveFileDialog
+ {
+ Filters =
+ [
+ new FileDialogFilter { Name = "PNG 图片", Extensions = ["png"] },
+ new FileDialogFilter { Name = "JPEG 图片", Extensions = ["jpg"] },
+ new FileDialogFilter { Name = "BMP 图片", Extensions = ["bmp"] }
+ ],
+ Title = "保存图片",
+ DefaultExtension = "png"
+ };
+
+ var result = await dialog.ShowAsync(window);
+ if (result == null) return;
+
+ var bmp = MakeWatermarkImage();
+ bmp.Save(result);
+ bmp.Dispose();
+
+ Status = $"已保存到 {result}";
+ }
+
+ /// <summary>生成 ICO 图标</summary>
+ [RelayCommand]
+ private async Task MakeIco()
+ {
+ if (_originalBitmap == null)
+ {
+ Status = "请先加载图片!";
+ return;
+ }
+
+ var sizes = IconSizes.Where(e => e.IsSelected).Select(e => e.Size).ToArray();
+ if (sizes.Length == 0)
+ {
+ Status = "请至少选择一个图标尺寸!";
+ return;
+ }
+
+ var window = GetParentWindow();
+ if (window == null) return;
+
+ var dialog = new SaveFileDialog
+ {
+ Filters =
+ [
+ new FileDialogFilter { Name = "ICO 图标", Extensions = ["ico"] }
+ ],
+ Title = "保存图标",
+ DefaultExtension = "ico"
+ };
+
+ var result = await dialog.ShowAsync(window);
+ if (result == null) return;
+
+ var bmp = MakeWatermarkImage();
+ var ms = new MemoryStream();
+ IconHelper.Convert(bmp, ms, sizes, [32]);
+ bmp.Dispose();
+
+ await File.WriteAllBytesAsync(result, ms.ToArray());
+
+ Status = $"已保存 ICO 到 {result}";
+ }
+ #endregion
+
+ #region 辅助
+ private Bitmap MakeWatermarkImage()
+ {
+ if (_originalBitmap == null) throw new InvalidOperationException("没有加载图片");
+
+ var size = _originalBitmap.PixelSize;
+ var dpi = _originalBitmap.Dpi;
+
+ // 使用渲染目标位图来绘制
+ var rtBmp = new RenderTargetBitmap(size, dpi);
+ using (var ctx = rtBmp.CreateDrawingContext())
+ {
+ ctx.DrawImage(_originalBitmap, new Rect(0, 0, size.Width, size.Height));
+
+ if (!WatermarkText.IsNullOrEmpty())
+ {
+ var color = Color.Parse(WatermarkColor);
+ var typeface = new Typeface(FontName);
+ var formattedText = new FormattedText(
+ WatermarkText,
+ CultureInfo.CurrentCulture,
+ FlowDirection.LeftToRight,
+ typeface,
+ FontSize,
+ new SolidColorBrush(color));
+ formattedText.TextAlignment = TextAlignment.Left;
+ formattedText.MaxTextWidth = size.Width;
+
+ ctx.DrawText(formattedText, new Point(WatermarkX, WatermarkY));
+ }
+ }
+
+ return rtBmp;
+ }
+
+ private static Window? GetParentWindow()
+ {
+ if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ return desktop.Windows?.OfType<Window>().FirstOrDefault(w => w.IsActive);
+ }
+ return null;
+ }
+ #endregion
+}
+
+/// <summary>ICO 尺寸选项</summary>
+public partial class IconSizeOption : ObservableObject
+{
+ /// <summary>显示名称</summary>
+ public String Name { get; set; } = "";
+
+ /// <summary>尺寸</summary>
+ public Int32 Size { get; set; }
+
+ /// <summary>是否选中</summary>
+ [ObservableProperty]
+ private Boolean _isSelected;
+}
diff --git a/XCoderAv/ViewModels/MainViewModel.cs b/XCoderAv/ViewModels/MainViewModel.cs
new file mode 100644
index 0000000..402d0ef
--- /dev/null
+++ b/XCoderAv/ViewModels/MainViewModel.cs
@@ -0,0 +1,76 @@
+using System.Collections.ObjectModel;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using XCoderAv.Models;
+using XCoderAv.Views;
+
+namespace XCoderAv.ViewModels;
+
+/// <summary>主视图模型</summary>
+public partial class MainViewModel : ObservableObject
+{
+ public MainViewModel()
+ {
+ Menus =
+ [
+ new MenuModel() { IconFont = "📊", Title = "数据建模", BackColor = "#218868" },
+ new MenuModel() { IconFont = "🌐", Title = "网络工具", BackColor = "#EE3B3B", ViewType = typeof(NetworkWindow) },
+ new MenuModel() { IconFont = "🔗", Title = "RPC工具", BackColor = "#218868" },
+ new MenuModel() { IconFont = "🔌", Title = "串口工具", BackColor = "#EE3B3B" },
+ new MenuModel() { IconFont = "🗺️", Title = "地图接口", BackColor = "#218868" },
+ new MenuModel() { IconFont = "🔍", Title = "正则表达式", BackColor = "#218868", ViewType = typeof(RegexWindow) },
+ new MenuModel() { IconFont = "🎨", Title = "图标水印", BackColor = "#EE3B3B", ViewType = typeof(IconToolWindow) },
+ new MenuModel() { IconFont = "🔒", Title = "加密解密", BackColor = "#218868", ViewType = typeof(SecurityWindow) },
+ new MenuModel() { IconFont = "🗣️", Title = "语音助手", BackColor = "#EE3B3B", ViewType = typeof(SpeechWindow) },
+ new MenuModel() { IconFont = "📁", Title = "文件夹统计", BackColor = "#218868", ViewType = typeof(FolderStatWindow) },
+ new MenuModel() { IconFont = "📄", Title = "文件编码", BackColor = "#218868" },
+ new MenuModel() { IconFont = "📍", Title = "GPS辅助", BackColor = "#218868", ViewType = typeof(GpsWindow) },
+ new MenuModel() { IconFont = "📡", Title = "MQTT客户端", BackColor = "#EE3B3B", ViewType = typeof(MqttWindow) },
+ ];
+
+ SelectedMenu = Menus[0];
+ }
+
+ /// <summary>菜单集合</summary>
+ public ObservableCollection<MenuModel> Menus { get; }
+
+ [ObservableProperty]
+ private MenuModel _selectedMenu = null!;
+
+ partial void OnSelectedMenuChanged(MenuModel value)
+ {
+ // 点击菜单项时打开对应的工具窗口
+ if (value.ViewType == null) return;
+
+ // 检查窗口是否已打开
+ var existing = GetOpenedWindow(value.ViewType);
+ if (existing != null)
+ {
+ existing.Activate();
+ return;
+ }
+
+ // 创建新窗口
+ if (Activator.CreateInstance(value.ViewType) is Window window)
+ {
+ window.Show();
+ }
+ }
+
+ /// <summary>获取已打开的窗口</summary>
+ private static Window? GetOpenedWindow(Type viewType)
+ {
+ if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ foreach (var w in desktop.Windows)
+ {
+ if (w.GetType() == viewType)
+ return w;
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/XCoderAv/ViewModels/MqttViewModel.cs b/XCoderAv/ViewModels/MqttViewModel.cs
new file mode 100644
index 0000000..5342d03
--- /dev/null
+++ b/XCoderAv/ViewModels/MqttViewModel.cs
@@ -0,0 +1,274 @@
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using NewLife;
+using NewLife.Data;
+using NewLife.Log;
+using NewLife.MQTT;
+using NewLife.MQTT.Messaging;
+using NewLife.Net;
+
+namespace XCoderAv.ViewModels;
+
+/// <summary>MQTT 客户端 ViewModel</summary>
+public partial class MqttViewModel : ObservableObject, IDisposable
+{
+ #region 属性
+ private MqttClient? _client;
+ private readonly ILog _log;
+
+ /// <summary>服务器地址</summary>
+ [ObservableProperty]
+ private String _server = "127.0.0.1:1883";
+
+ /// <summary>客户端标识</summary>
+ [ObservableProperty]
+ private String _clientId = "";
+
+ /// <summary>用户名</summary>
+ [ObservableProperty]
+ private String _userName = "";
+
+ /// <summary>密码</summary>
+ [ObservableProperty]
+ private String _password = "";
+
+ /// <summary>是否已连接</summary>
+ [ObservableProperty]
+ private Boolean _isConnected;
+
+ /// <summary>连接按钮文本</summary>
+ [ObservableProperty]
+ private String _connectButtonText = "连接";
+
+ /// <summary>订阅主题</summary>
+ [ObservableProperty]
+ private String _subscribeTopic = "";
+
+ /// <summary>订阅 QoS</summary>
+ [ObservableProperty]
+ private Int32 _subscribeQos;
+
+ /// <summary>发布主题</summary>
+ [ObservableProperty]
+ private String _publishTopic = "";
+
+ /// <summary>发布内容</summary>
+ [ObservableProperty]
+ private String _publishText = "";
+
+ /// <summary>发布 QoS</summary>
+ [ObservableProperty]
+ private Int32 _publishQos;
+
+ /// <summary>保留消息</summary>
+ [ObservableProperty]
+ private Boolean _retain;
+
+ /// <summary>QoS 选项</summary>
+ public String[] QosOptions { get; } = ["0", "1", "2"];
+
+ /// <summary>日志文本</summary>
+ [ObservableProperty]
+ private String _logText = "";
+
+ /// <summary>日志回调</summary>
+ public Action<String>? OnLog { get; set; }
+ #endregion
+
+ #region 构造
+ /// <summary>实例化 MQTT 客户端 ViewModel</summary>
+ public MqttViewModel()
+ {
+ ClientId = Environment.MachineName;
+ SubscribeTopic = "topic/test";
+ PublishTopic = "topic/test";
+ PublishText = "Hello MQTT";
+
+ _log = new MqttLog(this);
+ }
+ #endregion
+
+ #region 连接/断开
+ /// <summary>切换连接/断开</summary>
+ [RelayCommand]
+ private async Task ToggleConnect()
+ {
+ if (IsConnected)
+ await Disconnect();
+ else
+ await Connect();
+ }
+
+ private async Task Connect()
+ {
+ _client = null;
+
+ var remote = Server;
+ var uri = new NetUri(remote);
+ if (uri.Type == NetType.Unknown) uri.Type = NetType.Tcp;
+ if (uri.Port == 0) uri.Port = 1883;
+
+ var client = new MqttClient
+ {
+ Server = $"{uri}",
+ ClientId = ClientId,
+ UserName = UserName,
+ Password = Password,
+ Log = _log,
+ };
+ client.Received += OnReceived;
+ client.Connected += (s, e) => WriteLog("连接成功");
+ client.Disconnected += (s, e) => WriteLog("连接断开");
+
+ try
+ {
+ await client.ConnectAsync();
+ _client = client;
+
+ IsConnected = true;
+ ConnectButtonText = "断开";
+
+ WriteLog("已连接服务器");
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"连接失败:{ex.Message}");
+ client.Dispose();
+ }
+ }
+
+ private async Task Disconnect()
+ {
+ if (_client != null)
+ {
+ _client.Reconnect = false;
+ try
+ {
+ await _client.DisconnectAsync();
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"断开异常:{ex.Message}");
+ }
+ _client.Dispose();
+ _client = null;
+ }
+
+ IsConnected = false;
+ ConnectButtonText = "连接";
+ WriteLog("已断开连接");
+ }
+
+ private void OnReceived(Object? sender, EventArgs<PublishMessage> e)
+ {
+ var msg = e.Arg;
+ var payload = msg.Payload?.ToStr() ?? "";
+ WriteLog($"[{msg.Topic}] {payload}");
+ }
+ #endregion
+
+ #region 订阅/发布
+ /// <summary>订阅主题</summary>
+ [RelayCommand]
+ private async Task Subscribe()
+ {
+ if (_client == null) return;
+
+ var topic = SubscribeTopic;
+ if (topic.IsNullOrEmpty())
+ {
+ WriteLog("请输入订阅主题!");
+ return;
+ }
+
+ var qos = (QualityOfService)SubscribeQos;
+
+ try
+ {
+ var rs = await _client.SubscribeAsync([topic], qos);
+ WriteLog($"订阅成功,Id={rs?.Id}");
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"订阅失败:{ex.Message}");
+ }
+ }
+
+ /// <summary>发布消息</summary>
+ [RelayCommand]
+ private async Task Publish()
+ {
+ if (_client == null) return;
+
+ var topic = PublishTopic;
+ var str = PublishText;
+
+ if (topic.IsNullOrEmpty())
+ {
+ WriteLog("请输入发布主题!");
+ return;
+ }
+ if (str.IsNullOrEmpty())
+ {
+ WriteLog("请输入发布内容!");
+ return;
+ }
+
+ var qos = (QualityOfService)PublishQos;
+
+ try
+ {
+ WriteLog(str);
+ var rs = await _client.PublishAsync(topic, str, qos);
+ if (rs != null)
+ WriteLog($"发布成功,Id={rs.Id}");
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"发布失败:{ex.Message}");
+ }
+ }
+
+ /// <summary>清空日志</summary>
+ [RelayCommand]
+ private void ClearLog()
+ {
+ LogText = "";
+ OnLog?.Invoke("__CLEAR__");
+ }
+ #endregion
+
+ #region 日志
+ private void WriteLog(String msg)
+ {
+ OnLog?.Invoke(msg);
+ }
+
+ private class MqttLog : Logger
+ {
+ private readonly MqttViewModel _vm;
+ public MqttLog(MqttViewModel vm) => _vm = vm;
+ protected override void OnWrite(LogLevel level, String format, params Object?[] args)
+ {
+ var msg = args is { Length: > 0 } ? String.Format(format, args) : format;
+ _vm.WriteLog(msg);
+ }
+ }
+ #endregion
+
+ #region IDisposable
+ /// <summary>释放资源</summary>
+ public void Dispose()
+ {
+ if (_client != null)
+ {
+ _client.Reconnect = false;
+ try { _client.DisconnectAsync().GetAwaiter().GetResult(); } catch { }
+ _client.Dispose();
+ _client = null;
+ }
+ GC.SuppressFinalize(this);
+ }
+ #endregion
+}
diff --git a/XCoderAv/ViewModels/NetworkViewModel.cs b/XCoderAv/ViewModels/NetworkViewModel.cs
new file mode 100644
index 0000000..f58f91c
--- /dev/null
+++ b/XCoderAv/ViewModels/NetworkViewModel.cs
@@ -0,0 +1,409 @@
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Threading;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using NewLife;
+using NewLife.Data;
+using NewLife.Log;
+using NewLife.Net;
+using NewLife.Threading;
+
+namespace XCoderAv.ViewModels;
+
+/// <summary>网络调试工具工作模式</summary>
+public enum NetworkWorkMode
+{
+ /// <summary>TCP/UDP混合</summary>
+ UdpTcp = 1,
+
+ /// <summary>UDP服务端</summary>
+ UdpServer,
+
+ /// <summary>UDP客户端</summary>
+ UdpClient,
+
+ /// <summary>TCP服务端</summary>
+ TcpServer,
+
+ /// <summary>TCP客户端</summary>
+ TcpClient
+}
+
+/// <summary>网络调试工具ViewModel</summary>
+public partial class NetworkViewModel : ObservableObject
+{
+ #region 属性
+ private NetServer _server;
+ private ISocketClient _client;
+ private TimerX _timer;
+
+ /// <summary>工作模式列表</summary>
+ public ObservableCollection<String> Modes { get; } =
+ [
+ "TCP/UDP混合",
+ "UDP服务端",
+ "UDP客户端",
+ "TCP服务端",
+ "TCP客户端"
+ ];
+
+ /// <summary>本地地址列表</summary>
+ public ObservableCollection<String> LocalAddresses { get; } = [];
+
+ [ObservableProperty]
+ private Int32 _selectedModeIndex;
+
+ [ObservableProperty]
+ private String _localAddress = "";
+
+ [ObservableProperty]
+ private String _remoteAddress = "";
+
+ [ObservableProperty]
+ private Int32 _port = 8080;
+
+ [ObservableProperty]
+ private Boolean _isConnected;
+
+ [ObservableProperty]
+ private String _connectButtonText = "打开";
+
+ [ObservableProperty]
+ private String _sendText = "新生命开发团队,学无先后达者为师";
+
+ [ObservableProperty]
+ private Boolean _hexSend;
+
+ [ObservableProperty]
+ private Int32 _sendTimes = 1;
+
+ [ObservableProperty]
+ private Int32 _sendSleep = 1000;
+
+ [ObservableProperty]
+ private Int32 _sendThreads = 1;
+
+ [ObservableProperty]
+ private Boolean _showLog = true;
+
+ [ObservableProperty]
+ private Boolean _showSocketLog = true;
+
+ [ObservableProperty]
+ private Boolean _showSend = true;
+
+ [ObservableProperty]
+ private Boolean _showReceive = true;
+
+ [ObservableProperty]
+ private Boolean _showStat = true;
+
+ [ObservableProperty]
+ private Boolean _showReceiveString = true;
+
+ [ObservableProperty]
+ private Int64 _receivedBytes;
+
+ [ObservableProperty]
+ private Int64 _sentBytes;
+
+ [ObservableProperty]
+ private Int32 _sessionCount;
+
+ /// <summary>日志回调,由View绑定到TextBox</summary>
+ public Action<String> OnLog { get; set; }
+ #endregion
+
+ #region 构造
+ /// <summary>实例化网络调试工具ViewModel</summary>
+ public NetworkViewModel()
+ {
+ LoadLocalAddresses();
+ }
+
+ private void LoadLocalAddresses()
+ {
+ LocalAddresses.Clear();
+ LocalAddresses.Add("所有本地IPv4/IPv6");
+ LocalAddresses.Add(IPAddress.Any.ToString());
+ LocalAddresses.Add(IPAddress.IPv6Any.ToString());
+
+ try
+ {
+ var host = System.Net.Dns.GetHostEntry("");
+ foreach (var addr in host.AddressList)
+ {
+ LocalAddresses.Add(addr.ToString());
+ }
+ }
+ catch
+ {
+ }
+ }
+ #endregion
+
+ #region 连接/断开
+ [RelayCommand]
+ private void ToggleConnect()
+ {
+ if (IsConnected)
+ {
+ Disconnect();
+ }
+ else
+ {
+ Connect();
+ }
+ }
+
+ private void Connect()
+ {
+ _server = null;
+ _client = null;
+
+ var mode = (NetworkWorkMode)(SelectedModeIndex + 1);
+ var local = LocalAddress;
+ var remote = RemoteAddress;
+ var port = Port;
+
+ switch (mode)
+ {
+ case NetworkWorkMode.UdpTcp:
+ _server = new NetServer();
+ break;
+ case NetworkWorkMode.UdpServer:
+ _server = new NetServer
+ {
+ ProtocolType = NetType.Udp
+ };
+ break;
+ case NetworkWorkMode.TcpServer:
+ _server = new NetServer
+ {
+ ProtocolType = NetType.Tcp
+ };
+ break;
+ case NetworkWorkMode.TcpClient:
+ _client = new TcpSession();
+ break;
+ case NetworkWorkMode.UdpClient:
+ _client = new UdpServer();
+ break;
+ }
+
+ if (_client != null)
+ {
+ _client.Log = ShowLog ? CreateBizLog() : Logger.Null;
+ if (!local.IsNullOrEmpty() && !local.StartsWith("所有本地"))
+ _client.Local.Host = local;
+ _client.Received += OnReceived;
+ _client.Remote.Port = port;
+ _client.Remote.Host = remote;
+
+ _client.LogSend = ShowSend;
+ _client.LogReceive = ShowReceive;
+
+ if (!_client.Open())
+ {
+ WriteLog("连接失败");
+ return;
+ }
+
+ WriteLog("已连接服务器");
+ }
+ else if (_server != null)
+ {
+ _server.Log = ShowLog ? CreateBizLog() : Logger.Null;
+ _server.SocketLog = ShowSocketLog ? CreateBizLog() : Logger.Null;
+ _server.Port = port;
+ if (!local.IsNullOrEmpty() && !local.StartsWith("所有本地"))
+ _server.Local.Host = local;
+ _server.Received += OnReceived;
+
+ _server.LogSend = ShowSend;
+ _server.LogReceive = ShowReceive;
+
+ _server.Start();
+
+ WriteLog($"正在监听 {port}");
+ }
+
+ IsConnected = true;
+ ConnectButtonText = "关闭";
+
+ _timer = new TimerX(OnShowStat, null, 5000, 5000) { Async = true };
+ }
+
+ private void Disconnect()
+ {
+ if (_client != null)
+ {
+ _client.Dispose();
+ _client = null;
+ WriteLog("关闭连接");
+ }
+
+ if (_server != null)
+ {
+ WriteLog($"停止监听 {_server.Port}");
+ _server.Dispose();
+ _server = null;
+ }
+
+ _timer?.Dispose();
+ _timer = null;
+
+ IsConnected = false;
+ ConnectButtonText = "打开";
+ }
+ #endregion
+
+ #region 收发数据
+ private String _lastStat = "";
+
+ private void OnShowStat(Object state)
+ {
+ if (!ShowStat) return;
+
+ var msg = "";
+ if (_server != null)
+ msg = _server.GetStat();
+
+ if (!msg.IsNullOrEmpty() && msg != _lastStat)
+ {
+ _lastStat = msg;
+ WriteLog(msg);
+ }
+ }
+
+ private void OnReceived(Object sender, ReceivedEventArgs e)
+ {
+ var session = sender as ISocketSession;
+ if (session == null)
+ {
+ if (sender is INetSession ns)
+ session = ns.Session;
+ }
+
+ if (session == null) return;
+
+ if (ShowReceiveString)
+ {
+ var line = e.Packet.ToStr();
+ WriteLog(line);
+ }
+
+ ReceivedBytes += e.Packet.Total;
+ SessionCount = _server?.SessionCount ?? (_client != null ? 1 : 0);
+ }
+
+ [RelayCommand]
+ private async Task Send()
+ {
+ var str = SendText;
+ if (String.IsNullOrEmpty(str))
+ {
+ WriteLog("发送内容不能为空!");
+ return;
+ }
+
+ var count = SendTimes;
+ var sleep = SendSleep;
+ var ths = SendThreads;
+ if (count <= 0) count = 1;
+ if (sleep <= 0) sleep = 1;
+
+ // 处理换行
+ str = str.Replace("\n", "\r\n");
+ var buf = HexSend ? str.ToHex() : str.GetBytes();
+ var pk = new ArrayPacket(buf);
+
+ if (_client != null)
+ {
+ if (ths <= 1)
+ {
+ await SendHelper.SendConcurrency(_client, pk, count, sleep);
+ }
+ else
+ {
+ var any = _client.Local.Address.IsAny();
+ var list = new List<ISocketClient>();
+ for (var i = 0; i < ths; i++)
+ {
+ var client = _client.Remote.CreateRemote();
+ if (!any) client.Local.EndPoint = new IPEndPoint(_client.Local.Address, 2000 + i);
+ list.Add(client);
+ }
+
+ var tasks = list.Select(c => SendHelper.SendConcurrency(c, pk, count, sleep));
+ await Task.WhenAll(tasks);
+
+ foreach (var item in list)
+ {
+ item.TryDispose();
+ }
+ }
+
+ SentBytes += buf.Length * count;
+ }
+ else if (_server != null)
+ {
+ WriteLog($"准备向[{_server.SessionCount}]个客户端发送[{count}]次[{buf.Length}]的数据");
+ for (var i = 0; i < count && _server != null; i++)
+ {
+ var sw = System.Diagnostics.Stopwatch.StartNew();
+ var cs = await _server.SendAllAsync(pk);
+ sw.Stop();
+ WriteLog($"{i + 1}/{count} 已向[{cs}]个客户端发送[{buf.Length}]数据 {sw.ElapsedMilliseconds}ms");
+ if (sleep > 0) await Task.Delay(sleep);
+ }
+
+ SentBytes += buf.Length * count * (_server?.SessionCount ?? 0);
+ }
+ }
+
+ [RelayCommand]
+ private void ClearReceiveLog()
+ {
+ WriteLog("__CLEAR__");
+ ReceivedBytes = 0;
+ }
+ #endregion
+
+ #region 日志
+ private ILog CreateBizLog()
+ {
+ return TextFileLog.Create(null, "Net_{0:yyyy_MM_dd}.log");
+ }
+
+ /// <summary>写入日志到UI</summary>
+ /// <param name="msg">日志消息</param>
+ public void WriteLog(String msg)
+ {
+ OnLog?.Invoke(msg);
+ }
+ #endregion
+}
+
+/// <summary>异步多次发送数据</summary>
+internal static class SendHelper
+{
+ public static async Task SendConcurrency(ISocketRemote session, IPacket pk, Int32 times, Int32 msInterval)
+ {
+ await Task.Run(async () =>
+ {
+ for (var i = 0; i < times; i++)
+ {
+ session.Send(pk);
+
+ if (msInterval > 0)
+ await Task.Delay(msInterval);
+ }
+ });
+ }
+}
diff --git a/XCoderAv/ViewModels/RegexViewModel.cs b/XCoderAv/ViewModels/RegexViewModel.cs
new file mode 100644
index 0000000..312ecdf
--- /dev/null
+++ b/XCoderAv/ViewModels/RegexViewModel.cs
@@ -0,0 +1,440 @@
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Avalonia.Threading;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using XCoderAv.Models;
+
+namespace XCoderAv.ViewModels;
+
+/// <summary>正则表达式工具 ViewModel</summary>
+public partial class RegexViewModel : ObservableObject
+{
+ #region 属性
+
+ /// <summary>正则表达式</summary>
+ [ObservableProperty]
+ private String _pattern = "";
+
+ /// <summary>源文本</summary>
+ [ObservableProperty]
+ private String _sourceText = "";
+
+ /// <summary>替换文本</summary>
+ [ObservableProperty]
+ private String _replacement = "";
+
+ /// <summary>忽略大小写</summary>
+ [ObservableProperty]
+ private Boolean _ignoreCase;
+
+ /// <summary>多行模式</summary>
+ [ObservableProperty]
+ private Boolean _multiline;
+
+ /// <summary>单行模式</summary>
+ [ObservableProperty]
+ private Boolean _singleline;
+
+ /// <summary>忽略空白</summary>
+ [ObservableProperty]
+ private Boolean _ignorePatternWhitespace;
+
+ /// <summary>匹配模式(false 为替换模式)</summary>
+ [ObservableProperty]
+ private Boolean _isMatchMode = true;
+
+ /// <summary>当前选项文本</summary>
+ [ObservableProperty]
+ private String _optionText = "RegexOptions.None";
+
+ /// <summary>状态文本</summary>
+ [ObservableProperty]
+ private String _status = "就绪";
+
+ /// <summary>目录路径</summary>
+ [ObservableProperty]
+ private String _directoryPath = "";
+
+ /// <summary>文件过滤</summary>
+ [ObservableProperty]
+ private String _fileFilter = "*.*";
+
+ /// <summary>选中的匹配项</summary>
+ [ObservableProperty]
+ private RegexMatchItem? _selectedMatch;
+
+ /// <summary>选中的分组</summary>
+ [ObservableProperty]
+ private RegexGroupItem? _selectedGroup;
+
+ /// <summary>选中的捕获</summary>
+ [ObservableProperty]
+ private RegexCaptureItem? _selectedCapture;
+
+ /// <summary>匹配结果集合</summary>
+ public ObservableCollection<RegexMatchItem> Matches { get; } = [];
+
+ /// <summary>分组结果集合</summary>
+ public ObservableCollection<RegexGroupItem> Groups { get; } = [];
+
+ /// <summary>捕获结果集合</summary>
+ public ObservableCollection<RegexCaptureItem> Captures { get; } = [];
+
+ /// <summary>执行按钮文本</summary>
+ public String ExecuteButtonText => IsMatchMode ? "正则匹配" : "正则替换";
+
+ /// <summary>替换模式</summary>
+ public Boolean IsReplaceMode => !IsMatchMode;
+
+ #endregion
+
+ #region 构造
+
+ /// <summary>当前正则对象(用于分组名称查询)</summary>
+ private Regex? _regex;
+
+ #endregion
+
+ #region 选项变更
+
+ partial void OnIgnoreCaseChanged(Boolean value) => UpdateOptionText();
+ partial void OnMultilineChanged(Boolean value) => UpdateOptionText();
+ partial void OnSinglelineChanged(Boolean value) => UpdateOptionText();
+ partial void OnIgnorePatternWhitespaceChanged(Boolean value) => UpdateOptionText();
+
+ partial void OnIsMatchModeChanged(Boolean value)
+ {
+ OnPropertyChanged(nameof(ExecuteButtonText));
+ OnPropertyChanged(nameof(IsReplaceMode));
+ }
+
+ #endregion
+
+ #region 选中项变更
+
+ partial void OnSelectedMatchChanged(RegexMatchItem? value)
+ {
+ Groups.Clear();
+ Captures.Clear();
+
+ if (value?.Match == null) return;
+
+ var reg = _regex;
+ var m = value.Match;
+ for (var i = 0; i < m.Groups.Count; i++)
+ {
+ var g = m.Groups[i];
+ var name = reg?.GroupNameFromNumber(i) ?? i.ToString();
+ Groups.Add(new RegexGroupItem
+ {
+ Index = i,
+ Name = name,
+ Value = g.Value,
+ Position = g.Index,
+ Length = g.Length,
+ Group = g
+ });
+ }
+ }
+
+ partial void OnSelectedGroupChanged(RegexGroupItem? value)
+ {
+ Captures.Clear();
+
+ if (value?.Group == null) return;
+
+ var g = value.Group;
+ for (var i = 0; i < g.Captures.Count; i++)
+ {
+ var c = g.Captures[i];
+ Captures.Add(new RegexCaptureItem
+ {
+ Index = i,
+ Value = c.Value,
+ Position = c.Index,
+ Length = c.Length,
+ Capture = c
+ });
+ }
+ }
+
+ #endregion
+
+ #region 辅助方法
+
+ /// <summary>更新选项文本</summary>
+ void UpdateOptionText()
+ {
+ var sb = new StringBuilder();
+ var options = GetOptions();
+ if (options == RegexOptions.None)
+ {
+ sb.Append("RegexOptions.None");
+ }
+ else
+ {
+ if ((options & RegexOptions.IgnoreCase) != 0) AppendOption(sb, "IgnoreCase");
+ if ((options & RegexOptions.Multiline) != 0) AppendOption(sb, "Multiline");
+ if ((options & RegexOptions.Singleline) != 0) AppendOption(sb, "Singleline");
+ if ((options & RegexOptions.IgnorePatternWhitespace) != 0) AppendOption(sb, "IgnorePatternWhitespace");
+ }
+ OptionText = sb.ToString();
+ }
+
+ /// <summary>追加选项名称</summary>
+ static void AppendOption(StringBuilder sb, String name)
+ {
+ if (sb.Length > 0) sb.Append(" | ");
+ sb.Append("RegexOptions.");
+ sb.Append(name);
+ }
+
+ /// <summary>获取当前 RegexOptions</summary>
+ RegexOptions GetOptions()
+ {
+ var options = RegexOptions.None;
+ if (IgnoreCase) options |= RegexOptions.IgnoreCase;
+ if (Multiline) options |= RegexOptions.Multiline;
+ if (Singleline) options |= RegexOptions.Singleline;
+ if (IgnorePatternWhitespace) options |= RegexOptions.IgnorePatternWhitespace;
+ return options;
+ }
+
+ /// <summary>计算行号</summary>
+ static Int32 GetLineFromIndex(String text, Int32 index)
+ {
+ if (String.IsNullOrEmpty(text) || index <= 0) return 1;
+ var line = 1;
+ for (var i = 0; i < index && i < text.Length; i++)
+ {
+ if (text[i] == '\n') line++;
+ }
+ return line;
+ }
+
+ #endregion
+
+ #region 命令
+
+ /// <summary>执行匹配或替换</summary>
+ [RelayCommand]
+ async Task ExecuteAsync()
+ {
+ if (String.IsNullOrEmpty(Pattern)) return;
+ var text = SourceText;
+ if (String.IsNullOrEmpty(text)) return;
+
+ if (!IsMatchMode)
+ {
+ await ReplaceTextAsync(text);
+ return;
+ }
+
+ Status = "正在匹配...";
+ Matches.Clear();
+ Groups.Clear();
+ Captures.Clear();
+
+ var pattern = Pattern;
+ var options = GetOptions();
+ var timeout = TimeSpan.FromSeconds(5);
+
+ await Task.Run(() =>
+ {
+ try
+ {
+ var sw = Stopwatch.StartNew();
+ var reg = new Regex(pattern, options, timeout);
+ _regex = reg;
+
+ var ms = reg.Matches(text);
+ var idx = 1;
+ foreach (Match match in ms)
+ {
+ var line = GetLineFromIndex(text, match.Index);
+ var item = new RegexMatchItem
+ {
+ Index = idx++,
+ Value = match.Value,
+ Line = line,
+ Position = match.Index,
+ Length = match.Length,
+ Match = match
+ };
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ Matches.Add(item);
+ });
+ }
+
+ sw.Stop();
+ Dispatcher.UIThread.Post(() =>
+ {
+ Status = $"成功匹配 {Matches.Count} 项!耗时 {sw.ElapsedMilliseconds}ms";
+ });
+ }
+ catch (RegexMatchTimeoutException)
+ {
+ Dispatcher.UIThread.Post(() => Status = "正则匹配超时!");
+ }
+ catch (Exception ex)
+ {
+ Dispatcher.UIThread.Post(() => Status = $"错误:{ex.Message}");
+ }
+ });
+ }
+
+ /// <summary>执行替换</summary>
+ async Task ReplaceTextAsync(String text)
+ {
+ var replacement = Replacement;
+ if (String.IsNullOrEmpty(replacement))
+ {
+ Status = "请输入替换文本";
+ return;
+ }
+
+ Status = "正在替换...";
+ var pattern = Pattern;
+ var options = GetOptions();
+ var timeout = TimeSpan.FromSeconds(5);
+
+ await Task.Run(() =>
+ {
+ try
+ {
+ var sw = Stopwatch.StartNew();
+ var reg = new Regex(pattern, options, timeout);
+ _regex = reg;
+
+ var count = 0;
+ var ms = reg.Matches(text);
+ if (ms != null) count = ms.Count;
+
+ if (count > 0)
+ {
+ var result = reg.Replace(text, replacement);
+ Dispatcher.UIThread.Post(() =>
+ {
+ SourceText = result;
+ });
+ }
+
+ sw.Stop();
+ Dispatcher.UIThread.Post(() =>
+ {
+ Status = $"成功替换 {count} 项!耗时 {sw.ElapsedMilliseconds}ms";
+ });
+ }
+ catch (RegexMatchTimeoutException)
+ {
+ Dispatcher.UIThread.Post(() => Status = "正则替换超时!");
+ }
+ catch (Exception ex)
+ {
+ Dispatcher.UIThread.Post(() => Status = $"错误:{ex.Message}");
+ }
+ });
+ }
+
+ /// <summary>批量替换</summary>
+ [RelayCommand]
+ async Task BatchReplaceAsync()
+ {
+ if (String.IsNullOrEmpty(Pattern))
+ {
+ Status = "请输入正则表达式";
+ return;
+ }
+ var replacement = Replacement;
+ if (String.IsNullOrEmpty(replacement))
+ {
+ Status = "请输入替换文本";
+ return;
+ }
+
+ var path = DirectoryPath;
+ if (String.IsNullOrEmpty(path) || !Directory.Exists(path))
+ {
+ Status = "请先选择有效目录";
+ return;
+ }
+
+ var filter = FileFilter;
+ if (String.IsNullOrEmpty(filter)) filter = "*.*";
+
+ Status = "正在批量替换...";
+ var pattern = Pattern;
+ var options = GetOptions();
+
+ await Task.Run(() =>
+ {
+ try
+ {
+ var sw = Stopwatch.StartNew();
+ var reg = new Regex(pattern, options, TimeSpan.FromSeconds(5));
+
+ var files = Directory.GetFiles(path, filter, SearchOption.AllDirectories);
+ if (files == null || files.Length == 0)
+ {
+ Dispatcher.UIThread.Post(() => Status = "没有符合条件的文件");
+ return;
+ }
+
+ var totalCount = 0;
+ var changedCount = 0;
+ String? firstContent = null;
+
+ foreach (var item in files.Take(1000))
+ {
+ try
+ {
+ var content = File.ReadAllText(item);
+ var ms = reg.Matches(content);
+ if (ms == null || ms.Count == 0) continue;
+
+ totalCount += ms.Count;
+ var content2 = reg.Replace(content, replacement);
+ if (content != content2)
+ {
+ File.WriteAllText(item, content2);
+ changedCount++;
+ if (firstContent == null) firstContent = content2;
+ }
+ }
+ catch
+ {
+ // 跳过无法读取的文件
+ }
+ }
+
+ sw.Stop();
+
+ if (firstContent != null)
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ SourceText = firstContent;
+ });
+ }
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ Status = $"批量替换完成:{changedCount} 个文件,{totalCount} 项替换,耗时 {sw.ElapsedMilliseconds}ms";
+ });
+ }
+ catch (Exception ex)
+ {
+ Dispatcher.UIThread.Post(() => Status = $"错误:{ex.Message}");
+ }
+ });
+ }
+
+ #endregion
+}
diff --git a/XCoderAv/ViewModels/SecurityViewModel.cs b/XCoderAv/ViewModels/SecurityViewModel.cs
new file mode 100644
index 0000000..5c52198
--- /dev/null
+++ b/XCoderAv/ViewModels/SecurityViewModel.cs
@@ -0,0 +1,743 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+using System.Security.Cryptography;
+using System.Text;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using NewLife;
+using NewLife.Collections;
+using NewLife.Data;
+using NewLife.Reflection;
+using NewLife.Security;
+using NewLife.Serialization;
+using NewLife.Web;
+
+namespace XCoderAv.ViewModels;
+
+/// <summary>加密解密工具 ViewModel</summary>
+public partial class SecurityViewModel : ObservableObject
+{
+ #region 属性
+ /// <summary>原文</summary>
+ [ObservableProperty]
+ private String _sourceText = "学无先后达者为师";
+
+ /// <summary>密码</summary>
+ [ObservableProperty]
+ private String _passText = "NewLife";
+
+ /// <summary>结果</summary>
+ [ObservableProperty]
+ private String _resultText = "";
+
+ // 原文格式单选
+ /// <summary>原文格式-字符串</summary>
+ [ObservableProperty]
+ private Boolean _isSourceString = true;
+
+ /// <summary>原文格式-HEX</summary>
+ [ObservableProperty]
+ private Boolean _isSourceHex;
+
+ /// <summary>原文格式-Base64</summary>
+ [ObservableProperty]
+ private Boolean _isSourceBase64;
+
+ /// <summary>HEX单选可用</summary>
+ [ObservableProperty]
+ private Boolean _sourceHexEnabled = true;
+
+ /// <summary>Base64单选可用</summary>
+ [ObservableProperty]
+ private Boolean _sourceBase64Enabled = true;
+
+ // 密码格式单选
+ /// <summary>密码格式-字符串</summary>
+ [ObservableProperty]
+ private Boolean _isPassString = true;
+
+ /// <summary>密码格式-HEX</summary>
+ [ObservableProperty]
+ private Boolean _isPassHex;
+
+ /// <summary>密码格式-Base64</summary>
+ [ObservableProperty]
+ private Boolean _isPassBase64;
+
+ // 结果格式多选
+ /// <summary>结果格式-字符串</summary>
+ [ObservableProperty]
+ private Boolean _isResultString = true;
+
+ /// <summary>结果格式-HEX</summary>
+ [ObservableProperty]
+ private Boolean _isResultHex = true;
+
+ /// <summary>结果格式-Base64</summary>
+ [ObservableProperty]
+ private Boolean _isResultBase64 = true;
+ #endregion
+
+ #region 源文本变更 - 检测HEX/Base64可用性
+ partial void OnSourceTextChanged(String value)
+ {
+ if (value.IsNullOrEmpty()) return;
+
+ // 非纯ASCII(多字节字符)时禁用HEX/Base64单选
+ var enc = Encoding.UTF8;
+ if (enc.GetByteCount(value) != value.Length)
+ {
+ SourceHexEnabled = false;
+ SourceBase64Enabled = false;
+ return;
+ }
+
+ // 检测是否为合法HEX
+ try
+ {
+ SourceHexEnabled = value.ToHex().Length > 0;
+ }
+ catch
+ {
+ SourceHexEnabled = false;
+ }
+
+ // 检测是否为合法Base64
+ try
+ {
+ SourceBase64Enabled = value.ToBase64().Length > 0;
+ }
+ catch
+ {
+ SourceBase64Enabled = false;
+ }
+ }
+ #endregion
+
+ #region 辅助方法
+ /// <summary>从字符串中智能获取字节数组。自动识别 HEX(含-) → Base64 → UTF8</summary>
+ /// <param name="str">字符串</param>
+ /// <returns>字节数组</returns>
+ private static Byte[] GetBytes(String str)
+ {
+ if (str.IsNullOrEmpty()) return [];
+
+ try
+ {
+ if (str.Contains('-')) return str.ToHex();
+ }
+ catch { }
+
+ try
+ {
+ return str.ToBase64();
+ }
+ catch { }
+
+ return str.GetBytes();
+ }
+
+ /// <summary>按单选按钮状态从原文取字节</summary>
+ /// <returns>字节数组</returns>
+ private Byte[] GetSource()
+ {
+ var v = SourceText;
+
+ if (IsSourceString) return v.GetBytes();
+ if (IsSourceHex) return v.ToHex();
+ if (IsSourceBase64) return v.ToBase64();
+
+ return null;
+ }
+
+ /// <summary>按单选按钮状态从密码区取字节</summary>
+ /// <returns>字节数组</returns>
+ private Byte[] GetPass()
+ {
+ var v = PassText;
+
+ if (IsPassString) return v.GetBytes();
+ if (IsPassHex) return v.ToHex();
+ if (IsPassBase64) return v.ToBase64();
+
+ return null;
+ }
+
+ /// <summary>多行输出结果</summary>
+ /// <param name="rs">字符串数组</param>
+ private void SetResult(params String[] rs)
+ {
+ var sb = new StringBuilder();
+ foreach (var item in rs)
+ {
+ if (sb.Length > 0) sb.AppendLine();
+ sb.Append(item);
+ }
+ ResultText = sb.ToString();
+ }
+
+ /// <summary>按输出格式复选框输出字节数组</summary>
+ /// <param name="data">字节数组</param>
+ /// <returns>输出字符串列表</returns>
+ private List<String> SetResult(Byte[] data)
+ {
+ var list = new List<String>();
+ if (IsResultString) list.Add(data.ToStr());
+ if (IsResultHex)
+ {
+ list.Add(data.ToHex().ToUpper());
+ list.Add(data.ToHex().ToLower());
+ list.Add(data.ToHex("-"));
+ list.Add(data.ToHex(" "));
+ }
+ if (IsResultBase64)
+ {
+ list.Add(data.ToBase64());
+ list.Add(data.ToUrlBase64());
+ }
+
+ SetResult(list.ToArray());
+
+ return list;
+ }
+ #endregion
+
+ #region 功能命令
+ /// <summary>上下互换:交换原文区和结果区内容</summary>
+ [RelayCommand]
+ private void Exchange()
+ {
+ var v = SourceText;
+ var v2 = ResultText;
+ // 结果区只要第一行
+ if (!v2.IsNullOrEmpty())
+ {
+ var ss = v2.Split('\n');
+ var n = 0;
+ if (ss.Length > n + 1 && ss[n].StartsWith("/*") && ss[n].EndsWith("*/")) n++;
+ v2 = ss[n];
+ }
+ SourceText = v2;
+ ResultText = v;
+ }
+
+ /// <summary>HEX编码</summary>
+ [RelayCommand]
+ private void HexEncode()
+ {
+ var buf = GetSource();
+ SetResult(buf.ToHex(), buf.ToHex(" ", 32), buf.ToHex("-", 32));
+ }
+
+ /// <summary>HEX解码</summary>
+ [RelayCommand]
+ private void HexDecode()
+ {
+ var v = SourceText;
+ ResultText = v?.Trim().ToHex().ToStr();
+ }
+
+ /// <summary>Base64编码</summary>
+ [RelayCommand]
+ private void Base64Encode()
+ {
+ var buf = GetSource();
+ SetResult(buf.ToBase64(), buf.ToUrlBase64());
+ }
+
+ /// <summary>Base64解码</summary>
+ [RelayCommand]
+ private void Base64Decode()
+ {
+ var v = SourceText;
+
+ var vs = v.Split('.');
+ if (vs.Length <= 1)
+ {
+ var buf = v.Trim().ToBase64();
+ SetResult(buf);
+ }
+ else
+ {
+ SetResult(vs.Select(e2 => e2.Trim().ToBase64().ToStr()).ToArray());
+ }
+ }
+
+ /// <summary>MD5_32位</summary>
+ [RelayCommand]
+ private void MD5_32()
+ {
+ var buf = GetSource();
+ buf = buf.MD5();
+ SetResult(buf);
+ }
+
+ /// <summary>MD5_16位</summary>
+ [RelayCommand]
+ private void MD5_16()
+ {
+ var buf = GetSource();
+ buf = buf.MD5().Take(8).ToArray();
+ SetResult(buf);
+ }
+
+ /// <summary>SHA1</summary>
+ [RelayCommand]
+ private void SHA1()
+ {
+ var buf = GetSource();
+ var key = GetPass();
+
+ buf = buf.SHA1(key);
+ var rs = SetResult(buf);
+ rs.Add($"sha1${key.ToStr()}${buf.ToBase64()}");
+
+ SetResult(rs.ToArray());
+ }
+
+ /// <summary>SHA256</summary>
+ [RelayCommand]
+ private void SHA256()
+ {
+ var buf = GetSource();
+ var key = GetPass();
+
+ buf = buf.SHA256(key);
+ var rs = SetResult(buf);
+ rs.Add($"sha256${key.ToStr()}${buf.ToBase64()}");
+
+ SetResult(rs.ToArray());
+ }
+
+ /// <summary>SHA384</summary>
+ [RelayCommand]
+ private void SHA384()
+ {
+ var buf = GetSource();
+ var key = GetPass();
+
+ buf = buf.SHA384(key);
+ var rs = SetResult(buf);
+ rs.Add($"sha384${key.ToStr()}${buf.ToBase64()}");
+
+ SetResult(rs.ToArray());
+ }
+
+ /// <summary>SHA512</summary>
+ [RelayCommand]
+ private void SHA512()
+ {
+ var buf = GetSource();
+ var key = GetPass();
+
+ buf = buf.SHA512(key);
+ var rs = SetResult(buf);
+ rs.Add($"sha512${key.ToStr()}${buf.ToBase64()}");
+
+ SetResult(rs.ToArray());
+ }
+
+ /// <summary>CRC_32</summary>
+ [RelayCommand]
+ private void CRC_32()
+ {
+ var buf = GetSource();
+ var rs = buf.Crc();
+ var data = rs.GetBytes(false);
+ SetResult("/*数字、HEX编码、Base64编码*/", rs + "", data.ToHex(), data.ToBase64());
+ }
+
+ /// <summary>CRC_16</summary>
+ [RelayCommand]
+ private void CRC_16()
+ {
+ var buf = GetSource();
+ var rs = buf.Crc16();
+ var data = rs.GetBytes(false);
+ var mcrc = Modbus_CRC(buf, 0, buf.Length);
+ SetResult("/*数字、HEX编码、Base64编码、Modbus-Crc*/", rs + "", data.ToHex(), data.ToBase64(), mcrc.GetBytes().ToHex("-"));
+ }
+
+ /// <summary>RSA加密</summary>
+ [RelayCommand]
+ private void RSAEncrypt()
+ {
+ var buf = GetSource();
+ var key = PassText;
+
+ if (key.Length < 100)
+ {
+ key = RSAHelper.GenerateKey().First();
+ PassText = key;
+ }
+
+ buf = RSAHelper.Encrypt(buf, key);
+
+ SetResult(buf);
+ }
+
+ /// <summary>RSA解密</summary>
+ [RelayCommand]
+ private void RSADecrypt()
+ {
+ var buf = GetSource();
+ var pass = PassText;
+
+ try
+ {
+ buf = RSAHelper.Decrypt(buf, pass, true);
+ }
+ catch (CryptographicException)
+ {
+ // 换一种填充方式
+ buf = RSAHelper.Decrypt(buf, pass, false);
+ }
+
+ SetResult(buf);
+ }
+
+ /// <summary>DSA签名</summary>
+ [RelayCommand]
+ private void DSASign()
+ {
+ var buf = GetSource();
+ var key = PassText;
+
+ if (key.Length < 100)
+ {
+ key = DSAHelper.GenerateKey().First();
+ PassText = key;
+ }
+
+ buf = DSAHelper.Sign(buf, key);
+
+ SetResult(buf);
+ }
+
+ /// <summary>DSA验证</summary>
+ [RelayCommand]
+ private void DSAVerify()
+ {
+ var buf = GetSource();
+ var pass = PassText;
+
+ var v = ResultText;
+ if (v.Contains("\n\n")) v = v.Substring(null, "\n\n");
+ var sign = GetBytes(v);
+
+ var rs = DSAHelper.Verify(buf, pass, sign);
+ SetResult(rs ? "验证通过" : "验证失败");
+ }
+
+ /// <summary>Url编码</summary>
+ [RelayCommand]
+ private void UrlEncode()
+ {
+ var v = SourceText;
+ v = WebUtility.UrlEncode(v);
+ ResultText = v;
+ }
+
+ /// <summary>Url解码</summary>
+ [RelayCommand]
+ private void UrlDecode()
+ {
+ var v = SourceText;
+ v = WebUtility.UrlDecode(v);
+ ResultText = v;
+ }
+
+ /// <summary>Html编码</summary>
+ [RelayCommand]
+ private void HtmlEncode()
+ {
+ var v = SourceText;
+ v = WebUtility.HtmlEncode(v);
+ ResultText = v;
+ }
+
+ /// <summary>Html解码</summary>
+ [RelayCommand]
+ private void HtmlDecode()
+ {
+ var v = SourceText;
+ v = WebUtility.HtmlDecode(v);
+ ResultText = v;
+ }
+
+ /// <summary>时间戳</summary>
+ [RelayCommand]
+ private void Timestamp()
+ {
+ var v = SourceText;
+ if (v.IsNullOrEmpty()) return;
+
+ var sb = Pool.StringBuilder.Get();
+
+ DateTime.TryParse(PassText, out var baseTime);
+ var utc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+
+ if (baseTime > DateTime.MinValue) sb.AppendFormat("基准:{0:yyyy-MM-dd HH:mm:ss.fff} {1}\r\n", baseTime, baseTime.Kind);
+
+ var dt = v.ToDateTime();
+ if (dt.Year > 1 && dt.Year < 3000)
+ {
+ var s = dt.ToInt();
+ var m = dt.ToLong();
+
+ if (baseTime > DateTime.MinValue)
+ {
+ s -= baseTime.ToInt();
+ m -= baseTime.ToLong();
+ }
+
+ sb.AppendLine("Unix秒:" + s);
+ sb.AppendLine("Unix毫秒:" + m);
+ }
+
+ var now = DateTime.Now;
+ var n = v.ToLong();
+ if (n >= Int32.MaxValue)
+ {
+ dt = n.ToDateTime();
+ dt = dt.ToLocalTime().ToUniversalTime();
+ if (baseTime > DateTime.MinValue) dt = dt.Add(baseTime - utc);
+ if (dt.Year > 1000 && dt.Year < 3000)
+ {
+ sb.AppendFormat("时间:{0:yyyy-MM-dd HH:mm:ss.fff} (Unix毫秒) {1}\r\n", dt, dt.Kind);
+ dt = dt.ToLocalTime();
+ sb.AppendFormat("时间:{0:yyyy-MM-dd HH:mm:ss.fff} (现在) {1}\r\n", dt, dt.Kind);
+ }
+ }
+ else if (n > 0)
+ {
+ dt = v.ToInt().ToDateTime();
+ dt = dt.ToLocalTime().ToUniversalTime();
+ if (baseTime > DateTime.MinValue) dt = dt.Add(baseTime - utc);
+ if (dt.Year > 1000 && dt.Year < 3000)
+ {
+ sb.AppendFormat("时间:{0:yyyy-MM-dd HH:mm:ss} (Unix秒) {1}\r\n", dt, dt.Kind);
+ dt = dt.ToLocalTime();
+ sb.AppendFormat("时间:{0:yyyy-MM-dd HH:mm:ss} (现在) {1}\r\n", dt, dt.Kind);
+ }
+ }
+
+ // 过去/未来时间戳
+ if (n > 0 && n < 1000L * 365 * 24 * 3600 * 1000)
+ {
+ sb.AppendFormat("过去:{0:yyyy-MM-dd HH:mm:ss.fff} (now.AddMilliseconds(-n))\r\n", now.AddMilliseconds(-n));
+ sb.AppendFormat("未来:{0:yyyy-MM-dd HH:mm:ss.fff} (now.AddMilliseconds(n))\r\n", now.AddMilliseconds(n));
+
+ if (n < Int32.MaxValue)
+ {
+ sb.AppendFormat("过去:{0:yyyy-MM-dd HH:mm:ss} (now.AddSeconds(-n))\r\n", now.AddSeconds(-n));
+ sb.AppendFormat("未来:{0:yyyy-MM-dd HH:mm:ss} (now.AddSeconds(n))\r\n", now.AddSeconds(n));
+ }
+ }
+
+ ResultText = sb.Return(true);
+ }
+
+ /// <summary>机器信息</summary>
+ [RelayCommand]
+ private void ComputerInfo()
+ {
+ var sb = Pool.StringBuilder.Get();
+
+ var mi = MachineInfo.Current;
+ mi.Refresh();
+ sb.AppendLine(mi.ToJson(true));
+ sb.AppendLine();
+
+ var macs = GetMacs().ToList();
+ if (macs.Count > 0) sb.AppendFormat("MAC:\t{0}\r\n", macs.Join(",", x => x.ToHex("-")));
+
+ // 跨平台无法获取 WMI/Registry 信息,仅输出 MachineInfo
+ ResultText = sb.Return(true);
+ }
+
+ private static readonly String[] _Excludes = ["Loopback", "VMware", "VBox", "Virtual", "Teredo", "Microsoft", "VPN", "VNIC", "IEEE"];
+
+ /// <summary>获取所有网卡MAC地址</summary>
+ /// <returns>MAC地址集合</returns>
+ public static IEnumerable<Byte[]> GetMacs()
+ {
+ foreach (var item in NetworkInterface.GetAllNetworkInterfaces())
+ {
+ if (_Excludes.Any(e => item.Description.Contains(e))) continue;
+ if (item.Speed < 1_000_000) continue;
+
+ var addrs = item.GetIPProperties().UnicastAddresses.Where(e => e.Address.AddressFamily == AddressFamily.InterNetwork).ToArray();
+ if (addrs.All(e => IPAddress.IsLoopback(e.Address))) continue;
+
+ var mac = item.GetPhysicalAddress()?.GetAddressBytes();
+ if (mac != null && mac.Length == 6) yield return mac;
+ }
+ }
+
+ /// <summary>雪花Id</summary>
+ [RelayCommand]
+ private void Snowflake()
+ {
+ var v = SourceText.ToLong();
+ if (v <= 0) return;
+
+ var snow = new Snowflake();
+
+ // 指定基准时间
+ if (!PassText.IsNullOrEmpty())
+ {
+ var baseTime = PassText.ToDateTime();
+ if (baseTime.Year > 1000) snow.StartTimestamp = baseTime;
+ }
+
+ if (!snow.TryParse(v, out var time, out var workerId, out var sequence)) throw new Exception("解码失败!");
+
+ // 初始化一次,用于获取本机workerId
+ snow.NewId();
+
+ var t = (Int64)(time - snow.StartTimestamp).TotalMilliseconds;
+ SetResult(
+ $"十六:{v:X16}",
+ $"编码:{(t << 2):X8} {workerId:X2} {sequence:X3}",
+ $"基准:{snow.StartTimestamp.ToFullString()} {snow.StartTimestamp.Kind}",
+ $"时间:{time:yyyy-MM-dd HH:mm:ss.fff} {time.Kind} ({t} / {(t << 22):X8})",
+ $"节点:{workerId} ({workerId:X4})",
+ $"序号:{sequence} ({sequence:X4})",
+ $"本机:{snow.WorkerId} ({snow.WorkerId:X4})");
+ }
+
+ /// <summary>JWT令牌</summary>
+ [RelayCommand]
+ private void JwtToken()
+ {
+ var v = SourceText;
+ if (v.IsNullOrEmpty()) return;
+
+ var pass = PassText?.Trim();
+
+ var vs = v.Split('.');
+ if (vs.Length == 3)
+ {
+ var jwt = new JwtBuilder { Secret = "abcd" };
+
+ var ss = pass?.Split(':');
+ if (ss != null && ss.Length >= 2)
+ {
+ jwt.Algorithm = ss[0];
+ jwt.Secret = ss[1];
+ }
+
+ var rs = jwt.TryDecode(v, out var message);
+
+ SetResult($"验证结果:{rs}", jwt.ToJson(true));
+ }
+ else if (vs.Length == 2)
+ {
+ var prv = new TokenProvider
+ {
+ Key = pass
+ };
+
+ var rs = prv.TryDecode(v, out var user, out var expire);
+
+ SetResult($"验证结果:{rs}", new { user, expire }.ToJson(true));
+ }
+ else
+ {
+ SetResult(vs.Select(e2 => e2.ToBase64().ToStr()).ToArray());
+ }
+ }
+
+ /// <summary>版本号</summary>
+ [RelayCommand]
+ private void Version()
+ {
+ var v = SourceText;
+ if (v.IsNullOrEmpty()) return;
+
+ var dt = AssemblyX.GetCompileTime(v);
+
+ ResultText = dt.ToFullString();
+ }
+
+ /// <summary>TraceId</summary>
+ [RelayCommand]
+ private void TraceId()
+ {
+ var str = SourceText.Trim();
+ if (str.IsNullOrEmpty()) return;
+
+ var rs = new List<String>();
+ foreach (var item in str.Split('\n'))
+ {
+ var v = item.Trim();
+ if (v.IsNullOrEmpty()) continue;
+
+ // TraceId
+ if (v.Length >= 30)
+ {
+ if (v.Length >= 8)
+ rs.Add($"地址:{v[..8]} {new IPAddress(v[..8].ToHex())}");
+
+ if (v.Length >= 8 + 13)
+ {
+ var time = v.Substring(8, 13).ToLong();
+ rs.Add($"时间:{time}({time.ToDateTime().ToLocalTime().ToFullString()})");
+ }
+
+ if (v.Length >= 21 + 4)
+ rs.Add($"序列:{v[21..25]} {v[21..25].ToHex().ToUInt16()}");
+
+ if (v.Length >= 26 + 4)
+ rs.Add($"进程:{v[26..30]} {v[26..30].ToHex().ToUInt16()}");
+ }
+ else if (v.Length >= 16)
+ {
+ if (v.Length >= 8)
+ rs.Add($"地址:{v[..8]} {new IPAddress(v[..8].ToHex())}");
+
+ if (v.Length >= 8 + 4)
+ rs.Add($"进程:{v[8..12]} {v[8..12].ToHex().ToUInt16()}");
+
+ if (v.Length >= 12 + 4)
+ rs.Add($"序列:{v[12..16]} {v[12..16].ToHex().ToUInt16()}");
+ }
+ rs.Add("");
+ }
+
+ SetResult(rs.ToArray());
+ }
+ #endregion
+
+ #region Modbus_CRC
+ private static readonly UInt16[] crc_ta = [0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401, 0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400];
+
+ /// <summary>Crc校验</summary>
+ /// <param name="data">数据</param>
+ /// <param name="offset">偏移</param>
+ /// <param name="count">数量</param>
+ /// <returns>Crc结果</returns>
+ public static UInt16 Modbus_CRC(Byte[] data, Int32 offset, Int32 count = -1)
+ {
+ if (data == null || data.Length < 1) return 0;
+
+ UInt16 u = 0xFFFF;
+ Byte b;
+
+ if (count == 0) count = data.Length - offset;
+
+ for (var i = offset; i < count; i++)
+ {
+ b = data[i];
+ u = (UInt16)(crc_ta[(b ^ u) & 15] ^ (u >> 4));
+ u = (UInt16)(crc_ta[((b >> 4) ^ u) & 15] ^ (u >> 4));
+ }
+
+ return u;
+ }
+ #endregion
+}
diff --git a/XCoderAv/ViewModels/SpeechViewModel.cs b/XCoderAv/ViewModels/SpeechViewModel.cs
new file mode 100644
index 0000000..d61ba11
--- /dev/null
+++ b/XCoderAv/ViewModels/SpeechViewModel.cs
@@ -0,0 +1,219 @@
+using System.Collections.ObjectModel;
+using System.Linq;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using NewLife;
+
+namespace XCoderAv.ViewModels;
+
+/// <summary>语音助手 (TTS) ViewModel(跨平台版)</summary>
+public partial class SpeechViewModel : ObservableObject
+{
+ #region 属性
+ /// <summary>可用语音列表</summary>
+ public ObservableCollection<String> Voices { get; } = [];
+
+ /// <summary>选中语音索引</summary>
+ [ObservableProperty]
+ private Int32 _selectedVoiceIndex;
+
+ /// <summary>音量 (0-100)</summary>
+ [ObservableProperty]
+ private Int32 _volume = 100;
+
+ /// <summary>语速 (-10 到 10)</summary>
+ [ObservableProperty]
+ private Int32 _rate;
+
+ /// <summary>朗读文本</summary>
+ [ObservableProperty]
+ private String _speakText = "学无先后达者为师";
+
+ /// <summary>是否正在朗读</summary>
+ [ObservableProperty]
+ private Boolean _isSpeaking;
+
+ /// <summary>状态文本</summary>
+ [ObservableProperty]
+ private String _status = "就绪";
+
+ /// <summary>TTS 引擎可用</summary>
+ [ObservableProperty]
+ private Boolean _isTtsAvailable;
+
+ /// <summary>日志文本</summary>
+ [ObservableProperty]
+ private String _logText = "";
+ #endregion
+
+ #region 构造
+ /// <summary>实例化语音助手 ViewModel</summary>
+ public SpeechViewModel()
+ {
+ LoadVoices();
+ }
+
+ private void LoadVoices()
+ {
+ // 跨平台 TTS 支持:Windows 使用 System.Speech,其他平台暂不支持
+ // 可通过条件编译或运行时检测扩展
+ var available = false;
+ var voices = new List<String>();
+
+#if NETFRAMEWORK || WINDOWS
+ try
+ {
+ using var synth = new System.Speech.Synthesis.SpeechSynthesizer();
+ foreach (var vi in synth.GetInstalledVoices().Select(e => e.VoiceInfo))
+ {
+ voices.Add($"{vi.Name}[{vi.Culture}]");
+ }
+ available = voices.Count > 0;
+ }
+ catch (Exception ex)
+ {
+ voices.Add($"TTS 不可用:{ex.Message}");
+ }
+#else
+ // 跨平台 TTS 可通过 System.Speech 在 Windows 上工作
+ // Linux/macOS 需要额外库
+ try
+ {
+ // 尝试加载 System.Speech (仅 Windows)
+ var asm = System.Reflection.Assembly.Load("System.Speech");
+ if (asm != null)
+ {
+ var type = asm.GetType("System.Speech.Synthesis.SpeechSynthesizer");
+ if (type != null)
+ {
+ using var synth = (IDisposable)Activator.CreateInstance(type)!;
+ var getVoices = type.GetMethod("GetInstalledVoices");
+ if (getVoices != null)
+ {
+ var installedVoices = (System.Collections.IList)getVoices.Invoke(synth, null)!;
+ foreach (var iv in installedVoices)
+ {
+ var viProp = iv.GetType().GetProperty("VoiceInfo");
+ if (viProp?.GetValue(iv) is Object vi)
+ {
+ var name = vi.GetType().GetProperty("Name")?.GetValue(vi)?.ToString() ?? "";
+ var culture = vi.GetType().GetProperty("Culture")?.GetValue(vi)?.ToString() ?? "";
+ voices.Add($"{name}[{culture}]");
+ }
+ }
+ available = voices.Count > 0;
+ }
+ }
+ }
+ }
+ catch
+ {
+ // TTS 不可用
+ }
+#endif
+
+ IsTtsAvailable = available;
+
+ Voices.Clear();
+ if (voices.Count > 0)
+ {
+ foreach (var v in voices)
+ Voices.Add(v);
+ }
+ else
+ {
+ Voices.Add("TTS 引擎不可用(仅支持 Windows)");
+ Status = "TTS 引擎不可用,当前平台不支持语音合成";
+ }
+ }
+ #endregion
+
+ #region 命令
+ /// <summary>朗读</summary>
+ [RelayCommand]
+ private void Speak()
+ {
+ var txt = SpeakText;
+ if (txt.IsNullOrEmpty()) return;
+
+ Stop();
+
+#if NETFRAMEWORK || WINDOWS
+ try
+ {
+ var synth = new System.Speech.Synthesis.SpeechSynthesizer
+ {
+ Volume = Volume,
+ Rate = Rate
+ };
+
+ if (SelectedVoiceIndex >= 0 && SelectedVoiceIndex < Voices.Count)
+ {
+ // 获取实际语音名称
+ var voiceName = Voices[SelectedVoiceIndex];
+ if (voiceName.Contains('['))
+ voiceName = voiceName[..voiceName.IndexOf('[')];
+ synth.SelectVoice(voiceName);
+ }
+
+ IsSpeaking = true;
+ Status = "正在朗读...";
+ synth.SpeakCompleted += (s, e) =>
+ {
+ IsSpeaking = false;
+ Status = "朗读完成";
+ synth.Dispose();
+ };
+ synth.SpeakAsync(txt);
+ }
+ catch (Exception ex)
+ {
+ Status = $"朗读失败:{ex.Message}";
+ IsSpeaking = false;
+ }
+#else
+ Status = "当前平台不支持语音合成";
+#endif
+ }
+
+ /// <summary>停止朗读</summary>
+ [RelayCommand]
+ private void Stop()
+ {
+#if NETFRAMEWORK || WINDOWS
+ try
+ {
+ // 通过反射停止所有正在进行的朗读
+ var asm = System.Reflection.Assembly.Load("System.Speech");
+ var synthType = asm.GetType("System.Speech.Synthesis.SpeechSynthesizer");
+ if (synthType != null)
+ {
+ // 创建新实例来取消
+ using var synth = (IDisposable)Activator.CreateInstance(synthType)!;
+ var speakAsyncCancelAll = synthType.GetMethod("SpeakAsyncCancelAll");
+ speakAsyncCancelAll?.Invoke(synth, null);
+ }
+ }
+ catch
+ {
+ }
+#endif
+ IsSpeaking = false;
+ Status = "已停止";
+ }
+
+ /// <summary>保存为 WAV 文件</summary>
+ [RelayCommand]
+ private void SaveWav()
+ {
+ var txt = SpeakText;
+ if (txt.IsNullOrEmpty())
+ {
+ Status = "请输入要转换的文本!";
+ return;
+ }
+
+ Status = "当前平台不支持保存为 WAV 文件";
+ }
+ #endregion
+}
diff --git a/XCoderAv/Views/FolderStatWindow.axaml b/XCoderAv/Views/FolderStatWindow.axaml
new file mode 100644
index 0000000..5112d75
--- /dev/null
+++ b/XCoderAv/Views/FolderStatWindow.axaml
@@ -0,0 +1,46 @@
+<Window xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:vm="clr-namespace:XCoderAv.ViewModels"
+ mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="700"
+ x:Class="XCoderAv.Views.FolderStatWindow"
+ Title="文件夹大小统计" Width="1000" Height="700"
+ WindowStartupLocation="CenterOwner">
+ <Window.Styles>
+ <Style Selector="TreeViewItem">
+ <Setter Property="Foreground" Value="#D4D4D4"/>
+ <Setter Property="FontFamily" Value="Consolas"/>
+ <Setter Property="FontSize" Value="13"/>
+ </Style>
+ </Window.Styles>
+
+ <Grid Margin="4">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="*"/>
+ <RowDefinition Height="4"/>
+ <RowDefinition Height="150"/>
+ </Grid.RowDefinitions>
+
+ <!-- 树形目录 -->
+ <TreeView Grid.Row="0" x:Name="treeView" BorderThickness="1" BorderBrush="#D0D0D0"
+ Background="#1E1E1E">
+ <TreeView.ItemTemplate>
+ <TreeDataTemplate ItemsSource="{Binding Children}">
+ <Border Background="{Binding BackgroundColor}" Padding="4 2">
+ <TextBlock Text="{Binding DisplayName}" FontFamily="Consolas" FontSize="13"/>
+ </Border>
+ </TreeDataTemplate>
+ </TreeView.ItemTemplate>
+ </TreeView>
+
+ <GridSplitter Grid.Row="1" Height="4" HorizontalAlignment="Stretch" Background="#F0F0F0"/>
+
+ <!-- 日志输出 -->
+ <Border Grid.Row="2" BorderBrush="#D0D0D0" BorderThickness="1">
+ <TextBox x:Name="txtLog" IsReadOnly="True"
+ FontFamily="Consolas" FontSize="12" Background="#1E1E1E" Foreground="#D4D4D4"
+ Margin="2"/>
+ </Border>
+ </Grid>
+</Window>
diff --git a/XCoderAv/Views/FolderStatWindow.axaml.cs b/XCoderAv/Views/FolderStatWindow.axaml.cs
new file mode 100644
index 0000000..87bdcd8
--- /dev/null
+++ b/XCoderAv/Views/FolderStatWindow.axaml.cs
@@ -0,0 +1,55 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
+using XCoderAv.ViewModels;
+
+namespace XCoderAv.Views;
+
+/// <summary>文件夹大小统计工具窗口</summary>
+public partial class FolderStatWindow : Window
+{
+ /// <summary>实例化文件夹大小统计窗口</summary>
+ public FolderStatWindow()
+ {
+ InitializeComponent();
+
+ var vm = new FolderStatViewModel();
+ vm.OnLog = OnLog;
+ DataContext = vm;
+
+ // 加载根节点
+ treeView.ItemsSource = vm.Roots;
+
+ // 订阅 TreeViewItem 的 Expanded/Collapsed 路由事件
+ treeView.AddHandler(TreeViewItem.ExpandedEvent, OnItemExpanded);
+ treeView.AddHandler(TreeViewItem.CollapsedEvent, OnItemCollapsed);
+ }
+
+ private async void OnItemExpanded(Object? sender, RoutedEventArgs e)
+ {
+ if (e.Source is TreeViewItem tvi && tvi.DataContext is FolderItem folderItem && DataContext is FolderStatViewModel vm)
+ {
+ await vm.ExpandFolder(folderItem);
+ }
+ }
+
+ private void OnItemCollapsed(Object? sender, RoutedEventArgs e)
+ {
+ if (e.Source is TreeViewItem tvi && tvi.DataContext is FolderItem folderItem && DataContext is FolderStatViewModel vm)
+ {
+ vm.CollapseFolder(folderItem);
+ }
+ }
+
+ private void OnLog(String msg)
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ txtLog.Text += msg + "\n";
+ if (txtLog.Text.Length > 10000)
+ txtLog.Text = txtLog.Text[^5000..];
+
+ txtLog.CaretIndex = txtLog.Text.Length;
+ });
+ }
+}
diff --git a/XCoderAv/Views/GpsWindow.axaml b/XCoderAv/Views/GpsWindow.axaml
new file mode 100644
index 0000000..707f8cf
--- /dev/null
+++ b/XCoderAv/Views/GpsWindow.axaml
@@ -0,0 +1,152 @@
+<Window xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="450"
+ x:Class="XCoderAv.Views.GpsWindow"
+ Title="GPS 辅助工具" Width="600" Height="450"
+ WindowStartupLocation="CenterOwner">
+ <Window.Styles>
+ <Style Selector="Border.GroupBorder">
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="Margin" Value="0,0,0,6"/>
+ </Style>
+ <Style Selector="TextBlock.SectionTitle">
+ <Setter Property="FontWeight" Value="Bold"/>
+ <Setter Property="Margin" Value="4,3"/>
+ <Setter Property="FontSize" Value="13"/>
+ </Style>
+ <Style Selector="TextBox.ConfigTextBox">
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Height" Value="30"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ <Setter Property="FontFamily" Value="Consolas"/>
+ <Setter Property="FontSize" Value="14"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ </Style>
+ <Style Selector="Button.ActionButton">
+ <Setter Property="Height" Value="30"/>
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Padding" Value="12,0"/>
+ </Style>
+ <Style Selector="TextBox.ResultText">
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Height" Value="30"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ <Setter Property="FontFamily" Value="Consolas"/>
+ <Setter Property="FontSize" Value="14"/>
+ <Setter Property="FontWeight" Value="Bold"/>
+ <Setter Property="IsReadOnly" Value="True"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="Background" Value="#FFFFF0"/>
+ </Style>
+ </Window.Styles>
+
+ <Grid Margin="8">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <!-- 合并输入 -->
+ <Border Grid.Row="0" Classes="GroupBorder">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="HEX 合并输入(16位)" Classes="SectionTitle"/>
+ <Grid Grid.Row="1" Margin="4,0,4,4">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ <TextBox Grid.Column="0" Text="{Binding HexCombined}" Classes="ConfigTextBox"/>
+ <Button Grid.Column="1" Content="转换" Command="{Binding ConvertCombinedCommand}"
+ Classes="ActionButton" Margin="4,2" Width="80"/>
+ </Grid>
+ </Grid>
+ </Border>
+
+ <!-- 分离输入 -->
+ <Border Grid.Row="1" Classes="GroupBorder">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="HEX 分离输入(各8位)" Classes="SectionTitle"/>
+ <Grid Grid.Row="1" Margin="4,0,4,4">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Column="0" Text="纬度" VerticalAlignment="Center" Margin="4,0"/>
+ <TextBox Grid.Column="1" Text="{Binding HexLat}" Classes="ConfigTextBox"/>
+ <TextBlock Grid.Column="2" Text="经度" VerticalAlignment="Center" Margin="4,0"/>
+ <TextBox Grid.Column="3" Text="{Binding HexLong}" Classes="ConfigTextBox"/>
+ <Button Grid.Column="4" Content="转换" Command="{Binding ConvertCommand}"
+ Classes="ActionButton" Margin="4,2" Width="80"/>
+ </Grid>
+ </Grid>
+ </Border>
+
+ <!-- 结果 -->
+ <Border Grid.Row="2" Classes="GroupBorder">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="转换结果" Classes="SectionTitle"/>
+
+ <Grid Grid.Row="1" Margin="4,0,4,2">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+ <TextBlock Grid.Column="0" Text="纬度" VerticalAlignment="Center" Width="50"/>
+ <TextBox Grid.Column="1" Text="{Binding Latitude}" Classes="ResultText"/>
+ </Grid>
+
+ <Grid Grid.Row="2" Margin="4,0,4,4">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+ <TextBlock Grid.Column="0" Text="经度" VerticalAlignment="Center" Width="50"/>
+ <TextBox Grid.Column="1" Text="{Binding Longitude}" Classes="ResultText"/>
+ </Grid>
+ </Grid>
+ </Border>
+
+ <!-- 合并坐标 -->
+ <Border Grid.Row="3" Classes="GroupBorder">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="合并坐标" Classes="SectionTitle"/>
+
+ <TextBox Grid.Row="1" Text="{Binding LatLong}" Classes="ResultText" Margin="4,0,4,2"/>
+
+ <StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right"
+ VerticalAlignment="Bottom" Margin="4">
+ <Button Content="清空" Command="{Binding ClearCommand}" Classes="ActionButton" Width="80"/>
+ </StackPanel>
+ </Grid>
+ </Border>
+ </Grid>
+</Window>
diff --git a/XCoderAv/Views/GpsWindow.axaml.cs b/XCoderAv/Views/GpsWindow.axaml.cs
new file mode 100644
index 0000000..5839433
--- /dev/null
+++ b/XCoderAv/Views/GpsWindow.axaml.cs
@@ -0,0 +1,15 @@
+using Avalonia.Controls;
+using XCoderAv.ViewModels;
+
+namespace XCoderAv.Views;
+
+/// <summary>GPS 辅助工具窗口</summary>
+public partial class GpsWindow : Window
+{
+ /// <summary>实例化 GPS 辅助工具窗口</summary>
+ public GpsWindow()
+ {
+ InitializeComponent();
+ DataContext = new GpsViewModel();
+ }
+}
diff --git a/XCoderAv/Views/IconToolWindow.axaml b/XCoderAv/Views/IconToolWindow.axaml
new file mode 100644
index 0000000..fb12be7
--- /dev/null
+++ b/XCoderAv/Views/IconToolWindow.axaml
@@ -0,0 +1,150 @@
+<Window xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d" d:DesignWidth="1100" d:DesignHeight="750"
+ x:Class="XCoderAv.Views.IconToolWindow"
+ Title="图标水印处理工具" Width="1100" Height="750"
+ WindowStartupLocation="CenterOwner">
+ <Window.Styles>
+ <Style Selector="Border.GroupBorder">
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="Margin" Value="0,0,0,6"/>
+ </Style>
+ <Style Selector="TextBlock.SectionTitle">
+ <Setter Property="FontWeight" Value="Bold"/>
+ <Setter Property="Margin" Value="4,3"/>
+ <Setter Property="FontSize" Value="13"/>
+ </Style>
+ <Style Selector="TextBox.ConfigTextBox">
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Height" Value="26"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ </Style>
+ <Style Selector="TextBox.SmallNumeric">
+ <Setter Property="Width" Value="70"/>
+ <Setter Property="Height" Value="26"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ <Setter Property="HorizontalContentAlignment" Value="Right"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="Margin" Value="2"/>
+ </Style>
+ <Style Selector="Button.ActionButton">
+ <Setter Property="Height" Value="30"/>
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Padding" Value="8,0"/>
+ </Style>
+ </Window.Styles>
+
+ <Grid Margin="6">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="260"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- ============ 左侧:配置面板 ============ -->
+ <ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto" Margin="0,0,4,0">
+ <StackPanel>
+ <!-- 图片加载 -->
+ <Border Classes="GroupBorder">
+ <StackPanel>
+ <TextBlock Text="图片加载" Classes="SectionTitle"/>
+ <Button Content="选择图片" Command="{Binding LoadImageCommand}"
+ Classes="ActionButton" Margin="4,2"/>
+ <TextBlock Text="提示:支持拖放图片到右侧预览区" Margin="4,0,4,4"
+ Foreground="Gray" FontSize="11"/>
+ </StackPanel>
+ </Border>
+
+ <!-- 水印设置 -->
+ <Border Classes="GroupBorder">
+ <StackPanel>
+ <TextBlock Text="水印设置" Classes="SectionTitle"/>
+
+ <TextBlock Text="文字内容" Margin="4,2,4,0"/>
+ <TextBox Text="{Binding WatermarkText}" Classes="ConfigTextBox"/>
+
+ <Grid Margin="4,2">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="字体" VerticalAlignment="Center" Margin="0,0,4,0"/>
+ <ComboBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding FontNames}"
+ SelectedItem="{Binding FontName}" Height="28"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="大小" VerticalAlignment="Center" Margin="0,0,4,0"/>
+ <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding FontSize}" Classes="SmallNumeric"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="X位置" VerticalAlignment="Center" Margin="0,0,4,0"/>
+ <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding WatermarkX}" Classes="SmallNumeric"/>
+
+ <TextBlock Grid.Row="3" Grid.Column="0" Text="Y位置" VerticalAlignment="Center" Margin="0,0,4,0"/>
+ <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding WatermarkY}" Classes="SmallNumeric"/>
+ </Grid>
+
+ <Button Content="应用水印" Command="{Binding MakeWaterCommand}"
+ Classes="ActionButton" Margin="4,2"/>
+ </StackPanel>
+ </Border>
+
+ <!-- ICO 设置 -->
+ <Border Classes="GroupBorder">
+ <StackPanel>
+ <TextBlock Text="ICO 图标" Classes="SectionTitle"/>
+ <ItemsControl ItemsSource="{Binding IconSizes}">
+ <ItemsControl.ItemTemplate>
+ <DataTemplate>
+ <CheckBox Content="{Binding Name}" IsChecked="{Binding IsSelected}" Margin="4,2"/>
+ </DataTemplate>
+ </ItemsControl.ItemTemplate>
+ </ItemsControl>
+ <Button Content="生成 ICO" Command="{Binding MakeIcoCommand}"
+ Classes="ActionButton" Margin="4,2"/>
+ </StackPanel>
+ </Border>
+
+ <!-- 保存 -->
+ <Border Classes="GroupBorder">
+ <StackPanel>
+ <TextBlock Text="保存" Classes="SectionTitle"/>
+ <Button Content="保存图片" Command="{Binding SaveImageCommand}"
+ Classes="ActionButton" Margin="4,2"/>
+ </StackPanel>
+ </Border>
+ </StackPanel>
+ </ScrollViewer>
+
+ <!-- ============ 右侧:预览区 ============ -->
+ <Border Grid.Column="1" BorderBrush="#D0D0D0" BorderThickness="1" Padding="4">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <TextBlock Text="预览" FontWeight="Bold" Margin="4,2"/>
+
+ <ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
+ <Image Source="{Binding PreviewImage}" MaxWidth="800" MaxHeight="500" Stretch="Uniform"/>
+ </ScrollViewer>
+
+ <Border Grid.Row="2" BorderBrush="#D0D0D0" BorderThickness="0,1,0,0" Padding="4,2">
+ <TextBlock Text="{Binding Status}"/>
+ </Border>
+ </Grid>
+ </Border>
+ </Grid>
+</Window>
diff --git a/XCoderAv/Views/IconToolWindow.axaml.cs b/XCoderAv/Views/IconToolWindow.axaml.cs
new file mode 100644
index 0000000..ca6af81
--- /dev/null
+++ b/XCoderAv/Views/IconToolWindow.axaml.cs
@@ -0,0 +1,15 @@
+using Avalonia.Controls;
+using XCoderAv.ViewModels;
+
+namespace XCoderAv.Views;
+
+/// <summary>图标水印处理工具窗口</summary>
+public partial class IconToolWindow : Window
+{
+ /// <summary>实例化图标水印处理工具窗口</summary>
+ public IconToolWindow()
+ {
+ InitializeComponent();
+ DataContext = new IconToolViewModel();
+ }
+}
diff --git a/XCoderAv/Views/MqttWindow.axaml b/XCoderAv/Views/MqttWindow.axaml
new file mode 100644
index 0000000..774a838
--- /dev/null
+++ b/XCoderAv/Views/MqttWindow.axaml
@@ -0,0 +1,180 @@
+<Window xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d" d:DesignWidth="1100" d:DesignHeight="750"
+ x:Class="XCoderAv.Views.MqttWindow"
+ Title="MQTT 客户端" Width="1100" Height="750"
+ WindowStartupLocation="CenterOwner">
+ <Window.Styles>
+ <Style Selector="Border.GroupBorder">
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="Margin" Value="0,0,0,6"/>
+ </Style>
+ <Style Selector="TextBlock.SectionTitle">
+ <Setter Property="FontWeight" Value="Bold"/>
+ <Setter Property="Margin" Value="4,3"/>
+ <Setter Property="FontSize" Value="13"/>
+ </Style>
+ <Style Selector="TextBox.ConfigTextBox">
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Height" Value="26"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ </Style>
+ <Style Selector="Button.ActionButton">
+ <Setter Property="Height" Value="30"/>
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Padding" Value="8,0"/>
+ </Style>
+ <Style Selector="Button.SendButton">
+ <Setter Property="Height" Value="50"/>
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="FontSize" Value="14"/>
+ <Setter Property="FontWeight" Value="Bold"/>
+ </Style>
+ </Window.Styles>
+
+ <Grid Margin="6">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="300"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- ============ 左侧:配置面板 ============ -->
+ <ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto" Margin="0,0,4,0">
+ <StackPanel>
+ <!-- 连接配置 -->
+ <Border Classes="GroupBorder">
+ <StackPanel>
+ <TextBlock Text="连接配置" Classes="SectionTitle"/>
+
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="50"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="地址" VerticalAlignment="Center" Margin="4,2"/>
+ <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Server}" Classes="ConfigTextBox"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="标识" VerticalAlignment="Center" Margin="4,2"/>
+ <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding ClientId}" Classes="ConfigTextBox"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="用户" VerticalAlignment="Center" Margin="4,2"/>
+ <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding UserName}" Classes="ConfigTextBox"/>
+
+ <TextBlock Grid.Row="3" Grid.Column="0" Text="密码" VerticalAlignment="Center" Margin="4,2"/>
+ <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Password}" Classes="ConfigTextBox"/>
+
+ <Button Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2"
+ Content="{Binding ConnectButtonText}" Command="{Binding ToggleConnectCommand}"
+ Classes="ActionButton" Margin="4,4"/>
+ </Grid>
+ </StackPanel>
+ </Border>
+
+ <!-- 订阅配置 -->
+ <Border Classes="GroupBorder">
+ <StackPanel>
+ <TextBlock Text="订阅配置" Classes="SectionTitle"/>
+
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="50"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="主题" VerticalAlignment="Center" Margin="4,2"/>
+ <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding SubscribeTopic}" Classes="ConfigTextBox"/>
+ <ComboBox Grid.Row="0" Grid.Column="2" ItemsSource="{Binding QosOptions}"
+ SelectedIndex="{Binding SubscribeQos}" Width="60" Margin="2"/>
+
+ <Button Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3"
+ Content="订阅" Command="{Binding SubscribeCommand}"
+ Classes="ActionButton" Margin="4,4"/>
+ </Grid>
+ </StackPanel>
+ </Border>
+ </StackPanel>
+ </ScrollViewer>
+
+ <!-- ============ 右侧:发布 + 日志 ============ -->
+ <Grid Grid.Column="1">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <!-- 发布区 -->
+ <Border Grid.Row="0" Classes="GroupBorder">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <Grid Grid.Row="0">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="50"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ <TextBlock Grid.Column="0" Text="主题" VerticalAlignment="Center" Margin="4,2"/>
+ <TextBox Grid.Column="1" Text="{Binding PublishTopic}" Classes="ConfigTextBox"/>
+ <ComboBox Grid.Column="2" ItemsSource="{Binding QosOptions}"
+ SelectedIndex="{Binding PublishQos}" Width="60" Margin="2"/>
+ <CheckBox Grid.Column="3" Content="保留" IsChecked="{Binding Retain}" VerticalAlignment="Center" Margin="4,0"/>
+ </Grid>
+
+ <TextBox Grid.Row="1" Text="{Binding PublishText}" AcceptsReturn="True"
+ MinHeight="80" FontFamily="Consolas" Margin="2"/>
+
+ <Button Grid.Row="2" Content="发布" Command="{Binding PublishCommand}"
+ Classes="SendButton" Height="40" Margin="4,4"/>
+ </Grid>
+ </Border>
+
+ <!-- 日志区 -->
+ <Border Grid.Row="1" Classes="GroupBorder">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ <TextBlock Text="日志" FontWeight="Bold" Margin="4,2"/>
+ <Button Grid.Column="1" Content="清空" Command="{Binding ClearLogCommand}"
+ Height="26" Margin="2" Padding="8,0"/>
+ </Grid>
+ <TextBox Grid.Row="1" x:Name="txtLog" IsReadOnly="True"
+ AcceptsReturn="True"
+ FontFamily="Consolas" FontSize="12" Background="#1E1E1E" Foreground="#D4D4D4"
+ Margin="2"/>
+ </Grid>
+ </Border>
+ </Grid>
+ </Grid>
+</Window>
diff --git a/XCoderAv/Views/MqttWindow.axaml.cs b/XCoderAv/Views/MqttWindow.axaml.cs
new file mode 100644
index 0000000..74f1e54
--- /dev/null
+++ b/XCoderAv/Views/MqttWindow.axaml.cs
@@ -0,0 +1,37 @@
+using Avalonia.Controls;
+using Avalonia.Threading;
+using XCoderAv.ViewModels;
+
+namespace XCoderAv.Views;
+
+/// <summary>MQTT 客户端窗口</summary>
+public partial class MqttWindow : Window
+{
+ /// <summary>实例化 MQTT 客户端窗口</summary>
+ public MqttWindow()
+ {
+ InitializeComponent();
+
+ var vm = new MqttViewModel();
+ vm.OnLog = OnLog;
+ DataContext = vm;
+ }
+
+ private void OnLog(String msg)
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (msg == "__CLEAR__")
+ {
+ txtLog.Text = "";
+ return;
+ }
+
+ txtLog.Text += msg + "\n";
+ if (txtLog.Text.Length > 10000)
+ txtLog.Text = txtLog.Text[^5000..];
+
+ txtLog.CaretIndex = txtLog.Text.Length;
+ });
+ }
+}
diff --git a/XCoderAv/Views/NetworkWindow.axaml b/XCoderAv/Views/NetworkWindow.axaml
new file mode 100644
index 0000000..7944cfe
--- /dev/null
+++ b/XCoderAv/Views/NetworkWindow.axaml
@@ -0,0 +1,204 @@
+<Window xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:vm="clr-namespace:XCoderAv.ViewModels"
+ mc:Ignorable="d" d:DesignWidth="1100" d:DesignHeight="750"
+ x:Class="XCoderAv.Views.NetworkWindow"
+ Title="网络调试工具" Width="1100" Height="750"
+ WindowStartupLocation="CenterOwner">
+ <Window.Styles>
+ <Style Selector="Border.GroupBorder">
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="Margin" Value="0,0,0,6"/>
+ </Style>
+ <Style Selector="TextBlock.SectionTitle">
+ <Setter Property="FontWeight" Value="Bold"/>
+ <Setter Property="Margin" Value="4,3"/>
+ <Setter Property="FontSize" Value="13"/>
+ </Style>
+ <Style Selector="TextBox.ConfigTextBox">
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Height" Value="26"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ </Style>
+ <Style Selector="Button.ActionButton">
+ <Setter Property="Height" Value="30"/>
+ <Setter Property="Margin" Value="2"/>
+ </Style>
+ <Style Selector="Button.SendButton">
+ <Setter Property="Height" Value="50"/>
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="FontSize" Value="14"/>
+ <Setter Property="FontWeight" Value="Bold"/>
+ </Style>
+ <Style Selector="TextBox.SmallNumeric">
+ <Setter Property="Width" Value="60"/>
+ <Setter Property="Height" Value="26"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ <Setter Property="HorizontalContentAlignment" Value="Right"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="Margin" Value="2"/>
+ </Style>
+ </Window.Styles>
+
+ <Grid Margin="6">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="300"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- ============ 左侧:配置面板 ============ -->
+ <ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto" Margin="0,0,4,0">
+ <StackPanel>
+ <!-- 工作模式 -->
+ <Border Classes="GroupBorder">
+ <StackPanel>
+ <TextBlock Text="工作模式" Classes="SectionTitle"/>
+ <ComboBox ItemsSource="{Binding Modes}" SelectedIndex="{Binding SelectedModeIndex}"
+ Margin="4,2" Height="30"/>
+ </StackPanel>
+ </Border>
+
+ <!-- 连接配置 -->
+ <Border Classes="GroupBorder">
+ <StackPanel>
+ <TextBlock Text="连接配置" Classes="SectionTitle"/>
+
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="50"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="本地" VerticalAlignment="Center" Margin="4,2"/>
+ <ComboBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding LocalAddresses}"
+ SelectedItem="{Binding LocalAddress}" Margin="2"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="远程" VerticalAlignment="Center" Margin="4,2"/>
+ <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding RemoteAddress}" Classes="ConfigTextBox"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="端口" VerticalAlignment="Center" Margin="4,2"/>
+ <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Port}" Classes="ConfigTextBox"/>
+ </Grid>
+
+ <Button Content="{Binding ConnectButtonText}" Command="{Binding ToggleConnectCommand}"
+ Classes="ActionButton" Margin="4,4"/>
+ </StackPanel>
+ </Border>
+
+ <!-- 日志选项 -->
+ <Border Classes="GroupBorder">
+ <StackPanel>
+ <TextBlock Text="日志选项" Classes="SectionTitle"/>
+ <CheckBox Content="业务日志" IsChecked="{Binding ShowLog}" Margin="4,2"/>
+ <CheckBox Content="Socket日志" IsChecked="{Binding ShowSocketLog}" Margin="4,2"/>
+ <CheckBox Content="显示发送" IsChecked="{Binding ShowSend}" Margin="4,2"/>
+ <CheckBox Content="显示接收" IsChecked="{Binding ShowReceive}" Margin="4,2"/>
+ <CheckBox Content="显示统计" IsChecked="{Binding ShowStat}" Margin="4,2"/>
+ <CheckBox Content="接收字符串" IsChecked="{Binding ShowReceiveString}" Margin="4,2"/>
+ </StackPanel>
+ </Border>
+ </StackPanel>
+ </ScrollViewer>
+
+ <!-- ============ 右侧:收发区 ============ -->
+ <Grid Grid.Column="1">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <!-- 状态栏 -->
+ <Border Grid.Row="0" BorderBrush="#D0D0D0" BorderThickness="0,0,0,1" Padding="4,2" Margin="0,0,0,4">
+ <TextBlock>
+ <Run Text="状态:"/>
+ <Run Text="{Binding ConnectButtonText}"/>
+ <Run Text=" | 接收:"/>
+ <Run Text="{Binding ReceivedBytes}"/>
+ <Run Text=" | 发送:"/>
+ <Run Text="{Binding SentBytes}"/>
+ <Run Text=" | 会话:"/>
+ <Run Text="{Binding SessionCount}"/>
+ </TextBlock>
+ </Border>
+
+ <!-- 发送区 -->
+ <Border Grid.Row="1" Classes="GroupBorder">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBox Grid.Column="0" Text="{Binding SendText}" AcceptsReturn="True"
+ MinHeight="60" FontFamily="Consolas" Margin="2"/>
+ <Button Grid.Column="1" Content="发送" Command="{Binding SendCommand}"
+ Classes="SendButton" Width="100"/>
+ <StackPanel Grid.Column="2" Margin="4,0">
+ <CheckBox Content="HEX发送" IsChecked="{Binding HexSend}" Margin="2"/>
+ <StackPanel Orientation="Horizontal">
+ <TextBlock Text="次数" VerticalAlignment="Center" Margin="2,0"/>
+ <TextBox Text="{Binding SendTimes}" Classes="SmallNumeric"/>
+ </StackPanel>
+ <StackPanel Orientation="Horizontal">
+ <TextBlock Text="间隔" VerticalAlignment="Center" Margin="2,0"/>
+ <TextBox Text="{Binding SendSleep}" Classes="SmallNumeric"/>
+ </StackPanel>
+ <StackPanel Orientation="Horizontal">
+ <TextBlock Text="线程" VerticalAlignment="Center" Margin="2,0"/>
+ <TextBox Text="{Binding SendThreads}" Classes="SmallNumeric"/>
+ </StackPanel>
+ </StackPanel>
+ </Grid>
+ </Grid>
+ </Border>
+
+ <!-- 日志区 -->
+ <Border Grid.Row="3" Classes="GroupBorder">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ <TextBlock Text="日志" FontWeight="Bold" Margin="4,2"/>
+ <Button Grid.Column="1" Content="清空" Command="{Binding ClearReceiveLogCommand}"
+ Height="26" Margin="2" Padding="8,0"/>
+ </Grid>
+ <TextBox Grid.Row="2" x:Name="txtLog" IsReadOnly="True"
+ AcceptsReturn="True"
+ FontFamily="Consolas" FontSize="12" Background="#1E1E1E" Foreground="#D4D4D4"
+ Margin="2"/>
+ </Grid>
+ </Border>
+ </Grid>
+ </Grid>
+</Window>
diff --git a/XCoderAv/Views/NetworkWindow.axaml.cs b/XCoderAv/Views/NetworkWindow.axaml.cs
new file mode 100644
index 0000000..5f5b421
--- /dev/null
+++ b/XCoderAv/Views/NetworkWindow.axaml.cs
@@ -0,0 +1,37 @@
+using Avalonia.Controls;
+using Avalonia.Threading;
+using XCoderAv.ViewModels;
+
+namespace XCoderAv.Views;
+
+/// <summary>网络调试工具窗口</summary>
+public partial class NetworkWindow : Window
+{
+ /// <summary>实例化网络调试工具窗口</summary>
+ public NetworkWindow()
+ {
+ InitializeComponent();
+
+ var vm = new NetworkViewModel();
+ vm.OnLog = OnLog;
+ DataContext = vm;
+ }
+
+ private void OnLog(String msg)
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (msg == "__CLEAR__")
+ {
+ txtLog.Text = "";
+ return;
+ }
+
+ txtLog.Text += msg + "\n";
+ if (txtLog.Text.Length > 10000)
+ txtLog.Text = txtLog.Text[^5000..];
+
+ txtLog.CaretIndex = txtLog.Text.Length;
+ });
+ }
+}
diff --git a/XCoderAv/Views/RegexWindow.axaml b/XCoderAv/Views/RegexWindow.axaml
new file mode 100644
index 0000000..49fe53e
--- /dev/null
+++ b/XCoderAv/Views/RegexWindow.axaml
@@ -0,0 +1,220 @@
+<Window xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:vm="clr-namespace:XCoderAv.ViewModels"
+ xmlns:models="clr-namespace:XCoderAv.Models"
+ mc:Ignorable="d" d:DesignWidth="1100" d:DesignHeight="700"
+ x:Class="XCoderAv.Views.RegexWindow"
+ Title="正则表达式" Width="1100" Height="700"
+ WindowStartupLocation="CenterOwner">
+ <Window.Styles>
+ <Style Selector="TextBox.CodeTextBox">
+ <Setter Property="FontFamily" Value="Consolas"/>
+ <Setter Property="FontSize" Value="14"/>
+ <Setter Property="AcceptsReturn" Value="True"/>
+ <Setter Property="AcceptsTab" Value="True"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ </Style>
+ <Style Selector="ListBox.ResultListView">
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="Margin" Value="2"/>
+ </Style>
+ </Window.Styles>
+
+ <Grid Margin="8">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ <RowDefinition Height="3*"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <!-- 第1行:正则表达式输入 -->
+ <Border Grid.Row="0" BorderBrush="#D0D0D0" BorderThickness="1" Background="#FFF5E6" Margin="0,0,0,4">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="正则表达式" FontWeight="Bold" Margin="4,2"/>
+ <TextBox Grid.Row="1" Text="{Binding Pattern}"
+ Classes="CodeTextBox" Background="#FFF5E6" MinHeight="60"/>
+ </Grid>
+ </Border>
+
+ <!-- 第2行:选项 + 模式切换 + 执行按钮 -->
+ <Grid Grid.Row="1" Margin="0,0,0,4">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <CheckBox Grid.Column="0" Content="忽略大小写" IsChecked="{Binding IgnoreCase}" Margin="4,0,8,0"/>
+ <CheckBox Grid.Column="1" Content="多行模式" IsChecked="{Binding Multiline}" Margin="0,0,8,0"/>
+ <CheckBox Grid.Column="2" Content="单行模式" IsChecked="{Binding Singleline}" Margin="0,0,8,0"/>
+ <CheckBox Grid.Column="3" Content="忽略空白" IsChecked="{Binding IgnorePatternWhitespace}" Margin="0,0,8,0"/>
+
+ <RadioButton Grid.Column="4" Content="匹配" IsChecked="{Binding IsMatchMode}" Margin="8,0,4,0" VerticalAlignment="Center"/>
+ <RadioButton Grid.Column="5" Content="替换" IsChecked="{Binding IsReplaceMode}" Margin="4,0,8,0" VerticalAlignment="Center"/>
+
+ <Button Grid.Column="6" Content="{Binding ExecuteButtonText}" Command="{Binding ExecuteCommand}"
+ MinWidth="100" Height="28" HorizontalAlignment="Right" Margin="0,0,4,0"/>
+ </Grid>
+
+ <!-- 第3行:替换输入(替换模式可见) -->
+ <Border Grid.Row="2" BorderBrush="#D0D0D0" BorderThickness="1" Background="#E8FFE8" Margin="0,0,0,4"
+ IsVisible="{Binding IsReplaceMode}">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="替换为" FontWeight="Bold" Margin="4,2"/>
+ <TextBox Grid.Row="1" Text="{Binding Replacement}"
+ Classes="CodeTextBox" Background="#E8FFE8" MinHeight="40"/>
+ </Grid>
+ </Border>
+
+ <!-- 第4行:数据源输入 -->
+ <Border Grid.Row="3" BorderBrush="#D0D0D0" BorderThickness="1" Background="#FFFFF0" Margin="0,0,0,4">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="数据源" FontWeight="Bold" Margin="4,2"/>
+ <TextBox Grid.Row="1" Text="{Binding SourceText}"
+ Classes="CodeTextBox" Background="#FFFFF0"/>
+ </Grid>
+ </Border>
+
+ <!-- 第5行:匹配结果 -->
+ <Grid Grid.Row="4" Margin="0">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="4"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- 匹配列表 -->
+ <Grid Grid.Column="0">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="匹配结果" FontWeight="Bold" Margin="4,2"/>
+ <ListBox Grid.Row="1" ItemsSource="{Binding Matches}"
+ SelectedItem="{Binding SelectedMatch}" Classes="ResultListView">
+ <ListBox.ItemTemplate>
+ <DataTemplate>
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ <TextBlock Grid.Column="0" Text="{Binding Index}" Width="30" FontWeight="Bold"/>
+ <TextBlock Grid.Column="1" Text="{Binding Value}" FontFamily="Consolas"/>
+ <TextBlock Grid.Column="2" Text="{Binding Location}" Foreground="Gray"/>
+ </Grid>
+ </DataTemplate>
+ </ListBox.ItemTemplate>
+ </ListBox>
+ </Grid>
+
+ <!-- 分组 + 捕获 -->
+ <Grid Grid.Column="2">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="*"/>
+ <RowDefinition Height="4"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <Grid Grid.Row="0">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="分组" FontWeight="Bold" Margin="4,2"/>
+ <ListBox Grid.Row="1" ItemsSource="{Binding Groups}"
+ SelectedItem="{Binding SelectedGroup}" Classes="ResultListView">
+ <ListBox.ItemTemplate>
+ <DataTemplate>
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ <TextBlock Grid.Column="0" Text="{Binding Name}" Width="40" FontWeight="Bold"/>
+ <TextBlock Grid.Column="1" Text="{Binding Value}" FontFamily="Consolas"/>
+ <TextBlock Grid.Column="2" Text="{Binding Location}" Foreground="Gray"/>
+ </Grid>
+ </DataTemplate>
+ </ListBox.ItemTemplate>
+ </ListBox>
+ </Grid>
+
+ <Grid Grid.Row="2">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="捕获" FontWeight="Bold" Margin="4,2"/>
+ <ListBox Grid.Row="1" ItemsSource="{Binding Captures}"
+ SelectedItem="{Binding SelectedCapture}" Classes="ResultListView">
+ <ListBox.ItemTemplate>
+ <DataTemplate>
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ <TextBlock Grid.Column="0" Text="{Binding Index}" Width="30" FontWeight="Bold"/>
+ <TextBlock Grid.Column="1" Text="{Binding Value}" FontFamily="Consolas"/>
+ <TextBlock Grid.Column="2" Text="{Binding Location}" Foreground="Gray"/>
+ </Grid>
+ </DataTemplate>
+ </ListBox.ItemTemplate>
+ </ListBox>
+ </Grid>
+ </Grid>
+ </Grid>
+
+ <!-- 第6行:状态栏 -->
+ <Border Grid.Row="5" BorderBrush="#D0D0D0" BorderThickness="0,1,0,0" Padding="4,2" Margin="0,4,0,0">
+ <TextBlock Text="{Binding Status}"/>
+ </Border>
+
+ <!-- 第7行:批量替换面板 -->
+ <Border Grid.Row="6" BorderBrush="#D0D0D0" BorderThickness="1" Background="#E8E8FF" Margin="0,4,0,0"
+ IsVisible="{Binding IsReplaceMode}">
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Column="0" Text="目录:" VerticalAlignment="Center" Margin="4,2"/>
+ <TextBox Grid.Column="1" Text="{Binding DirectoryPath}" Margin="2" Height="26"/>
+ <TextBox Grid.Column="2" Text="{Binding FileFilter}" Width="80" Margin="2" Height="26"/>
+ <Button Grid.Column="3" Content="批量替换" Command="{Binding BatchReplaceCommand}"
+ Height="26" Margin="2" Padding="8,0"/>
+ </Grid>
+ </Border>
+ </Grid>
+</Window>
diff --git a/XCoderAv/Views/RegexWindow.axaml.cs b/XCoderAv/Views/RegexWindow.axaml.cs
new file mode 100644
index 0000000..42c2e12
--- /dev/null
+++ b/XCoderAv/Views/RegexWindow.axaml.cs
@@ -0,0 +1,15 @@
+using Avalonia.Controls;
+using XCoderAv.ViewModels;
+
+namespace XCoderAv.Views;
+
+/// <summary>正则表达式工具窗口</summary>
+public partial class RegexWindow : Window
+{
+ /// <summary>实例化正则表达式工具窗口</summary>
+ public RegexWindow()
+ {
+ InitializeComponent();
+ DataContext = new RegexViewModel();
+ }
+}
diff --git a/XCoderAv/Views/SecurityWindow.axaml b/XCoderAv/Views/SecurityWindow.axaml
new file mode 100644
index 0000000..77f30ca
--- /dev/null
+++ b/XCoderAv/Views/SecurityWindow.axaml
@@ -0,0 +1,164 @@
+<Window xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d" d:DesignWidth="1100" d:DesignHeight="750"
+ x:Class="XCoderAv.Views.SecurityWindow"
+ Title="加密解密" Width="1100" Height="750"
+ WindowStartupLocation="CenterOwner">
+ <Window.Styles>
+ <Style Selector="Button.FuncButton">
+ <Setter Property="Width" Value="110"/>
+ <Setter Property="Height" Value="46"/>
+ <Setter Property="Margin" Value="3"/>
+ <Setter Property="FontSize" Value="13"/>
+ </Style>
+ <Style Selector="TextBox.CodeTextBox">
+ <Setter Property="FontFamily" Value="Consolas"/>
+ <Setter Property="FontSize" Value="13"/>
+ <Setter Property="AcceptsReturn" Value="True"/>
+ <Setter Property="AcceptsTab" Value="True"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="TextWrapping" Value="Wrap"/>
+ </Style>
+ <Style Selector="Border.GroupBorder">
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="Margin" Value="0,0,0,4"/>
+ </Style>
+ </Window.Styles>
+
+ <Grid Margin="6">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="240"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- 左侧:功能按钮区 -->
+ <Border Grid.Column="0" BorderBrush="#D0D0D0" BorderThickness="1" Margin="0,0,4,0">
+ <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
+ <WrapPanel Orientation="Horizontal" Margin="4">
+ <Button Content="HEX编码" Classes="FuncButton" Command="{Binding HexEncodeCommand}"/>
+ <Button Content="HEX解码" Classes="FuncButton" Command="{Binding HexDecodeCommand}"/>
+ <Button Content="Base64编码" Classes="FuncButton" Command="{Binding Base64EncodeCommand}"/>
+ <Button Content="Base64解码" Classes="FuncButton" Command="{Binding Base64DecodeCommand}"/>
+ <Button Content="MD5_32" Classes="FuncButton" Command="{Binding MD5_32Command}"/>
+ <Button Content="MD5_16" Classes="FuncButton" Command="{Binding MD5_16Command}"/>
+ <Button Content="SHA1" Classes="FuncButton" Command="{Binding SHA1Command}"/>
+ <Button Content="SHA256" Classes="FuncButton" Command="{Binding SHA256Command}"/>
+ <Button Content="SHA384" Classes="FuncButton" Command="{Binding SHA384Command}"/>
+ <Button Content="SHA512" Classes="FuncButton" Command="{Binding SHA512Command}"/>
+ <Button Content="CRC_32" Classes="FuncButton" Command="{Binding CRC_32Command}"/>
+ <Button Content="CRC_16" Classes="FuncButton" Command="{Binding CRC_16Command}"/>
+ <Button Content="RSA加密" Classes="FuncButton" Command="{Binding RSAEncryptCommand}"/>
+ <Button Content="RSA解密" Classes="FuncButton" Command="{Binding RSADecryptCommand}"/>
+ <Button Content="DSA签名" Classes="FuncButton" Command="{Binding DSASignCommand}"/>
+ <Button Content="DSA验证" Classes="FuncButton" Command="{Binding DSAVerifyCommand}"/>
+ <Button Content="Url编码" Classes="FuncButton" Command="{Binding UrlEncodeCommand}"/>
+ <Button Content="Url解码" Classes="FuncButton" Command="{Binding UrlDecodeCommand}"/>
+ <Button Content="Html编码" Classes="FuncButton" Command="{Binding HtmlEncodeCommand}"/>
+ <Button Content="Html解码" Classes="FuncButton" Command="{Binding HtmlDecodeCommand}"/>
+ <Button Content="时间戳" Classes="FuncButton" Command="{Binding TimestampCommand}"/>
+ <Button Content="机器信息" Classes="FuncButton" Command="{Binding ComputerInfoCommand}"/>
+ <Button Content="雪花Id" Classes="FuncButton" Command="{Binding SnowflakeCommand}"/>
+ <Button Content="JWT令牌" Classes="FuncButton" Command="{Binding JwtTokenCommand}"/>
+ <Button Content="版本号" Classes="FuncButton" Command="{Binding VersionCommand}"/>
+ <Button Content="TraceId" Classes="FuncButton" Command="{Binding TraceIdCommand}"/>
+ </WrapPanel>
+ </ScrollViewer>
+ </Border>
+
+ <!-- 右侧:输入输出区 -->
+ <Grid Grid.Column="1">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <!-- 原文输入 -->
+ <Border Grid.Row="0" Classes="GroupBorder">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+ <TextBlock Grid.Column="0" Text="原文" FontWeight="Bold" Margin="4,2"/>
+ <RadioButton Grid.Column="1" Content="字符串" IsChecked="{Binding IsSourceString}" Margin="4,0"/>
+ <RadioButton Grid.Column="2" Content="HEX" IsChecked="{Binding IsSourceHex}" Margin="4,0"
+ IsEnabled="{Binding SourceHexEnabled}"/>
+ <RadioButton Grid.Column="3" Content="Base64" IsChecked="{Binding IsSourceBase64}" Margin="4,0"
+ IsEnabled="{Binding SourceBase64Enabled}"/>
+ </Grid>
+ <TextBox Grid.Row="2" Text="{Binding SourceText}" Classes="CodeTextBox" MinHeight="60"/>
+ </Grid>
+ </Border>
+
+ <!-- 密码输入 -->
+ <Border Grid.Row="1" Classes="GroupBorder">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ <TextBlock Grid.Column="0" Text="密码" FontWeight="Bold" Margin="4,2"/>
+ <RadioButton Grid.Column="1" Content="字符串" IsChecked="{Binding IsPassString}" Margin="4,0"/>
+ <RadioButton Grid.Column="2" Content="HEX" IsChecked="{Binding IsPassHex}" Margin="4,0"/>
+ <RadioButton Grid.Column="3" Content="Base64" IsChecked="{Binding IsPassBase64}" Margin="4,0"/>
+ <Button Grid.Column="4" Content="上下互换" Command="{Binding ExchangeCommand}"
+ Height="26" Margin="2" Padding="8,0"/>
+ </Grid>
+ <TextBox Grid.Row="2" Text="{Binding PassText}" Classes="CodeTextBox" MinHeight="40"/>
+ </Grid>
+ </Border>
+
+ <!-- 输出格式 -->
+ <Border Grid.Row="2" Classes="GroupBorder" Padding="4">
+ <StackPanel Orientation="Horizontal">
+ <TextBlock Text="输出格式" FontWeight="Bold" VerticalAlignment="Center" Margin="4,0"/>
+ <CheckBox Content="字符串" IsChecked="{Binding IsResultString}" Margin="8,0"/>
+ <CheckBox Content="HEX" IsChecked="{Binding IsResultHex}" Margin="8,0"/>
+ <CheckBox Content="Base64" IsChecked="{Binding IsResultBase64}" Margin="8,0"/>
+ </StackPanel>
+ </Border>
+
+ <!-- 结果输出 -->
+ <Border Grid.Row="3" Classes="GroupBorder">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="结果" FontWeight="Bold" Margin="4,2"/>
+ <TextBox Grid.Row="1" Text="{Binding ResultText}" Classes="CodeTextBox"
+ IsReadOnly="True" Background="#FFFFF0"/>
+ </Grid>
+ </Border>
+
+ <!-- 状态栏 -->
+ <Border Grid.Row="4" BorderBrush="#D0D0D0" BorderThickness="0,1,0,0" Padding="4,2">
+ <TextBlock Text="提示:RSA加密时可自动生成密钥对"/>
+ </Border>
+ </Grid>
+ </Grid>
+</Window>
diff --git a/XCoderAv/Views/SecurityWindow.axaml.cs b/XCoderAv/Views/SecurityWindow.axaml.cs
new file mode 100644
index 0000000..39a9acc
--- /dev/null
+++ b/XCoderAv/Views/SecurityWindow.axaml.cs
@@ -0,0 +1,15 @@
+using Avalonia.Controls;
+using XCoderAv.ViewModels;
+
+namespace XCoderAv.Views;
+
+/// <summary>加密解密工具窗口</summary>
+public partial class SecurityWindow : Window
+{
+ /// <summary>实例化加密解密工具窗口</summary>
+ public SecurityWindow()
+ {
+ InitializeComponent();
+ DataContext = new SecurityViewModel();
+ }
+}
diff --git a/XCoderAv/Views/SpeechWindow.axaml b/XCoderAv/Views/SpeechWindow.axaml
new file mode 100644
index 0000000..1770ae2
--- /dev/null
+++ b/XCoderAv/Views/SpeechWindow.axaml
@@ -0,0 +1,112 @@
+<Window xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="500"
+ x:Class="XCoderAv.Views.SpeechWindow"
+ Title="语音助手" Width="700" Height="500"
+ WindowStartupLocation="CenterOwner">
+ <Window.Styles>
+ <Style Selector="Border.GroupBorder">
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="Margin" Value="0,0,0,6"/>
+ </Style>
+ <Style Selector="TextBlock.SectionTitle">
+ <Setter Property="FontWeight" Value="Bold"/>
+ <Setter Property="Margin" Value="4,3"/>
+ <Setter Property="FontSize" Value="13"/>
+ </Style>
+ <Style Selector="Button.ActionButton">
+ <Setter Property="Height" Value="34"/>
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Padding" Value="12,0"/>
+ <Setter Property="FontSize" Value="13"/>
+ </Style>
+ </Window.Styles>
+
+ <Grid Margin="8">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <!-- 语音设置 -->
+ <Border Grid.Row="0" Classes="GroupBorder">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <TextBlock Text="语音设置" Classes="SectionTitle"/>
+
+ <Grid Grid.Row="1" Margin="4,0,4,2">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Column="0" Text="语音角色" VerticalAlignment="Center" Width="70"/>
+ <ComboBox Grid.Column="1" ItemsSource="{Binding Voices}"
+ SelectedIndex="{Binding SelectedVoiceIndex}"
+ Height="28" Margin="2"/>
+ </Grid>
+
+ <Grid Grid.Row="2" Margin="4,0,4,4">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Column="0" Text="音量" VerticalAlignment="Center" Width="40"/>
+ <Slider Grid.Column="1" Minimum="0" Maximum="100"
+ Value="{Binding Volume}" Margin="4,0"
+ TickFrequency="10" IsSnapToTickEnabled="True"
+ VerticalAlignment="Center"/>
+ <TextBlock Grid.Column="2" Text="{Binding Volume}"
+ VerticalAlignment="Center" Width="40" TextAlignment="Center"/>
+
+ <TextBlock Grid.Column="3" Text="语速" VerticalAlignment="Center" Width="40"/>
+ <Slider Grid.Column="4" Minimum="-10" Maximum="10"
+ Value="{Binding Rate}" Margin="4,0"
+ TickFrequency="1" IsSnapToTickEnabled="True"
+ VerticalAlignment="Center"/>
+ <TextBlock Grid.Column="5" Text="{Binding Rate}"
+ VerticalAlignment="Center" Width="30" TextAlignment="Center"/>
+ </Grid>
+ </Grid>
+ </Border>
+
+ <!-- 朗读内容 -->
+ <Border Grid.Row="1" Classes="GroupBorder">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="朗读内容" Classes="SectionTitle"/>
+ <TextBox Grid.Row="1" Text="{Binding SpeakText}" AcceptsReturn="True"
+ MinHeight="60" FontSize="14" Margin="4,0,4,4"/>
+ </Grid>
+ </Border>
+
+ <!-- 操作按钮 -->
+ <StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center"
+ VerticalAlignment="Center" Spacing="10">
+ <Button Content="朗读" Command="{Binding SpeakCommand}" Classes="ActionButton" Width="100"/>
+ <Button Content="停止" Command="{Binding StopCommand}" Classes="ActionButton" Width="100"/>
+ <Button Content="保存 WAV" Command="{Binding SaveWavCommand}" Classes="ActionButton" Width="100"/>
+ </StackPanel>
+
+ <!-- 状态栏 -->
+ <Border Grid.Row="3" BorderBrush="#D0D0D0" BorderThickness="0,1,0,0" Padding="4,2">
+ <TextBlock Text="{Binding Status}"/>
+ </Border>
+ </Grid>
+</Window>
diff --git a/XCoderAv/Views/SpeechWindow.axaml.cs b/XCoderAv/Views/SpeechWindow.axaml.cs
new file mode 100644
index 0000000..1c0a235
--- /dev/null
+++ b/XCoderAv/Views/SpeechWindow.axaml.cs
@@ -0,0 +1,15 @@
+using Avalonia.Controls;
+using XCoderAv.ViewModels;
+
+namespace XCoderAv.Views;
+
+/// <summary>语音助手窗口</summary>
+public partial class SpeechWindow : Window
+{
+ /// <summary>实例化语音助手窗口</summary>
+ public SpeechWindow()
+ {
+ InitializeComponent();
+ DataContext = new SpeechViewModel();
+ }
+}
diff --git a/XCoderAv/XCoderAv.csproj b/XCoderAv/XCoderAv.csproj
index a9f0206..11ea373 100644
--- a/XCoderAv/XCoderAv.csproj
+++ b/XCoderAv/XCoderAv.csproj
@@ -1,41 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
- <TargetFramework>netcoreapp2.1</TargetFramework>
- <RootNamespace>XCoder</RootNamespace>
- <AssemblyName>XCoder</AssemblyName>
- <AssemblyTitle>新生命码神工具</AssemblyTitle>
- <Description>众多开发者工具</Description>
+ <TargetFramework>net8.0</TargetFramework>
+ <RootNamespace>XCoderAv</RootNamespace>
+ <AssemblyName>XCoderAv</AssemblyName>
+ <AssemblyTitle>新生命码神工具(跨平台)</AssemblyTitle>
+ <Description>码神工具。代码生成、网络工具、API工具、串口工具、正则工具、图标工具、加解密工具、地图接口</Description>
<Company>新生命开发团队</Company>
<Copyright>©2002-2026 新生命开发团队</Copyright>
- <Version>8.0.2019.1007</Version>
- <FileVersion>8.0.2019.1007</FileVersion>
- <AssemblyVersion>8.0.*</AssemblyVersion>
+ <VersionPrefix>8.2</VersionPrefix>
+ <VersionSuffix>$([System.DateTime]::Now.ToString(`yyyy.MMdd`))</VersionSuffix>
+ <Version>$(VersionPrefix).$(VersionSuffix)</Version>
+ <FileVersion>$(Version)</FileVersion>
+ <AssemblyVersion>$(VersionPrefix).*</AssemblyVersion>
<Deterministic>false</Deterministic>
- <ApplicationIcon>..\XCoder\leaf.ico</ApplicationIcon>
- <OutputPath>..\..\XCoder\</OutputPath>
- <DebugType>pdbonly</DebugType>
- <Optimize>true</Optimize>
- <DefineConstants>TRACE;NC30;__CORE__</DefineConstants>
+ <Nullable>enable</Nullable>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <LangVersion>latest</LangVersion>
+ <OutputPath>..\Bin\XCoderAv</OutputPath>
+ <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
- <PropertyGroup Condition="'$(Configuration)'=='Debug'">
- <DefineConstants>$(DefineConstants);DEBUG</DefineConstants>
- <DebugType>full</DebugType>
- <Optimize>false</Optimize>
- </PropertyGroup>
- <ItemGroup>
- <Compile Update="**\*.xaml.cs">
- <DependentUpon>%(Filename)</DependentUpon>
- </Compile>
- <AvaloniaResource Include="**\*.xaml">
- <SubType>Designer</SubType>
- </AvaloniaResource>
- </ItemGroup>
<ItemGroup>
- <PackageReference Include="Avalonia" Version="0.8.2" />
- <PackageReference Include="Avalonia.Desktop" Version="0.8.2" />
- <PackageReference Include="NewLife.Core" Version="8.4.2019.1007" />
- <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.0" />
+ <PackageReference Include="Avalonia" Version="11.2.3" />
+ <PackageReference Include="Avalonia.Desktop" Version="11.2.3" />
+ <PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.3" />
+ <PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.3" />
+ <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
+ <PackageReference Include="NewLife.Core" Version="11.15.2026.501" />
+ <PackageReference Include="NewLife.Stardust" Version="3.7.2026.501" />
+ <PackageReference Include="NewLife.XCode" Version="11.26.2026.501" />
+ <PackageReference Include="NewLife.Net" Version="4.4.2026.206" />
+ <PackageReference Include="NewLife.Remoting" Version="3.7.2026.501" />
+ <PackageReference Include="NewLife.Security" Version="11.15.2026.501" />
+ <PackageReference Include="NewLife.MQTT" Version="3.0.2026.501" />
+ <PackageReference Include="NewLife.Redis" Version="6.5.2026.501" />
+ <PackageReference Include="NewLife.Map" Version="2.6.2026.102" />
+ <PackageReference Include="NewLife.ModbusRTU" Version="2.0.2025.701" />
+ <PackageReference Include="SSH.NET" Version="2025.1.0" />
+ <PackageReference Include="System.IO.Ports" Version="10.0.7" />
</ItemGroup>
</Project>