1fbd6694c1050f86c8697b9f7022eb17192f6660..9d1660270272180cd71147d6ddd53d1382643d2e
2026-11-02 wenxiangchao
merge: 并入远程 Initial commit 作为历史根节点
9d1660 对比 | 目录
2026-11-02 wenxiangchao
feat: 集成CAN驱动/模拟器 + 仪器仪表注册/硬件调试模块
fe2aa4 对比 | 目录
2026-06-15 alielie
优化标准老化界面
f08871 对比 | 目录
2026-06-04 alielie
优化老化;
Dev
771a0e 对比 | 目录
2026-04-23 alielie
优化;
ef52fd 对比 | 目录
2026-04-16 alielie
优化;
1afe54 对比 | 目录
2026-03-02 alielie
优化
12e606 对比 | 目录
2025-11-25 alielie
优化一些东西;
006c98 对比 | 目录
2025-11-24 alielie
优化项目结构
9211ec 对比 | 目录
2025-11-21 alielie
优化
3a471a 对比 | 目录
2025-10-13 alielie
优化HMI;
e3868d 对比 | 目录
2025-10-10 alielie
优化bug;
15bbd6 对比 | 目录
2025-08-25 alielie
上传代码;
c4a3f8 对比 | 目录
2025-07-04 alielie
优化插件,完善TreeListView
0940f3 对比 | 目录
2025-06-12 alielie
新增SeqEditor
c1c377 对比 | 目录
2025-05-20 alielie
优化
99df05 对比 | 目录
2025-05-12 alielie
优化项目;
7bf89f 对比 | 目录
2025-04-10 alielie
优化插件结构;
6bcafc 对比 | 目录
2025-03-28 alielie
优化UI组件;
dedcf4 对比 | 目录
2025-03-26 alielie
优化UI,新增日志自动删除;
4cac1b 对比 | 目录
2025-03-23 alielie
新增DOControl
40014e 对比 | 目录
2025-03-18 alielie
Winform支持遮罩层;
a06138 对比 | 目录
2025-03-17 alielie
完善框架,新增两个close方法;
c401a2 对比 | 目录
2025-03-13 alielie
优化项目;
a3ba25 对比 | 目录
2025-03-13 alielie
完善国际化语言;
ba6207 对比 | 目录
2025-03-11 alielie
完善程序; Merge branch 'master' of http://58.211.88.246:3600/r/WPF/WPFProject
2e54b4 对比 | 目录
2025-03-04 alielie
优化UI;
2b1a64 对比 | 目录
2025-03-03 chr
整合TestResult;
7b903b 对比 | 目录
2025-03-03 alielie
上传依赖;
ee9b94 对比 | 目录
2025-03-03 alielie
sb
f90615 对比 | 目录
2025-03-03 alielie
优化结构;
21e28b 对比 | 目录
2025-03-03 alielie
完善UI;新增TestResult;
4c3eb7 对比 | 目录
2025-03-03 alielie
完善代码;
1fca21 对比 | 目录
2025-02-24 chr
上传长时间代码;
104cad 对比 | 目录
2024-03-12 chenghaoran
优化项目结构;
375b48 对比 | 目录
2024-03-12 chr
初版Step运行代码;
25dd22 对比 | 目录
2024-03-07 chr
添加不同平台的行尾文件;
303e61 对比 | 目录
2024-03-07 chr
优化InnerWindow的Resize;
402130 对比 | 目录
2024-03-05 chr
优化输出目录结构;
588143 对比 | 目录
2024-03-04 chr
新增变量类型
63f80f 对比 | 目录
2024-03-01 chr
新增序列化操作;
e0b729 对比 | 目录
2024-03-01 chr
优化DataGridUI;完善拖拽;
8d344c 对比 | 目录
2024-02-29 chr
优化UI样式;
d9e38e 对比 | 目录
2024-02-28 chr
优化程序UI;
e64d24 对比 | 目录
2024-02-24 chr
完善Zindex层级显示
4adf06 对比 | 目录
2024-02-21 chenghaoran
完善基础组件;
3b877b 对比 | 目录
2024-02-06 chenghaoran
初版代码
ee0ccd 对比 | 目录
1个文件已修改
938个文件已添加
184722 ■■■■■ 已修改文件
.gitattributes 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Aging.Infrastructure.csproj 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Contracts/DeviceCapability.cs 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Contracts/IDeviceDriver.cs 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Contracts/IDeviceManager.cs 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Contracts/IDeviceService.cs 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Contracts/IOperationLease.cs 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Contracts/IResourceManager.cs 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Contracts/ResourceKey.cs 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Contracts/ResourceOwnerToken.cs 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Contracts/ResourceReservation.cs 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Core/DeviceManager.cs 226 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Core/OperationLease.cs 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Core/ResourceManager.cs 258 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Drivers/CanDriverAdapter.cs 576 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Drivers/DriverFactory.cs 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Drivers/ModBusDriverAdapter.cs 347 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Drivers/S7DriverAdapter.cs 407 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Drivers/ScpiDriverAdapter.cs 248 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Drivers/TcpRawDriverAdapter.cs 222 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/InfrastructureBootstrapper.cs 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Services/DeviceService.cs 331 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Services/PowerSupplyService.cs 543 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Aging.Infrastructure/Services/SlotDeviceMappingResolver.cs 218 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
CanSimulator/CanBusSimulator.cs 403 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
CanSimulator/CanFrame.cs 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
CanSimulator/CanNode.cs 311 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
CanSimulator/CanSimulator.csproj 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
CellStepSender/CellStepSender.csproj 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
CellStepSender/Properties/AssemblyInfo.cs 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
CellStepSender/StepSender.cs 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
CellStepSender/TcpClientTool.cs 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
CellStepSender/TcpClientTool3.cs 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Config/Database/database.db 补丁 | 查看 | 原始文档 | blame | 历史
Config/LogViewer/log_2026-10-23.db 补丁 | 查看 | 原始文档 | blame | 历史
ConnectionUtil/ConnectionUtil.csproj 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ConnectionUtil/PipeClient.cs 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ConnectionUtil/PipeClient2.cs 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ConnectionUtil/PipeServer.cs 347 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ConnectionUtil/PipeServer2.cs 192 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ConnectionUtil/Properties/AssemblyInfo.cs 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ConnectionUtil/SCADAApi.cs 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ConnectionUtil/TcpClient.cs 254 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ConsoleTest/App.config 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ConsoleTest/ConsoleTest.csproj 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ConsoleTest/Program.cs 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ConsoleTest/Properties/AssemblyInfo.cs 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ConsoleTest/Properties/app.manifest 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/AlarmInfoManage.xaml 265 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/AlarmInfoManage.xaml.cs 424 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/AlarmInfoManage_SettingA.xaml 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/AlarmInfoManage_SettingA.xaml.cs 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/AlarmSettingList.xaml 461 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/AlarmSettingList.xaml.cs 269 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/Entity/AimSetting.cs 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/HMI.AlarmInfoManage.csproj 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/Modal/AlarmSettingModal.xaml 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/Modal/AlarmSettingModal.xaml.cs 156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/Model/UIModel.cs 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/Model/XmlModel.cs 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/Properties/AssemblyInfo.cs 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/Properties/Resources.Designer.cs 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/Properties/Resources.resx 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/Properties/Settings.Designer.cs 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/Properties/Settings.settings 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/SimpleAlarm.xaml 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/SimpleAlarm.xaml.cs 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/packages.config 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Base/DtConverter.cs 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Base/HMI.Base.csproj 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Base/MaskControl.xaml 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Base/NotifyControl.cs 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Base/PluginControl.cs 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Base/PluginInfo.cs 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Base/PluginLoader.cs 234 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Base/PluginMaskControl.cs 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Base/Properties/AssemblyInfo.cs 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Base/Properties/Resources.Designer.cs 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Base/Properties/Resources.resx 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Base/Properties/Settings.Designer.cs 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Base/Properties/Settings.settings 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Base/SimpleControl.cs 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Base/SimpleMaskControl.cs 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Base/Themes/Generic.xaml 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Base/packages.config 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Annotation/ExcelColumn.cs 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Annotation/InitAttribute.cs 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Api/AlarmShowHelper.cs 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Api/IAlarmShow.cs 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Api/IConfigManager.cs 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Api/IConfigRefresh.cs 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Api/IFolderOwner.cs 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Api/ILogWriter.cs 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Api/IPlugin.cs 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Api/IPluginContainer.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Api/PluginPackage.cs 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/DpUtil/PermissionHelper.cs 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Entity/AlarmSetting.cs 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Entity/AlarmText.cs 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Entity/LangInfo.cs 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Entity/LoadMsg.cs 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Entity/NotifyBase.cs 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Entity/ParameterData.cs 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Entity/PermissionData.cs 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Entity/ProductData.cs 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Entity/SelectBase.cs 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Entity/SortParam.cs 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Entity/TestItemData.cs 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Entity/UserInfo.cs 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Event/TabChange.cs 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Exceptions/FormValidateException.cs 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/FontSizeConverter.cs 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/HMI.Core.csproj 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/LangConverter.cs 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Manager/ParameterManager.cs 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Manager/ProductManager.cs 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Manager/TestItemManager.cs 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Properties/AssemblyInfo.cs 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Setting/ParamSetting.cs 241 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Setting/ProductSetting.cs 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Setting/SystemSetting.cs 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Util/BeanUtil.cs 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Util/BindingProxy.cs 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Util/DllHelper.cs 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Util/HexUtil.cs 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Util/JsonUtil.cs 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Util/LangUtil.cs 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Util/LoggerHolder.cs 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Util/MSMQHelper.cs 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Util/PathHelper.cs 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Util/Sqlite3Helper.cs 270 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Util/SqliteHelper.cs 238 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Util/UtilContainer.cs 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Util/XmlHelper.cs 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/packages.config 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.DataViewer/DataViewer.xaml 267 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.DataViewer/DataViewer.xaml.cs 547 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.DataViewer/DbSettingModal.xaml 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.DataViewer/DbSettingModal.xaml.cs 297 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.DataViewer/Entity/DataBase_Setting.cs 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.DataViewer/Entity/QueryCondition.cs 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.DataViewer/Entity/QueryData.cs 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.DataViewer/HMI.DataViewer.csproj 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.DataViewer/Properties/AssemblyInfo.cs 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.DataViewer/Properties/Resources.Designer.cs 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.DataViewer/Properties/Resources.resx 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.DataViewer/Properties/Settings.Designer.cs 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.DataViewer/Properties/Settings.settings 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.DataViewer/packages.config 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.LogViewer/HMI.LogViewer.csproj 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.LogViewer/LogData.cs 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.LogViewer/LogViewer.xaml 380 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.LogViewer/LogViewer.xaml.cs 396 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.LogViewer/Properties/AssemblyInfo.cs 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.LogViewer/Properties/Resources.Designer.cs 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.LogViewer/Properties/Resources.resx 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.LogViewer/Properties/Settings.Designer.cs 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.LogViewer/Properties/Settings.settings 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.LogViewer/packages.config 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Entity/IconHolder.cs 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Entity/IconMeta.cs 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Entity/MenuData.cs 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Entity/RoleData.cs 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Entity/RoleMenuRel.cs 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Entity/UserData.cs 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Entity/UserRoleRel.cs 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/HMI.MenuManage.csproj 207 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/MenuManage.xaml 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/MenuManage.xaml.cs 270 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Modal/IconSelector.xaml 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Modal/IconSelector.xaml.cs 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Modal/MenuModal.xaml 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Modal/MenuModal.xaml.cs 271 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Modal/RoleModal.xaml 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Modal/RoleModal.xaml.cs 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Modal/UserModal.xaml 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Modal/UserModal.xaml.cs 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Model/MenuManageModel.cs 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Model/RoleManageModel.cs 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Model/UserManageModel.cs 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Properties/AssemblyInfo.cs 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Properties/Resources.Designer.cs 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Properties/Resources.resx 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Properties/Settings.Designer.cs 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Properties/Settings.settings 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/RoleManage.xaml 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/RoleManage.xaml.cs 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/UserManage.xaml 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/UserManage.xaml.cs 228 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Util/RoleHelper.cs 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Util/RoleMenuHelper.cs 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/Util/UserRoleRelHelper.cs 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.MenuManage/packages.config 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/HMI.ParameterManage.csproj 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/Model/SqlModel.cs 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/Model/XmlModel.cs 287 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/ParameterManage.xaml 205 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/ParameterManage.xaml.cs 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/ParameterModal.xaml 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/ParameterModal.xaml.cs 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/ParameterSetting.xaml 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/ParameterSetting.xaml.cs 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/ProductModal.xaml 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/ProductModal.xaml.cs 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/Properties/AssemblyInfo.cs 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/Properties/Resources.Designer.cs 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/Properties/Resources.resx 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/Properties/Settings.Designer.cs 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/Properties/Settings.settings 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/SettingModal.xaml 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/SettingModal.xaml.cs 388 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/TestItem.xaml 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/TestItem.xaml.cs 318 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/TestItemModal.xaml 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/TestItemModal.xaml.cs 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/packages.config 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.SettingControl/HMI.SettingControl.csproj 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.SettingControl/Properties/AssemblyInfo.cs 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.SettingControl/Properties/Resources.Designer.cs 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.SettingControl/Properties/Resources.resx 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.SettingControl/Properties/Settings.Designer.cs 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.SettingControl/Properties/Settings.settings 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.SettingControl/SettingControl.xaml 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.SettingControl/SettingControl.xaml.cs 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.SettingControl/SystemConfig.xaml 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.SettingControl/SystemConfig.xaml.cs 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.SettingControl/packages.config 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.StdControl/HMI.StdControl.csproj 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.StdControl/Properties/AssemblyInfo.cs 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.StdControl/StdEnableMes.xaml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.StdControl/StdEnableMes.xaml.cs 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.StdControl/StdSettingCheckBox.xaml 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.StdControl/StdSettingCheckBox.xaml.cs 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.StdControl/StdUIEventArg.cs 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Controls/KeyboradWindow.xaml 234 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Controls/KeyboradWindow.xaml.cs 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Controls/LightBall.cs 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Controls/MultiCombox.xaml 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Controls/MultiCombox.xaml.cs 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Controls/NumberInput.cs 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Controls/NumberPanel.xaml 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Controls/NumberPanel.xaml.cs 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Controls/VisualHost.cs 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Controls/treegrid/Controls/TreeDataGrid.cs 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Controls/treegrid/Controls/TreeGrid.cs 396 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Controls/treegrid/Controls/TreeItemContentControl.cs 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Controls/treegrid/Controls/TreeItemData.cs 587 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Controls/treegrid/Controls/TreeListView.cs 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Controls/treegrid/Themes/TreeGrid.xaml 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Controls/treegrid/Themes/TreeItemContentControl.xaml 178 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Controls/treegrid/Themes/TreeListView.xaml 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Controls/treegrid/TreeItemConverter.cs 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Controls/treegrid/TreeNodeExtend.cs 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Converter/CollectionHasItem.cs 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Converter/DataGridRowFlagConverter.cs 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Converter/DataGridRowNumberConverter.cs 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Converter/DateStrConverter.cs 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Converter/IndentConverter.cs 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Converter/PathImageConverter.cs 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Converter/RadioEnumConverter.cs 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Converter/ResultConverter.cs 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Converter/ReverseBoolConverter.cs 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Converter/VisibilityBoolConverter.cs 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/DpUtil/CanvasHelper.cs 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/DpUtil/DataGridHelper.cs 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/DpUtil/DataGridTextColumnHelper.cs 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/DpUtil/DpPlugin.cs 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/DpUtil/FormGroupHelper.cs 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/DpUtil/GridHelper.cs 229 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/DpUtil/Resize.cs 640 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/DpUtil/StackPanelHelper.cs 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/DpUtil/TreeViewHelper.cs 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/DpUtil/UniformGridHelper.cs 238 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/EditBox.cs 194 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Entity/ScannerConfig.cs 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/HMI.UI.csproj 315 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/MDI/BottomBar.xaml 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/MDI/BottomBar.xaml.cs 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/MDI/InnerWindow.xaml 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/MDI/InnerWindow.xaml.cs 267 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/MDI/MinInnerWindow.xaml 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/MDI/MinInnerWindow.xaml.cs 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/MDI/Workspace.xaml 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/MDI/Workspace.xaml.cs 366 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/MaskControl.xaml 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/MaskControl.xaml.cs 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/MaskWindow.xaml 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/MaskWindow.xaml.cs 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Properties/AssemblyInfo.cs 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Properties/Resources.Designer.cs 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Properties/Resources.resx 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Properties/Settings.Designer.cs 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Properties/Settings.settings 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/RequirePasswordByUser.xaml 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/RequirePasswordByUser.xaml.cs 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/RequirePasswordWindow.xaml 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/RequirePasswordWindow.xaml.cs 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Resources/Font/Font.xaml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Resources/Font/iconfont.json 324 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Resources/Font/iconfont.ttf 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Resources/Icon/dot.png 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Resources/Icon/dot.svg 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Resources/Icon/dot2.png 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Resources/Icon/xinzeng_1.png 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/ScannerSetting.xaml 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/ScannerSetting.xaml.cs 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/ScrollLabel.cs 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Style/BaseWindow.xaml 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Style/BaseWindow.xaml.cs 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Style/Colors.xaml 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Style/FormStyle.xaml 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Style/MsgBoxStyle.xaml 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Style/PanuonDataGrid.xaml 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Style/PanuonListView.xaml 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Style/PanuonResource.xaml 348 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Style/ScrollBarResource.xaml 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Style/WindowResources.xaml 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/TableComboBox.cs 249 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Themes/Generic.xaml 187 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Util/IValueValidator.cs 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Util/MaskHolder.cs 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Util/MsgBoxHelper.cs 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Util/MsgBoxHelper2.cs 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Util/ToastHelper.cs 256 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/packages.config 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UnitTest/App.config 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UnitTest/App.xaml 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UnitTest/App.xaml.cs 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UnitTest/HMI.UnitTest.csproj 198 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UnitTest/MainWindow.xaml 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UnitTest/MainWindow.xaml.cs 273 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UnitTest/Properties/AssemblyInfo.cs 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UnitTest/Properties/Resources.Designer.cs 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UnitTest/Properties/Resources.resx 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UnitTest/Properties/Settings.Designer.cs 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UnitTest/Properties/Settings.settings 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UnitTest/log4net.config 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UnitTest/packages.config 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Util/Alarm/AlarmUtil.cs 394 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Util/DateUtil.cs 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Util/DbFileHelper.cs 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Util/ExcelHelper.cs 237 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Util/HMI.Util.csproj 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Util/IniFileHelper.cs 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Util/Properties/AssemblyInfo.cs 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Util/PropertyHelper.cs 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Util/packages.config 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Libs/25/NationalInstruments.TestStand.Interop.API.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/25/NationalInstruments.TestStand.Interop.API.xml 19856 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Libs/25/NationalInstruments.TestStand.Interop.UI.AxControls.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/25/NationalInstruments.TestStand.Interop.UI.Support.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/25/NationalInstruments.TestStand.Interop.UI.Support.xml 1841 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Libs/25/NationalInstruments.TestStand.Interop.UI.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/25/NationalInstruments.TestStand.Interop.UI.xml 9207 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Libs/25/NationalInstruments.TestStand.Utility.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/25/NationalInstruments.TestStand.Utility.xml 2303 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Libs/DatabaseHelper.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/HslCommunication.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/Microsoft.DwayneNeed.Win32.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/Microsoft.DwayneNeed.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/NationalInstruments.TestStand.Interop.API.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/NationalInstruments.TestStand.Interop.API.xml 21060 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Libs/NationalInstruments.TestStand.Interop.AdapterAPI.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/NationalInstruments.TestStand.Interop.AdapterAPI.xml 6819 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Libs/NationalInstruments.TestStand.Interop.SequenceAnalyzer.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/NationalInstruments.TestStand.Interop.SequenceAnalyzer.xml 744 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Libs/NationalInstruments.TestStand.Interop.UI.AxControls.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/NationalInstruments.TestStand.Interop.UI.Support.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/NationalInstruments.TestStand.Interop.UI.Support.xml 1277 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Libs/NationalInstruments.TestStand.Interop.UI.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/NationalInstruments.TestStand.Interop.UI.xml 10517 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Libs/NationalInstruments.TestStand.Utility.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/NationalInstruments.TestStand.Utility.xml 2303 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Libs/SQLite.Interop.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/SimensDriver.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/SupersocketCommon.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/System.Data.SQLite.EF6.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/System.Data.SQLite.Linq.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/System.Data.SQLite.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/TSSync.Interop.dll 补丁 | 查看 | 原始文档 | blame | 历史
Libs/lmes.dll 补丁 | 查看 | 原始文档 | blame | 历史
ModBusTcpController/ModBusTcpController.cs 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ModBusTcpController/ModBusTcpController.csproj 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ModBusTcpController/Properties/AssemblyInfo.cs 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
OPCClient/Commond.cs 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
OPCClient/OPCClient.cs 664 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
OPCClient/OPCClient.csproj 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
OPCClient/Properties/AssemblyInfo.cs 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
OPCClient/packages.config 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
PLCHelper/BoolReader.cs 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
PLCHelper/Entity/PLCTagData.cs 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
PLCHelper/IntReader.cs 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
PLCHelper/ModbusHelper.cs 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
PLCHelper/NXI_5010Helper.cs 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
PLCHelper/PLCHelper.csproj 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
PLCHelper/Properties/AssemblyInfo.cs 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
PLCHelper/RestTcpClient.cs 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
PLCHelper/S7Context.cs 313 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/AssemblyInfo.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/BurnInUI.xaml 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/BurnInUI.xaml.cs 492 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/ChartWindow.xaml 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/ChartWindow.xaml.cs 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Constant/CommonConstant.cs 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Cooler.xaml 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Cooler.xaml.cs 206 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/CoolerStatus.xaml 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/CoolerStatus.xaml.cs 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/DataTable.xaml 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/DataTable.xaml.cs 928 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/InfoTable.xaml 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/InfoTable.xaml.cs 203 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/InfoWindow.xaml 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/InfoWindow.xaml.cs 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/ManualDebug/ManualCell.xaml 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/ManualDebug/ManualCell.xaml.cs 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/ManualDebug/ManualDebug.xaml 212 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/ManualDebug/ManualDebug.xaml.cs 722 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/ManualDebug/ManualDebugStyle.xaml 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/ManualDebug/ManualDebugWindow.xaml 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/ManualDebug/ManualDebugWindow.xaml.cs 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/ManualDebug/StatusBtn.xaml 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/ManualDebug/StatusBtn.xaml.cs 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/ManualDebug/StatusCell.xaml 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/ManualDebug/StatusCell.xaml.cs 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Model/BaseConfig.cs 198 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Model/BaseModel.cs 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Model/CellInfo.cs 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Model/DataTableModel.cs 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Model/Info.cs 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Model/ScannerConfig.cs 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Model/TcpServer.cs 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Plugin.BurnIn.csproj 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Resource/abort.png 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Resource/alert.png 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Resource/barcode.png 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Resource/checkIn.png 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Resource/checkInFailed2.png 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Resource/ng2.png 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Resource/ok2.png 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Resource/ready.png 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Resource/testing2.png 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Resource/waiting2.png 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Scanner.xaml 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Scanner.xaml.cs 465 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/ScannerSetting.xaml 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/ScannerSetting.xaml.cs 147 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/SingleCell.xaml 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/SingleCell.xaml.cs 1180 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/TestResultView.xaml 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/TestResultView.xaml.cs 684 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/UI/LiveChart.xaml 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/UI/LiveChart.xaml.cs 516 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Util/CommonCodeParser.cs 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Util/DpUtil.cs 220 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Util/FontSizeConverter.cs 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Util/ICodeParser.cs 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Util/JKCodeParser.cs 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Util/ModelCodeParser.cs 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Util/PNUtil.cs 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Util/RunTimeVariablePool.cs 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Util/SNValidator.cs 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.BurnIn/Util/ScannerParser.cs 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/AssemblyInfo.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/BurnInCell.xaml 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/BurnInCell.xaml.cs 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/BurnInOven.xaml 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/BurnInOven.xaml.cs 260 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/BurnInOvenCollection.xaml 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/BurnInOvenCollection.xaml.cs 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/BurnInSettingModal.xaml 220 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/BurnInSettingModal.xaml.cs 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/ControlPage - 复制.xaml 353 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/ControlPage - 复制.xaml.cs 450 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/ControlPage.xaml 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/ControlPage.xaml.cs 450 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/DataModal.xaml 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/DataModal.xaml.cs 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/Entity/BaseConfig.cs 252 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/Entity/OPCContext.cs 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/Entity/SensorData.cs 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/Plugin.CurrentSensorControl.csproj 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/SingleSensor.xaml 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/SingleSensor.xaml.cs 180 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DOControl/ControlList.xaml 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DOControl/ControlList.xaml.cs 224 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DOControl/DOControl.xaml 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DOControl/DOControl.xaml.cs 534 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DOControl/DataModal.xaml 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DOControl/DataModal.xaml.cs 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DOControl/Entity/DOControlData.cs 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DOControl/Plugin.DOControl.csproj 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DOControl/Properties/AssemblyInfo.cs 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DOControl/Properties/Resources.Designer.cs 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DOControl/Properties/Resources.resx 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DOControl/Properties/Settings.Designer.cs 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DOControl/Properties/Settings.settings 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DOControl/packages.config 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DODriverControl/Api/Dask.cs 801 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DODriverControl/Api/DaskHelper.cs 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DODriverControl/Api/IDIOController.cs 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DODriverControl/Api/ModBusTcpController.cs 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DODriverControl/Api/PCIController.cs 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DODriverControl/AssemblyInfo.cs 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DODriverControl/ConfigModal.xaml 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DODriverControl/ConfigModal.xaml.cs 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DODriverControl/DOControl.xaml 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DODriverControl/DOControl.xaml.cs 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DODriverControl/DataModal.xaml 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DODriverControl/DataModal.xaml.cs 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DODriverControl/Entity/ConfigData.cs 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DODriverControl/Entity/DIOData.cs 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DODriverControl/Plugin.DODriverControl.csproj 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DODriverControl/SingleControl.xaml 365 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DODriverControl/SingleControl.xaml.cs 585 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataBoard/DataBoard.xaml 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataBoard/DataBoard.xaml.cs 1046 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataBoard/Libs/Static/echarts.min.js 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataBoard/Libs/Static/test.html 512 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataBoard/Libs/echarts.min.js 1782 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataBoard/OptionData.cs 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataBoard/PassRateData.cs 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataBoard/Plugin.DataBoard.csproj 230 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataBoard/Properties/AssemblyInfo.cs 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataBoard/Properties/Resources.Designer.cs 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataBoard/Properties/Resources.resx 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataBoard/Properties/Settings.Designer.cs 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataBoard/Properties/Settings.settings 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataBoard/SerialData.cs 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataBoard/SimpleData.cs 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataBoard/app.config 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataBoard/packages.config 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataViewer/AssemblyInfo.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataViewer/CustomSortColumn.cs 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataViewer/DataViewerControl.xaml 480 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataViewer/DataViewerControl.xaml.cs 800 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataViewer/Plugin.DataViewer.csproj 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceConfig/BatchGenerateModal.xaml 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceConfig/BatchGenerateModal.xaml.cs 252 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceConfig/DeviceConfig.xaml 293 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceConfig/DeviceConfig.xaml.cs 611 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceConfig/DeviceEditModal.xaml 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceConfig/DeviceEditModal.xaml.cs 368 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceConfig/Entity/DeviceChannel.cs 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceConfig/Entity/DeviceConfigVersion.cs 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceConfig/Entity/DeviceDefinition.cs 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceConfig/Entity/DeviceGroupTemplate.cs 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceConfig/Entity/SlotDeviceMapping.cs 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceConfig/Model/DeviceConfigModel.cs 645 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceConfig/Plugin.DeviceConfig.csproj 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceConfig/VersionManageModal.xaml 166 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceConfig/VersionManageModal.xaml.cs 207 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceDebug/DeviceDebug.xaml 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceDebug/DeviceDebug.xaml.cs 668 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceDebug/Entity/DebugCommand.cs 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceDebug/Entity/DebugOperation.cs 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceDebug/Entity/DebugSession.cs 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceDebug/Entity/DeviceRuntimeStatus.cs 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceDebug/Model/DebugSessionManager.cs 380 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceDebug/Model/DeviceDebugModel.cs 374 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceDebug/Panels/CanDebugPanel.xaml 220 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceDebug/Panels/CanDebugPanel.xaml.cs 543 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceDebug/Panels/PlcDebugPanel.xaml 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceDebug/Panels/PlcDebugPanel.xaml.cs 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceDebug/Panels/PowerSupplyDebugPanel.xaml 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceDebug/Panels/PowerSupplyDebugPanel.xaml.cs 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DeviceDebug/Plugin.DeviceDebug.csproj 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.IntegrationTest/App.xaml 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.IntegrationTest/App.xaml.cs 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.IntegrationTest/MainWindow.xaml 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.IntegrationTest/MainWindow.xaml.cs 840 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.IntegrationTest/Plugin.IntegrationTest.csproj 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.JarUI/JarCell.xaml 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.JarUI/JarCell.xaml.cs 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.JarUI/JarUI.xaml 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.JarUI/JarUI.xaml.cs 635 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.JarUI/Plugin.JarUI.csproj 163 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.JarUI/Properties/AssemblyInfo.cs 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.JarUI/Properties/Resources.Designer.cs 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.JarUI/Properties/Resources.resx 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.JarUI/Properties/Settings.Designer.cs 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.JarUI/Properties/Settings.settings 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.JarUI/packages.config 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.ModelManage/AssemblyInfo.cs 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.ModelManage/DataModal.xaml 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.ModelManage/DataModal.xaml.cs 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.ModelManage/Entity/ModelData.cs 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.ModelManage/ModelManage.xaml 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.ModelManage/ModelManage.xaml.cs 300 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.ModelManage/Plugin.ModelManage.csproj 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.QualityReport/AssemblyInfo.cs 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.QualityReport/Libs/Static/CMK模板.xls 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.QualityReport/Libs/Static/MSA模板.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.QualityReport/Libs/Static/重复性模板.xls 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.QualityReport/Plugin.QualityReport.csproj 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.QualityReport/QualityReport.xaml 249 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.QualityReport/QualityReport.xaml.cs 705 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/AssemblyInfo.cs 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/Constant/DefaultStepName.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/Constant/StepTypeStr.cs 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/Entity/EditablePO/IEPO.cs 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/Entity/EditablePO/NumValTestEPO.cs 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/Entity/EditablePO/StringValTestEPO.cs 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/Entity/EditablePO/ViCallEPO.cs 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/Entity/EditablePO/WaitEPO.cs 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/Entity/SeqUI.cs 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/Entity/StepUI.cs 395 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/Entity/VarUI.cs 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/Plugin.SeqEditor.csproj 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/RowNumberConverter.cs 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/SeqEditor.xaml 574 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/SeqEditor.xaml.cs 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/SettingPanel/ActionPanel.xaml 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/SettingPanel/ActionPanel.xaml.cs 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/SettingPanel/CallSeqPanel.xaml 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/SettingPanel/CallSeqPanel.xaml.cs 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/SettingPanel/CommonProperty.xaml 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/SettingPanel/CommonProperty.xaml.cs 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/SettingPanel/LoopPanel.xaml 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/SettingPanel/LoopPanel.xaml.cs 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/SettingPanel/NumValTestPanel.xaml 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/SettingPanel/NumValTestPanel.xaml.cs 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/SettingPanel/StringValTestPanel.xaml 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/SettingPanel/StringValTestPanel.xaml.cs 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/SettingPanel/ViPanel.xaml 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/SettingPanel/ViPanel.xaml.cs 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/SettingPanel/WaitPanel.xaml 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/SettingPanel/WaitPanel.xaml.cs 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/Util/ItemData.cs 196 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/Util/PoExplain.cs 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/Util/Step2EPO.cs 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/ViewModel/SeqEditorViewModel.cs 715 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/ViewModel/StepDropHandler.cs 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.SeqEditor/ViewModel/TreeListViewHandler.cs 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.StandardTest/AssemblyInfo.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.StandardTest/Entity/TestStep.cs 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.StandardTest/Plugin.StandardTest.csproj 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.StandardTest/Scanner.xaml 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.StandardTest/Scanner.xaml.cs 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.StandardTest/ScannerManager.cs 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.StandardTest/ScreenGrid.xaml 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.StandardTest/ScreenGrid.xaml.cs 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.StandardTest/ScreenTouch.xaml 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.StandardTest/ScreenTouch.xaml.cs 179 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.StandardTest/ScreenTouchWin.xaml 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.StandardTest/ScreenTouchWin.xaml.cs 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.StandardTest/Setting/TcpServer.cs 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.StandardTest/Setting/TsSetting.cs 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.StandardTest/SingleControl.xaml 419 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.StandardTest/SingleControl.xaml.cs 916 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.StandardTest/StandardTestUI.xaml 315 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.StandardTest/StandardTestUI.xaml.cs 1906 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.StandardTest/WorkOrderWin.xaml 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.StandardTest/WorkOrderWin.xaml.cs 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestPlanEditor/AssemblyInfo.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestPlanEditor/EditorUI.xaml 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestPlanEditor/EditorUI.xaml.cs 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestPlanEditor/Plugin.TestPlanEditor.csproj 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/CSVPrinter.cs 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Chart.xaml 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Chart.xaml.cs 496 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/ChartWindow.xaml 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/ChartWindow.xaml.cs 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Config/log4net.config 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Config/log4net.unix.config 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Constant/CommonConstant.cs 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Cooler.xaml 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Cooler.xaml.cs 218 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/CoolerStatus.xaml 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/CoolerStatus.xaml.cs 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/DataTable.xaml 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/DataTable.xaml.cs 897 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/InfoTable.xaml 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/InfoTable.xaml.cs 205 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/InfoWindow.xaml 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/InfoWindow.xaml.cs 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/LiveChart.xaml 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/LiveChart.xaml.cs 519 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Lixun/SNValidator.cs 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/ManualDebugWindow.xaml 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/ManualDebugWindow.xaml.cs 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Model/BaseConfig.cs 198 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Model/BaseModel.cs 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Model/CellInfo.cs 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Model/DataTableModel.cs 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Model/Info.cs 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Model/ScannerConfig.cs 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Model/TcpServer.cs 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Plugin.TestResult.csproj 433 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Properties/AssemblyInfo.cs 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Properties/Resources.Designer.cs 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Properties/Resources.resx 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Properties/Settings.Designer.cs 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Properties/Settings.settings 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Resource/abort.png 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Resource/alert.png 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Resource/barcode.png 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Resource/checkIn.png 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Resource/checkInFailed2.png 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Resource/ng2.png 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Resource/ok2.png 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Resource/ready.png 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Resource/testing2.png 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Resource/waiting2.png 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Scanner.xaml 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Scanner.xaml.cs 469 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/ScannerSetting.xaml 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/ScannerSetting.xaml.cs 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/SingleCell.xaml 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/SingleCell.xaml.cs 1179 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/TestResultView.xaml 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/TestResultView.xaml.cs 668 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Util/CommonCodeParser.cs 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Util/DpUtil.cs 221 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Util/FontSizeConverter.cs 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Util/ICodeParser.cs 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Util/JKCodeParser.cs 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Util/ModelCodeParser.cs 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Util/PNUtil.cs 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Util/RunTimeVariablePool.cs 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Util/ScannerParser.cs 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/ViewControl.xaml 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/ViewControl.xaml.cs 495 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/packages.config 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/Entity/TestStep.cs 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/OpenTK.dll.config 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/Plugin.TsExecution.csproj 309 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/Properties/AssemblyInfo.cs 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/Properties/Resources.Designer.cs 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/Properties/Resources.resx 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/Properties/Settings.Designer.cs 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/Properties/Settings.settings 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/Scanner.xaml 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/Scanner.xaml.cs 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/ScannerManager.cs 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/ScreenGrid.xaml 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/ScreenGrid.xaml.cs 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/ScreenTouch.xaml 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/ScreenTouch.xaml.cs 179 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/ScreenTouchWin.xaml 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/ScreenTouchWin.xaml.cs 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/Setting/TsSetting.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/SingleControl.xaml 580 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/SingleControl.xaml.cs 795 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/TsExecution.xaml 311 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/TsExecution.xaml.cs 1950 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/WorkOrderWin.xaml 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/WorkOrderWin.xaml.cs 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/packages.config 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsSupport/ITestExecutionWrapper.cs 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsSupport/Plugin.TsSupport.csproj 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsSupport/Properties/AssemblyInfo.cs 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsSupport/TapWrapper.cs 237 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsSupport/TestAppManager.cs 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsSupport/TeststandWrapper.cs 248 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.UICollection/AssemblyInfo.cs 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.UICollection/Plugin.UICollection.csproj 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.UICollection/TcpDebugger.xaml 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.UICollection/TcpDebugger.xaml.cs 393 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
S7Driver/PLCS7Driver.cs 212 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
S7Driver/Properties/AssemblyInfo.cs 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
S7Driver/S7Driver.csproj 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/DIOController/DIOController.csproj 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/DIOController/DIOControllerCreator.cs 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/DIOController/Dask.cs 803 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/DIOController/DaskHelper.cs 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/DIOController/IDIOController.cs 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/DIOController/IniFileHelper.cs 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/DIOController/ModBusTcpController.cs 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/DIOController/PCI-Dask64.dll 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/DIOController/PCIController.cs 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/DIOController/Properties/AssemblyInfo.cs 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/DIOController/template.ini 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.LogUtil/HMI.LogUtil.csproj 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.LogUtil/LogHelper.cs 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.LogUtil/Properties/AssemblyInfo.cs 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.MQUtil/HMI.MQUtil.csproj 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.MQUtil/MQConnector.cs 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.MQUtil/Properties/AssemblyInfo.cs 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.SerialData.Util/HMI.SerialData.Util.csproj 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.SerialData.Util/Properties/AssemblyInfo.cs 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.SerialData.Util/SerialDataHelper.cs 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.Simple.Util/AlarmUtil.cs 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.Simple.Util/DataSender.cs 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.Simple.Util/HMI.Simple.Util.csproj 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.Simple.Util/MSMQHelper.cs 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.Simple.Util/Parameter.cs 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.Simple.Util/ParameterUtil.cs 244 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.Simple.Util/ProductModelHelper.cs 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.Simple.Util/ProductUtil.cs 147 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.Simple.Util/Properties/AssemblyInfo.cs 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.Simple.Util/SerialDataHelper.cs 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.Simple.Util/SqliteHelper.cs 238 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.Simple.Util/XmlHelper.cs 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.TapApi/HMI.TapApi.csproj 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.TapApi/ProductUtil.cs 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.TapApi/Properties/AssemblyInfo.cs 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.Util.Base/HMI.Util.Base.csproj 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.Util.Base/Properties/AssemblyInfo.cs 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.Util.Base/RegUtil.cs 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
StationGlobalsReader/App.xaml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
StationGlobalsReader/App.xaml.cs 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
StationGlobalsReader/AssemblyInfo.cs 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
StationGlobalsReader/MainWindow.xaml 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
StationGlobalsReader/MainWindow.xaml.cs 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
StationGlobalsReader/StationGlobalsReader.csproj 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/AssemblyInfo.cs 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/ManualCell.xaml 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/ManualCell.xaml.cs 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/ManualDebug.xaml 212 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/ManualDebug.xaml.cs 755 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/ManualDebugStyle.xaml 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/ManualDebugWindow.xaml 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/ManualDebugWindow.xaml.cs 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/Model/CellConfig.cs 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/Model/TcpServer.cs 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/OldManualDebug.xaml 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/OldManualDebug.xaml.cs 368 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/StatusBtn.xaml 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/StatusBtn.xaml.cs 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/StatusCell.xaml 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/StatusCell.xaml.cs 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/Util.ManualDebug.csproj 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Api/SQLite.Interop.dll 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/App.config 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/App.xaml 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/App.xaml.cs 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/BuildCmd.bat 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Config/Database/database.db 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Config/Put Your Config Here.txt 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Config/log4net.config 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Controls/DpPlugin.cs 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Controls/HolderForm.Designer.cs 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Controls/HolderForm.cs 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Controls/HolderForm.resx 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Controls/IconTabItem.cs 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Controls/MainTabControl.xaml 230 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Controls/MainTabControl.xaml.cs 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Controls/MaskControl.Designer.cs 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Controls/MaskControl.cs 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Controls/MenuTabControl.xaml 244 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Controls/MenuTabControl.xaml.cs 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Controls/WinFormHolder.xaml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Controls/WinFormHolder.xaml.cs 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Entity/PluginLoadMessage.cs 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Icon/JingCeLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Icon/JingCeLogo2.png 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Icon/LoginLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Icon/b1.jpg 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Icon/b2.jpg 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Icon/b3.jpg 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Icon/b4.jpg 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Icon/pic1.png 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Icon/t1.png 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Icon/t2.png 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Icon/t3.png 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Icon/t4.png 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Icon/xinzeng_1.png 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/JingCeLogo2.ico 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/en-US/AlarmInfoManage.json 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/en-US/ChangeModel.json 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/en-US/DOControl.json 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/en-US/DataBoard.json 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/en-US/DataViewer.json 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/en-US/IOMonitor.json 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/en-US/LogViewer.json 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/en-US/ParamManage.json 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/en-US/SeqEditor.json 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/en-US/ServoManage.json 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/en-US/TestExecution.json 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/en-US/TestResult.json 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/en-US/app.json 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/en-US/setting.json 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/zh-CN/AlarmInfoManage.json 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/zh-CN/ChangeModel.json 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/zh-CN/DOControl.json 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/zh-CN/DataBoard.json 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/zh-CN/DataViewer.json 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/zh-CN/IOMonitor.json 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/zh-CN/LogViewer.json 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/zh-CN/ParamManage.json 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/zh-CN/SeqEditor.json 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/zh-CN/ServoManage.json 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/zh-CN/TestExecution.json 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/zh-CN/TestResult.json 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/zh-CN/app.json 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/zh-CN/setting.json 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/LoadingWindow.xaml 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/LoadingWindow.xaml.cs 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Login.xaml 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Login.xaml.cs 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/MigrationBackup/fde93b81/VisionBuilder.Core/NuGetUpgradeLog.html 167 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/MigrationBackup/fde93b81/VisionBuilder.Core/VisionBuilder.Core.csproj 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/MigrationBackup/fde93b81/VisionBuilder.Core/packages.config 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/PanuonWindow.xaml 291 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/PanuonWindow.xaml.cs 375 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Properties/AssemblyInfo.cs 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Properties/Resources.Designer.cs 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Properties/Resources.resx 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Properties/Settings.Designer.cs 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Properties/Settings.settings 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Properties/app.manifest 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/ResourceListener.cs 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Resources/Background.png 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Resources/JingCeLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/SQLite.Interop.dll 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Settings.ini 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Style/CommonTree.xaml 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Style/ListViewStyle.xaml 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Style/MainTab.xaml 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/SuperSocket.SocketEngine.dll 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/TapProject.csproj 705 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/TapProject.sln 748 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/UI/ResourceUI.xaml 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/UI/ResourceUI.xaml.cs 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/UI/TestWindow.xaml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/UI/TestWindow.xaml.cs 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/UI/WorkWindow.xaml 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/UI/WorkWindow.xaml.cs 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Utils/BinUtil.cs 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Utils/CommonLogger.cs 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Utils/Encryptor.cs 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Utils/LogCleanner.cs 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Utils/MainToastHandler.cs 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Utils/Win32Helper.cs 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/packages.config 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/x86/SQLite.Interop.dll 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/变更记录/2025-12-24.txt 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/安装MSMQ.bat 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
check_parent.py 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
debug_check.py 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/TapProject_代码结构.drawio 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/TapProject_代码结构_01.drawio.png 补丁 | 查看 | 原始文档 | blame | 历史
docs/TapProject_代码结构_01_预览.png 补丁 | 查看 | 原始文档 | blame | 历史
docs/TapProject_代码结构_02.drawio.png 补丁 | 查看 | 原始文档 | blame | 历史
docs/TapProject_代码结构_02_预览.png 补丁 | 查看 | 原始文档 | blame | 历史
docs/TapProject_代码结构分析.md 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
fix_menu.py 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitattributes
.gitignore
@@ -16,6 +16,7 @@
bld/
[Bb]in/
[Oo]bj/
**/packages/
# Roslyn cache directories
*.ide/
@@ -73,6 +74,7 @@
*.psess
*.vsp
*.vspx
*.vs
# TFS 2012 Local Workspace
$tf/
Aging.Infrastructure/Aging.Infrastructure.csproj
New file
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Library</OutputType>
    <TargetFramework>net472</TargetFramework>
    <Nullable>enable</Nullable>
    <LangVersion>10.0</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
    <AutoGenerateBindingRedirects>True</AutoGenerateBindingRedirects>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="log4net" Version="3.0.4" />
    <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
  </ItemGroup>
  <ItemGroup>
    <Reference Include="HslCommunication">
      <HintPath>..\Libs\HslCommunication.dll</HintPath>
    </Reference>
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\HMI.Core\HMI.Core.csproj" />
    <ProjectReference Include="..\HMI.Util\HMI.Util.csproj" />
    <ProjectReference Include="..\ConnectionUtil\ConnectionUtil.csproj" />
    <ProjectReference Include="..\PLCHelper\PLCHelper.csproj" />
    <ProjectReference Include="..\S7Driver\S7Driver.csproj" />
    <ProjectReference Include="..\ModBusTcpController\ModBusTcpController.csproj" />
    <ProjectReference Include="..\CanSimulator\CanSimulator.csproj" />
  </ItemGroup>
</Project>
Aging.Infrastructure/Contracts/DeviceCapability.cs
New file
@@ -0,0 +1,48 @@
namespace Aging.Infrastructure.Contracts
{
    /// <summary>
    /// 设备能力枚举,标识硬件设备支持的功能类型
    /// </summary>
    public enum DeviceCapability
    {
        /// <summary>
        /// 电源设备
        /// </summary>
        PowerSupply,
        /// <summary>
        /// CAN总线设备
        /// </summary>
        CAN,
        /// <summary>
        /// LIN总线设备
        /// </summary>
        LIN,
        /// <summary>
        /// PLC数字IO设备
        /// </summary>
        PLC_IO,
        /// <summary>
        /// 温箱设备
        /// </summary>
        Chamber,
        /// <summary>
        /// 数据采集设备
        /// </summary>
        DAQ,
        /// <summary>
        /// 串口设备
        /// </summary>
        Serial,
        /// <summary>
        /// 通用设备
        /// </summary>
        Generic
    }
}
Aging.Infrastructure/Contracts/IDeviceDriver.cs
New file
@@ -0,0 +1,114 @@
namespace Aging.Infrastructure.Contracts
{
    /// <summary>
    /// 设备驱动状态信息
    /// </summary>
    public sealed class DeviceDriverStatus
    {
        /// <summary>
        /// 设备是否已连接
        /// </summary>
        public bool IsConnected { get; set; }
        /// <summary>
        /// 设备是否健康(无故障)
        /// </summary>
        public bool IsHealthy { get; set; }
        /// <summary>
        /// 最后一次错误信息
        /// </summary>
        public string LastError { get; set; } = "";
        /// <summary>
        /// 最后一次错误发生的时间
        /// </summary>
        public DateTime? LastErrorTime { get; set; }
        /// <summary>
        /// 通信延迟(毫秒)
        /// </summary>
        public double LatencyMs { get; set; }
        /// <summary>
        /// 重连次数
        /// </summary>
        public int ReconnectCount { get; set; }
    }
    /// <summary>
    /// 设备连接状态变化事件参数
    /// </summary>
    public sealed class DeviceConnectionEventArgs : EventArgs
    {
        /// <summary>
        /// 设备标识符
        /// </summary>
        public string DeviceId { get; set; } = "";
        /// <summary>
        /// 是否已连接
        /// </summary>
        public bool IsConnected { get; set; }
        /// <summary>
        /// 状态变化原因(可选)
        /// </summary>
        public string? Reason { get; set; }
    }
    /// <summary>
    /// 设备驱动统一接口。
    /// 所有硬件设备驱动必须实现此接口,提供连接、断开、命令发送和查询功能。
    /// </summary>
    public interface IDeviceDriver : IDisposable
    {
        /// <summary>
        /// 设备唯一标识符
        /// </summary>
        string DeviceId { get; }
        /// <summary>
        /// 设备当前是否已连接
        /// </summary>
        bool IsConnected { get; }
        /// <summary>
        /// 异步连接设备
        /// </summary>
        /// <param name="ct">取消令牌</param>
        Task ConnectAsync(CancellationToken ct);
        /// <summary>
        /// 异步断开设备连接
        /// </summary>
        Task DisconnectAsync();
        /// <summary>
        /// 发送命令到设备
        /// </summary>
        /// <param name="command">命令字符串</param>
        /// <param name="ct">取消令牌</param>
        /// <returns>设备响应字符串</returns>
        Task<string> SendCommandAsync(string command, CancellationToken ct);
        /// <summary>
        /// 向设备发送查询并获取响应
        /// </summary>
        /// <param name="query">查询字符串</param>
        /// <param name="ct">取消令牌</param>
        /// <returns>设备查询响应字符串</returns>
        Task<string> QueryAsync(string query, CancellationToken ct);
        /// <summary>
        /// 获取设备当前状态信息
        /// </summary>
        /// <returns>设备驱动状态</returns>
        DeviceDriverStatus GetStatus();
        /// <summary>
        /// 设备连接状态变化事件
        /// </summary>
        event EventHandler<DeviceConnectionEventArgs>? ConnectionChanged;
    }
}
Aging.Infrastructure/Contracts/IDeviceManager.cs
New file
@@ -0,0 +1,65 @@
using System.Threading;
using System.Threading.Tasks;
namespace Aging.Infrastructure.Contracts
{
    /// <summary>
    /// 设备管理接口。
    /// 提供设备驱动的注册、连接管理和状态查询功能。
    /// </summary>
    public interface IDeviceManager
    {
        /// <summary>
        /// 根据设备ID获取设备驱动实例
        /// </summary>
        /// <param name="deviceId">设备标识符</param>
        /// <returns>设备驱动实例,不存在时返回 null</returns>
        IDeviceDriver? GetDriver(string deviceId);
        /// <summary>
        /// 获取设备当前状态
        /// </summary>
        /// <param name="deviceId">设备标识符</param>
        /// <returns>设备驱动状态</returns>
        DeviceDriverStatus GetStatus(string deviceId);
        /// <summary>
        /// 注册设备驱动
        /// </summary>
        /// <param name="deviceId">设备标识符</param>
        /// <param name="driver">设备驱动实例</param>
        void RegisterDriver(string deviceId, IDeviceDriver driver);
        /// <summary>
        /// 注销设备驱动
        /// </summary>
        /// <param name="deviceId">设备标识符</param>
        void UnregisterDriver(string deviceId);
        /// <summary>
        /// 异步连接设备
        /// </summary>
        /// <param name="deviceId">设备标识符</param>
        /// <param name="ct">取消令牌</param>
        Task ConnectAsync(string deviceId, CancellationToken ct);
        /// <summary>
        /// 异步断开设备连接
        /// </summary>
        /// <param name="deviceId">设备标识符</param>
        Task DisconnectAsync(string deviceId);
        /// <summary>
        /// 获取所有已注册设备的ID列表
        /// </summary>
        /// <returns>设备ID只读列表</returns>
        IReadOnlyList<string> GetRegisteredDevices();
        /// <summary>
        /// 检查设备是否已注册
        /// </summary>
        /// <param name="deviceId">设备标识符</param>
        /// <returns>设备是否已注册</returns>
        bool IsDeviceRegistered(string deviceId);
    }
}
Aging.Infrastructure/Contracts/IDeviceService.cs
New file
@@ -0,0 +1,81 @@
using System.Threading;
using System.Threading.Tasks;
namespace Aging.Infrastructure.Contracts
{
    /// <summary>
    /// 设备命令执行结果
    /// </summary>
    public sealed class DeviceCommandResult
    {
        /// <summary>
        /// 命令是否执行成功
        /// </summary>
        public bool Success { get; set; }
        /// <summary>
        /// 设备原始响应字符串
        /// </summary>
        public string? RawResponse { get; set; }
        /// <summary>
        /// 回读值(解析后的有效数据)
        /// </summary>
        public string? Readback { get; set; }
        /// <summary>
        /// 错误代码
        /// </summary>
        public string? ErrorCode { get; set; }
        /// <summary>
        /// 错误信息
        /// </summary>
        public string? ErrorMessage { get; set; }
        /// <summary>
        /// 命令执行延迟(毫秒)
        /// </summary>
        public double LatencyMs { get; set; }
    }
    /// <summary>
    /// 设备服务接口。
    /// 提供基于资源预约的命令执行和安全关机功能,协调资源管理和设备驱动。
    /// </summary>
    public interface IDeviceService
    {
        /// <summary>
        /// 异步执行设备命令。
        /// 需要有效的资源预约才能执行。
        /// </summary>
        /// <param name="deviceId">设备标识符</param>
        /// <param name="command">命令字符串</param>
        /// <param name="owner">所有者令牌</param>
        /// <param name="ct">取消令牌</param>
        /// <returns>命令执行结果</returns>
        Task<DeviceCommandResult> ExecuteCommandAsync(
            string deviceId, string command, ResourceOwnerToken owner, CancellationToken ct);
        /// <summary>
        /// 异步执行设备查询。
        /// 需要有效的资源预约才能执行。
        /// </summary>
        /// <param name="deviceId">设备标识符</param>
        /// <param name="query">查询字符串</param>
        /// <param name="owner">所有者令牌</param>
        /// <param name="ct">取消令牌</param>
        /// <returns>命令执行结果</returns>
        Task<DeviceCommandResult> ExecuteQueryAsync(
            string deviceId, string query, ResourceOwnerToken owner, CancellationToken ct);
        /// <summary>
        /// 异步安全关闭设备。
        /// 执行设备特定的关机序列,确保设备处于安全状态。
        /// </summary>
        /// <param name="deviceId">设备标识符</param>
        /// <param name="owner">所有者令牌</param>
        /// <param name="ct">取消令牌</param>
        Task SafeShutdownAsync(string deviceId, ResourceOwnerToken owner, CancellationToken ct);
    }
}
Aging.Infrastructure/Contracts/IOperationLease.cs
New file
@@ -0,0 +1,40 @@
namespace Aging.Infrastructure.Contracts
{
    /// <summary>
    /// 短期操作租约接口。
    /// 在执行具体硬件操作前获取,确保操作期间资源不被抢占。
    /// 支持同步和异步释放。
    /// </summary>
    public interface IOperationLease : IDisposable, IAsyncDisposable
    {
        /// <summary>
        /// 租约唯一标识
        /// </summary>
        string LeaseId { get; }
        /// <summary>
        /// 租约关联的资源键
        /// </summary>
        ResourceKey Resource { get; }
        /// <summary>
        /// 租约所有者令牌
        /// </summary>
        ResourceOwnerToken Owner { get; }
        /// <summary>
        /// 租约获取时间
        /// </summary>
        DateTime AcquiredAt { get; }
        /// <summary>
        /// 租约过期时间(可选)
        /// </summary>
        DateTime? ExpiresAt { get; }
        /// <summary>
        /// 租约是否仍然有效
        /// </summary>
        bool IsValid { get; }
    }
}
Aging.Infrastructure/Contracts/IResourceManager.cs
New file
@@ -0,0 +1,84 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Aging.Infrastructure.Contracts
{
    /// <summary>
    /// 资源冲突异常。
    /// 当尝试预约已被其他所有者占用的资源时抛出。
    /// </summary>
    public sealed class ResourceConflictException : Exception
    {
        /// <summary>
        /// 初始化资源冲突异常实例
        /// </summary>
        /// <param name="message">冲突描述信息</param>
        public ResourceConflictException(string message) : base(message)
        {
        }
        /// <summary>
        /// 初始化资源冲突异常实例
        /// </summary>
        /// <param name="message">冲突描述信息</param>
        /// <param name="innerException">内部异常</param>
        public ResourceConflictException(string message, Exception innerException) : base(message, innerException)
        {
        }
    }
    /// <summary>
    /// 资源管理接口。
    /// 提供硬件资源的预约、租约获取和释放功能,确保资源不被多个会话同时占用。
    /// </summary>
    public interface IResourceManager
    {
        /// <summary>
        /// 异步预约资源。
        /// 创建长期预约,阻止其他所有者使用该资源。
        /// </summary>
        /// <param name="owner">所有者令牌</param>
        /// <param name="resource">资源键</param>
        /// <param name="ct">取消令牌</param>
        /// <returns>资源预约实体</returns>
        /// <exception cref="ResourceConflictException">资源已被其他所有者预约时抛出</exception>
        Task<ResourceReservation> ReserveAsync(ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct);
        /// <summary>
        /// 异步获取操作租约。
        /// 在执行具体硬件操作前调用,确保操作期间资源不被抢占。
        /// </summary>
        /// <param name="owner">所有者令牌</param>
        /// <param name="resource">资源键</param>
        /// <param name="ct">取消令牌</param>
        /// <returns>操作租约</returns>
        /// <exception cref="InvalidOperationException">资源未预约或所有者不匹配时抛出</exception>
        Task<IOperationLease> AcquireOperationAsync(ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct);
        /// <summary>
        /// 异步释放资源预约。
        /// 取消对资源的占用,允许其他所有者预约。
        /// </summary>
        /// <param name="owner">所有者令牌</param>
        /// <param name="resource">资源键</param>
        /// <param name="ct">取消令牌</param>
        Task ReleaseReservationAsync(ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct);
        /// <summary>
        /// 检查资源是否已被预约
        /// </summary>
        /// <param name="resource">资源键</param>
        /// <param name="reservation">输出预约实体(如果已预约)</param>
        /// <returns>资源是否已被预约</returns>
        bool IsReserved(ResourceKey resource, out ResourceReservation? reservation);
        /// <summary>
        /// 验证所有者令牌是否匹配指定资源的预约
        /// </summary>
        /// <param name="owner">所有者令牌</param>
        /// <param name="resource">资源键</param>
        /// <returns>所有者是否匹配</returns>
        bool ValidateOwner(ResourceOwnerToken owner, ResourceKey resource);
    }
}
Aging.Infrastructure/Contracts/ResourceKey.cs
New file
@@ -0,0 +1,134 @@
using System;
namespace Aging.Infrastructure.Contracts
{
    /// <summary>
    /// 资源键值对象,唯一标识一个可管理的硬件资源。
    /// 格式: "DeviceId:Channel",如 "PSU024:CH1"、"CAN_CARD01:CH2"、"PLC01:OUTPUT_GROUP_A"
    /// </summary>
    public sealed class ResourceKey : IEquatable<ResourceKey>
    {
        /// <summary>
        /// 设备标识符
        /// </summary>
        public string DeviceId { get; }
        /// <summary>
        /// 通道标识(可选)
        /// </summary>
        public string? Channel { get; }
        /// <summary>
        /// 资源类型(PowerSupply/CAN/PLC/Chamber等)
        /// </summary>
        public string ResourceType { get; }
        /// <summary>
        /// 初始化资源键实例
        /// </summary>
        /// <param name="deviceId">设备标识符</param>
        /// <param name="resourceType">资源类型</param>
        /// <param name="channel">通道标识(可选)</param>
        public ResourceKey(string deviceId, string resourceType, string? channel = null)
        {
            DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId));
            ResourceType = resourceType ?? throw new ArgumentNullException(nameof(resourceType));
            Channel = channel;
        }
        /// <summary>
        /// 从字符串解析资源键,格式为 "DeviceId:Channel"
        /// </summary>
        /// <param name="key">资源键字符串</param>
        /// <returns>解析后的资源键实例</returns>
        /// <exception cref="ArgumentException">格式无效时抛出</exception>
        public static ResourceKey FromString(string key)
        {
            if (string.IsNullOrWhiteSpace(key))
                throw new ArgumentException("资源键不能为空", nameof(key));
            var parts = key.Split(':');
            if (parts.Length < 2)
                throw new ArgumentException($"无效的资源键格式: '{key}',期望格式为 'DeviceId:Channel'");
            var deviceId = parts[0].Trim();
            var channel = string.Join(":", parts.Skip(1)).Trim();
            if (string.IsNullOrEmpty(deviceId))
                throw new ArgumentException($"无效的资源键格式: '{key}',DeviceId 不能为空");
            // 根据设备ID前缀推断资源类型
            var resourceType = InferResourceType(deviceId);
            return new ResourceKey(deviceId, resourceType, string.IsNullOrEmpty(channel) ? null : channel);
        }
        /// <summary>
        /// 获取操作资源键,用于物理会话锁
        /// </summary>
        public string OperationResourceKey => $"{DeviceId}:SESSION";
        /// <summary>
        /// 根据设备ID前缀推断资源类型
        /// </summary>
        private static string InferResourceType(string deviceId)
        {
            var upper = deviceId.ToUpperInvariant();
            if (upper.StartsWith("PSU") || upper.StartsWith("POWER"))
                return "PowerSupply";
            if (upper.StartsWith("CAN"))
                return "CAN";
            if (upper.StartsWith("PLC"))
                return "PLC";
            if (upper.StartsWith("CHAMBER") || upper.StartsWith("TEMP"))
                return "Chamber";
            if (upper.StartsWith("DAQ"))
                return "DAQ";
            if (upper.StartsWith("SERIAL") || upper.StartsWith("COM"))
                return "Serial";
            return "Generic";
        }
        /// <inheritdoc/>
        public bool Equals(ResourceKey? other)
        {
            if (other is null) return false;
            if (ReferenceEquals(this, other)) return true;
            return string.Equals(DeviceId, other.DeviceId, StringComparison.OrdinalIgnoreCase) &&
                   string.Equals(Channel, other.Channel, StringComparison.OrdinalIgnoreCase) &&
                   string.Equals(ResourceType, other.ResourceType, StringComparison.OrdinalIgnoreCase);
        }
        /// <inheritdoc/>
        public override bool Equals(object? obj) => Equals(obj as ResourceKey);
        /// <inheritdoc/>
        public override int GetHashCode()
        {
            unchecked
            {
                int hash = 17;
                hash = hash * 31 + (DeviceId?.ToUpperInvariant().GetHashCode() ?? 0);
                hash = hash * 31 + (Channel?.ToUpperInvariant().GetHashCode() ?? 0);
                hash = hash * 31 + (ResourceType?.ToUpperInvariant().GetHashCode() ?? 0);
                return hash;
            }
        }
        /// <inheritdoc/>
        public override string ToString() =>
            string.IsNullOrEmpty(Channel) ? DeviceId : $"{DeviceId}:{Channel}";
        /// <summary>
        /// 相等运算符重载
        /// </summary>
        public static bool operator ==(ResourceKey? left, ResourceKey? right) =>
            Equals(left, right);
        /// <summary>
        /// 不等运算符重载
        /// </summary>
        public static bool operator !=(ResourceKey? left, ResourceKey? right) =>
            !Equals(left, right);
    }
}
Aging.Infrastructure/Contracts/ResourceOwnerToken.cs
New file
@@ -0,0 +1,84 @@
using System;
namespace Aging.Infrastructure.Contracts
{
    /// <summary>
    /// 所有者类型枚举
    /// </summary>
    public enum OwnerType
    {
        /// <summary>
        /// 生产模式(正常老化测试流程)
        /// </summary>
        Production,
        /// <summary>
        /// 维护模式(调试/维护会话)
        /// </summary>
        Maintenance
    }
    /// <summary>
    /// 所有者令牌,标识资源预约的归属。
    /// 用于区分不同会话对硬件资源的所有权。
    /// </summary>
    public sealed class ResourceOwnerToken
    {
        /// <summary>
        /// 令牌唯一标识
        /// </summary>
        public string TokenId { get; }
        /// <summary>
        /// 所有者类型(生产/维护)
        /// </summary>
        public OwnerType OwnerType { get; }
        /// <summary>
        /// 所有者标识(RunID 或 DebugSessionID)
        /// </summary>
        public string OwnerId { get; }
        /// <summary>
        /// 世代号,每次新预约递增,防止重放攻击
        /// </summary>
        public long Generation { get; set; }
        /// <summary>
        /// 令牌签发时间
        /// </summary>
        public DateTime IssuedAt { get; }
        /// <summary>
        /// 令牌过期时间(可选)
        /// </summary>
        public DateTime? ExpiresAt { get; }
        /// <summary>
        /// 初始化所有者令牌实例
        /// </summary>
        /// <param name="ownerType">所有者类型</param>
        /// <param name="ownerId">所有者标识</param>
        /// <param name="expiresAt">过期时间(可选)</param>
        public ResourceOwnerToken(OwnerType ownerType, string ownerId, DateTime? expiresAt = null)
        {
            TokenId = Guid.NewGuid().ToString();
            OwnerType = ownerType;
            OwnerId = ownerId ?? throw new ArgumentNullException(nameof(ownerId));
            Generation = 0;
            IssuedAt = DateTime.UtcNow;
            ExpiresAt = expiresAt;
        }
        /// <summary>
        /// 检查令牌是否已过期
        /// </summary>
        /// <returns>过期返回 true;未设置过期时间或尚未过期返回 false</returns>
        public bool IsExpired()
        {
            if (!ExpiresAt.HasValue)
                return false;
            return DateTime.UtcNow > ExpiresAt.Value;
        }
    }
}
Aging.Infrastructure/Contracts/ResourceReservation.cs
New file
@@ -0,0 +1,78 @@
using System;
namespace Aging.Infrastructure.Contracts
{
    /// <summary>
    /// 预约状态枚举
    /// </summary>
    public enum ReservationState
    {
        /// <summary>
        /// 活跃状态
        /// </summary>
        Active,
        /// <summary>
        /// 已释放
        /// </summary>
        Released,
        /// <summary>
        /// 冲突状态
        /// </summary>
        Conflicted
    }
    /// <summary>
    /// 长期预约实体,表示对某个硬件资源的持久占用。
    /// 预约期间,其他所有者无法获取该资源。
    /// </summary>
    public sealed class ResourceReservation
    {
        /// <summary>
        /// 预约唯一标识
        /// </summary>
        public string ReservationId { get; }
        /// <summary>
        /// 被预约的资源键
        /// </summary>
        public ResourceKey Resource { get; }
        /// <summary>
        /// 预约所有者令牌
        /// </summary>
        public ResourceOwnerToken Owner { get; }
        /// <summary>
        /// 预约状态
        /// </summary>
        public ReservationState State { get; set; }
        /// <summary>
        /// 预约创建时间
        /// </summary>
        public DateTime ReservedAt { get; }
        /// <summary>
        /// 预约描述(可选)
        /// </summary>
        public string? Description { get; set; }
        /// <summary>
        /// 初始化资源预约实例
        /// </summary>
        /// <param name="resource">资源键</param>
        /// <param name="owner">所有者令牌</param>
        /// <param name="description">预约描述(可选)</param>
        public ResourceReservation(ResourceKey resource, ResourceOwnerToken owner, string? description = null)
        {
            ReservationId = Guid.NewGuid().ToString();
            Resource = resource ?? throw new ArgumentNullException(nameof(resource));
            Owner = owner ?? throw new ArgumentNullException(nameof(owner));
            State = ReservationState.Active;
            ReservedAt = DateTime.UtcNow;
            Description = description;
        }
    }
}
Aging.Infrastructure/Core/DeviceManager.cs
New file
@@ -0,0 +1,226 @@
using System.Collections.Concurrent;
using Aging.Infrastructure.Contracts;
using log4net;
namespace Aging.Infrastructure.Core
{
    /// <summary>
    /// 设备管理实现。
    /// 提供设备驱动的注册、连接管理和状态查询功能。
    /// 使用 ConcurrentDictionary 管理驱动实例和缓存状态。
    /// </summary>
    public sealed class DeviceManager : IDeviceManager
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(DeviceManager));
        /// <summary>
        /// 设备驱动字典(key = deviceId)
        /// </summary>
        private readonly ConcurrentDictionary<string, IDeviceDriver> _drivers = new();
        /// <summary>
        /// 设备状态缓存字典(key = deviceId)
        /// </summary>
        private readonly ConcurrentDictionary<string, DeviceDriverStatus> _statusCache = new();
        /// <inheritdoc/>
        public void RegisterDriver(string deviceId, IDeviceDriver driver)
        {
            if (string.IsNullOrWhiteSpace(deviceId))
                throw new ArgumentException("设备ID不能为空", nameof(deviceId));
            if (driver == null) throw new ArgumentNullException(nameof(driver));
            if (_drivers.ContainsKey(deviceId))
            {
                Log.Warn($"[DeviceManager] 设备 {deviceId} 已注册,将覆盖现有驱动");
                UnregisterDriver(deviceId);
            }
            _drivers[deviceId] = driver;
            // 初始化状态缓存
            var initialStatus = driver.GetStatus();
            _statusCache[deviceId] = initialStatus;
            // 订阅连接状态变化事件
            driver.ConnectionChanged += OnConnectionChanged;
            Log.Info($"[DeviceManager] 设备 {deviceId} 驱动注册成功,当前连接状态: {initialStatus.IsConnected}");
        }
        /// <inheritdoc/>
        public void UnregisterDriver(string deviceId)
        {
            if (string.IsNullOrWhiteSpace(deviceId))
                throw new ArgumentException("设备ID不能为空", nameof(deviceId));
            if (_drivers.TryRemove(deviceId, out var driver))
            {
                // 取消事件订阅
                driver.ConnectionChanged -= OnConnectionChanged;
                // 清理状态缓存
                _statusCache.TryRemove(deviceId, out _);
                Log.Info($"[DeviceManager] 设备 {deviceId} 驱动已注销");
            }
            else
            {
                Log.Warn($"[DeviceManager] 尝试注销不存在的设备: {deviceId}");
            }
        }
        /// <inheritdoc/>
        public IDeviceDriver? GetDriver(string deviceId)
        {
            if (string.IsNullOrWhiteSpace(deviceId))
                throw new ArgumentException("设备ID不能为空", nameof(deviceId));
            _drivers.TryGetValue(deviceId, out var driver);
            return driver;
        }
        /// <inheritdoc/>
        public DeviceDriverStatus GetStatus(string deviceId)
        {
            if (string.IsNullOrWhiteSpace(deviceId))
                throw new ArgumentException("设备ID不能为空", nameof(deviceId));
            if (_statusCache.TryGetValue(deviceId, out var status))
            {
                return status;
            }
            // 如果缓存中没有,返回默认状态
            Log.Warn($"[DeviceManager] 设备 {deviceId} 状态缓存不存在,返回默认状态");
            return new DeviceDriverStatus
            {
                IsConnected = false,
                IsHealthy = false,
                LastError = "设备未注册或状态未初始化"
            };
        }
        /// <inheritdoc/>
        public async Task ConnectAsync(string deviceId, CancellationToken ct)
        {
            if (string.IsNullOrWhiteSpace(deviceId))
                throw new ArgumentException("设备ID不能为空", nameof(deviceId));
            if (!_drivers.TryGetValue(deviceId, out var driver))
            {
                throw new InvalidOperationException($"设备 {deviceId} 未注册驱动");
            }
            try
            {
                Log.Info($"[DeviceManager] 正在连接设备: {deviceId}");
                await driver.ConnectAsync(ct).ConfigureAwait(false);
                // 更新状态缓存
                _statusCache[deviceId] = driver.GetStatus();
                Log.Info($"[DeviceManager] 设备 {deviceId} 连接成功");
            }
            catch (Exception ex)
            {
                // 更新错误状态
                if (_statusCache.TryGetValue(deviceId, out var status))
                {
                    status.IsConnected = false;
                    status.IsHealthy = false;
                    status.LastError = ex.Message;
                    status.LastErrorTime = DateTime.UtcNow;
                    _statusCache[deviceId] = status;
                }
                Log.Error($"[DeviceManager] 设备 {deviceId} 连接失败: {ex.Message}", ex);
                throw;
            }
        }
        /// <inheritdoc/>
        public async Task DisconnectAsync(string deviceId)
        {
            if (string.IsNullOrWhiteSpace(deviceId))
                throw new ArgumentException("设备ID不能为空", nameof(deviceId));
            if (!_drivers.TryGetValue(deviceId, out var driver))
            {
                Log.Warn($"[DeviceManager] 尝试断开未注册的设备: {deviceId}");
                return;
            }
            try
            {
                Log.Info($"[DeviceManager] 正在断开设备: {deviceId}");
                await driver.DisconnectAsync().ConfigureAwait(false);
                // 更新状态缓存
                _statusCache[deviceId] = driver.GetStatus();
                Log.Info($"[DeviceManager] 设备 {deviceId} 已断开连接");
            }
            catch (Exception ex)
            {
                Log.Error($"[DeviceManager] 设备 {deviceId} 断开连接失败: {ex.Message}", ex);
                throw;
            }
        }
        /// <inheritdoc/>
        public IReadOnlyList<string> GetRegisteredDevices()
        {
            return _drivers.Keys.ToList().AsReadOnly();
        }
        /// <inheritdoc/>
        public bool IsDeviceRegistered(string deviceId)
        {
            if (string.IsNullOrWhiteSpace(deviceId))
                return false;
            return _drivers.ContainsKey(deviceId);
        }
        /// <summary>
        /// 处理设备连接状态变化事件
        /// </summary>
        /// <param name="sender">事件发送者</param>
        /// <param name="e">事件参数</param>
        private void OnConnectionChanged(object? sender, DeviceConnectionEventArgs e)
        {
            var deviceId = e.DeviceId;
            if (string.IsNullOrWhiteSpace(deviceId))
            {
                Log.Warn("[DeviceManager] 收到连接变化事件但 DeviceId 为空");
                return;
            }
            if (_statusCache.TryGetValue(deviceId, out var status))
            {
                status.IsConnected = e.IsConnected;
                if (!e.IsConnected)
                {
                    status.IsHealthy = false;
                    if (!string.IsNullOrEmpty(e.Reason))
                    {
                        status.LastError = e.Reason;
                        status.LastErrorTime = DateTime.UtcNow;
                    }
                }
                else
                {
                    status.IsHealthy = true;
                    status.LastError = "";
                }
                _statusCache[deviceId] = status;
                Log.Info($"[DeviceManager] 设备 {deviceId} 连接状态变化: {(e.IsConnected ? "已连接" : "已断开")}" +
                        (string.IsNullOrEmpty(e.Reason) ? "" : $", 原因: {e.Reason}"));
            }
        }
    }
}
Aging.Infrastructure/Core/OperationLease.cs
New file
@@ -0,0 +1,82 @@
using Aging.Infrastructure.Contracts;
namespace Aging.Infrastructure.Core
{
    /// <summary>
    /// 操作租约实现。
    /// 提供短期资源占用保证,支持同步和异步释放。
    /// 释放时通过回调通知 ResourceManager。
    /// </summary>
    public sealed class OperationLease : IOperationLease
    {
        private readonly Action? _onRelease;
        private bool _disposed;
        /// <inheritdoc/>
        public string LeaseId { get; }
        /// <inheritdoc/>
        public ResourceKey Resource { get; }
        /// <inheritdoc/>
        public ResourceOwnerToken Owner { get; }
        /// <inheritdoc/>
        public DateTime AcquiredAt { get; }
        /// <inheritdoc/>
        public DateTime? ExpiresAt { get; }
        /// <inheritdoc/>
        public bool IsValid => !_disposed && !IsExpired();
        /// <summary>
        /// 初始化操作租约实例
        /// </summary>
        /// <param name="resource">资源键</param>
        /// <param name="owner">所有者令牌</param>
        /// <param name="onRelease">释放时的回调通知</param>
        /// <param name="leaseDurationSeconds">租约有效期(秒),默认30秒</param>
        public OperationLease(
            ResourceKey resource,
            ResourceOwnerToken owner,
            Action? onRelease = null,
            int leaseDurationSeconds = 30)
        {
            LeaseId = Guid.NewGuid().ToString();
            Resource = resource ?? throw new ArgumentNullException(nameof(resource));
            Owner = owner ?? throw new ArgumentNullException(nameof(owner));
            AcquiredAt = DateTime.UtcNow;
            ExpiresAt = leaseDurationSeconds > 0
                ? AcquiredAt.AddSeconds(leaseDurationSeconds)
                : null;
            _onRelease = onRelease;
            _disposed = false;
        }
        /// <summary>
        /// 检查租约是否已过期
        /// </summary>
        private bool IsExpired()
        {
            if (!ExpiresAt.HasValue)
                return false;
            return DateTime.UtcNow > ExpiresAt.Value;
        }
        /// <inheritdoc/>
        public void Dispose()
        {
            if (_disposed) return;
            _disposed = true;
            _onRelease?.Invoke();
        }
        /// <inheritdoc/>
        public ValueTask DisposeAsync()
        {
            Dispose();
            return default;
        }
    }
}
Aging.Infrastructure/Core/ResourceManager.cs
New file
@@ -0,0 +1,258 @@
using System.Collections.Concurrent;
using Aging.Infrastructure.Contracts;
using log4net;
namespace Aging.Infrastructure.Core
{
    /// <summary>
    /// 资源管理实现。
    /// 提供线程安全的资源预约、租约获取和释放功能。
    /// 使用 ConcurrentDictionary 存储活跃预约,使用 SemaphoreSlim 实现 per-resource 锁。
    /// </summary>
    public sealed class ResourceManager : IResourceManager
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(ResourceManager));
        /// <summary>
        /// 活跃预约字典(key = ResourceKey.ToString())
        /// </summary>
        private readonly ConcurrentDictionary<string, ResourceReservation> _reservations = new();
        /// <summary>
        /// 每资源信号量字典,用于实现细粒度锁
        /// </summary>
        private readonly ConcurrentDictionary<string, SemaphoreSlim> _resourceLocks = new();
        /// <summary>
        /// 全局世代号,每次新预约递增
        /// </summary>
        private long _generation;
        /// <summary>
        /// 默认租约有效期(秒)
        /// </summary>
        private const int DefaultLeaseDurationSeconds = 30;
        /// <inheritdoc/>
        public async Task<ResourceReservation> ReserveAsync(
            ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct)
        {
            if (owner == null) throw new ArgumentNullException(nameof(owner));
            if (resource == null) throw new ArgumentNullException(nameof(resource));
            var resourceKey = resource.ToString();
            var semaphore = _resourceLocks.GetOrAdd(resourceKey, _ => new SemaphoreSlim(1, 1));
            try
            {
                await semaphore.WaitAsync(ct).ConfigureAwait(false);
                try
                {
                    // 检查是否已有 Active 预约
                    if (_reservations.TryGetValue(resourceKey, out var existingReservation))
                    {
                        if (existingReservation.State == ReservationState.Active)
                        {
                            // 检查是否为同一 Owner(续期场景)
                            if (existingReservation.Owner.TokenId == owner.TokenId)
                            {
                                // 同一所有者续期:更新世代号
                                var renewedGeneration = Interlocked.Increment(ref _generation);
                                owner.Generation = renewedGeneration;
                                existingReservation.Owner.Generation = renewedGeneration;
                                Log.Info($"[ResourceManager] 资源 {resourceKey} 续期成功,新世代号: {renewedGeneration}");
                                return existingReservation;
                            }
                            // 不同所有者冲突
                            var conflictMsg = $"资源 {resourceKey} 已被所有者 {existingReservation.Owner.OwnerId} 预约," +
                                             $"当前请求者: {owner.OwnerId}";
                            Log.Warn($"[ResourceManager] 资源冲突: {conflictMsg}");
                            throw new ResourceConflictException(conflictMsg);
                        }
                    }
                    // 创建新预约
                    var newGeneration = Interlocked.Increment(ref _generation);
                    owner.Generation = newGeneration;
                    var reservation = new ResourceReservation(resource, owner)
                    {
                        State = ReservationState.Active
                    };
                    _reservations[resourceKey] = reservation;
                    Log.Info($"[ResourceManager] 资源 {resourceKey} 预约成功,所有者: {owner.OwnerId},世代号: {newGeneration}");
                    return reservation;
                }
                finally
                {
                    semaphore.Release();
                }
            }
            catch (OperationCanceledException)
            {
                Log.Warn($"[ResourceManager] 资源 {resourceKey} 预约被取消");
                throw;
            }
            catch (ResourceConflictException)
            {
                throw;
            }
            catch (Exception ex)
            {
                Log.Error($"[ResourceManager] 资源 {resourceKey} 预约失败: {ex.Message}", ex);
                throw;
            }
        }
        /// <inheritdoc/>
        public async Task<IOperationLease> AcquireOperationAsync(
            ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct)
        {
            if (owner == null) throw new ArgumentNullException(nameof(owner));
            if (resource == null) throw new ArgumentNullException(nameof(resource));
            var resourceKey = resource.ToString();
            try
            {
                // 检查资源是否有有效预约
                if (!_reservations.TryGetValue(resourceKey, out var reservation))
                {
                    throw new InvalidOperationException($"资源 {resourceKey} 未被预约,无法获取操作租约");
                }
                if (reservation.State != ReservationState.Active)
                {
                    throw new InvalidOperationException($"资源 {resourceKey} 预约状态为 {reservation.State},无法获取操作租约");
                }
                // 验证 Owner 匹配(TokenId + Generation)
                if (reservation.Owner.TokenId != owner.TokenId || reservation.Owner.Generation != owner.Generation)
                {
                    throw new InvalidOperationException(
                        $"资源 {resourceKey} 所有者不匹配,期望 TokenId: {reservation.Owner.TokenId},Generation: {reservation.Owner.Generation}");
                }
                // 创建 OperationLease(短期,默认30秒过期)
                var lease = new OperationLease(
                    resource,
                    owner,
                    onRelease: () => Log.Debug($"[ResourceManager] 租约释放: {resourceKey}"),
                    leaseDurationSeconds: DefaultLeaseDurationSeconds);
                Log.Info($"[ResourceManager] 获取操作租约: {resourceKey},所有者: {owner.OwnerId},有效期: {DefaultLeaseDurationSeconds}秒");
                await Task.CompletedTask; // 保持异步签名
                return lease;
            }
            catch (InvalidOperationException)
            {
                throw;
            }
            catch (Exception ex)
            {
                Log.Error($"[ResourceManager] 获取操作租约失败: {ex.Message}", ex);
                throw;
            }
        }
        /// <inheritdoc/>
        public async Task ReleaseReservationAsync(
            ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct)
        {
            if (owner == null) throw new ArgumentNullException(nameof(owner));
            if (resource == null) throw new ArgumentNullException(nameof(resource));
            var resourceKey = resource.ToString();
            var semaphore = _resourceLocks.GetOrAdd(resourceKey, _ => new SemaphoreSlim(1, 1));
            try
            {
                await semaphore.WaitAsync(ct).ConfigureAwait(false);
                try
                {
                    if (!_reservations.TryGetValue(resourceKey, out var reservation))
                    {
                        Log.Warn($"[ResourceManager] 尝试释放不存在的资源预约: {resourceKey}");
                        return;
                    }
                    // 验证 Owner 匹配
                    if (reservation.Owner.TokenId != owner.TokenId)
                    {
                        throw new InvalidOperationException(
                            $"资源 {resourceKey} 释放失败:所有者不匹配,期望 TokenId: {reservation.Owner.TokenId}");
                    }
                    // 标记预约为 Released
                    reservation.State = ReservationState.Released;
                    _reservations.TryRemove(resourceKey, out _);
                    Log.Info($"[ResourceManager] 资源 {resourceKey} 预约已释放,所有者: {owner.OwnerId}");
                }
                finally
                {
                    semaphore.Release();
                }
                await Task.CompletedTask; // 保持异步签名
            }
            catch (InvalidOperationException)
            {
                throw;
            }
            catch (OperationCanceledException)
            {
                Log.Warn($"[ResourceManager] 资源 {resourceKey} 释放操作被取消");
                throw;
            }
            catch (Exception ex)
            {
                Log.Error($"[ResourceManager] 释放资源 {resourceKey} 失败: {ex.Message}", ex);
                throw;
            }
        }
        /// <inheritdoc/>
        public bool IsReserved(ResourceKey resource, out ResourceReservation? reservation)
        {
            if (resource == null) throw new ArgumentNullException(nameof(resource));
            var resourceKey = resource.ToString();
            var result = _reservations.TryGetValue(resourceKey, out var res);
            if (result && res != null && res.State == ReservationState.Active)
            {
                reservation = res;
                return true;
            }
            reservation = null;
            return false;
        }
        /// <inheritdoc/>
        public bool ValidateOwner(ResourceOwnerToken owner, ResourceKey resource)
        {
            if (owner == null) throw new ArgumentNullException(nameof(owner));
            if (resource == null) throw new ArgumentNullException(nameof(resource));
            var resourceKey = resource.ToString();
            if (!_reservations.TryGetValue(resourceKey, out var reservation))
                return false;
            if (reservation.State != ReservationState.Active)
                return false;
            return reservation.Owner.TokenId == owner.TokenId &&
                   reservation.Owner.Generation == owner.Generation;
        }
    }
}
Aging.Infrastructure/Drivers/CanDriverAdapter.cs
New file
@@ -0,0 +1,576 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Aging.Infrastructure.Contracts;
using CanSimulator;
using log4net;
namespace Aging.Infrastructure.Drivers
{
    /// <summary>
    /// CAN 通讯驱动适配器。
    /// 封装 CanSimulator,实现统一的 IDeviceDriver 接口。
    /// 命令格式约定:
    ///   发送: "SEND:frameId:data"  如 "SEND:0x100:01 02 03 04"
    ///   查询:
    ///     "RECV"           - 获取接收邮箱中的下一帧
    ///     "RECV_ALL"       - 获取接收邮箱中的所有帧
    ///     "STATUS"         - 获取节点/总线状态
    ///     "FILTER:id:mask" - 设置接收滤波器
    ///     "FILTER_CLEAR"   - 清除所有滤波器
    ///     "PROMISCUOUS"    - 设置混杂模式(接收所有帧)
    ///     "LOG"            - 获取总线日志
    ///     "BUS_STATUS"     - 获取总线状态摘要
    /// </summary>
    public sealed class CanDriverAdapter : IDeviceDriver
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(CanDriverAdapter));
        /// <summary>
        /// 全局 CAN 总线模拟器实例(所有 CAN 驱动共享)
        /// </summary>
        private static readonly Dictionary<string, CanBusSimulator> _sharedBuses = new();
        private static readonly object _busLock = new();
        private readonly string _ip;      // 总线名称(用 ip 字段映射)
        private readonly int _port;       // 节点编号(用 port 字段映射)
        private CanBusSimulator? _bus;
        private CanNode? _node;
        private bool _disposed;
        private readonly Stopwatch _latencyStopwatch = new();
        private double _lastLatencyMs;
        private string _lastError = string.Empty;
        private DateTime? _lastErrorTime;
        private int _reconnectCount;
        /// <summary>
        /// 接收到的帧缓存(用于 RECV 查询)
        /// </summary>
        private readonly Queue<CanFrame> _recvBuffer = new();
        /// <inheritdoc/>
        public string DeviceId { get; }
        /// <inheritdoc/>
        public bool IsConnected => _node != null && _node.IsActive;
        /// <inheritdoc/>
        public event EventHandler<DeviceConnectionEventArgs>? ConnectionChanged;
        /// <summary>
        /// 初始化 CAN 驱动适配器
        /// </summary>
        /// <param name="deviceId">设备唯一标识符</param>
        /// <param name="busName">总线名称(如 CAN_BUS_1)</param>
        /// <param name="nodeIndex">节点编号(用于区分同一总线上的不同设备)</param>
        public CanDriverAdapter(string deviceId, string busName, int nodeIndex)
        {
            DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId));
            _ip = busName ?? "CAN_BUS_1";
            _port = nodeIndex;
        }
        /// <summary>
        /// 获取或创建共享总线
        /// </summary>
        private CanBusSimulator GetOrCreateBus(string busName)
        {
            lock (_busLock)
            {
                if (!_sharedBuses.TryGetValue(busName, out var bus))
                {
                    bus = new CanBusSimulator(busName);
                    _sharedBuses[busName] = bus;
                    Log.Info($"[CAN] 创建新的虚拟总线: {busName}");
                }
                return bus;
            }
        }
        /// <inheritdoc/>
        public Task ConnectAsync(CancellationToken ct)
        {
            ThrowIfDisposed();
            Log.Info($"[CAN] 设备 {DeviceId} 正在连接到总线 {_ip} (节点: {_port})...");
            try
            {
                // 获取或创建共享总线
                _bus = GetOrCreateBus(_ip);
                // 如果总线未启动,自动启动
                if (_bus.State != CanBusState.Running)
                {
                    _bus.Start();
                }
                // 注册节点
                string nodeId = $"{DeviceId}_node{_port}";
                _node = _bus.RegisterNode(nodeId, DeviceId);
                // 设置默认滤波器(接收所有帧)
                _node.SetPromiscuousMode();
                // 激活节点
                _node.Activate();
                // 订阅接收事件
                _node.FrameReceived += OnFrameReceived;
                _lastError = string.Empty;
                Log.Info($"[CAN] 设备 {DeviceId} 已成功连接到总线 {_ip} (节点: {_node.NodeId})");
                OnConnectionChanged(true, "连接成功");
            }
            catch (Exception ex)
            {
                _lastError = ex.Message;
                _lastErrorTime = DateTime.Now;
                Log.Error($"[CAN] 设备 {DeviceId} 连接异常: {ex.Message}", ex);
                _node?.Deactivate();
                _node = null;
                throw;
            }
            return Task.CompletedTask;
        }
        /// <inheritdoc/>
        public Task DisconnectAsync()
        {
            ThrowIfDisposed();
            Log.Info($"[CAN] 设备 {DeviceId} 正在断开连接...");
            try
            {
                if (_node != null)
                {
                    _node.FrameReceived -= OnFrameReceived;
                    _node.Deactivate();
                    _bus?.UnregisterNode(_node.NodeId);
                    _node = null;
                }
                Log.Info($"[CAN] 设备 {DeviceId} 已断开连接");
                OnConnectionChanged(false, "主动断开");
            }
            catch (Exception ex)
            {
                _lastError = ex.Message;
                _lastErrorTime = DateTime.Now;
                Log.Error($"[CAN] 设备 {DeviceId} 断开连接异常: {ex.Message}", ex);
                _node = null;
            }
            return Task.CompletedTask;
        }
        /// <inheritdoc/>
        public Task<string> SendCommandAsync(string command, CancellationToken ct)
        {
            ThrowIfDisposed();
            if (!IsConnected) throw new InvalidOperationException("[CAN] 设备未连接");
            _latencyStopwatch.Restart();
            try
            {
                Log.Debug($"[CAN] 设备 {DeviceId} 发送命令: {command}");
                // 解析命令格式: "SEND:frameId:data"
                string[] parts = command.Split(':');
                if (parts.Length < 3)
                {
                    throw new ArgumentException($"[CAN] 命令格式错误,应为 SEND:frameId:data,实际: {command}");
                }
                string operation = parts[0].ToUpper();
                if (operation != "SEND")
                {
                    throw new ArgumentException($"[CAN] 不支持的操作类型: {operation},应为 SEND");
                }
                // 解析帧 ID
                string frameIdStr = parts[1];
                uint frameId = CanFrame.ParseFrameId(frameIdStr);
                // 解析数据
                string dataStr = parts[2];
                byte[] data = CanFrame.ParseData(dataStr);
                // 验证数据长度
                if (data.Length > 8)
                {
                    throw new ArgumentException($"[CAN] 数据长度超限: {data.Length} 字节(最大 8 字节)");
                }
                // 创建并发送帧
                var frame = new CanFrame
                {
                    FrameId = frameId,
                    Format = frameId > 0x7FF ? CanFrameFormat.Extended : CanFrameFormat.Standard,
                    Type = CanFrameType.Data,
                    Data = data
                };
                _node!.SendFrame(frame);
                _latencyStopwatch.Stop();
                _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
                string response = $"OK:TX:{frame.FrameIdHex}:{frame.Dlc}";
                Log.Debug($"[CAN] 设备 {DeviceId} 发送成功 (延迟 {_lastLatencyMs:F2}ms): {response}");
                return Task.FromResult(response);
            }
            catch (Exception ex)
            {
                _latencyStopwatch.Stop();
                _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
                _lastError = ex.Message;
                _lastErrorTime = DateTime.Now;
                Log.Error($"[CAN] 设备 {DeviceId} 发送命令失败: {ex.Message}", ex);
                throw;
            }
        }
        /// <inheritdoc/>
        public Task<string> QueryAsync(string query, CancellationToken ct)
        {
            ThrowIfDisposed();
            if (!IsConnected) throw new InvalidOperationException("[CAN] 设备未连接");
            _latencyStopwatch.Restart();
            try
            {
                Log.Debug($"[CAN] 设备 {DeviceId} 查询: {query}");
                string[] parts = query.Split(':');
                string operation = parts[0].ToUpper();
                string response;
                switch (operation)
                {
                    case "RECV":
                        response = HandleRecv();
                        break;
                    case "RECV_ALL":
                        response = HandleRecvAll();
                        break;
                    case "STATUS":
                        response = HandleStatus();
                        break;
                    case "FILTER":
                        response = HandleFilter(parts);
                        break;
                    case "FILTER_CLEAR":
                        _node!.ClearFilters();
                        response = "OK:滤波器已清除";
                        break;
                    case "PROMISCUOUS":
                        _node!.SetPromiscuousMode();
                        response = "OK:混杂模式已启用";
                        break;
                    case "AUTO_RESPONSE":
                        response = HandleAutoResponse(parts);
                        break;
                    case "RESPONSE_DELAY":
                        response = HandleResponseDelay(parts);
                        break;
                    case "LOG":
                        response = HandleLog();
                        break;
                    case "BUS_STATUS":
                        response = _bus?.GetStatusSummary() ?? "总线未初始化";
                        break;
                    default:
                        throw new ArgumentException($"[CAN] 不支持的查询类型: {operation}");
                }
                _latencyStopwatch.Stop();
                _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
                Log.Debug($"[CAN] 设备 {DeviceId} 查询响应: {response} (延迟 {_lastLatencyMs:F2}ms)");
                return Task.FromResult(response);
            }
            catch (Exception ex)
            {
                _latencyStopwatch.Stop();
                _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
                _lastError = ex.Message;
                _lastErrorTime = DateTime.Now;
                Log.Error($"[CAN] 设备 {DeviceId} 查询失败: {ex.Message}", ex);
                throw;
            }
        }
        /// <summary>
        /// 处理 RECV 查询 - 获取下一帧
        /// </summary>
        private string HandleRecv()
        {
            // 先从缓冲区取
            if (_recvBuffer.Count > 0)
            {
                var frame = _recvBuffer.Dequeue();
                return $"RECV:{frame.FrameIdHex}:{frame.DataHex}:{frame.Timestamp:HH:mm:ss.fff}";
            }
            // 从节点邮箱取
            if (_node!.TryDequeueReceived(out var frame2))
            {
                return $"RECV:{frame2!.FrameIdHex}:{frame2.DataHex}:{frame2.Timestamp:HH:mm:ss.fff}";
            }
            return "RECV:EMPTY";
        }
        /// <summary>
        /// 处理 RECV_ALL 查询 - 获取所有帧
        /// </summary>
        private string HandleRecvAll()
        {
            var frames = new List<string>();
            // 先取缓冲区
            while (_recvBuffer.Count > 0)
            {
                var f = _recvBuffer.Dequeue();
                frames.Add($"{f.FrameIdHex}:{f.DataHex}");
            }
            // 再取节点邮箱
            var mailbox = _node!.PeekReceiveMailbox();
            _node.ClearReceiveMailbox();
            foreach (var f in mailbox)
            {
                frames.Add($"{f.FrameIdHex}:{f.DataHex}:{f.Timestamp:HH:mm:ss.fff}");
            }
            return frames.Count > 0 ? $"RECV_ALL:{frames.Count}:{string.Join("|", frames)}" : "RECV_ALL:0";
        }
        /// <summary>
        /// 处理 STATUS 查询
        /// </summary>
        private string HandleStatus()
        {
            var nodeStatus = _node?.GetStatusSummary() ?? "节点未初始化";
            var busStatus = _bus?.GetStatusSummary() ?? "总线未初始化";
            return $"STATUS:{nodeStatus}|{busStatus}";
        }
        /// <summary>
        /// 处理 FILTER 设置
        /// </summary>
        private string HandleFilter(string[] parts)
        {
            if (parts.Length < 3)
            {
                throw new ArgumentException("[CAN] FILTER 命令格式错误,应为 FILTER:id:mask");
            }
            uint filterId = CanFrame.ParseFrameId(parts[1]);
            uint mask = CanFrame.ParseFrameId(parts[2]);
            _node!.AddFilter(new AcceptanceFilter
            {
                FilterId = filterId,
                Mask = mask,
                IsExtended = filterId > 0x7FF
            });
            return $"OK:滤波器已添加 ID=0x{filterId:X3}, Mask=0x{mask:X3}";
        }
        /// <summary>
        /// 处理 AUTO_RESPONSE 命令
        /// 格式: AUTO_RESPONSE:on|off
        /// </summary>
        private string HandleAutoResponse(string[] parts)
        {
            if (parts.Length < 2)
            {
                // 查询当前状态
                bool current = _node!.AutoResponseEnabled;
                return $"AUTO_RESPONSE:{(current ? "ON" : "OFF")}";
            }
            string mode = parts[1].Trim().ToUpper();
            switch (mode)
            {
                case "ON":
                case "1":
                case "TRUE":
                case "ENABLE":
                    _node!.AutoResponseEnabled = true;
                    Log.Info($"[CAN] 设备 {DeviceId} 启用自动响应模式");
                    return "OK:自动响应模式已启用";
                case "OFF":
                case "0":
                case "FALSE":
                case "DISABLE":
                    _node!.AutoResponseEnabled = false;
                    Log.Info($"[CAN] 设备 {DeviceId} 禁用自动响应模式");
                    return "OK:自动响应模式已禁用";
                default:
                    throw new ArgumentException($"[CAN] AUTO_RESPONSE 参数错误: {mode},应为 on/off");
            }
        }
        /// <summary>
        /// 处理 RESPONSE_DELAY 命令
        /// 格式: RESPONSE_DELAY:毫秒数
        /// </summary>
        private string HandleResponseDelay(string[] parts)
        {
            if (parts.Length < 2)
            {
                // 查询当前延迟
                return $"RESPONSE_DELAY:{_node!.AutoResponseDelayMs}ms";
            }
            if (!int.TryParse(parts[1].Trim(), out int delayMs) || delayMs < 0)
            {
                throw new ArgumentException($"[CAN] RESPONSE_DELAY 参数错误: {parts[1]},应为非负整数(毫秒)");
            }
            _node!.AutoResponseDelayMs = delayMs;
            Log.Info($"[CAN] 设备 {DeviceId} 设置响应延迟: {delayMs}ms");
            return $"OK:响应延迟已设置为 {delayMs}ms";
        }
        /// <summary>
        /// 处理 LOG 查询
        /// </summary>
        private string HandleLog()
        {
            var log = _bus?.GetBusLog(50) ?? new List<CanFrame>();
            if (log.Count == 0) return "LOG:EMPTY";
            var entries = log.Select(f =>
                $"[{f.Timestamp:HH:mm:ss.fff}]{f.SourceNodeId}>{f.FrameIdHex}:{f.DataHex}");
            return $"LOG:{log.Count}:{string.Join("|", entries)}";
        }
        /// <summary>
        /// 帧接收事件(公开给外部订阅)
        /// </summary>
        public event EventHandler<CanFrame>? DriverFrameReceived;
        /// <summary>
        /// 帧接收事件处理
        /// </summary>
        private void OnFrameReceived(object? sender, CanFrame frame)
        {
            // 放入接收缓冲区(用于 RECV/RECV_ALL 查询)
            while (_recvBuffer.Count >= 1000)
            {
                _recvBuffer.Dequeue();
            }
            _recvBuffer.Enqueue(frame);
            // 触发公开事件(用于即时显示)
            DriverFrameReceived?.Invoke(this, frame);
        }
        /// <inheritdoc/>
        public DeviceDriverStatus GetStatus()
        {
            return new DeviceDriverStatus
            {
                IsConnected = IsConnected,
                IsHealthy = IsConnected && string.IsNullOrEmpty(_lastError),
                LastError = _lastError,
                LastErrorTime = _lastErrorTime,
                LatencyMs = _lastLatencyMs,
                ReconnectCount = _reconnectCount
            };
        }
        /// <inheritdoc/>
        public void Dispose()
        {
            if (_disposed) return;
            _disposed = true;
            try
            {
                if (_node != null)
                {
                    _node.FrameReceived -= OnFrameReceived;
                    _node.Deactivate();
                    _bus?.UnregisterNode(_node.NodeId);
                }
            }
            catch (Exception ex)
            {
                Log.Error($"[CAN] 设备 {DeviceId} 释放资源异常: {ex.Message}", ex);
            }
        }
        /// <summary>
        /// 触发所有总线和节点(用于测试清理)
        /// </summary>
        public static void ResetAllBuses()
        {
            lock (_busLock)
            {
                foreach (var bus in _sharedBuses.Values)
                {
                    bus.Dispose();
                }
                _sharedBuses.Clear();
                Log.Info("[CAN] 所有虚拟总线已重置");
            }
        }
        /// <summary>
        /// 触发所有总线实例(供外部查询)
        /// </summary>
        public static IReadOnlyDictionary<string, CanBusSimulator> GetAllBuses()
        {
            lock (_busLock)
            {
                return new Dictionary<string, CanBusSimulator>(_sharedBuses);
            }
        }
        /// <summary>
        /// 触发连接状态变化事件
        /// </summary>
        private void OnConnectionChanged(bool isConnected, string reason)
        {
            ConnectionChanged?.Invoke(this, new DeviceConnectionEventArgs
            {
                DeviceId = DeviceId,
                IsConnected = isConnected,
                Reason = reason
            });
            if (!isConnected)
            {
                _reconnectCount++;
            }
        }
        /// <summary>
        /// 检查是否已释放
        /// </summary>
        private void ThrowIfDisposed()
        {
            if (_disposed) throw new ObjectDisposedException(nameof(CanDriverAdapter));
        }
    }
}
Aging.Infrastructure/Drivers/DriverFactory.cs
New file
@@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using Aging.Infrastructure.Contracts;
using log4net;
namespace Aging.Infrastructure.Drivers
{
    /// <summary>
    /// 设备驱动工厂。
    /// 根据传输协议类型和端点信息创建对应的驱动适配器实例。
    /// </summary>
    public static class DriverFactory
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(DriverFactory));
        /// <summary>
        /// 根据传输协议类型和端点创建驱动适配器
        /// </summary>
        /// <param name="deviceId">设备唯一标识符</param>
        /// <param name="transport">传输协议类型: "Tcp", "SCPI", "ModBus", "S7", "Serial"</param>
        /// <param name="endpoint">端点信息,格式: "ip:port" 或 "ip:port:extra"</param>
        /// <returns>IDeviceDriver 实例</returns>
        /// <exception cref="NotSupportedException">不支持的传输协议类型</exception>
        /// <exception cref="ArgumentException">端点格式错误</exception>
        public static IDeviceDriver CreateDriver(string deviceId, string transport, string endpoint)
        {
            return CreateDriver(deviceId, transport, endpoint, null);
        }
        /// <summary>
        /// 根据传输协议类型、端点和额外参数创建驱动适配器
        /// </summary>
        /// <param name="deviceId">设备唯一标识符</param>
        /// <param name="transport">传输协议类型: "Tcp", "SCPI", "ModBus", "S7", "Serial"</param>
        /// <param name="endpoint">端点信息,格式: "ip:port" 或 "ip:port:extra"</param>
        /// <param name="extraParams">额外参数(可选)</param>
        /// <returns>IDeviceDriver 实例</returns>
        /// <exception cref="NotSupportedException">不支持的传输协议类型</exception>
        /// <exception cref="ArgumentException">端点格式错误</exception>
        public static IDeviceDriver CreateDriver(string deviceId, string transport, string endpoint, Dictionary<string, string>? extraParams)
        {
            if (string.IsNullOrWhiteSpace(deviceId))
                throw new ArgumentNullException(nameof(deviceId));
            if (string.IsNullOrWhiteSpace(transport))
                throw new ArgumentNullException(nameof(transport));
            if (string.IsNullOrWhiteSpace(endpoint))
                throw new ArgumentNullException(nameof(endpoint));
            Log.Info($"[DriverFactory] 创建设备驱动: DeviceId={deviceId}, Transport={transport}, Endpoint={endpoint}");
            string[] parts = endpoint.Split(':');
            if (parts.Length < 2)
            {
                throw new ArgumentException($"端点格式错误,应为 'ip:port' 或 'ip:port:extra',实际: {endpoint}");
            }
            string ip = parts[0];
            if (!int.TryParse(parts[1], out int port))
            {
                throw new ArgumentException($"端口号格式错误: {parts[1]}");
            }
            switch (transport.ToUpper())
            {
                case "TCP":
                    Log.Info($"[DriverFactory] 创建 TCP 原始通信驱动: {ip}:{port}");
                    return new TcpRawDriverAdapter(deviceId, ip, port);
                case "SCPI":
                    Log.Info($"[DriverFactory] 创建 SCPI 协议驱动: {ip}:{port}");
                    return new ScpiDriverAdapter(deviceId, ip, port);
                case "MODBUS":
                    byte slaveId = 1;
                    if (extraParams != null && extraParams.TryGetValue("slaveId", out string slaveIdStr))
                    {
                        if (!byte.TryParse(slaveIdStr, out slaveId))
                        {
                            slaveId = 1;
                        }
                    }
                    // 如果 endpoint 有第三部分,作为 slaveId
                    else if (parts.Length >= 3 && byte.TryParse(parts[2], out byte parsedSlaveId))
                    {
                        slaveId = parsedSlaveId;
                    }
                    Log.Info($"[DriverFactory] 创建 ModBus TCP 驱动: {ip}:{port} (从站: {slaveId})");
                    return new ModBusDriverAdapter(deviceId, ip, port, slaveId);
                case "S7":
                    Log.Info($"[DriverFactory] 创建 S7 PLC 驱动: {ip}:{port}");
                    return new S7DriverAdapter(deviceId, ip, port);
                case "SERIAL":
                    // TODO: 实现串口驱动适配器
                    throw new NotSupportedException("串口驱动适配器尚未实现");
                case "CAN":
                    // CAN 驱动: endpoint 格式为 "busName:nodeIndex" 如 "CAN_BUS_1:0"
                    string busName = ip; // ip 字段用作总线名称
                    int nodeIndex = port; // port 字段用作节点编号
                    Log.Info($"[DriverFactory] 创建 CAN 虚拟驱动: Bus={busName}, Node={nodeIndex}");
                    return new CanDriverAdapter(deviceId, busName, nodeIndex);
                default:
                    throw new NotSupportedException($"不支持的传输协议类型: {transport}。支持的类型: Tcp, SCPI, ModBus, S7, Serial, CAN");
            }
        }
    }
}
Aging.Infrastructure/Drivers/ModBusDriverAdapter.cs
New file
@@ -0,0 +1,347 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Aging.Infrastructure.Contracts;
using HslCommunication;
using HslCommunication.ModBus;
using log4net;
namespace Aging.Infrastructure.Drivers
{
    /// <summary>
    /// ModBus TCP 驱动适配器。
    /// 封装 HslCommunication.ModBusTcpNet,实现统一的 IDeviceDriver 接口。
    /// 命令格式约定:
    ///   写入: "WRITE:address:value" 如 "WRITE:40001:100" 或 "WRITE:00001:TRUE"
    ///   读取: "READ:address" 如 "READ:00001" 或 "READ:40001"
    /// </summary>
    public sealed class ModBusDriverAdapter : IDeviceDriver
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(ModBusDriverAdapter));
        private readonly string _ip;
        private readonly int _port;
        private readonly byte _slaveId;
        private ModbusTcpNet? _client;
        private bool _disposed;
        private readonly Stopwatch _latencyStopwatch = new Stopwatch();
        private double _lastLatencyMs;
        private string _lastError = string.Empty;
        private DateTime? _lastErrorTime;
        private int _reconnectCount;
        /// <inheritdoc/>
        public string DeviceId { get; }
        /// <inheritdoc/>
        public bool IsConnected => _client != null;
        /// <inheritdoc/>
        public event EventHandler<DeviceConnectionEventArgs>? ConnectionChanged;
        /// <summary>
        /// 初始化 ModBus TCP 驱动适配器
        /// </summary>
        /// <param name="deviceId">设备唯一标识符</param>
        /// <param name="ip">目标 IP 地址</param>
        /// <param name="port">目标端口号</param>
        /// <param name="slaveId">从站地址(默认为 1)</param>
        public ModBusDriverAdapter(string deviceId, string ip, int port, byte slaveId = 1)
        {
            DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId));
            _ip = ip ?? throw new ArgumentNullException(nameof(ip));
            _port = port;
            _slaveId = slaveId;
        }
        /// <inheritdoc/>
        public Task ConnectAsync(CancellationToken ct)
        {
            ThrowIfDisposed();
            Log.Info($"[ModBus] 设备 {DeviceId} 正在连接 {_ip}:{_port} (从站: {_slaveId})...");
            try
            {
                _client = new ModbusTcpNet(_ip, _port, _slaveId);
                OperateResult result = _client.ConnectServer();
                if (!result.IsSuccess)
                {
                    _lastError = result.Message;
                    _lastErrorTime = DateTime.Now;
                    throw new InvalidOperationException($"[ModBus] 设备 {DeviceId} 连接失败: {result.Message}");
                }
                _lastError = string.Empty;
                Log.Info($"[ModBus] 设备 {DeviceId} 已成功连接到 {_ip}:{_port}");
                OnConnectionChanged(true, "连接成功");
            }
            catch (Exception ex)
            {
                _lastError = ex.Message;
                _lastErrorTime = DateTime.Now;
                Log.Error($"[ModBus] 设备 {DeviceId} 连接异常: {ex.Message}", ex);
                _client?.ConnectClose();
                _client?.Dispose();
                _client = null;
                throw;
            }
            return Task.CompletedTask;
        }
        /// <inheritdoc/>
        public Task DisconnectAsync()
        {
            ThrowIfDisposed();
            Log.Info($"[ModBus] 设备 {DeviceId} 正在断开连接...");
            try
            {
                if (_client != null)
                {
                    _client.ConnectClose();
                    _client.Dispose();
                    _client = null;
                }
                Log.Info($"[ModBus] 设备 {DeviceId} 已断开连接");
                OnConnectionChanged(false, "主动断开");
            }
            catch (Exception ex)
            {
                _lastError = ex.Message;
                _lastErrorTime = DateTime.Now;
                Log.Error($"[ModBus] 设备 {DeviceId} 断开连接异常: {ex.Message}", ex);
                _client = null;
            }
            return Task.CompletedTask;
        }
        /// <inheritdoc/>
        public Task<string> SendCommandAsync(string command, CancellationToken ct)
        {
            ThrowIfDisposed();
            if (!IsConnected) throw new InvalidOperationException("[ModBus] 设备未连接");
            _latencyStopwatch.Restart();
            try
            {
                Log.Debug($"[ModBus] 设备 {DeviceId} 发送命令: {command}");
                // 解析命令格式: "WRITE:address:value"
                string[] parts = command.Split(':');
                if (parts.Length < 3)
                {
                    throw new ArgumentException($"[ModBus] 命令格式错误,应为 WRITE:address:value,实际: {command}");
                }
                string operation = parts[0].ToUpper();
                string address = parts[1];
                string value = parts[2];
                if (operation != "WRITE")
                {
                    throw new ArgumentException($"[ModBus] 不支持的操作类型: {operation},应为 WRITE");
                }
                // 根据值判断数据类型并写入
                OperateResult result;
                if (bool.TryParse(value, out bool boolVal))
                {
                    result = _client.Write(address, boolVal);
                }
                else if (short.TryParse(value, out short shortVal))
                {
                    result = _client.Write(address, shortVal);
                }
                else if (int.TryParse(value, out int intVal))
                {
                    result = _client.Write(address, intVal);
                }
                else if (float.TryParse(value, out float floatVal))
                {
                    result = _client.Write(address, floatVal);
                }
                else
                {
                    throw new ArgumentException($"[ModBus] 无法解析的值: {value}");
                }
                _latencyStopwatch.Stop();
                _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
                if (!result.IsSuccess)
                {
                    _lastError = result.Message;
                    _lastErrorTime = DateTime.Now;
                    throw new InvalidOperationException($"[ModBus] 写入失败: {result.Message}");
                }
                string response = "OK";
                Log.Debug($"[ModBus] 设备 {DeviceId} 写入成功 (延迟 {_lastLatencyMs:F2}ms)");
                return Task.FromResult(response);
            }
            catch (Exception ex)
            {
                _latencyStopwatch.Stop();
                _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
                _lastError = ex.Message;
                _lastErrorTime = DateTime.Now;
                Log.Error($"[ModBus] 设备 {DeviceId} 发送命令失败: {ex.Message}", ex);
                throw;
            }
        }
        /// <inheritdoc/>
        public Task<string> QueryAsync(string query, CancellationToken ct)
        {
            ThrowIfDisposed();
            if (!IsConnected) throw new InvalidOperationException("[ModBus] 设备未连接");
            _latencyStopwatch.Restart();
            try
            {
                Log.Debug($"[ModBus] 设备 {DeviceId} 发送查询: {query}");
                // 解析查询格式: "READ:address"
                string[] parts = query.Split(':');
                if (parts.Length < 2)
                {
                    throw new ArgumentException($"[ModBus] 查询格式错误,应为 READ:address,实际: {query}");
                }
                string operation = parts[0].ToUpper();
                string address = parts[1];
                if (operation != "READ")
                {
                    throw new ArgumentException($"[ModBus] 不支持的操作类型: {operation},应为 READ");
                }
                // 尝试读取不同类型(根据地址范围自动判断)
                // 00001-09999: 线圈(BOOL)
                // 10001-19999: 离散输入(BOOL)
                // 30001-39999: 输入寄存器(INT16)
                // 40001-49999: 保持寄存器(INT16)
                string response;
                int addrNum;
                if (int.TryParse(address, out addrNum))
                {
                    if (addrNum >= 1 && addrNum <= 9999)
                    {
                        // 线圈读取
                        var result = _client.ReadBool(address);
                        response = result.IsSuccess ? result.Content.ToString() : $"ERROR: {result.Message}";
                    }
                    else if (addrNum >= 10001 && addrNum <= 19999)
                    {
                        // 离散输入读取
                        var result = _client.ReadBool(address);
                        response = result.IsSuccess ? result.Content.ToString() : $"ERROR: {result.Message}";
                    }
                    else if (addrNum >= 30001 && addrNum <= 39999)
                    {
                        // 输入寄存器读取
                        var result = _client.ReadInt16(address);
                        response = result.IsSuccess ? result.Content.ToString() : $"ERROR: {result.Message}";
                    }
                    else if (addrNum >= 40001 && addrNum <= 49999)
                    {
                        // 保持寄存器读取
                        var result = _client.ReadInt16(address);
                        response = result.IsSuccess ? result.Content.ToString() : $"ERROR: {result.Message}";
                    }
                    else
                    {
                        // 默认读取 INT16
                        var result = _client.ReadInt16(address);
                        response = result.IsSuccess ? result.Content.ToString() : $"ERROR: {result.Message}";
                    }
                }
                else
                {
                    // 非数字地址,尝试读取 INT16
                    var result = _client.ReadInt16(address);
                    response = result.IsSuccess ? result.Content.ToString() : $"ERROR: {result.Message}";
                }
                _latencyStopwatch.Stop();
                _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
                Log.Debug($"[ModBus] 设备 {DeviceId} 查询响应: {response} (延迟 {_lastLatencyMs:F2}ms)");
                return Task.FromResult(response);
            }
            catch (Exception ex)
            {
                _latencyStopwatch.Stop();
                _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
                _lastError = ex.Message;
                _lastErrorTime = DateTime.Now;
                Log.Error($"[ModBus] 设备 {DeviceId} 查询失败: {ex.Message}", ex);
                throw;
            }
        }
        /// <inheritdoc/>
        public DeviceDriverStatus GetStatus()
        {
            return new DeviceDriverStatus
            {
                IsConnected = IsConnected,
                IsHealthy = IsConnected && string.IsNullOrEmpty(_lastError),
                LastError = _lastError,
                LastErrorTime = _lastErrorTime,
                LatencyMs = _lastLatencyMs,
                ReconnectCount = _reconnectCount
            };
        }
        /// <inheritdoc/>
        public void Dispose()
        {
            if (_disposed) return;
            _disposed = true;
            try
            {
                if (_client != null)
                {
                    _client.ConnectClose();
                    _client.Dispose();
                }
            }
            catch (Exception ex)
            {
                Log.Error($"[ModBus] 设备 {DeviceId} 释放资源异常: {ex.Message}", ex);
            }
        }
        /// <summary>
        /// 触发连接状态变化事件
        /// </summary>
        private void OnConnectionChanged(bool isConnected, string reason)
        {
            ConnectionChanged?.Invoke(this, new DeviceConnectionEventArgs
            {
                DeviceId = DeviceId,
                IsConnected = isConnected,
                Reason = reason
            });
            if (!isConnected)
            {
                _reconnectCount++;
            }
        }
        /// <summary>
        /// 检查是否已释放
        /// </summary>
        private void ThrowIfDisposed()
        {
            if (_disposed) throw new ObjectDisposedException(nameof(ModBusDriverAdapter));
        }
    }
}
Aging.Infrastructure/Drivers/S7DriverAdapter.cs
New file
@@ -0,0 +1,407 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Aging.Infrastructure.Contracts;
using log4net;
using S7Driver;
namespace Aging.Infrastructure.Drivers
{
    /// <summary>
    /// 西门子 S7 PLC 驱动适配器。
    /// 封装 S7Driver.PLCS7Driver,实现统一的 IDeviceDriver 接口。
    /// 命令格式约定:
    ///   写入: "WRITE:DBx.DBXy.z:value" 如 "WRITE:DB1.DBX0.0:TRUE"
    ///   读取: "READ:DBx.DBWy" 如 "READ:DB1.DBD0"
    /// 支持数据类型: BOOL, INT, REAL
    /// </summary>
    public sealed class S7DriverAdapter : IDeviceDriver
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(S7DriverAdapter));
        private readonly string _ip;
        private readonly int _port;
        private PLCS7Driver? _driver;
        private bool _disposed;
        private readonly Stopwatch _latencyStopwatch = new Stopwatch();
        private double _lastLatencyMs;
        private string _lastError = string.Empty;
        private DateTime? _lastErrorTime;
        private int _reconnectCount;
        /// <inheritdoc/>
        public string DeviceId { get; }
        /// <inheritdoc/>
        public bool IsConnected => _driver != null;
        /// <inheritdoc/>
        public event EventHandler<DeviceConnectionEventArgs>? ConnectionChanged;
        /// <summary>
        /// 初始化 S7 PLC 驱动适配器
        /// </summary>
        /// <param name="deviceId">设备唯一标识符</param>
        /// <param name="ip">PLC IP 地址</param>
        /// <param name="port">PLC 端口号(通常为 102)</param>
        public S7DriverAdapter(string deviceId, string ip, int port)
        {
            DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId));
            _ip = ip ?? throw new ArgumentNullException(nameof(ip));
            _port = port;
        }
        /// <inheritdoc/>
        public Task ConnectAsync(CancellationToken ct)
        {
            ThrowIfDisposed();
            Log.Info($"[S7] 设备 {DeviceId} 正在连接 {_ip}:{_port}...");
            try
            {
                _driver = new PLCS7Driver(_ip, _port);
                bool success = _driver.Connect(out string errMsg);
                if (!success)
                {
                    _lastError = errMsg;
                    _lastErrorTime = DateTime.Now;
                    throw new InvalidOperationException($"[S7] 设备 {DeviceId} 连接失败: {errMsg}");
                }
                _lastError = string.Empty;
                Log.Info($"[S7] 设备 {DeviceId} 已成功连接到 {_ip}:{_port}");
                OnConnectionChanged(true, "连接成功");
            }
            catch (Exception ex)
            {
                _lastError = ex.Message;
                _lastErrorTime = DateTime.Now;
                Log.Error($"[S7] 设备 {DeviceId} 连接异常: {ex.Message}", ex);
                _driver?.Dispose();
                _driver = null;
                throw;
            }
            return Task.CompletedTask;
        }
        /// <inheritdoc/>
        public Task DisconnectAsync()
        {
            ThrowIfDisposed();
            Log.Info($"[S7] 设备 {DeviceId} 正在断开连接...");
            try
            {
                if (_driver != null)
                {
                    _driver.DisConnect(out string errMsg);
                    _driver.Dispose();
                    _driver = null;
                }
                Log.Info($"[S7] 设备 {DeviceId} 已断开连接");
                OnConnectionChanged(false, "主动断开");
            }
            catch (Exception ex)
            {
                _lastError = ex.Message;
                _lastErrorTime = DateTime.Now;
                Log.Error($"[S7] 设备 {DeviceId} 断开连接异常: {ex.Message}", ex);
                _driver?.Dispose();
                _driver = null;
            }
            return Task.CompletedTask;
        }
        /// <inheritdoc/>
        public Task<string> SendCommandAsync(string command, CancellationToken ct)
        {
            ThrowIfDisposed();
            if (!IsConnected) throw new InvalidOperationException("[S7] 设备未连接");
            _latencyStopwatch.Restart();
            try
            {
                Log.Debug($"[S7] 设备 {DeviceId} 发送命令: {command}");
                // 解析命令格式: "WRITE:DBx.DBXy.z:value" 或 "WRITE:DBx.DBWy:value"
                string[] parts = command.Split(':');
                if (parts.Length < 3)
                {
                    throw new ArgumentException($"[S7] 命令格式错误,应为 WRITE:address:value,实际: {command}");
                }
                string operation = parts[0].ToUpper();
                string address = parts[1];
                string value = parts[2];
                if (operation != "WRITE")
                {
                    throw new ArgumentException($"[S7] 不支持的操作类型: {operation},应为 WRITE");
                }
                // 解析地址和值,执行写入
                bool success = ParseAndWrite(address, value, out string msg);
                _latencyStopwatch.Stop();
                _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
                if (!success)
                {
                    _lastError = msg;
                    _lastErrorTime = DateTime.Now;
                    throw new InvalidOperationException($"[S7] 写入失败: {msg}");
                }
                string response = "OK";
                Log.Debug($"[S7] 设备 {DeviceId} 写入成功 (延迟 {_lastLatencyMs:F2}ms)");
                return Task.FromResult(response);
            }
            catch (Exception ex)
            {
                _latencyStopwatch.Stop();
                _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
                _lastError = ex.Message;
                _lastErrorTime = DateTime.Now;
                Log.Error($"[S7] 设备 {DeviceId} 发送命令失败: {ex.Message}", ex);
                throw;
            }
        }
        /// <inheritdoc/>
        public Task<string> QueryAsync(string query, CancellationToken ct)
        {
            ThrowIfDisposed();
            if (!IsConnected) throw new InvalidOperationException("[S7] 设备未连接");
            _latencyStopwatch.Restart();
            try
            {
                Log.Debug($"[S7] 设备 {DeviceId} 发送查询: {query}");
                // 解析查询格式: "READ:DBx.DBWy" 或 "READ:DBx.DBXy.z"
                string[] parts = query.Split(':');
                if (parts.Length < 2)
                {
                    throw new ArgumentException($"[S7] 查询格式错误,应为 READ:address,实际: {query}");
                }
                string operation = parts[0].ToUpper();
                string address = parts[1];
                if (operation != "READ")
                {
                    throw new ArgumentException($"[S7] 不支持的操作类型: {operation},应为 READ");
                }
                // 解析地址并执行读取
                string response = ParseAndRead(address);
                _latencyStopwatch.Stop();
                _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
                Log.Debug($"[S7] 设备 {DeviceId} 查询响应: {response} (延迟 {_lastLatencyMs:F2}ms)");
                return Task.FromResult(response);
            }
            catch (Exception ex)
            {
                _latencyStopwatch.Stop();
                _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
                _lastError = ex.Message;
                _lastErrorTime = DateTime.Now;
                Log.Error($"[S7] 设备 {DeviceId} 查询失败: {ex.Message}", ex);
                throw;
            }
        }
        /// <inheritdoc/>
        public DeviceDriverStatus GetStatus()
        {
            return new DeviceDriverStatus
            {
                IsConnected = IsConnected,
                IsHealthy = IsConnected && string.IsNullOrEmpty(_lastError),
                LastError = _lastError,
                LastErrorTime = _lastErrorTime,
                LatencyMs = _lastLatencyMs,
                ReconnectCount = _reconnectCount
            };
        }
        /// <inheritdoc/>
        public void Dispose()
        {
            if (_disposed) return;
            _disposed = true;
            try
            {
                _driver?.Dispose();
            }
            catch (Exception ex)
            {
                Log.Error($"[S7] 设备 {DeviceId} 释放资源异常: {ex.Message}", ex);
            }
        }
        /// <summary>
        /// 解析地址并执行写入操作
        /// </summary>
        /// <param name="address">S7 地址,如 "DB1.DBX0.0" 或 "DB1.DBW10"</param>
        /// <param name="value">要写入的值</param>
        /// <param name="msg">错误信息</param>
        /// <returns>是否成功</returns>
        private bool ParseAndWrite(string address, string value, out string msg)
        {
            msg = null;
            try
            {
                // 解析地址格式: DBx.DBXy.z (BOOL) 或 DBx.DBWy (INT/REAL)
                if (address.Contains(".DBX"))
                {
                    // BOOL 类型: DB1.DBX0.0
                    if (bool.TryParse(value, out bool boolVal))
                    {
                        var addrParts = ParseBoolAddress(address);
                        return _driver.Simens_S7_WriteBool(addrParts.db, addrParts.startAddr, addrParts.pos, boolVal, out msg);
                    }
                    throw new ArgumentException($"[S7] BOOL 值解析失败: {value}");
                }
                else if (address.Contains(".DBW"))
                {
                    // 尝试解析为不同类型
                    if (float.TryParse(value, out float realVal))
                    {
                        var addrParts = ParseWordAddress(address);
                        return _driver.Simens_S7_WriteReal(addrParts.db, addrParts.startAddr, realVal, out msg);
                    }
                    else if (short.TryParse(value, out short shortVal))
                    {
                        var addrParts = ParseWordAddress(address);
                        return _driver.Simens_S7_WriteInt(addrParts.db, addrParts.startAddr, shortVal, out msg);
                    }
                    throw new ArgumentException($"[S7] 值解析失败: {value}");
                }
                else
                {
                    msg = $"[S7] 不支持的地址格式: {address}";
                    return false;
                }
            }
            catch (Exception ex)
            {
                msg = ex.Message;
                return false;
            }
        }
        /// <summary>
        /// 解析地址并执行读取操作
        /// </summary>
        /// <param name="address">S7 地址,如 "DB1.DBX0.0" 或 "DB1.DBW10"</param>
        /// <returns>读取的值字符串</returns>
        private string ParseAndRead(string address)
        {
            if (address.Contains(".DBX"))
            {
                // BOOL 类型读取
                var addrParts = ParseBoolAddress(address);
                if (_driver.Simens_S7_ReadBool(addrParts.db, addrParts.startAddr, addrParts.pos, out bool value, out string msg))
                {
                    return value.ToString();
                }
                throw new InvalidOperationException($"[S7] 读取失败: {msg}");
            }
            else if (address.Contains(".DBD"))
            {
                // REAL 类型读取 (DBD = Double Word)
                var addrParts = ParseWordAddress(address);
                if (_driver.Simens_S7_ReadReal(addrParts.db, addrParts.startAddr, out float realValue, out string msg))
                {
                    return realValue.ToString();
                }
                throw new InvalidOperationException($"[S7] 读取失败: {msg}");
            }
            else if (address.Contains(".DBW"))
            {
                // INT 类型读取
                var addrParts = ParseWordAddress(address);
                if (_driver.Simens_S7_ReadInt(addrParts.db, addrParts.startAddr, out int value, out string msg))
                {
                    return value.ToString();
                }
                throw new InvalidOperationException($"[S7] 读取失败: {msg}");
            }
            else
            {
                throw new ArgumentException($"[S7] 不支持的地址格式: {address}");
            }
        }
        /// <summary>
        /// 解析 BOOL 地址格式: DBx.DBXy.z
        /// </summary>
        private (int db, int startAddr, int pos) ParseBoolAddress(string address)
        {
            // 格式: DB1.DBX0.0
            string upper = address.ToUpper();
            int dbStart = upper.IndexOf("DB") + 2;
            int dbxStart = upper.IndexOf(".DBX");
            int db = int.Parse(upper.Substring(dbStart, dbxStart - dbStart));
            int posStart = dbxStart + 5; // .DBX 后
            int dotPos = address.IndexOf('.', posStart);
            int startAddr = int.Parse(address.Substring(posStart, dotPos - posStart));
            int pos = int.Parse(address.Substring(dotPos + 1));
            return (db, startAddr, pos);
        }
        /// <summary>
        /// 解析 Word 地址格式: DBx.DBWy 或 DBx.DBDy
        /// </summary>
        private (int db, int startAddr) ParseWordAddress(string address)
        {
            // 格式: DB1.DBW10 或 DB1.DBD0
            string upper = address.ToUpper();
            int dbStart = upper.IndexOf("DB") + 2;
            int dbwStart = upper.Contains(".DBW") ? upper.IndexOf(".DBW") : upper.IndexOf(".DBD");
            int db = int.Parse(upper.Substring(dbStart, dbwStart - dbStart));
            int addrStart = dbwStart + 5; // .DBW 或 .DBD 后
            int startAddr = int.Parse(address.Substring(addrStart));
            return (db, startAddr);
        }
        /// <summary>
        /// 触发连接状态变化事件
        /// </summary>
        private void OnConnectionChanged(bool isConnected, string reason)
        {
            ConnectionChanged?.Invoke(this, new DeviceConnectionEventArgs
            {
                DeviceId = DeviceId,
                IsConnected = isConnected,
                Reason = reason
            });
            if (!isConnected)
            {
                _reconnectCount++;
            }
        }
        /// <summary>
        /// 检查是否已释放
        /// </summary>
        private void ThrowIfDisposed()
        {
            if (_disposed) throw new ObjectDisposedException(nameof(S7DriverAdapter));
        }
    }
}
Aging.Infrastructure/Drivers/ScpiDriverAdapter.cs
New file
@@ -0,0 +1,248 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Aging.Infrastructure.Contracts;
using log4net;
using PLCHelper;
namespace Aging.Infrastructure.Drivers
{
    /// <summary>
    /// SCPI 协议驱动适配器。
    /// 封装 PLCHelper.RestTcpClient,实现统一的 IDeviceDriver 接口。
    /// 用于控制支持 SCPI 命令的仪器(如电源、电子负载等)。
    /// </summary>
    public sealed class ScpiDriverAdapter : IDeviceDriver
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(ScpiDriverAdapter));
        private readonly string _ip;
        private readonly int _port;
        private RestTcpClient? _client;
        private bool _disposed;
        private readonly Stopwatch _latencyStopwatch = new Stopwatch();
        private double _lastLatencyMs;
        private string _lastError = string.Empty;
        private DateTime? _lastErrorTime;
        private int _reconnectCount;
        /// <inheritdoc/>
        public string DeviceId { get; }
        /// <inheritdoc/>
        public bool IsConnected => _client != null;
        /// <inheritdoc/>
        public event EventHandler<DeviceConnectionEventArgs>? ConnectionChanged;
        /// <summary>
        /// 初始化 SCPI 协议驱动适配器
        /// </summary>
        /// <param name="deviceId">设备唯一标识符</param>
        /// <param name="ip">目标 IP 地址</param>
        /// <param name="port">目标端口号</param>
        public ScpiDriverAdapter(string deviceId, string ip, int port)
        {
            DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId));
            _ip = ip ?? throw new ArgumentNullException(nameof(ip));
            _port = port;
        }
        /// <inheritdoc/>
        public Task ConnectAsync(CancellationToken ct)
        {
            ThrowIfDisposed();
            Log.Info($"[SCPI] 设备 {DeviceId} 正在连接 {_ip}:{_port}...");
            try
            {
                _client = new RestTcpClient(_ip, _port);
                _client.ReceiveTimeout = 5000;
                _client.ConnectServer();
                // 发送复位命令验证连接
                if (_client.Request("*RST", out string response, out string errMsg))
                {
                    Log.Info($"[SCPI] 设备 {DeviceId} 复位响应: {response}");
                }
                else
                {
                    Log.Warn($"[SCPI] 设备 {DeviceId} 复位失败: {errMsg}");
                }
                _lastError = string.Empty;
                Log.Info($"[SCPI] 设备 {DeviceId} 已成功连接到 {_ip}:{_port}");
                OnConnectionChanged(true, "连接成功");
            }
            catch (Exception ex)
            {
                _lastError = ex.Message;
                _lastErrorTime = DateTime.Now;
                Log.Error($"[SCPI] 设备 {DeviceId} 连接异常: {ex.Message}", ex);
                _client?.Dispose();
                _client = null;
                throw;
            }
            return Task.CompletedTask;
        }
        /// <inheritdoc/>
        public Task DisconnectAsync()
        {
            ThrowIfDisposed();
            Log.Info($"[SCPI] 设备 {DeviceId} 正在断开连接...");
            try
            {
                _client?.Dispose();
                _client = null;
                Log.Info($"[SCPI] 设备 {DeviceId} 已断开连接");
                OnConnectionChanged(false, "主动断开");
            }
            catch (Exception ex)
            {
                _lastError = ex.Message;
                _lastErrorTime = DateTime.Now;
                Log.Error($"[SCPI] 设备 {DeviceId} 断开连接异常: {ex.Message}", ex);
                _client = null;
            }
            return Task.CompletedTask;
        }
        /// <inheritdoc/>
        public Task<string> SendCommandAsync(string command, CancellationToken ct)
        {
            ThrowIfDisposed();
            if (!IsConnected) throw new InvalidOperationException("[SCPI] 设备未连接");
            _latencyStopwatch.Restart();
            try
            {
                Log.Debug($"[SCPI] 设备 {DeviceId} 发送命令: {command}");
                // SCPI 命令以 \r 结尾(RestTcpClient 协议自动处理)
                bool success = _client.Request(command, out string response, out string errMsg);
                _latencyStopwatch.Stop();
                _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
                if (!success)
                {
                    _lastError = errMsg;
                    _lastErrorTime = DateTime.Now;
                    throw new InvalidOperationException($"[SCPI] 命令执行失败: {errMsg}");
                }
                Log.Debug($"[SCPI] 设备 {DeviceId} 命令响应: {response} (延迟 {_lastLatencyMs:F2}ms)");
                return Task.FromResult(response);
            }
            catch (Exception ex)
            {
                _latencyStopwatch.Stop();
                _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
                _lastError = ex.Message;
                _lastErrorTime = DateTime.Now;
                Log.Error($"[SCPI] 设备 {DeviceId} 发送命令失败: {ex.Message}", ex);
                throw;
            }
        }
        /// <inheritdoc/>
        public Task<string> QueryAsync(string query, CancellationToken ct)
        {
            ThrowIfDisposed();
            if (!IsConnected) throw new InvalidOperationException("[SCPI] 设备未连接");
            _latencyStopwatch.Restart();
            try
            {
                Log.Debug($"[SCPI] 设备 {DeviceId} 发送查询: {query}");
                // 发送查询命令并等待回读(如 "MEAS:CURR?")
                bool success = _client.Request(query, out string response, out string errMsg);
                _latencyStopwatch.Stop();
                _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
                if (!success)
                {
                    _lastError = errMsg;
                    _lastErrorTime = DateTime.Now;
                    throw new TimeoutException($"[SCPI] 查询超时或失败: {errMsg}");
                }
                Log.Debug($"[SCPI] 设备 {DeviceId} 查询响应: {response} (延迟 {_lastLatencyMs:F2}ms)");
                return Task.FromResult(response);
            }
            catch (Exception ex)
            {
                _latencyStopwatch.Stop();
                _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
                _lastError = ex.Message;
                _lastErrorTime = DateTime.Now;
                Log.Error($"[SCPI] 设备 {DeviceId} 查询失败: {ex.Message}", ex);
                throw;
            }
        }
        /// <inheritdoc/>
        public DeviceDriverStatus GetStatus()
        {
            return new DeviceDriverStatus
            {
                IsConnected = IsConnected,
                IsHealthy = IsConnected && string.IsNullOrEmpty(_lastError),
                LastError = _lastError,
                LastErrorTime = _lastErrorTime,
                LatencyMs = _lastLatencyMs,
                ReconnectCount = _reconnectCount
            };
        }
        /// <inheritdoc/>
        public void Dispose()
        {
            if (_disposed) return;
            _disposed = true;
            try
            {
                _client?.Dispose();
            }
            catch (Exception ex)
            {
                Log.Error($"[SCPI] 设备 {DeviceId} 释放资源异常: {ex.Message}", ex);
            }
        }
        /// <summary>
        /// 触发连接状态变化事件
        /// </summary>
        private void OnConnectionChanged(bool isConnected, string reason)
        {
            ConnectionChanged?.Invoke(this, new DeviceConnectionEventArgs
            {
                DeviceId = DeviceId,
                IsConnected = isConnected,
                Reason = reason
            });
            if (!isConnected)
            {
                _reconnectCount++;
            }
        }
        /// <summary>
        /// 检查是否已释放
        /// </summary>
        private void ThrowIfDisposed()
        {
            if (_disposed) throw new ObjectDisposedException(nameof(ScpiDriverAdapter));
        }
    }
}
Aging.Infrastructure/Drivers/TcpRawDriverAdapter.cs
New file
@@ -0,0 +1,222 @@
using System;
using System.Diagnostics;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Aging.Infrastructure.Contracts;
using ConnectionUtil;
using log4net;
namespace Aging.Infrastructure.Drivers
{
    /// <summary>
    /// TCP 原始通信驱动适配器。
    /// 封装 ConnectionUtil.TcpClientWrapper,实现统一的 IDeviceDriver 接口。
    /// </summary>
    public sealed class TcpRawDriverAdapter : IDeviceDriver
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(TcpRawDriverAdapter));
        private readonly string _ip;
        private readonly int _port;
        private TcpClientWrapper? _client;
        private bool _disposed;
        private readonly Stopwatch _latencyStopwatch = new Stopwatch();
        private double _lastLatencyMs;
        private string _lastError = string.Empty;
        private DateTime? _lastErrorTime;
        private int _reconnectCount;
        /// <inheritdoc/>
        public string DeviceId { get; }
        /// <inheritdoc/>
        public bool IsConnected => _client?.IsConnected ?? false;
        /// <inheritdoc/>
        public event EventHandler<DeviceConnectionEventArgs>? ConnectionChanged;
        /// <summary>
        /// 初始化 TCP 原始通信驱动适配器
        /// </summary>
        /// <param name="deviceId">设备唯一标识符</param>
        /// <param name="ip">目标 IP 地址</param>
        /// <param name="port">目标端口号</param>
        public TcpRawDriverAdapter(string deviceId, string ip, int port)
        {
            DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId));
            _ip = ip ?? throw new ArgumentNullException(nameof(ip));
            _port = port;
        }
        /// <inheritdoc/>
        public async Task ConnectAsync(CancellationToken ct)
        {
            ThrowIfDisposed();
            Log.Info($"[TCP] 设备 {DeviceId} 正在连接 {_ip}:{_port}...");
            try
            {
                _client = new TcpClientWrapper(_ip, _port);
                bool success = await _client.ConnectAsync();
                if (!success)
                {
                    _lastError = "连接失败";
                    _lastErrorTime = DateTime.Now;
                    throw new InvalidOperationException($"[TCP] 设备 {DeviceId} 连接 {_ip}:{_port} 失败");
                }
                _lastError = string.Empty;
                Log.Info($"[TCP] 设备 {DeviceId} 已成功连接到 {_ip}:{_port}");
                OnConnectionChanged(true, "连接成功");
            }
            catch (Exception ex)
            {
                _lastError = ex.Message;
                _lastErrorTime = DateTime.Now;
                Log.Error($"[TCP] 设备 {DeviceId} 连接异常: {ex.Message}", ex);
                throw;
            }
        }
        /// <inheritdoc/>
        public Task DisconnectAsync()
        {
            ThrowIfDisposed();
            Log.Info($"[TCP] 设备 {DeviceId} 正在断开连接...");
            try
            {
                if (_client != null)
                {
                    _client?.Close();
                    _client = null;
                }
                Log.Info($"[TCP] 设备 {DeviceId} 已断开连接");
                OnConnectionChanged(false, "主动断开");
            }
            catch (Exception ex)
            {
                _lastError = ex.Message;
                _lastErrorTime = DateTime.Now;
                Log.Error($"[TCP] 设备 {DeviceId} 断开连接异常: {ex.Message}", ex);
            }
            return Task.CompletedTask;
        }
        /// <inheritdoc/>
        public async Task<string> SendCommandAsync(string command, CancellationToken ct)
        {
            ThrowIfDisposed();
            if (!IsConnected) throw new InvalidOperationException("[TCP] 设备未连接");
            _latencyStopwatch.Restart();
            try
            {
                Log.Debug($"[TCP] 设备 {DeviceId} 发送命令: {command}");
                string response = await _client.SendMessageAsync(command);
                _latencyStopwatch.Stop();
                _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
                Log.Debug($"[TCP] 设备 {DeviceId} 收到响应: {response} (延迟 {_lastLatencyMs:F2}ms)");
                return response;
            }
            catch (Exception ex)
            {
                _latencyStopwatch.Stop();
                _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
                _lastError = ex.Message;
                _lastErrorTime = DateTime.Now;
                Log.Error($"[TCP] 设备 {DeviceId} 发送命令失败: {ex.Message}", ex);
                throw;
            }
        }
        /// <inheritdoc/>
        public async Task<string> QueryAsync(string query, CancellationToken ct)
        {
            ThrowIfDisposed();
            if (!IsConnected) throw new InvalidOperationException("[TCP] 设备未连接");
            _latencyStopwatch.Restart();
            try
            {
                Log.Debug($"[TCP] 设备 {DeviceId} 发送查询: {query}");
                // 使用较短超时进行查询操作
                string response = await _client.SendMessageAsync(query, TimeSpan.FromSeconds(5));
                _latencyStopwatch.Stop();
                _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
                Log.Debug($"[TCP] 设备 {DeviceId} 查询响应: {response} (延迟 {_lastLatencyMs:F2}ms)");
                return response;
            }
            catch (Exception ex)
            {
                _latencyStopwatch.Stop();
                _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
                _lastError = ex.Message;
                _lastErrorTime = DateTime.Now;
                Log.Error($"[TCP] 设备 {DeviceId} 查询失败: {ex.Message}", ex);
                throw;
            }
        }
        /// <inheritdoc/>
        public DeviceDriverStatus GetStatus()
        {
            return new DeviceDriverStatus
            {
                IsConnected = IsConnected,
                IsHealthy = IsConnected && string.IsNullOrEmpty(_lastError),
                LastError = _lastError,
                LastErrorTime = _lastErrorTime,
                LatencyMs = _lastLatencyMs,
                ReconnectCount = _reconnectCount
            };
        }
        /// <inheritdoc/>
        public void Dispose()
        {
            if (_disposed) return;
            _disposed = true;
            try
            {
                _client?.Dispose();
            }
            catch (Exception ex)
            {
                Log.Error($"[TCP] 设备 {DeviceId} 释放资源异常: {ex.Message}", ex);
            }
        }
        /// <summary>
        /// 触发连接状态变化事件
        /// </summary>
        private void OnConnectionChanged(bool isConnected, string reason)
        {
            ConnectionChanged?.Invoke(this, new DeviceConnectionEventArgs
            {
                DeviceId = DeviceId,
                IsConnected = isConnected,
                Reason = reason
            });
            if (!isConnected)
            {
                _reconnectCount++;
            }
        }
        /// <summary>
        /// 检查是否已释放
        /// </summary>
        private void ThrowIfDisposed()
        {
            if (_disposed) throw new ObjectDisposedException(nameof(TcpRawDriverAdapter));
        }
    }
}
Aging.Infrastructure/InfrastructureBootstrapper.cs
New file
@@ -0,0 +1,152 @@
using log4net;
using HMI.Core.Util;
using Aging.Infrastructure.Contracts;
using Aging.Infrastructure.Core;
using Aging.Infrastructure.Services;
using Aging.Infrastructure.Drivers;
namespace Aging.Infrastructure
{
    /// <summary>
    /// 基础设施启动引导 - 初始化并注册所有核心服务到 UtilContainer
    /// </summary>
    public static class InfrastructureBootstrapper
    {
        private static readonly ILog log = LogManager.GetLogger(typeof(InfrastructureBootstrapper));
        private static bool _initialized = false;
        /// <summary>
        /// 初始化基础设施服务
        /// 应在 WPF 主程序启动时或插件 InitFinish 中调用
        /// </summary>
        /// <param name="deviceConfigDbPath">DeviceConfig 插件的 SQLite 数据库路径</param>
        public static void Initialize(string deviceConfigDbPath)
        {
            if (_initialized)
            {
                log.Warn("基础设施已初始化,跳过重复初始化");
                return;
            }
            try
            {
                log.Info("开始初始化基础设施服务...");
                // 1. 创建 ResourceManager 单例
                var resourceManager = new ResourceManager();
                UtilContainer.RegisterUtil("ResourceManager", resourceManager);
                log.Info("ResourceManager 已注册");
                // 2. 创建 DeviceManager 单例
                var deviceManager = new DeviceManager();
                UtilContainer.RegisterUtil("DeviceManager", deviceManager);
                log.Info("DeviceManager 已注册");
                // 3. 创建 SlotDeviceMappingResolver(需要 DeviceConfig DB 路径)
                var mappingResolver = new SlotDeviceMappingResolver(deviceConfigDbPath);
                UtilContainer.RegisterUtil("SlotDeviceMappingResolver", mappingResolver);
                log.Info("SlotDeviceMappingResolver 已注册");
                // 4. 创建 DeviceService
                var deviceService = new DeviceService(resourceManager, deviceManager, mappingResolver);
                UtilContainer.RegisterUtil("DeviceService", deviceService);
                log.Info("DeviceService 已注册");
                // 5. 创建 PowerSupplyService
                var powerSupplyService = new PowerSupplyService(resourceManager, deviceManager, mappingResolver);
                UtilContainer.RegisterUtil("PowerSupplyService", powerSupplyService);
                log.Info("PowerSupplyService 已注册");
                // 6. 从数据库加载设备定义并注册驱动
                LoadDevicesFromConfig(deviceManager, mappingResolver);
                _initialized = true;
                log.Info("基础设施服务初始化完成");
            }
            catch (Exception ex)
            {
                log.Error("基础设施初始化失败", ex);
                throw;
            }
        }
        /// <summary>
        /// 从 DeviceConfig 数据库加载设备定义并注册驱动
        /// </summary>
        private static void LoadDevicesFromConfig(IDeviceManager deviceManager, SlotDeviceMappingResolver mappingResolver)
        {
            try
            {
                // 读取所有设备映射
                var mappings = mappingResolver.GetAllMappings();
                // 从映射中提取唯一的设备ID
                var deviceIds = mappings
                    .Select(m => m.DeviceId)
                    .Distinct()
                    .ToList();
                int loaded = 0;
                foreach (var deviceId in deviceIds)
                {
                    try
                    {
                        // 这里暂时使用通用 TCP 驱动作为占位
                        // 实际应该从 device_definition 表读取 transport 和 endpoint
                        // 然后通过 DriverFactory.CreateDriver 创建对应驱动
                        //
                        // TODO: 从数据库读取设备详细信息(transport, endpoint)
                        // var device = GetDeviceDefinition(deviceId);
                        // var driver = DriverFactory.CreateDriver(deviceId, device.Transport, device.Endpoint);
                        log.Debug($"设备 {deviceId} 驱动注册待配置详细信息");
                        loaded++;
                    }
                    catch (Exception ex)
                    {
                        log.Error($"注册设备 {deviceId} 驱动失败: {ex.Message}", ex);
                    }
                }
                log.Info($"从设备配置加载了 {loaded} 个设备驱动");
            }
            catch (Exception ex)
            {
                log.Error("加载设备配置失败", ex);
            }
        }
        /// <summary>
        /// 注册单个设备驱动(供外部调用,用于动态添加设备)
        /// </summary>
        public static void RegisterDevice(string deviceId, string transport, string endpoint)
        {
            var container = new UtilContainer();
            var deviceManager = container.GetUtil<IDeviceManager>("DeviceManager");
            if (deviceManager == null)
            {
                log.Error("DeviceManager 未初始化");
                return;
            }
            try
            {
                var driver = DriverFactory.CreateDriver(deviceId, transport, endpoint);
                deviceManager.RegisterDriver(deviceId, driver);
                log.Info($"设备 {deviceId} ({transport}) 驱动已注册");
            }
            catch (Exception ex)
            {
                log.Error($"注册设备 {deviceId} 失败: {ex.Message}", ex);
            }
        }
        /// <summary>
        /// 重置基础设施(用于测试或重新初始化)
        /// </summary>
        public static void Reset()
        {
            _initialized = false;
            log.Info("基础设施已重置");
        }
    }
}
Aging.Infrastructure/Services/DeviceService.cs
New file
@@ -0,0 +1,331 @@
using System.Diagnostics;
using Aging.Infrastructure.Contracts;
using log4net;
namespace Aging.Infrastructure.Services
{
    /// <summary>
    /// 设备服务实现。
    /// 协调 ResourceManager、DeviceManager 和 SlotMapping,
    /// 提供面向 DeviceId 的命令执行、查询和安全关机功能。
    /// 所有操作均需通过资源预约验证和租约管理,确保硬件资源的安全访问。
    /// </summary>
    public sealed class DeviceService : IDeviceService
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(DeviceService));
        /// <summary>
        /// 资源管理器
        /// </summary>
        private readonly IResourceManager _resourceManager;
        /// <summary>
        /// 设备管理器
        /// </summary>
        private readonly IDeviceManager _deviceManager;
        /// <summary>
        /// Slot-设备-通道映射解析器
        /// </summary>
        private readonly SlotDeviceMappingResolver _slotMappingResolver;
        /// <summary>
        /// 初始化设备服务实例
        /// </summary>
        /// <param name="resourceManager">资源管理器</param>
        /// <param name="deviceManager">设备管理器</param>
        /// <param name="slotMappingResolver">Slot 映射解析器</param>
        public DeviceService(
            IResourceManager resourceManager,
            IDeviceManager deviceManager,
            SlotDeviceMappingResolver slotMappingResolver)
        {
            _resourceManager = resourceManager ?? throw new ArgumentNullException(nameof(resourceManager));
            _deviceManager = deviceManager ?? throw new ArgumentNullException(nameof(deviceManager));
            _slotMappingResolver = slotMappingResolver ?? throw new ArgumentNullException(nameof(slotMappingResolver));
        }
        /// <inheritdoc/>
        public async Task<DeviceCommandResult> ExecuteCommandAsync(
            string deviceId, string command, ResourceOwnerToken owner, CancellationToken ct)
        {
            if (string.IsNullOrWhiteSpace(deviceId))
                throw new ArgumentException("设备ID不能为空", nameof(deviceId));
            if (string.IsNullOrWhiteSpace(command))
                throw new ArgumentException("命令不能为空", nameof(command));
            if (owner == null)
                throw new ArgumentNullException(nameof(owner));
            var stopwatch = Stopwatch.StartNew();
            var resourceKey = new ResourceKey(deviceId, "Generic");
            try
            {
                Log.Info($"[DeviceService] 执行命令: Device={deviceId}, Command={command}, Owner={owner.OwnerId}");
                // 1. 获取设备驱动
                var driver = _deviceManager.GetDriver(deviceId);
                if (driver == null)
                {
                    stopwatch.Stop();
                    Log.Warn($"[DeviceService] 设备 {deviceId} 驱动不存在");
                    return new DeviceCommandResult
                    {
                        Success = false,
                        ErrorCode = "DRIVER_NOT_FOUND",
                        ErrorMessage = $"设备 {deviceId} 的驱动未注册",
                        LatencyMs = stopwatch.Elapsed.TotalMilliseconds
                    };
                }
                // 2. 验证预约
                if (!_resourceManager.ValidateOwner(owner, resourceKey))
                {
                    stopwatch.Stop();
                    Log.Warn($"[DeviceService] 权限验证失败: Device={deviceId}, Owner={owner.OwnerId}");
                    return new DeviceCommandResult
                    {
                        Success = false,
                        ErrorCode = "PERMISSION_DENIED",
                        ErrorMessage = $"所有者 {owner.OwnerId} 未预约设备 {deviceId} 或令牌已过期",
                        LatencyMs = stopwatch.Elapsed.TotalMilliseconds
                    };
                }
                // 3. 获取租约并执行命令
                using var lease = await _resourceManager.AcquireOperationAsync(owner, resourceKey, ct).ConfigureAwait(false);
                var response = await driver.SendCommandAsync(command, ct).ConfigureAwait(false);
                stopwatch.Stop();
                Log.Info($"[DeviceService] 命令执行成功: Device={deviceId}, Command={command}, 延迟={stopwatch.Elapsed.TotalMilliseconds:F2}ms");
                return new DeviceCommandResult
                {
                    Success = true,
                    RawResponse = response,
                    Readback = response,
                    LatencyMs = stopwatch.Elapsed.TotalMilliseconds
                };
            }
            catch (ResourceConflictException ex)
            {
                stopwatch.Stop();
                Log.Error($"[DeviceService] 资源冲突: Device={deviceId}, 错误: {ex.Message}", ex);
                return new DeviceCommandResult
                {
                    Success = false,
                    ErrorCode = "RESOURCE_CONFLICT",
                    ErrorMessage = ex.Message,
                    LatencyMs = stopwatch.Elapsed.TotalMilliseconds
                };
            }
            catch (OperationCanceledException)
            {
                stopwatch.Stop();
                Log.Warn($"[DeviceService] 命令执行被取消: Device={deviceId}");
                return new DeviceCommandResult
                {
                    Success = false,
                    ErrorCode = "OPERATION_CANCELLED",
                    ErrorMessage = "操作已被取消",
                    LatencyMs = stopwatch.Elapsed.TotalMilliseconds
                };
            }
            catch (TimeoutException ex)
            {
                stopwatch.Stop();
                Log.Error($"[DeviceService] 命令执行超时: Device={deviceId}, 错误: {ex.Message}", ex);
                return new DeviceCommandResult
                {
                    Success = false,
                    ErrorCode = "TIMEOUT",
                    ErrorMessage = $"设备 {deviceId} 命令执行超时: {ex.Message}",
                    LatencyMs = stopwatch.Elapsed.TotalMilliseconds
                };
            }
            catch (Exception ex)
            {
                stopwatch.Stop();
                Log.Error($"[DeviceService] 命令执行异常: Device={deviceId}, 错误: {ex.Message}", ex);
                return new DeviceCommandResult
                {
                    Success = false,
                    ErrorCode = "UNKNOWN_ERROR",
                    ErrorMessage = $"设备 {deviceId} 命令执行失败: {ex.Message}",
                    LatencyMs = stopwatch.Elapsed.TotalMilliseconds
                };
            }
        }
        /// <inheritdoc/>
        public async Task<DeviceCommandResult> ExecuteQueryAsync(
            string deviceId, string query, ResourceOwnerToken owner, CancellationToken ct)
        {
            if (string.IsNullOrWhiteSpace(deviceId))
                throw new ArgumentException("设备ID不能为空", nameof(deviceId));
            if (string.IsNullOrWhiteSpace(query))
                throw new ArgumentException("查询命令不能为空", nameof(query));
            if (owner == null)
                throw new ArgumentNullException(nameof(owner));
            var stopwatch = Stopwatch.StartNew();
            var resourceKey = new ResourceKey(deviceId, "Generic");
            try
            {
                Log.Info($"[DeviceService] 执行查询: Device={deviceId}, Query={query}, Owner={owner.OwnerId}");
                // 1. 获取设备驱动
                var driver = _deviceManager.GetDriver(deviceId);
                if (driver == null)
                {
                    stopwatch.Stop();
                    Log.Warn($"[DeviceService] 设备 {deviceId} 驱动不存在");
                    return new DeviceCommandResult
                    {
                        Success = false,
                        ErrorCode = "DRIVER_NOT_FOUND",
                        ErrorMessage = $"设备 {deviceId} 的驱动未注册",
                        LatencyMs = stopwatch.Elapsed.TotalMilliseconds
                    };
                }
                // 2. 验证预约
                if (!_resourceManager.ValidateOwner(owner, resourceKey))
                {
                    stopwatch.Stop();
                    Log.Warn($"[DeviceService] 权限验证失败: Device={deviceId}, Owner={owner.OwnerId}");
                    return new DeviceCommandResult
                    {
                        Success = false,
                        ErrorCode = "PERMISSION_DENIED",
                        ErrorMessage = $"所有者 {owner.OwnerId} 未预约设备 {deviceId} 或令牌已过期",
                        LatencyMs = stopwatch.Elapsed.TotalMilliseconds
                    };
                }
                // 3. 获取租约并执行查询
                using var lease = await _resourceManager.AcquireOperationAsync(owner, resourceKey, ct).ConfigureAwait(false);
                var response = await driver.QueryAsync(query, ct).ConfigureAwait(false);
                stopwatch.Stop();
                Log.Info($"[DeviceService] 查询执行成功: Device={deviceId}, Query={query}, 延迟={stopwatch.Elapsed.TotalMilliseconds:F2}ms");
                return new DeviceCommandResult
                {
                    Success = true,
                    RawResponse = response,
                    Readback = response,
                    LatencyMs = stopwatch.Elapsed.TotalMilliseconds
                };
            }
            catch (ResourceConflictException ex)
            {
                stopwatch.Stop();
                Log.Error($"[DeviceService] 资源冲突: Device={deviceId}, 错误: {ex.Message}", ex);
                return new DeviceCommandResult
                {
                    Success = false,
                    ErrorCode = "RESOURCE_CONFLICT",
                    ErrorMessage = ex.Message,
                    LatencyMs = stopwatch.Elapsed.TotalMilliseconds
                };
            }
            catch (OperationCanceledException)
            {
                stopwatch.Stop();
                Log.Warn($"[DeviceService] 查询执行被取消: Device={deviceId}");
                return new DeviceCommandResult
                {
                    Success = false,
                    ErrorCode = "OPERATION_CANCELLED",
                    ErrorMessage = "操作已被取消",
                    LatencyMs = stopwatch.Elapsed.TotalMilliseconds
                };
            }
            catch (TimeoutException ex)
            {
                stopwatch.Stop();
                Log.Error($"[DeviceService] 查询执行超时: Device={deviceId}, 错误: {ex.Message}", ex);
                return new DeviceCommandResult
                {
                    Success = false,
                    ErrorCode = "TIMEOUT",
                    ErrorMessage = $"设备 {deviceId} 查询执行超时: {ex.Message}",
                    LatencyMs = stopwatch.Elapsed.TotalMilliseconds
                };
            }
            catch (Exception ex)
            {
                stopwatch.Stop();
                Log.Error($"[DeviceService] 查询执行异常: Device={deviceId}, 错误: {ex.Message}", ex);
                return new DeviceCommandResult
                {
                    Success = false,
                    ErrorCode = "UNKNOWN_ERROR",
                    ErrorMessage = $"设备 {deviceId} 查询执行失败: {ex.Message}",
                    LatencyMs = stopwatch.Elapsed.TotalMilliseconds
                };
            }
        }
        /// <inheritdoc/>
        public async Task SafeShutdownAsync(string deviceId, ResourceOwnerToken owner, CancellationToken ct)
        {
            if (string.IsNullOrWhiteSpace(deviceId))
                throw new ArgumentException("设备ID不能为空", nameof(deviceId));
            if (owner == null)
                throw new ArgumentNullException(nameof(owner));
            var resourceKey = new ResourceKey(deviceId, "Generic");
            try
            {
                Log.Info($"[DeviceService] 开始安全关机: Device={deviceId}, Owner={owner.OwnerId}");
                // 1. 获取驱动
                var driver = _deviceManager.GetDriver(deviceId);
                if (driver == null)
                {
                    Log.Warn($"[DeviceService] 安全关机失败: 设备 {deviceId} 驱动不存在");
                    return;
                }
                // 2. 验证预约
                if (!_resourceManager.ValidateOwner(owner, resourceKey))
                {
                    Log.Warn($"[DeviceService] 安全关机权限验证失败: Device={deviceId}, Owner={owner.OwnerId}");
                    return;
                }
                // 3. 获取租约
                using var lease = await _resourceManager.AcquireOperationAsync(owner, resourceKey, ct).ConfigureAwait(false);
                // 4. 发送安全关闭命令
                Log.Info($"[DeviceService] 发送安全关闭命令: Device={deviceId}");
                await driver.SendCommandAsync("OUTP OFF", ct).ConfigureAwait(false);
                // 5. 等待确认(短暂延迟确保命令执行完成)
                await Task.Delay(500, ct).ConfigureAwait(false);
                // 6. 验证关闭状态
                var statusResponse = await driver.QueryAsync("OUTP?", ct).ConfigureAwait(false);
                Log.Info($"[DeviceService] 设备 {deviceId} 关闭状态确认: {statusResponse}");
                Log.Info($"[DeviceService] 安全关机完成: Device={deviceId}");
            }
            catch (OperationCanceledException)
            {
                Log.Warn($"[DeviceService] 安全关机操作被取消: Device={deviceId}");
                throw;
            }
            catch (Exception ex)
            {
                Log.Error($"[DeviceService] 安全关机异常: Device={deviceId}, 错误: {ex.Message}", ex);
                throw;
            }
        }
    }
}
Aging.Infrastructure/Services/PowerSupplyService.cs
New file
@@ -0,0 +1,543 @@
using System.Diagnostics;
using Aging.Infrastructure.Contracts;
using log4net;
namespace Aging.Infrastructure.Services
{
    /// <summary>
    /// 电源配置命令
    /// </summary>
    public sealed class PowerCommand
    {
        /// <summary>
        /// 目标电压值(可选,单位: V)
        /// </summary>
        public double? Voltage { get; set; }
        /// <summary>
        /// 目标电流限制值(可选,单位: A)
        /// </summary>
        public double? CurrentLimit { get; set; }
        /// <summary>
        /// 输出开关状态(true=ON, false=OFF, null=不改变)
        /// </summary>
        public bool? Output { get; set; }
    }
    /// <summary>
    /// 电源操作结果
    /// </summary>
    public sealed class PowerOperationResult
    {
        /// <summary>
        /// 操作是否成功
        /// </summary>
        public bool Success { get; set; }
        /// <summary>
        /// 实测电压值(单位: V)
        /// </summary>
        public double? MeasuredVoltage { get; set; }
        /// <summary>
        /// 实测电流值(单位: A)
        /// </summary>
        public double? MeasuredCurrent { get; set; }
        /// <summary>
        /// 设备原始响应字符串
        /// </summary>
        public string? RawResponse { get; set; }
        /// <summary>
        /// 错误代码
        /// </summary>
        public string? ErrorCode { get; set; }
        /// <summary>
        /// 错误信息
        /// </summary>
        public string? ErrorMessage { get; set; }
        /// <summary>
        /// 操作延迟(毫秒)
        /// </summary>
        public double LatencyMs { get; set; }
        /// <summary>
        /// 创建一个成功的结果
        /// </summary>
        public static PowerOperationResult Succeed(double? voltage, double? current, string? rawResponse, double latencyMs)
        {
            return new PowerOperationResult
            {
                Success = true,
                MeasuredVoltage = voltage,
                MeasuredCurrent = current,
                RawResponse = rawResponse,
                LatencyMs = latencyMs
            };
        }
        /// <summary>
        /// 创建一个失败的结果
        /// </summary>
        public static PowerOperationResult Failed(string errorCode, string errorMessage, double latencyMs)
        {
            return new PowerOperationResult
            {
                Success = false,
                ErrorCode = errorCode,
                ErrorMessage = errorMessage,
                LatencyMs = latencyMs
            };
        }
    }
    /// <summary>
    /// 电源配置服务 - 面向 Slot 的高层电源操作。
    /// 通过 SlotDeviceMappingResolver 解析 Slot 到具体设备端点的映射,
    /// 协调 ResourceManager 和 DeviceManager 完成电源配置、测量和状态查询。
    /// </summary>
    public sealed class PowerSupplyService
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(PowerSupplyService));
        /// <summary>
        /// 资源管理器
        /// </summary>
        private readonly IResourceManager _resourceManager;
        /// <summary>
        /// 设备管理器
        /// </summary>
        private readonly IDeviceManager _deviceManager;
        /// <summary>
        /// Slot-设备-通道映射解析器
        /// </summary>
        private readonly SlotDeviceMappingResolver _slotMappingResolver;
        /// <summary>
        /// 初始化电源配置服务实例
        /// </summary>
        /// <param name="resourceManager">资源管理器</param>
        /// <param name="deviceManager">设备管理器</param>
        /// <param name="slotMappingResolver">Slot 映射解析器</param>
        public PowerSupplyService(
            IResourceManager resourceManager,
            IDeviceManager deviceManager,
            SlotDeviceMappingResolver slotMappingResolver)
        {
            _resourceManager = resourceManager ?? throw new ArgumentNullException(nameof(resourceManager));
            _deviceManager = deviceManager ?? throw new ArgumentNullException(nameof(deviceManager));
            _slotMappingResolver = slotMappingResolver ?? throw new ArgumentNullException(nameof(slotMappingResolver));
        }
        /// <summary>
        /// 异步配置 Slot 对应的电源参数。
        /// 根据 SlotId 解析映射获取设备端点,验证资源预约后发送 SCPI 命令配置电源。
        /// </summary>
        /// <param name="slotId">Slot 标识符</param>
        /// <param name="command">电源配置命令</param>
        /// <param name="owner">所有者令牌</param>
        /// <param name="ct">取消令牌</param>
        /// <returns>电源操作结果,包含测量值</returns>
        public async Task<PowerOperationResult> ConfigureAsync(
            string slotId, PowerCommand command, ResourceOwnerToken owner, CancellationToken ct)
        {
            if (string.IsNullOrWhiteSpace(slotId))
                throw new ArgumentException("SlotId 不能为空", nameof(slotId));
            if (command == null)
                throw new ArgumentNullException(nameof(command));
            if (owner == null)
                throw new ArgumentNullException(nameof(owner));
            var stopwatch = Stopwatch.StartNew();
            try
            {
                Log.Info($"[PowerSupplyService] 配置电源: Slot={slotId}, Owner={owner.OwnerId}");
                // 1. 解析 Slot 映射
                var endpoint = _slotMappingResolver.ResolveEndpoint(slotId, "PowerSupply");
                if (endpoint == null)
                {
                    stopwatch.Stop();
                    Log.Warn($"[PowerSupplyService] Slot {slotId} 未找到 PowerSupply 映射");
                    return PowerOperationResult.Failed("MAPPING_NOT_FOUND",
                        $"Slot {slotId} 未找到 PowerSupply 类型的设备映射", stopwatch.Elapsed.TotalMilliseconds);
                }
                var resourceKey = endpoint.ToResourceKey();
                // 2. 验证预约
                if (!_resourceManager.ValidateOwner(owner, resourceKey))
                {
                    stopwatch.Stop();
                    Log.Warn($"[PowerSupplyService] 权限验证失败: Slot={slotId}, Device={endpoint.DeviceId}, Owner={owner.OwnerId}");
                    return PowerOperationResult.Failed("PERMISSION_DENIED",
                        $"所有者 {owner.OwnerId} 未预约 Slot {slotId} 对应的设备 {endpoint.DeviceId} 或令牌已过期",
                        stopwatch.Elapsed.TotalMilliseconds);
                }
                // 3. 获取租约
                using var lease = await _resourceManager.AcquireOperationAsync(owner, resourceKey, ct).ConfigureAwait(false);
                // 4. 获取设备驱动
                var driver = _deviceManager.GetDriver(endpoint.DeviceId);
                if (driver == null)
                {
                    stopwatch.Stop();
                    Log.Warn($"[PowerSupplyService] 设备 {endpoint.DeviceId} 驱动不存在");
                    return PowerOperationResult.Failed("DRIVER_NOT_FOUND",
                        $"设备 {endpoint.DeviceId} 的驱动未注册", stopwatch.Elapsed.TotalMilliseconds);
                }
                // 5. 构建并发送 SCPI 命令
                var scpiCommands = BuildScpiCommands(command);
                foreach (var scpiCommand in scpiCommands)
                {
                    Log.Info($"[PowerSupplyService] 发送 SCPI 命令: Device={endpoint.DeviceId}, Command={scpiCommand}");
                    await driver.SendCommandAsync(scpiCommand, ct).ConfigureAwait(false);
                }
                // 6. 回读测量值
                string rawResponse;
                try
                {
                    // 优先尝试 MEAS:ALL? 一次性获取所有测量值
                    rawResponse = await driver.QueryAsync("MEAS:ALL?", ct).ConfigureAwait(false);
                    Log.Info($"[PowerSupplyService] MEAS:ALL? 响应: {rawResponse}");
                }
                catch
                {
                    // 如果 MEAS:ALL? 失败,分别查询电压和电流
                    Log.Info($"[PowerSupplyService] MEAS:ALL? 失败,尝试分别查询 VOLT? 和 CURR?");
                    var voltageResponse = await driver.QueryAsync("VOLT?", ct).ConfigureAwait(false);
                    var currentResponse = await driver.QueryAsync("CURR?", ct).ConfigureAwait(false);
                    rawResponse = $"{voltageResponse},{currentResponse}";
                }
                // 7. 解析回读值
                ParseMeasurementResponse(rawResponse, out var measuredVoltage, out var measuredCurrent);
                stopwatch.Stop();
                Log.Info($"[PowerSupplyService] 电源配置完成: Slot={slotId}, Device={endpoint.DeviceId}, " +
                         $"V={measuredVoltage}, I={measuredCurrent}, 延迟={stopwatch.Elapsed.TotalMilliseconds:F2}ms");
                return PowerOperationResult.Succeed(measuredVoltage, measuredCurrent, rawResponse,
                    stopwatch.Elapsed.TotalMilliseconds);
            }
            catch (ResourceConflictException ex)
            {
                stopwatch.Stop();
                Log.Error($"[PowerSupplyService] 资源冲突: Slot={slotId}, 错误: {ex.Message}", ex);
                return PowerOperationResult.Failed("RESOURCE_CONFLICT", ex.Message, stopwatch.Elapsed.TotalMilliseconds);
            }
            catch (OperationCanceledException)
            {
                stopwatch.Stop();
                Log.Warn($"[PowerSupplyService] 电源配置被取消: Slot={slotId}");
                return PowerOperationResult.Failed("OPERATION_CANCELLED", "操作已被取消",
                    stopwatch.Elapsed.TotalMilliseconds);
            }
            catch (TimeoutException ex)
            {
                stopwatch.Stop();
                Log.Error($"[PowerSupplyService] 电源配置超时: Slot={slotId}, 错误: {ex.Message}", ex);
                return PowerOperationResult.Failed("TIMEOUT", $"电源配置超时: {ex.Message}",
                    stopwatch.Elapsed.TotalMilliseconds);
            }
            catch (Exception ex)
            {
                stopwatch.Stop();
                Log.Error($"[PowerSupplyService] 电源配置异常: Slot={slotId}, 错误: {ex.Message}", ex);
                return PowerOperationResult.Failed("UNKNOWN_ERROR", $"电源配置失败: {ex.Message}",
                    stopwatch.Elapsed.TotalMilliseconds);
            }
        }
        /// <summary>
        /// 异步测量 Slot 对应电源的输出值(电压、电流)。
        /// 根据 SlotId 解析映射获取设备端点,查询实时测量值。
        /// </summary>
        /// <param name="slotId">Slot 标识符</param>
        /// <param name="owner">所有者令牌</param>
        /// <param name="ct">取消令牌</param>
        /// <returns>电源操作结果,包含测量值</returns>
        public async Task<PowerOperationResult> MeasureAsync(
            string slotId, ResourceOwnerToken owner, CancellationToken ct)
        {
            if (string.IsNullOrWhiteSpace(slotId))
                throw new ArgumentException("SlotId 不能为空", nameof(slotId));
            if (owner == null)
                throw new ArgumentNullException(nameof(owner));
            var stopwatch = Stopwatch.StartNew();
            try
            {
                Log.Info($"[PowerSupplyService] 测量电源: Slot={slotId}, Owner={owner.OwnerId}");
                // 1. 解析 Slot 映射
                var endpoint = _slotMappingResolver.ResolveEndpoint(slotId, "PowerSupply");
                if (endpoint == null)
                {
                    stopwatch.Stop();
                    Log.Warn($"[PowerSupplyService] Slot {slotId} 未找到 PowerSupply 映射");
                    return PowerOperationResult.Failed("MAPPING_NOT_FOUND",
                        $"Slot {slotId} 未找到 PowerSupply 类型的设备映射", stopwatch.Elapsed.TotalMilliseconds);
                }
                var resourceKey = endpoint.ToResourceKey();
                // 2. 验证预约
                if (!_resourceManager.ValidateOwner(owner, resourceKey))
                {
                    stopwatch.Stop();
                    Log.Warn($"[PowerSupplyService] 权限验证失败: Slot={slotId}, Device={endpoint.DeviceId}, Owner={owner.OwnerId}");
                    return PowerOperationResult.Failed("PERMISSION_DENIED",
                        $"所有者 {owner.OwnerId} 未预约 Slot {slotId} 对应的设备 {endpoint.DeviceId} 或令牌已过期",
                        stopwatch.Elapsed.TotalMilliseconds);
                }
                // 3. 获取租约
                using var lease = await _resourceManager.AcquireOperationAsync(owner, resourceKey, ct).ConfigureAwait(false);
                // 4. 获取设备驱动
                var driver = _deviceManager.GetDriver(endpoint.DeviceId);
                if (driver == null)
                {
                    stopwatch.Stop();
                    Log.Warn($"[PowerSupplyService] 设备 {endpoint.DeviceId} 驱动不存在");
                    return PowerOperationResult.Failed("DRIVER_NOT_FOUND",
                        $"设备 {endpoint.DeviceId} 的驱动未注册", stopwatch.Elapsed.TotalMilliseconds);
                }
                // 5. 查询测量值
                string rawResponse;
                try
                {
                    rawResponse = await driver.QueryAsync("MEAS:ALL?", ct).ConfigureAwait(false);
                }
                catch
                {
                    var voltageResponse = await driver.QueryAsync("VOLT?", ct).ConfigureAwait(false);
                    var currentResponse = await driver.QueryAsync("CURR?", ct).ConfigureAwait(false);
                    rawResponse = $"{voltageResponse},{currentResponse}";
                }
                // 6. 解析回读值
                ParseMeasurementResponse(rawResponse, out var measuredVoltage, out var measuredCurrent);
                stopwatch.Stop();
                Log.Info($"[PowerSupplyService] 测量完成: Slot={slotId}, Device={endpoint.DeviceId}, " +
                         $"V={measuredVoltage}, I={measuredCurrent}, 延迟={stopwatch.Elapsed.TotalMilliseconds:F2}ms");
                return PowerOperationResult.Succeed(measuredVoltage, measuredCurrent, rawResponse,
                    stopwatch.Elapsed.TotalMilliseconds);
            }
            catch (ResourceConflictException ex)
            {
                stopwatch.Stop();
                Log.Error($"[PowerSupplyService] 资源冲突: Slot={slotId}, 错误: {ex.Message}", ex);
                return PowerOperationResult.Failed("RESOURCE_CONFLICT", ex.Message, stopwatch.Elapsed.TotalMilliseconds);
            }
            catch (OperationCanceledException)
            {
                stopwatch.Stop();
                Log.Warn($"[PowerSupplyService] 测量操作被取消: Slot={slotId}");
                return PowerOperationResult.Failed("OPERATION_CANCELLED", "操作已被取消",
                    stopwatch.Elapsed.TotalMilliseconds);
            }
            catch (TimeoutException ex)
            {
                stopwatch.Stop();
                Log.Error($"[PowerSupplyService] 测量操作超时: Slot={slotId}, 错误: {ex.Message}", ex);
                return PowerOperationResult.Failed("TIMEOUT", $"测量操作超时: {ex.Message}",
                    stopwatch.Elapsed.TotalMilliseconds);
            }
            catch (Exception ex)
            {
                stopwatch.Stop();
                Log.Error($"[PowerSupplyService] 测量操作异常: Slot={slotId}, 错误: {ex.Message}", ex);
                return PowerOperationResult.Failed("UNKNOWN_ERROR", $"测量操作失败: {ex.Message}",
                    stopwatch.Elapsed.TotalMilliseconds);
            }
        }
        /// <summary>
        /// 异步获取 Slot 对应电源的当前状态(电压、电流、输出开关状态)。
        /// </summary>
        /// <param name="slotId">Slot 标识符</param>
        /// <param name="owner">所有者令牌</param>
        /// <param name="ct">取消令牌</param>
        /// <returns>电源操作结果,包含状态信息</returns>
        public async Task<PowerOperationResult> GetPowerStatusAsync(
            string slotId, ResourceOwnerToken owner, CancellationToken ct)
        {
            if (string.IsNullOrWhiteSpace(slotId))
                throw new ArgumentException("SlotId 不能为空", nameof(slotId));
            if (owner == null)
                throw new ArgumentNullException(nameof(owner));
            var stopwatch = Stopwatch.StartNew();
            try
            {
                Log.Info($"[PowerSupplyService] 查询电源状态: Slot={slotId}, Owner={owner.OwnerId}");
                // 1. 解析 Slot 映射
                var endpoint = _slotMappingResolver.ResolveEndpoint(slotId, "PowerSupply");
                if (endpoint == null)
                {
                    stopwatch.Stop();
                    Log.Warn($"[PowerSupplyService] Slot {slotId} 未找到 PowerSupply 映射");
                    return PowerOperationResult.Failed("MAPPING_NOT_FOUND",
                        $"Slot {slotId} 未找到 PowerSupply 类型的设备映射", stopwatch.Elapsed.TotalMilliseconds);
                }
                var resourceKey = endpoint.ToResourceKey();
                // 2. 验证预约
                if (!_resourceManager.ValidateOwner(owner, resourceKey))
                {
                    stopwatch.Stop();
                    Log.Warn($"[PowerSupplyService] 权限验证失败: Slot={slotId}, Device={endpoint.DeviceId}, Owner={owner.OwnerId}");
                    return PowerOperationResult.Failed("PERMISSION_DENIED",
                        $"所有者 {owner.OwnerId} 未预约 Slot {slotId} 对应的设备 {endpoint.DeviceId} 或令牌已过期",
                        stopwatch.Elapsed.TotalMilliseconds);
                }
                // 3. 获取租约
                using var lease = await _resourceManager.AcquireOperationAsync(owner, resourceKey, ct).ConfigureAwait(false);
                // 4. 获取设备驱动
                var driver = _deviceManager.GetDriver(endpoint.DeviceId);
                if (driver == null)
                {
                    stopwatch.Stop();
                    Log.Warn($"[PowerSupplyService] 设备 {endpoint.DeviceId} 驱动不存在");
                    return PowerOperationResult.Failed("DRIVER_NOT_FOUND",
                        $"设备 {endpoint.DeviceId} 的驱动未注册", stopwatch.Elapsed.TotalMilliseconds);
                }
                // 5. 查询电压、电流和输出状态
                var voltageResponse = await driver.QueryAsync("VOLT?", ct).ConfigureAwait(false);
                var currentResponse = await driver.QueryAsync("CURR?", ct).ConfigureAwait(false);
                var outputResponse = await driver.QueryAsync("OUTP?", ct).ConfigureAwait(false);
                var rawResponse = $"VOLT={voltageResponse},CURR={currentResponse},OUTP={outputResponse}";
                // 6. 解析测量值
                ParseMeasurementResponse($"{voltageResponse},{currentResponse}",
                    out var measuredVoltage, out var measuredCurrent);
                stopwatch.Stop();
                Log.Info($"[PowerSupplyService] 电源状态查询完成: Slot={slotId}, Device={endpoint.DeviceId}, " +
                         $"V={measuredVoltage}, I={measuredCurrent}, OUTP={outputResponse}, " +
                         $"延迟={stopwatch.Elapsed.TotalMilliseconds:F2}ms");
                return new PowerOperationResult
                {
                    Success = true,
                    MeasuredVoltage = measuredVoltage,
                    MeasuredCurrent = measuredCurrent,
                    RawResponse = rawResponse,
                    LatencyMs = stopwatch.Elapsed.TotalMilliseconds
                };
            }
            catch (ResourceConflictException ex)
            {
                stopwatch.Stop();
                Log.Error($"[PowerSupplyService] 资源冲突: Slot={slotId}, 错误: {ex.Message}", ex);
                return PowerOperationResult.Failed("RESOURCE_CONFLICT", ex.Message, stopwatch.Elapsed.TotalMilliseconds);
            }
            catch (OperationCanceledException)
            {
                stopwatch.Stop();
                Log.Warn($"[PowerSupplyService] 状态查询被取消: Slot={slotId}");
                return PowerOperationResult.Failed("OPERATION_CANCELLED", "操作已被取消",
                    stopwatch.Elapsed.TotalMilliseconds);
            }
            catch (TimeoutException ex)
            {
                stopwatch.Stop();
                Log.Error($"[PowerSupplyService] 状态查询超时: Slot={slotId}, 错误: {ex.Message}", ex);
                return PowerOperationResult.Failed("TIMEOUT", $"状态查询超时: {ex.Message}",
                    stopwatch.Elapsed.TotalMilliseconds);
            }
            catch (Exception ex)
            {
                stopwatch.Stop();
                Log.Error($"[PowerSupplyService] 状态查询异常: Slot={slotId}, 错误: {ex.Message}", ex);
                return PowerOperationResult.Failed("UNKNOWN_ERROR", $"状态查询失败: {ex.Message}",
                    stopwatch.Elapsed.TotalMilliseconds);
            }
        }
        /// <summary>
        /// 根据电源配置命令构建 SCPI 命令列表
        /// </summary>
        /// <param name="command">电源配置命令</param>
        /// <returns>SCPI 命令字符串列表</returns>
        private static List<string> BuildScpiCommands(PowerCommand command)
        {
            var commands = new List<string>();
            if (command.Voltage.HasValue)
            {
                commands.Add($"VOLT {command.Voltage.Value:G}");
            }
            if (command.CurrentLimit.HasValue)
            {
                commands.Add($"CURR {command.CurrentLimit.Value:G}");
            }
            if (command.Output.HasValue)
            {
                commands.Add(command.Output.Value ? "OUTP ON" : "OUTP OFF");
            }
            return commands;
        }
        /// <summary>
        /// 解析设备返回的测量值响应字符串。
        /// 支持格式: "voltage,current" 或单个数值。
        /// </summary>
        /// <param name="rawResponse">原始响应字符串</param>
        /// <param name="voltage">解析后的电压值</param>
        /// <param name="current">解析后的电流值</param>
        private static void ParseMeasurementResponse(string rawResponse, out double? voltage, out double? current)
        {
            voltage = null;
            current = null;
            if (string.IsNullOrWhiteSpace(rawResponse))
                return;
            try
            {
                var parts = rawResponse.Split(',');
                if (parts.Length >= 1 && double.TryParse(parts[0].Trim(), out var v))
                {
                    voltage = v;
                }
                if (parts.Length >= 2 && double.TryParse(parts[1].Trim(), out var c))
                {
                    current = c;
                }
            }
            catch (Exception)
            {
                // 解析失败时保持 null 值
            }
        }
    }
}
Aging.Infrastructure/Services/SlotDeviceMappingResolver.cs
New file
@@ -0,0 +1,218 @@
using System.Data;
using Aging.Infrastructure.Contracts;
using HMI.Core.Util;
using log4net;
namespace Aging.Infrastructure.Services
{
    /// <summary>
    /// 电源端点信息,描述 Slot 映射到的具体设备通道
    /// </summary>
    public sealed class PowerEndpoint
    {
        /// <summary>
        /// 设备标识符
        /// </summary>
        public string DeviceId { get; }
        /// <summary>
        /// 通道标识
        /// </summary>
        public int ChannelId { get; }
        /// <summary>
        /// 生效版本号
        /// </summary>
        public string EffectiveVersion { get; }
        /// <summary>
        /// 初始化电源端点实例
        /// </summary>
        /// <param name="deviceId">设备标识符</param>
        /// <param name="channelId">通道标识</param>
        /// <param name="effectiveVersion">生效版本号</param>
        public PowerEndpoint(string deviceId, int channelId, string effectiveVersion)
        {
            DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId));
            ChannelId = channelId;
            EffectiveVersion = effectiveVersion ?? string.Empty;
        }
        /// <summary>
        /// 转换为资源键,用于资源管理器的预约和租约操作
        /// </summary>
        /// <returns>对应的资源键实例</returns>
        public ResourceKey ToResourceKey()
        {
            return new ResourceKey(DeviceId, "PowerSupply", ChannelId.ToString());
        }
        /// <inheritdoc/>
        public override string ToString() => $"{DeviceId}:CH{ChannelId}(v{EffectiveVersion})";
    }
    /// <summary>
    /// Slot-设备-通道映射解析器。
    /// 从 Plugin.DeviceConfig 的 SQLite 数据库读取 Slot-Device-Channel 映射关系,
    /// 提供根据 SlotId 和能力类型解析具体设备端点的功能。
    /// </summary>
    public sealed class SlotDeviceMappingResolver
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(SlotDeviceMappingResolver));
        /// <summary>
        /// DeviceConfig 的 SQLite 数据库文件路径
        /// </summary>
        private readonly string _deviceConfigDbPath;
        /// <summary>
        /// 初始化映射解析器实例
        /// </summary>
        /// <param name="deviceConfigDbPath">DeviceConfig 的 SQLite 数据库文件路径</param>
        public SlotDeviceMappingResolver(string deviceConfigDbPath)
        {
            _deviceConfigDbPath = deviceConfigDbPath ?? throw new ArgumentNullException(nameof(deviceConfigDbPath));
            Log.Info($"[SlotDeviceMappingResolver] 初始化完成,数据库路径: {deviceConfigDbPath}");
        }
        /// <summary>
        /// 根据 SlotId 和能力类型解析具体的电源端点。
        /// 从 slot_device_mapping 表查询匹配的最新版本映射记录。
        /// </summary>
        /// <param name="slotId">Slot 标识符</param>
        /// <param name="capability">能力类型(如 "PowerSupply")</param>
        /// <returns>解析后的电源端点;未找到匹配映射时返回 null</returns>
        public PowerEndpoint? ResolveEndpoint(string slotId, string capability)
        {
            if (string.IsNullOrWhiteSpace(slotId))
                throw new ArgumentException("SlotId 不能为空", nameof(slotId));
            if (string.IsNullOrWhiteSpace(capability))
                throw new ArgumentException("能力类型不能为空", nameof(capability));
            SqliteHelper? sqliteHelper = null;
            try
            {
                sqliteHelper = new SqliteHelper(_deviceConfigDbPath);
                var sql = $"SELECT slot_id, capability, version, device_id, channel_id, mapping_version " +
                          $"FROM slot_device_mapping " +
                          $"WHERE slot_id = '{slotId}' AND capability = '{capability}' " +
                          $"ORDER BY mapping_version DESC LIMIT 1";
                var dataTable = sqliteHelper.GetDataTable(sql);
                if (dataTable.Rows.Count == 0)
                {
                    Log.Warn($"[SlotDeviceMappingResolver] 未找到 Slot={slotId}, Capability={capability} 的映射记录");
                    return null;
                }
                var row = dataTable.Rows[0];
                var deviceId = row["device_id"]?.ToString() ?? string.Empty;
                var channelId = Convert.ToInt32(row["channel_id"]);
                var effectiveVersion = row["mapping_version"]?.ToString() ?? string.Empty;
                var endpoint = new PowerEndpoint(deviceId, channelId, effectiveVersion);
                Log.Info($"[SlotDeviceMappingResolver] 解析映射: Slot={slotId}, Capability={capability} → {endpoint}");
                return endpoint;
            }
            catch (Exception ex)
            {
                Log.Error($"[SlotDeviceMappingResolver] 解析映射失败: Slot={slotId}, Capability={capability}, 错误: {ex.Message}", ex);
                throw;
            }
            finally
            {
                sqliteHelper?.Close();
            }
        }
        /// <summary>
        /// 获取数据库中所有的 Slot-设备-通道映射记录
        /// </summary>
        /// <returns>所有映射记录的列表</returns>
        public List<PowerEndpoint> GetAllMappings()
        {
            SqliteHelper? sqliteHelper = null;
            try
            {
                sqliteHelper = new SqliteHelper(_deviceConfigDbPath);
                var sql = "SELECT slot_id, capability, version, device_id, channel_id, mapping_version " +
                          "FROM slot_device_mapping " +
                          "ORDER BY slot_id, capability, mapping_version DESC";
                var dataTable = sqliteHelper.GetDataTable(sql);
                var results = new List<PowerEndpoint>();
                foreach (DataRow row in dataTable.Rows)
                {
                    var deviceId = row["device_id"]?.ToString() ?? string.Empty;
                    var channelId = Convert.ToInt32(row["channel_id"]);
                    var effectiveVersion = row["mapping_version"]?.ToString() ?? string.Empty;
                    results.Add(new PowerEndpoint(deviceId, channelId, effectiveVersion));
                }
                Log.Info($"[SlotDeviceMappingResolver] 获取全部映射记录,共 {results.Count} 条");
                return results;
            }
            catch (Exception ex)
            {
                Log.Error($"[SlotDeviceMappingResolver] 获取全部映射记录失败: {ex.Message}", ex);
                throw;
            }
            finally
            {
                sqliteHelper?.Close();
            }
        }
        /// <summary>
        /// 按能力类型筛选映射记录
        /// </summary>
        /// <param name="capability">能力类型(如 "PowerSupply"、"CAN" 等)</param>
        /// <returns>匹配指定能力类型的映射记录列表</returns>
        public List<PowerEndpoint> GetMappingsByCapability(string capability)
        {
            if (string.IsNullOrWhiteSpace(capability))
                throw new ArgumentException("能力类型不能为空", nameof(capability));
            SqliteHelper? sqliteHelper = null;
            try
            {
                sqliteHelper = new SqliteHelper(_deviceConfigDbPath);
                var sql = $"SELECT slot_id, capability, version, device_id, channel_id, mapping_version " +
                          $"FROM slot_device_mapping " +
                          $"WHERE capability = '{capability}' " +
                          $"ORDER BY slot_id, mapping_version DESC";
                var dataTable = sqliteHelper.GetDataTable(sql);
                var results = new List<PowerEndpoint>();
                foreach (DataRow row in dataTable.Rows)
                {
                    var deviceId = row["device_id"]?.ToString() ?? string.Empty;
                    var channelId = Convert.ToInt32(row["channel_id"]);
                    var effectiveVersion = row["mapping_version"]?.ToString() ?? string.Empty;
                    results.Add(new PowerEndpoint(deviceId, channelId, effectiveVersion));
                }
                Log.Info($"[SlotDeviceMappingResolver] 按能力类型 {capability} 筛选映射记录,共 {results.Count} 条");
                return results;
            }
            catch (Exception ex)
            {
                Log.Error($"[SlotDeviceMappingResolver] 按能力类型筛选映射记录失败: {ex.Message}", ex);
                throw;
            }
            finally
            {
                sqliteHelper?.Close();
            }
        }
    }
}
CanSimulator/CanBusSimulator.cs
New file
@@ -0,0 +1,403 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using log4net;
namespace CanSimulator
{
    /// <summary>
    /// CAN 总线状态
    /// </summary>
    public enum CanBusState
    {
        /// <summary>停止状态</summary>
        Stopped,
        /// <summary>运行中</summary>
        Running,
        /// <summary>暂停</summary>
        Paused,
        /// <summary>错误</summary>
        Error
    }
    /// <summary>
    /// CAN 总线模拟器事件参数
    /// </summary>
    public sealed class CanBusFrameEventArgs : EventArgs
    {
        public CanFrame Frame { get; }
        public string SourceNodeId { get; }
        public CanBusFrameEventArgs(CanFrame frame, string sourceNodeId)
        {
            Frame = frame;
            SourceNodeId = sourceNodeId;
        }
    }
    /// <summary>
    /// CAN 总线模拟器
    /// 模拟一条虚拟 CAN 总线,多个虚拟节点可以挂载到总线上进行通讯
    /// </summary>
    public sealed class CanBusSimulator : IDisposable
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(CanBusSimulator));
        /// <summary>
        /// 总线名称
        /// </summary>
        public string BusName { get; }
        /// <summary>
        /// 总线状态
        /// </summary>
        public CanBusState State { get; private set; } = CanBusState.Stopped;
        /// <summary>
        /// 波特率(仅用于模拟显示,不影响实际逻辑)
        /// </summary>
        public int BaudRate { get; set; } = 500000; // 默认 500kbps
        /// <summary>
        /// 模拟延迟(毫秒)- 模拟真实总线传输延迟
        /// </summary>
        public int SimulatedLatencyMs { get; set; } = 0;
        /// <summary>
        /// 已注册的节点
        /// </summary>
        private readonly ConcurrentDictionary<string, CanNode> _nodes = new();
        /// <summary>
        /// 总线日志(最近 N 条帧)
        /// </summary>
        private readonly ConcurrentQueue<CanFrame> _busLog = new();
        private int _maxLogSize = 5000;
        /// <summary>
        /// 总线统计
        /// </summary>
        public long TotalFramesTransmitted { get; private set; }
        public long TotalFramesReceived { get; private set; }
        public DateTime? LastActivityTime { get; private set; }
        /// <summary>
        /// 帧在总线上传输事件(可用于监控/日志)
        /// </summary>
        public event EventHandler<CanBusFrameEventArgs>? FrameTransmitted;
        /// <summary>
        /// 总线状态变化事件
        /// </summary>
        public event EventHandler<CanBusState>? StateChanged;
        public CanBusSimulator(string busName = "CAN_BUS_1")
        {
            BusName = busName ?? throw new ArgumentNullException(nameof(busName));
        }
        /// <summary>
        /// 启动总线
        /// </summary>
        public void Start()
        {
            if (State == CanBusState.Running)
            {
                Log.Warn($"[CanBus] 总线 {BusName} 已在运行中");
                return;
            }
            State = CanBusState.Running;
            Log.Info($"[CanBus] 总线 {BusName} 已启动 (波特率: {BaudRate}bps)");
            StateChanged?.Invoke(this, State);
        }
        /// <summary>
        /// 停止总线
        /// </summary>
        public void Stop()
        {
            if (State == CanBusState.Stopped) return;
            State = CanBusState.Stopped;
            Log.Info($"[CanBus] 总线 {BusName} 已停止");
            StateChanged?.Invoke(this, State);
        }
        /// <summary>
        /// 暂停总线(帧仍在保留但不路由)
        /// </summary>
        public void Pause()
        {
            if (State != CanBusState.Running) return;
            State = CanBusState.Paused;
            Log.Info($"[CanBus] 总线 {BusName} 已暂停");
            StateChanged?.Invoke(this, State);
        }
        /// <summary>
        /// 恢复总线
        /// </summary>
        public void Resume()
        {
            if (State != CanBusState.Paused) return;
            State = CanBusState.Running;
            Log.Info($"[CanBus] 总线 {BusName} 已恢复");
            StateChanged?.Invoke(this, State);
        }
        /// <summary>
        /// 注册节点到总线
        /// </summary>
        public CanNode RegisterNode(string nodeId, string? name = null)
        {
            if (string.IsNullOrWhiteSpace(nodeId))
                throw new ArgumentException("节点 ID 不能为空", nameof(nodeId));
            var node = new CanNode(nodeId, name);
            node.FrameSent += OnNodeFrameSent;
            if (!_nodes.TryAdd(nodeId, node))
            {
                Log.Warn($"[CanBus] 节点 {nodeId} 已存在,将被替换");
                _nodes.TryRemove(nodeId, out var oldNode);
                oldNode?.Deactivate();
                _nodes.TryAdd(nodeId, node);
            }
            // 如果总线已运行,自动激活节点
            if (State == CanBusState.Running)
            {
                node.Activate();
            }
            Log.Info($"[CanBus] 节点 {nodeId} ({name ?? nodeId}) 已注册到总线 {BusName}");
            return node;
        }
        /// <summary>
        /// 注销节点
        /// </summary>
        public bool UnregisterNode(string nodeId)
        {
            if (_nodes.TryRemove(nodeId, out var node))
            {
                node.FrameSent -= OnNodeFrameSent;
                node.Deactivate();
                Log.Info($"[CanBus] 节点 {nodeId} 已从总线 {BusName} 注销");
                return true;
            }
            return false;
        }
        /// <summary>
        /// 获取节点
        /// </summary>
        public CanNode? GetNode(string nodeId)
        {
            _nodes.TryGetValue(nodeId, out var node);
            return node;
        }
        /// <summary>
        /// 获取所有已注册节点
        /// </summary>
        public IReadOnlyList<CanNode> GetAllNodes()
        {
            return _nodes.Values.ToList();
        }
        /// <summary>
        /// 向总线发送帧(由节点发送,总线负责路由)
        /// </summary>
        private void OnNodeFrameSent(object? sender, CanFrame frame)
        {
            if (State != CanBusState.Running) return;
            // 模拟延迟
            if (SimulatedLatencyMs > 0)
            {
                Thread.Sleep(SimulatedLatencyMs);
            }
            // 记录到总线日志
            while (_busLog.Count >= _maxLogSize)
            {
                _busLog.TryDequeue(out _);
            }
            _busLog.Enqueue(frame);
            TotalFramesTransmitted++;
            LastActivityTime = DateTime.Now;
            // 路由到所有其他节点
            int routed = 0;
            foreach (var kvp in _nodes)
            {
                if (kvp.Key == frame.SourceNodeId) continue; // 不路由回源节点
                try
                {
                    kvp.Value.ReceiveFrame(frame);
                    routed++;
                    TotalFramesReceived++;
                }
                catch (Exception ex)
                {
                    Log.Error($"[CanBus] 路由帧到节点 {kvp.Key} 失败: {ex.Message}");
                }
            }
            // 检查源节点是否启用自动响应模式
            if (_nodes.TryGetValue(frame.SourceNodeId, out var sourceNode) && sourceNode.AutoResponseEnabled)
            {
                try
                {
                    // 模拟响应延迟
                    if (sourceNode.AutoResponseDelayMs > 0)
                    {
                        Thread.Sleep(sourceNode.AutoResponseDelayMs);
                    }
                    // 创建响应帧(复制原始帧数据,来源为源节点)
                    var responseFrame = new CanFrame
                    {
                        FrameId = frame.FrameId,
                        Format = frame.Format,
                        Type = frame.Type,
                        Data = (byte[])frame.Data.Clone(),
                        SourceNodeId = sourceNode.NodeId,
                        Timestamp = DateTime.Now
                    };
                    // 将响应帧路由到所有其他节点(不包括响应源自身)
                    foreach (var kvp in _nodes)
                    {
                        if (kvp.Key == sourceNode.NodeId) continue;
                        try
                        {
                            kvp.Value.ReceiveFrame(responseFrame);
                            TotalFramesReceived++;
                        }
                        catch (Exception ex)
                        {
                            Log.Error($"[CanBus] 路由响应帧到节点 {kvp.Key} 失败: {ex.Message}");
                        }
                    }
                    // 将响应帧送入源节点的接收邮箱(让源节点能看到自己的响应)
                    sourceNode.ReceiveFrame(responseFrame);
                    TotalFramesReceived++;
                    // 记录响应帧到总线日志
                    _busLog.Enqueue(responseFrame);
                    TotalFramesTransmitted++;
                    Log.Debug($"[CanBus] 节点 {sourceNode.NodeId} 自动响应帧 {responseFrame.FrameIdHex}");
                }
                catch (Exception ex)
                {
                    Log.Error($"[CanBus] 源节点自动响应失败: {ex.Message}");
                }
            }
            Log.Debug($"[CanBus] 帧 {frame.FrameIdHex} 从 {frame.SourceNodeId} 路由到 {routed} 个节点");
            FrameTransmitted?.Invoke(this, new CanBusFrameEventArgs(frame, frame.SourceNodeId));
        }
        /// <summary>
        /// 直接注入帧到总线(用于模拟外部设备发送)
        /// </summary>
        public void InjectFrame(CanFrame frame)
        {
            if (State != CanBusState.Running)
            {
                Log.Warn($"[CanBus] 总线未运行,无法注入帧");
                return;
            }
            frame.Timestamp = DateTime.Now;
            // 记录到日志
            while (_busLog.Count >= _maxLogSize)
            {
                _busLog.TryDequeue(out _);
            }
            _busLog.Enqueue(frame);
            TotalFramesTransmitted++;
            LastActivityTime = DateTime.Now;
            // 路由到所有节点
            foreach (var kvp in _nodes)
            {
                try
                {
                    kvp.Value.ReceiveFrame(frame);
                    TotalFramesReceived++;
                }
                catch (Exception ex)
                {
                    Log.Error($"[CanBus] 注入帧路由到节点 {kvp.Key} 失败: {ex.Message}");
                }
            }
            FrameTransmitted?.Invoke(this, new CanBusFrameEventArgs(frame, frame.SourceNodeId));
            Log.Debug($"[CanBus] 注入帧 {frame.FrameIdHex} 到总线");
        }
        /// <summary>
        /// 获取总线日志
        /// </summary>
        public List<CanFrame> GetBusLog(int maxCount = 100)
        {
            var all = _busLog.ToList();
            if (all.Count <= maxCount) return all;
            return all.Skip(all.Count - maxCount).ToList();
        }
        /// <summary>
        /// 清空总线日志
        /// </summary>
        public void ClearBusLog()
        {
            while (_busLog.TryDequeue(out _)) { }
        }
        /// <summary>
        /// 重置所有统计
        /// </summary>
        public void ResetStatistics()
        {
            TotalFramesTransmitted = 0;
            TotalFramesReceived = 0;
            LastActivityTime = null;
            foreach (var node in _nodes.Values)
            {
                node.ResetCounters();
            }
        }
        /// <summary>
        /// 获取总线状态摘要
        /// </summary>
        public string GetStatusSummary()
        {
            return $"Bus={BusName}, State={State}, BaudRate={BaudRate}, Nodes={_nodes.Count}, " +
                   $"Tx={TotalFramesTransmitted}, Rx={TotalFramesReceived}, " +
                   $"LastActivity={LastActivityTime?.ToString("HH:mm:ss") ?? "N/A"}";
        }
        public void Dispose()
        {
            Stop();
            foreach (var node in _nodes.Values)
            {
                node.Deactivate();
            }
            _nodes.Clear();
        }
    }
}
CanSimulator/CanFrame.cs
New file
@@ -0,0 +1,130 @@
using System;
namespace CanSimulator
{
    /// <summary>
    /// CAN 帧格式类型
    /// </summary>
    public enum CanFrameFormat
    {
        /// <summary>标准帧 (11-bit ID)</summary>
        Standard,
        /// <summary>扩展帧 (29-bit ID)</summary>
        Extended
    }
    /// <summary>
    /// CAN 帧类型
    /// </summary>
    public enum CanFrameType
    {
        /// <summary>数据帧</summary>
        Data,
        /// <summary>远程帧</summary>
        Remote
    }
    /// <summary>
    /// CAN 总线帧数据结构
    /// </summary>
    public sealed class CanFrame
    {
        /// <summary>
        /// 帧 ID(标准帧 11-bit,扩展帧 29-bit)
        /// </summary>
        public uint FrameId { get; set; }
        /// <summary>
        /// 帧格式(标准/扩展)
        /// </summary>
        public CanFrameFormat Format { get; set; } = CanFrameFormat.Standard;
        /// <summary>
        /// 帧类型(数据帧/远程帧)
        /// </summary>
        public CanFrameType Type { get; set; } = CanFrameType.Data;
        /// <summary>
        /// 数据负载(最大 8 字节,CAN FD 可达 64 字节)
        /// </summary>
        public byte[] Data { get; set; } = Array.Empty<byte>();
        /// <summary>
        /// 时间戳
        /// </summary>
        public DateTime Timestamp { get; set; } = DateTime.Now;
        /// <summary>
        /// 发送节点标识
        /// </summary>
        public string SourceNodeId { get; set; } = string.Empty;
        /// <summary>
        /// DLC (Data Length Code)
        /// </summary>
        public int Dlc => Data.Length;
        /// <summary>
        /// 将帧 ID 格式化为十六进制字符串
        /// </summary>
        public string FrameIdHex => Format == CanFrameFormat.Extended
            ? $"0x{FrameId:X8}"
            : $"0x{FrameId:X3}";
        /// <summary>
        /// 将数据格式化为十六进制字符串
        /// </summary>
        public string DataHex => BitConverter.ToString(Data).Replace("-", " ");
        /// <summary>
        /// 从十六进制字符串解析帧 ID
        /// </summary>
        public static uint ParseFrameId(string frameIdStr)
        {
            if (string.IsNullOrWhiteSpace(frameIdStr))
                throw new ArgumentException("帧 ID 不能为空", nameof(frameIdStr));
            if (frameIdStr.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
            {
                if (!uint.TryParse(frameIdStr.Substring(2), System.Globalization.NumberStyles.HexNumber, null, out uint id))
                    throw new ArgumentException($"无法解析帧 ID: {frameIdStr}");
                return id;
            }
            else
            {
                if (!uint.TryParse(frameIdStr, out uint id))
                    throw new ArgumentException($"无法解析帧 ID: {frameIdStr}");
                return id;
            }
        }
        /// <summary>
        /// 从十六进制字符串解析数据
        /// </summary>
        public static byte[] ParseData(string dataStr)
        {
            if (string.IsNullOrWhiteSpace(dataStr))
                return Array.Empty<byte>();
            try
            {
                var parts = dataStr.Split(new[] { ' ', '-', ',' }, StringSplitOptions.RemoveEmptyEntries);
                var bytes = new byte[parts.Length];
                for (int i = 0; i < parts.Length; i++)
                {
                    bytes[i] = byte.Parse(parts[i], System.Globalization.NumberStyles.HexNumber);
                }
                return bytes;
            }
            catch (Exception ex)
            {
                throw new ArgumentException($"无法解析数据字符串: {dataStr}, 错误: {ex.Message}");
            }
        }
        public override string ToString()
        {
            return $"[{Timestamp:HH:mm:ss.fff}] {FrameIdHex} [{Dlc}] {DataHex} (from {SourceNodeId})";
        }
    }
}
CanSimulator/CanNode.cs
New file
@@ -0,0 +1,311 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using log4net;
namespace CanSimulator
{
    /// <summary>
    /// 接收滤波器配置
    /// </summary>
    public sealed class AcceptanceFilter
    {
        /// <summary>
        /// 滤波器 ID(匹配值)
        /// </summary>
        public uint FilterId { get; set; }
        /// <summary>
        /// 掩码(为 1 的位参与比较)
        /// </summary>
        public uint Mask { get; set; } = 0x7FF; // 默认标准帧全匹配
        /// <summary>
        /// 是否为扩展帧滤波器
        /// </summary>
        public bool IsExtended { get; set; }
        /// <summary>
        /// 检查给定帧 ID 是否通过此滤波器
        /// </summary>
        public bool Accepts(uint frameId, bool isExtended)
        {
            if (IsExtended != isExtended) return false;
            return (frameId & Mask) == (FilterId & Mask);
        }
    }
    /// <summary>
    /// 虚拟 CAN 节点
    /// 模拟一个 CAN 通讯卡的端点,具有发送和接收功能
    /// </summary>
    public sealed class CanNode
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(CanNode));
        /// <summary>
        /// 节点唯一标识
        /// </summary>
        public string NodeId { get; }
        /// <summary>
        /// 节点名称(可选描述)
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 节点是否已激活(连接到总线)
        /// </summary>
        public bool IsActive { get; private set; }
        /// <summary>
        /// 接收滤波器列表
        /// </summary>
        private readonly List<AcceptanceFilter> _filters = new();
        /// <summary>
        /// 接收邮箱(FIFO 队列)
        /// </summary>
        private readonly ConcurrentQueue<CanFrame> _receiveMailbox = new();
        /// <summary>
        /// 发送历史记录
        /// </summary>
        private readonly ConcurrentQueue<CanFrame> _sendHistory = new();
        /// <summary>
        /// 最大邮箱容量
        /// </summary>
        public int MaxMailboxSize { get; set; } = 1000;
        /// <summary>
        /// 最大历史记录容量
        /// </summary>
        public int MaxHistorySize { get; set; } = 500;
        /// <summary>
        /// 接收帧计数器
        /// </summary>
        public long RxCount { get; private set; }
        /// <summary>
        /// 发送帧计数器
        /// </summary>
        public long TxCount { get; private set; }
        /// <summary>
        /// 错误计数器
        /// </summary>
        public long ErrorCount { get; private set; }
        /// <summary>
        /// 是否启用自动响应模式(收到帧后原封不动回复)
        /// </summary>
        public bool AutoResponseEnabled { get; set; }
        /// <summary>
        /// 自动响应延迟(毫秒)- 模拟真实设备响应时间
        /// </summary>
        public int AutoResponseDelayMs { get; set; } = 0;
        /// <summary>
        /// 自动响应事件(用于日志/监控)
        /// </summary>
        public event EventHandler<CanFrame>? AutoResponseSent;
        /// <summary>
        /// 帧接收事件
        /// </summary>
        public event EventHandler<CanFrame>? FrameReceived;
        /// <summary>
        /// 帧发送事件
        /// </summary>
        public event EventHandler<CanFrame>? FrameSent;
        public CanNode(string nodeId, string? name = null)
        {
            NodeId = nodeId ?? throw new ArgumentNullException(nameof(nodeId));
            Name = name ?? nodeId;
        }
        /// <summary>
        /// 激活节点(连接到总线)
        /// </summary>
        public void Activate()
        {
            IsActive = true;
            Log.Info($"[CanNode] 节点 {NodeId} ({Name}) 已激活");
        }
        /// <summary>
        /// 停用节点(从总线断开)
        /// </summary>
        public void Deactivate()
        {
            IsActive = false;
            Log.Info($"[CanNode] 节点 {NodeId} ({Name}) 已停用");
        }
        /// <summary>
        /// 添加接收滤波器
        /// </summary>
        public void AddFilter(AcceptanceFilter filter)
        {
            lock (_filters)
            {
                _filters.Add(filter);
            }
            Log.Debug($"[CanNode] 节点 {NodeId} 添加滤波器: ID=0x{filter.FilterId:X3}, Mask=0x{filter.Mask:X3}");
        }
        /// <summary>
        /// 清除所有滤波器
        /// </summary>
        public void ClearFilters()
        {
            lock (_filters)
            {
                _filters.Clear();
            }
            Log.Debug($"[CanNode] 节点 {NodeId} 清除所有滤波器");
        }
        /// <summary>
        /// 设置通配符滤波器(接收所有帧)
        /// </summary>
        public void SetPromiscuousMode()
        {
            lock (_filters)
            {
                _filters.Clear();
                _filters.Add(new AcceptanceFilter { FilterId = 0, Mask = 0, IsExtended = false });
                _filters.Add(new AcceptanceFilter { FilterId = 0, Mask = 0, IsExtended = true });
            }
            Log.Debug($"[CanNode] 节点 {NodeId} 设置为混杂模式(接收所有帧)");
        }
        /// <summary>
        /// 检查帧是否通过滤波器
        /// </summary>
        internal bool AcceptsFrame(CanFrame frame)
        {
            lock (_filters)
            {
                // 如果没有设置滤波器,默认拒绝
                if (_filters.Count == 0) return false;
                // 任一滤波器匹配即接受
                return _filters.Any(f => f.Accepts(frame.FrameId, frame.Format == CanFrameFormat.Extended));
            }
        }
        /// <summary>
        /// 接收帧(由总线调用)
        /// </summary>
        internal void ReceiveFrame(CanFrame frame)
        {
            if (!IsActive) return;
            if (!AcceptsFrame(frame)) return;
            // 入队接收邮箱
            while (_receiveMailbox.Count >= MaxMailboxSize)
            {
                _receiveMailbox.TryDequeue(out _); // 丢弃最旧的帧
            }
            _receiveMailbox.Enqueue(frame);
            RxCount++;
            // 触发事件
            FrameReceived?.Invoke(this, frame);
            Log.Debug($"[CanNode] 节点 {NodeId} 接收帧: {frame}");
        }
        /// <summary>
        /// 发送帧
        /// </summary>
        public void SendFrame(CanFrame frame)
        {
            if (!IsActive)
            {
                ErrorCount++;
                throw new InvalidOperationException($"节点 {NodeId} 未激活,无法发送帧");
            }
            frame.SourceNodeId = NodeId;
            frame.Timestamp = DateTime.Now;
            // 记录发送历史
            while (_sendHistory.Count >= MaxHistorySize)
            {
                _sendHistory.TryDequeue(out _);
            }
            _sendHistory.Enqueue(frame);
            TxCount++;
            // 触发事件(由总线处理路由)
            FrameSent?.Invoke(this, frame);
            Log.Debug($"[CanNode] 节点 {NodeId} 发送帧: {frame}");
        }
        /// <summary>
        /// 从接收邮箱中取出一帧
        /// </summary>
        public bool TryDequeueReceived(out CanFrame? frame)
        {
            return _receiveMailbox.TryDequeue(out frame);
        }
        /// <summary>
        /// 查看接收邮箱中的所有帧(不移除)
        /// </summary>
        public List<CanFrame> PeekReceiveMailbox()
        {
            return _receiveMailbox.ToList();
        }
        /// <summary>
        /// 清空接收邮箱
        /// </summary>
        public void ClearReceiveMailbox()
        {
            while (_receiveMailbox.TryDequeue(out _)) { }
            Log.Debug($"[CanNode] 节点 {NodeId} 清空接收邮箱");
        }
        /// <summary>
        /// 获取发送历史
        /// </summary>
        public List<CanFrame> GetSendHistory()
        {
            return _sendHistory.ToList();
        }
        /// <summary>
        /// 清空发送历史
        /// </summary>
        public void ClearSendHistory()
        {
            while (_sendHistory.TryDequeue(out _)) { }
        }
        /// <summary>
        /// 重置节点统计
        /// </summary>
        public void ResetCounters()
        {
            RxCount = 0;
            TxCount = 0;
            ErrorCount = 0;
        }
        /// <summary>
        /// 获取节点状态摘要
        /// </summary>
        public string GetStatusSummary()
        {
            return $"Node={NodeId}, Active={IsActive}, Rx={RxCount}, Tx={TxCount}, Err={ErrorCount}, Mailbox={_receiveMailbox.Count}";
        }
    }
}
CanSimulator/CanSimulator.csproj
New file
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Library</OutputType>
    <TargetFramework>net472</TargetFramework>
    <Nullable>enable</Nullable>
    <LangVersion>10.0</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="log4net" Version="3.0.4" />
  </ItemGroup>
</Project>
CellStepSender/CellStepSender.csproj
New file
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{84EDEAC7-6763-46CD-B098-0B508AAA705F}</ProjectGuid>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>CellStepSender</RootNamespace>
    <AssemblyName>CellStepSender</AssemblyName>
    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Data" />
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="StepSender.cs" />
    <Compile Include="TcpClientTool.cs" />
    <Compile Include="TcpClientTool3.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
CellStepSender/Properties/AssemblyInfo.cs
New file
@@ -0,0 +1,33 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// 有关程序集的一般信息由以下
// 控制。更改这些特性值可修改
// 与程序集关联的信息。
[assembly: AssemblyTitle("CellStepSender")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("CellStepSender")]
[assembly: AssemblyCopyright("Copyright ©  2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 将 ComVisible 设置为 false 会使此程序集中的类型
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
//请将此类型的 ComVisible 特性设置为 true。
[assembly: ComVisible(false)]
// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID
[assembly: Guid("84edeac7-6763-46cd-b098-0b508aaa705f")]
// 程序集的版本信息由下列四个值组成:
//
//      主版本
//      次版本
//      生成号
//      修订号
//
[assembly: AssemblyVersion("1.0.0.1")]
[assembly: AssemblyFileVersion("1.0.0.1")]
CellStepSender/StepSender.cs
New file
@@ -0,0 +1,69 @@
using System;
using System.Threading;
namespace CellStepSender
{
    internal class StepSender
    {
        /// <summary>
        /// 发送测试数据
        /// </summary>
        /// <param name="ip">IP</param>
        /// <param name="port">端口</param>
        /// <param name="ovenIndex">炉下标</param>
        /// <param name="cellIndex">穴位下标</param>
        /// <param name="step">测试项</param>
        /// <param name="value">测试值</param>
        /// <param name="limitUp">上限</param>
        /// <param name="limitDown">下限</param>
        /// <param name="unit">单位</param>
        /// <param name="result">测试结果</param>
        /// <param name="updateTime">测试时间</param>
        /// <param name="errorMsg">异常信息</param>
        /// <returns>是否发送成功</returns>
        public static bool SendStep(
            string ip, int port,
            int ovenIndex, int cellIndex, string step, string value, string limitUp,
            string limitDown, string unit, string result, string updateTime, out string errorMsg)
        {
            errorMsg = null;
            TcpClientTool3 client = null;
            try
            {
                client = new TcpClientTool3(ip, port);
                string res = null;
                client.ExecuteMessageChanged += (sender, data) =>
                {
                    res = data;
                };
                client.Connect();
                string msg = $"{ovenIndex};{cellIndex};{step},{value},{limitUp},{limitDown},{unit},{result},{updateTime}\n";
                client.Send(msg);
                int time = 1;
                while (res == null && time <= 50)
                {
                    Thread.Sleep(100);
                    time++;
                }
                if (res == null)
                {
                    errorMsg = "Time Out";
                }
                return res == "1";
            }
            catch (Exception ex)
            {
                errorMsg = ex.Message;
                return false;
            }
            finally
            {
                client?.Disconnect();
            }
        }
    }
}
CellStepSender/TcpClientTool.cs
New file
@@ -0,0 +1,104 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace CellStepSender
{
    public class TcpClientTool
    {
        private string _ip;
        private int _port;
        private bool _isConnected = false; // 是否连接
        private Socket _clientSocket; // 套接字
        private Thread _receiveThread = null; // 接收线程
        private volatile bool needRecieve = true;
        public event EventHandler<string> ExecuteMessageChanged;
        public TcpClientTool(string ip, int port)
        {
            this._ip = ip;
            this._port = port;
        }
        public void Connect()
        {
            try
            {
                _clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                IPAddress ipaddress = IPAddress.Parse(this._ip);
                IPEndPoint endpoint = new IPEndPoint(ipaddress, _port);
                _clientSocket.Connect(endpoint);
                _isConnected = true;
                _receiveThread = new Thread(Received);
                _receiveThread.IsBackground = true;
                _receiveThread.Start();
            }
            catch (Exception ex)
            {
                needRecieve = false;
                _receiveThread?.Abort();
                _receiveThread = null;
                _clientSocket.Close();
                _isConnected = false;
                throw ex;
            }
        }
        public void Disconnect()
        {
            needRecieve = false;
            _clientSocket.Close();
            _receiveThread?.Abort();
            _receiveThread = null;
            _isConnected = false;
        }
        public void Send(string data)
        {
            byte[] arrClientSendMsg = Encoding.UTF8.GetBytes(data);
            _clientSocket.Send(arrClientSendMsg);
        }
        public void Received()
        {
            //while (needRecieve)
            //{
            //    byte[] arrRecMsg = new byte[1024 * 1024];
            //    int length = _clientSocket.Receive(arrRecMsg);
            //    string strRecMsg = Encoding.UTF8.GetString(arrRecMsg, 0, length);
            //    ExecuteMessageChanged?.Invoke(this, strRecMsg);
            //    Thread.Sleep(100);
            //}
        }
        public bool SendStep(
            string ip, int port,
            int ovenIndex, int cellIndex, string step, string value, string limitUp,
            string limitDown, string unit, string result, string updateTime, out string errorMsg)
        {
            errorMsg = null;
            try
            {
                if (!_isConnected)
                {
                    errorMsg = "Not Connected";
                    return false;
                }
                string msg = $"{ovenIndex};{cellIndex};{step},{value},{limitUp},{limitDown},{unit},{result},{updateTime}\n";
                Send(msg);
                return true;
            }
            catch (Exception ex)
            {
                errorMsg = ex.Message;
                return false;
            }
        }
    }
}
CellStepSender/TcpClientTool3.cs
New file
@@ -0,0 +1,81 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace CellStepSender
{
    public class TcpClientTool3
    {
        private string _ip;
        private int _port;
        private bool _isConnected = false; // 是否连接
        private Socket _clientSocket; // 套接字
        public event EventHandler<string> ExecuteMessageChanged;
        public TcpClientTool3(string ip, int port)
        {
            this._ip = ip;
            this._port = port;
        }
        public void Connect()
        {
            try
            {
                _clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                IPAddress ipaddress = IPAddress.Parse(this._ip);
                IPEndPoint endpoint = new IPEndPoint(ipaddress, _port);
                _clientSocket.Connect(endpoint);
                _isConnected = true;
            }
            catch (Exception ex)
            {
                _clientSocket.Dispose();
                _isConnected = false;
                throw ex;
            }
        }
        public void Disconnect()
        {
            _clientSocket.Dispose();
        }
        public void Send(string data)
        {
            byte[] arrClientSendMsg = Encoding.UTF8.GetBytes(data);
            lock (this)
            {
                _clientSocket.Send(arrClientSendMsg);
            }
        }
        public bool SendStep(
            string ip, int port,
            int ovenIndex, int cellIndex, string step, string value, string limitUp,
            string limitDown, string unit, string result, string updateTime, out string errorMsg)
        {
            errorMsg = null;
            try
            {
                if (!_isConnected)
                {
                    errorMsg = "Not Connected";
                    return false;
                }
                string msg = $"{ovenIndex};{cellIndex};{step},{value},{limitUp},{limitDown},{unit},{result},{updateTime}\n";
                Send(msg);
                return true;
            }
            catch (Exception ex)
            {
                errorMsg = ex.Message;
                return false;
            }
        }
    }
}
Config/Database/database.db
Config/LogViewer/log_2026-10-23.db
Binary files differ
ConnectionUtil/ConnectionUtil.csproj
New file
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{D0806FA6-3F6D-4304-81B8-EAD9DA7CB5B2}</ProjectGuid>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>ConnectionUtil</RootNamespace>
    <AssemblyName>ConnectionUtil</AssemblyName>
    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
    <DebugSymbols>true</DebugSymbols>
    <OutputPath>bin\x64\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <DebugType>full</DebugType>
    <PlatformTarget>x64</PlatformTarget>
    <LangVersion>7.3</LangVersion>
    <ErrorReport>prompt</ErrorReport>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
    <OutputPath>bin\x64\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <Optimize>true</Optimize>
    <DebugType>pdbonly</DebugType>
    <PlatformTarget>x64</PlatformTarget>
    <LangVersion>7.3</LangVersion>
    <ErrorReport>prompt</ErrorReport>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Data" />
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="PipeClient.cs" />
    <Compile Include="PipeClient2.cs" />
    <Compile Include="PipeServer.cs" />
    <Compile Include="PipeServer2.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
    <Compile Include="SCADAApi.cs" />
    <Compile Include="TcpClient.cs" />
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
ConnectionUtil/PipeClient.cs
New file
@@ -0,0 +1,100 @@
using System;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;
namespace ConnectionUtil
{
    public class PipeClient : IDisposable
    {
        private readonly string _pipeName;
        private readonly string _serverName;
        private readonly CancellationTokenSource _cancellationTokenSource;
        private NamedPipeClientStream _pipeClient;
        private bool _isDisposed;
        private bool _isConnected;
        private readonly object _sendLock = new object();
        // 事件定义
        public event EventHandler<PipeDataEventArgs> OnData;
        public event EventHandler OnConnected;
        public event EventHandler OnDisconnected;
        public PipeClient(string pipeName, string serverName = ".")
        {
            _pipeName = pipeName;
            _serverName = serverName;
            _cancellationTokenSource = new CancellationTokenSource();
        }
        public void Connect(int timeoutMs = 5000)
        {
            if (_isConnected)
                return;
            _pipeClient = new NamedPipeClientStream(_pipeName);
            _pipeClient.Connect(timeoutMs);
            _pipeClient.ReadMode = PipeTransmissionMode.Message;
            _isConnected = true;
            Console.WriteLine("连接成功!");
            // 触发连接成功事件
            OnConnected?.Invoke(this, EventArgs.Empty);
        }
        public bool SendMessage(string message, out string response, int timeoutMs = 5000)
        {
            if (!_isConnected)
            {
                response = "客户端未连接到服务器";
                return false;
            }
            response = null;
            lock (_sendLock)
            {
                try
                {
                    // 发送消息
                    _pipeClient.WriteMessage(message);
                    return true;
                    while (_isConnected && !_cancellationTokenSource.Token.IsCancellationRequested)
                    {
                        try
                        {
                            response = _pipeClient.ReadMessage();
                            break;
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine($"接收数据时发生错误: {ex.Message}");
                            break;
                        }
                    }
                    return true;
                }
                catch (Exception ex)
                {
                    return false;
                }
            }
        }
        public void Disconnect()
        {
            _isConnected = false;
            _cancellationTokenSource.Cancel();
        }
        public void Dispose()
        {
            if (!_isDisposed)
            {
                Disconnect();
                _pipeClient?.Dispose();
                _cancellationTokenSource.Dispose();
                _isDisposed = true;
            }
        }
    }
}
ConnectionUtil/PipeClient2.cs
New file
@@ -0,0 +1,130 @@
using System;
using System.IO;
using System.IO.Pipes;
using System.Threading;
namespace ConnectionUtil
{
    public class PipeClient2 : IDisposable
    {
        private readonly string _pipeName;
        private readonly string _serverName;
        private readonly CancellationTokenSource _cancellationTokenSource;
        private readonly ReaderWriterLockSlim _lock;
        private NamedPipeClientStream _pipeClient;
        private bool _isDisposed;
        private bool _isConnected;
        private Thread _receiveThread;
        // 事件定义
        public event EventHandler<PipeDataEventArgs> OnData;
        public event EventHandler OnConnected;
        public event EventHandler OnDisconnected;
        public PipeClient2(string pipeName, string serverName = ".")
        {
            _pipeName = pipeName;
            _serverName = serverName;
            _cancellationTokenSource = new CancellationTokenSource();
            _lock = new ReaderWriterLockSlim();
        }
        public void Connect(int timeoutMs = 5000)
        {
            if (_isConnected)
                return;
            try
            {
                // 加上 PipeOptions.Asynchronous 可以异步接受服务器发送的数据,但是很影响性能
                _pipeClient = new NamedPipeClientStream(_serverName, _pipeName, PipeDirection.InOut);
                // 创建超时取消令牌
                using (var timeoutCts = new CancellationTokenSource(timeoutMs))
                {
                    _pipeClient.Connect(timeoutMs);
                    _pipeClient.ReadMode = PipeTransmissionMode.Message;
                    _isConnected = true;
                    // 触发连接成功事件
                    OnConnected?.Invoke(this, EventArgs.Empty);
                    // 开始接收数据
                    //_receiveThread = new Thread(ReceiveData);
                    //_receiveThread.Start();
                }
            }
            catch (Exception ex)
            {
                throw;
            }
        }
        private void ReceiveData()
        {
            try
            {
                while (_isConnected && !_cancellationTokenSource.Token.IsCancellationRequested)
                {
                    string message = _pipeClient.ReadMessage();
                    if (string.IsNullOrEmpty(message))
                        break;
                    // 触发数据接收事件
                    OnData?.Invoke(this, new PipeDataEventArgs("", message));
                }
            }
            finally
            {
                _isConnected = false;
                // 触发断开连接事件
                OnDisconnected?.Invoke(this, EventArgs.Empty);
            }
        }
        public bool SendMessage(string message, out string response, int timeoutMs = 5000)
        {
            response = null;
            if (!_isConnected)
                return false;
            try
            {
                _lock.EnterWriteLock();
                try
                {
                    _pipeClient.WriteMessage(message);
                }
                finally
                {
                    _lock.ExitWriteLock();
                }
                response = _pipeClient.ReadMessage();
                return true;
            }
            catch (Exception ex)
            {
                response = ex.Message;
                return false;
            }
        }
        public void Disconnect()
        {
            _isConnected = false;
            _cancellationTokenSource.Cancel();
            _receiveThread?.Join(1000); // 等待接收线程结束
        }
        public void Dispose()
        {
            if (!_isDisposed)
            {
                Disconnect();
                _pipeClient?.Dispose();
                _cancellationTokenSource.Dispose();
                _lock.Dispose();
                _isDisposed = true;
            }
        }
    }
}
ConnectionUtil/PipeServer.cs
New file
@@ -0,0 +1,347 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ConnectionUtil
{
    public class PipeServer : IDisposable
    {
        private readonly string _pipeName;
        private readonly int _maxNumberOfServerInstances;
        private readonly CancellationTokenSource _cancellationTokenSource;
        private readonly ConcurrentDictionary<string, NamedPipeServerStream> _activeConnections;
        private bool _isDisposed;
        // 事件定义
        public event EventHandler<PipeDataEventArgs> OnData;
        public event EventHandler<PipeConnectionEventArgs> OnNewConnection;
        public event EventHandler<PipeConnectionEventArgs> OnClientDisconnected;
        public event EventHandler<PipeErrorEventArgs> OnError;
        public PipeServer(string pipeName, int maxNumberOfServerInstances = NamedPipeServerStream.MaxAllowedServerInstances)
        {
            _pipeName = pipeName;
            _maxNumberOfServerInstances = maxNumberOfServerInstances;
            _cancellationTokenSource = new CancellationTokenSource();
            _activeConnections = new ConcurrentDictionary<string, NamedPipeServerStream>();
        }
        public void Start()
        {
            Task.Run(() =>
            {
                try
                {
                    while (!_cancellationTokenSource.Token.IsCancellationRequested)
                    {
                        var pipeServer = new NamedPipeServerStream(_pipeName, PipeDirection.InOut,
                            _maxNumberOfServerInstances, PipeTransmissionMode.Message);
                        try
                        {
                            pipeServer.WaitForConnection();
                            // 生成唯一的连接ID
                            string connectionId = Guid.NewGuid().ToString();
                            _activeConnections.TryAdd(connectionId, pipeServer);
                            // 触发新连接事件
                            OnNewConnection?.Invoke(this, new PipeConnectionEventArgs(connectionId));
                            // 开始处理该连接的数据
                            HandleClient(connectionId, pipeServer);
                        }
                        catch (OperationCanceledException)
                        {
                            pipeServer.Dispose();
                            break;
                        }
                        catch (Exception ex)
                        {
                            pipeServer.Dispose();
                            OnError?.Invoke(this, new PipeErrorEventArgs("等待连接时发生错误", ex));
                        }
                    }
                }
                catch (Exception ex)
                {
                    OnError?.Invoke(this, new PipeErrorEventArgs("服务器运行时发生错误", ex));
                }
            });
        }
        static string ReadMessage(PipeStream s)
        {
            MemoryStream ms = new MemoryStream();
            byte[] buffer = new byte[0x1000]; // Read in 4 KB blocks
            do
            {
                ms.Write(buffer, 0, s.Read(buffer, 0, buffer.Length));
            }
            while (!s.IsMessageComplete);
            return Encoding.UTF8.GetString(ms.ToArray());
        }
        private void HandleClient(string connectionId, NamedPipeServerStream pipeServer)
        {
            Task.Run(() =>
            {
                try
                {
                    while (pipeServer.IsConnected && !_cancellationTokenSource.Token.IsCancellationRequested)
                    {
                        try
                        {
                            string message = ReadMessage(pipeServer);
                            if (string.IsNullOrEmpty(message))
                                continue;
                            Console.WriteLine(message);
                            if (message == "ping")
                            {
                                message = "pong";
                            }
                            else
                            {
                                message += "Response";
                            }
                            SendMessage(connectionId, message);
                        }
                        catch (Exception ex)
                        {
                            OnError?.Invoke(this, new PipeErrorEventArgs($"处理客户端 {connectionId} 数据时发生错误", ex));
                            break;
                        }
                    }
                }
                finally
                {
                    // 清理连接
                    _activeConnections.TryRemove(connectionId, out _);
                    pipeServer.Dispose();
                    // 触发断开连接事件
                    OnClientDisconnected?.Invoke(this, new PipeConnectionEventArgs(connectionId));
                }
            });
        }
        public void SendMessage(string connectionId, string message)
        {
            if (_activeConnections.TryGetValue(connectionId, out var pipeServer))
            {
                try
                {
                    var msg = Encoding.UTF8.GetBytes(message);
                    pipeServer.Write(msg, 0, msg.Length);
                }
                catch (Exception ex)
                {
                    OnError?.Invoke(this, new PipeErrorEventArgs($"向客户端 {connectionId} 发送消息时发生错误", ex));
                }
            }
        }
        public async Task StartAsync()
        {
            try
            {
                while (!_cancellationTokenSource.Token.IsCancellationRequested)
                {
                    var pipeServer = new NamedPipeServerStream(_pipeName, PipeDirection.InOut,
                        _maxNumberOfServerInstances, PipeTransmissionMode.Message);
                    try
                    {
                        await pipeServer.WaitForConnectionAsync(_cancellationTokenSource.Token);
                        // 生成唯一的连接ID
                        string connectionId = Guid.NewGuid().ToString();
                        _activeConnections.TryAdd(connectionId, pipeServer);
                        // 触发新连接事件
                        OnNewConnection?.Invoke(this, new PipeConnectionEventArgs(connectionId));
                        // 开始处理该连接的数据
                        _ = HandleClientAsync(connectionId, pipeServer);
                    }
                    catch (OperationCanceledException)
                    {
                        pipeServer.Dispose();
                        break;
                    }
                    catch (Exception ex)
                    {
                        pipeServer.Dispose();
                        OnError?.Invoke(this, new PipeErrorEventArgs("等待连接时发生错误", ex));
                    }
                }
            }
            catch (Exception ex)
            {
                OnError?.Invoke(this, new PipeErrorEventArgs("服务器运行时发生错误", ex));
            }
        }
        private async Task HandleClientAsync(string connectionId, NamedPipeServerStream pipeServer)
        {
            try
            {
                using (var reader = new StreamReader(pipeServer))
                using (var writer = new StreamWriter(pipeServer))
                {
                    writer.AutoFlush = true;
                    while (pipeServer.IsConnected && !_cancellationTokenSource.Token.IsCancellationRequested)
                    {
                        try
                        {
                            string message = await reader.ReadLineAsync();
                            if (string.IsNullOrEmpty(message))
                                continue;
                            // 触发数据接收事件
                            OnData?.Invoke(this, new PipeDataEventArgs(connectionId, message));
                            // 发送响应
                            await writer.WriteLineAsync($"服务器已收到消息: {message}");
                        }
                        catch (Exception ex)
                        {
                            OnError?.Invoke(this, new PipeErrorEventArgs($"处理客户端 {connectionId} 数据时发生错误", ex));
                            break;
                        }
                    }
                }
            }
            finally
            {
                // 清理连接
                _activeConnections.TryRemove(connectionId, out _);
                pipeServer.Dispose();
                // 触发断开连接事件
                OnClientDisconnected?.Invoke(this, new PipeConnectionEventArgs(connectionId));
            }
        }
        public async Task SendMessageAsync(string connectionId, string message)
        {
            if (_activeConnections.TryGetValue(connectionId, out var pipeServer))
            {
                try
                {
                    using (var writer = new StreamWriter(pipeServer))
                    {
                        writer.AutoFlush = true;
                        await writer.WriteLineAsync(message);
                    }
                }
                catch (Exception ex)
                {
                    OnError?.Invoke(this, new PipeErrorEventArgs($"向客户端 {connectionId} 发送消息时发生错误", ex));
                }
            }
        }
        public void Stop()
        {
            _cancellationTokenSource.Cancel();
        }
        public void Dispose()
        {
            if (!_isDisposed)
            {
                Stop();
                foreach (var connection in _activeConnections.Values)
                {
                    connection.Dispose();
                }
                _activeConnections.Clear();
                _cancellationTokenSource.Dispose();
                _isDisposed = true;
            }
        }
    }
    public class PipeDataEventArgs : EventArgs
    {
        public string ConnectionId { get; }
        public string Data { get; }
        public PipeDataEventArgs(string connectionId, string data)
        {
            ConnectionId = connectionId;
            Data = data;
        }
    }
    public class PipeConnectionEventArgs : EventArgs
    {
        public string ConnectionId { get; }
        public PipeConnectionEventArgs(string connectionId)
        {
            ConnectionId = connectionId;
        }
    }
    public class PipeErrorEventArgs : EventArgs
    {
        public string Message { get; }
        public Exception Exception { get; }
        public PipeErrorEventArgs(string message, Exception exception)
        {
            Message = message;
            Exception = exception;
        }
    }
    public class StreamString
    {
        private Stream ioStream;
        private UnicodeEncoding streamEncoding;
        public StreamString(Stream ioStream)
        {
            this.ioStream = ioStream;
            streamEncoding = new UnicodeEncoding();
        }
        public string ReadString()
        {
            int len = 0;
            len = ioStream.ReadByte() * 256;
            len += ioStream.ReadByte();
            byte[] inBuffer = new byte[len];
            ioStream.Read(inBuffer, 0, len);
            return streamEncoding.GetString(inBuffer);
        }
        public int WriteString(string outString)
        {
            byte[] outBuffer = streamEncoding.GetBytes(outString);
            int len = outBuffer.Length;
            if (len > UInt16.MaxValue)
            {
                len = (int)UInt16.MaxValue;
            }
            ioStream.WriteByte((byte)(len / 256));
            ioStream.WriteByte((byte)(len & 255));
            ioStream.Write(outBuffer, 0, len);
            ioStream.Flush();
            return outBuffer.Length + 2;
        }
    }
}
ConnectionUtil/PipeServer2.cs
New file
@@ -0,0 +1,192 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ConnectionUtil
{
    public static class PipeStreamUtil
    {
        public static string ReadMessage(this PipeStream ps)
        {
            MemoryStream ms = new MemoryStream();
            byte[] buffer = new byte[0x1000];
            do
            {
                ms.Write(buffer, 0, ps.Read(buffer, 0, buffer.Length));
            }
            while (!ps.IsMessageComplete);
            return Encoding.UTF8.GetString(ms.ToArray());
        }
        public static void WriteMessage(this PipeStream ps, string message)
        {
            var msg = Encoding.UTF8.GetBytes(message);
            ps.Write(msg, 0, msg.Length);
            ps.Flush();
        }
    }
    public class PipeServer2 : IDisposable
    {
        private readonly string _pipeName;
        private readonly int _maxNumberOfServerInstances;
        private readonly CancellationTokenSource _cancellationTokenSource;
        private readonly ConcurrentDictionary<string, NamedPipeServerStream> _activeConnections;
        private readonly ReaderWriterLockSlim _lock;
        private bool _isDisposed;
        private bool _isRunning;
        // 事件定义
        public event EventHandler<PipeDataEventArgs> OnData;
        public event EventHandler<PipeConnectionEventArgs> OnClientConnected;
        public event EventHandler<PipeConnectionEventArgs> OnClientDisconnected;
        public event EventHandler<PipeErrorEventArgs> OnError;
        public PipeServer2(string pipeName, int maxNumberOfServerInstances = 254)
        {
            _pipeName = pipeName;
            _maxNumberOfServerInstances = maxNumberOfServerInstances;
            _cancellationTokenSource = new CancellationTokenSource();
            _activeConnections = new ConcurrentDictionary<string, NamedPipeServerStream>();
            _lock = new ReaderWriterLockSlim();
        }
        public void Start()
        {
            if (_isRunning)
                return;
            _isRunning = true;
            Task.Run(() =>
            {
                while (_isRunning && !_cancellationTokenSource.Token.IsCancellationRequested)
                {
                    try
                    {
                        var pipeServer = new NamedPipeServerStream(_pipeName, PipeDirection.InOut, _maxNumberOfServerInstances
                            , PipeTransmissionMode.Message);
                        pipeServer.WaitForConnection();
                        string clientId = Guid.NewGuid().ToString();
                        if (_activeConnections.TryAdd(clientId, pipeServer))
                        {
                            // 启动处理客户端消息的线程
                            Thread clientThread = new Thread(() => HandleClient(clientId, pipeServer));
                            clientThread.Start();
                            OnClientConnected?.Invoke(this, new PipeConnectionEventArgs(clientId));
                        }
                    }
                    catch (Exception ex)
                    {
                        OnError?.Invoke(this, new PipeErrorEventArgs("Connect", ex));
                        Thread.Sleep(1000); // 发生错误时等待一秒再重试
                    }
                }
            });
        }
        private void HandleClient(string clientId, NamedPipeServerStream pipeServer)
        {
            try
            {
                while (!_cancellationTokenSource.Token.IsCancellationRequested)
                {
                    try
                    {
                        // 等待客户端发送消息
                        string message = pipeServer.ReadMessage();
                        if (string.IsNullOrEmpty(message))
                            break;
                        OnData?.Invoke(this, new PipeDataEventArgs(clientId, message));
                    }
                    catch (Exception ex)
                    {
                        OnError?.Invoke(this, new PipeErrorEventArgs("HandleClientMessageError", ex));
                        break;
                    }
                }
            }
            finally
            {
                if (_activeConnections.TryRemove(clientId, out var pipe))
                {
                    pipe.Dispose();
                }
                OnClientDisconnected?.Invoke(this, new PipeConnectionEventArgs(clientId));
            }
        }
        public void SendMessage(string clientId, string message)
        {
            if (!_activeConnections.TryGetValue(clientId, out var pipeServer))
            {
                throw new InvalidOperationException($"找不到客户端 {clientId}");
            }
            _lock.EnterWriteLock();
            try
            {
                try
                {
                    pipeServer.WriteMessage(message);
                }
                catch (Exception ex)
                {
                    OnError?.Invoke(this, new PipeErrorEventArgs($"向客户端 {clientId} 发送消息时发生错误", ex));
                }
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }
        public void BroadcastMessage(string message)
        {
            _lock.EnterWriteLock();
            try
            {
                foreach (var pipeServer in _activeConnections.Values)
                {
                    pipeServer.WriteMessage(message);
                }
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }
        public void Stop()
        {
            _isRunning = false;
            _cancellationTokenSource.Cancel();
        }
        public void Dispose()
        {
            if (!_isDisposed)
            {
                Stop();
                foreach (var pipeServer in _activeConnections.Values)
                {
                    pipeServer.Dispose();
                }
                _activeConnections.Clear();
                _cancellationTokenSource.Dispose();
                _lock.Dispose();
                _isDisposed = true;
            }
        }
    }
}
ConnectionUtil/Properties/AssemblyInfo.cs
New file
@@ -0,0 +1,33 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// 有关程序集的一般信息由以下
// 控制。更改这些特性值可修改
// 与程序集关联的信息。
[assembly: AssemblyTitle("ConnectionUtil")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ConnectionUtil")]
[assembly: AssemblyCopyright("Copyright ©  2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 将 ComVisible 设置为 false 会使此程序集中的类型
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
//请将此类型的 ComVisible 特性设置为 true。
[assembly: ComVisible(false)]
// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID
[assembly: Guid("d0806fa6-3f6d-4304-81b8-ead9da7cb5b2")]
// 程序集的版本信息由下列四个值组成:
//
//      主版本
//      次版本
//      生成号
//      修订号
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
ConnectionUtil/SCADAApi.cs
New file
@@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using System.Net.Sockets;
namespace ConnectionUtil
{
    public interface SCADAApi : IDisposable
    {
        /// <summary>
        /// 连接方法
        /// </summary>
        /// <param name="config"></param>
        /// <param name="errMsg"></param>
        /// <returns></returns>
        bool Connect(object config, out string errMsg);
        /// <summary>
        /// IP连接Scada服务器
        /// </summary>
        /// <param name="IP">MES服务器IP</param>
        /// <param name="Port"> MES服务器Port口</param>
        /// <param name="LocalIP">本地连接IP</param>
        /// <param name="LocalPort">本地连接端口</param>
        /// <returns></returns>
        bool Connect(string IP, int Port, string LocalIP, int LocalPort);
        /// <summary>
        /// 连接Scada管理服务器
        /// </summary>
        /// <param name="IP"> Scada服务器IP</param>
        /// <param name="Port">ScadPort口</param>
        /// <param name="LocalPort">本地连接端口</param>
        /// <param name="errMsg"></param>
        /// <param name="socket"></param>
        /// <returns></returns>
        bool Connect2(string IP, int Port, int LocalPort, out string errMsg, Socket socket = null);
        /// <summary>
        ///  连接Scada管理服务器
        /// </summary>
        /// <param name="IP">ScadaManagerIP</param>
        /// <param name="Port">ScadaManagerPort口</param>
        /// <param name="LocalPort">本地连接端口</param>
        /// <param name="errMsg"></param>
        /// <returns></returns>
        bool ConnectManager(string IP, int Port, int LocalPort, out string errMsg);
        /// <summary>
        /// 断开服务器连接
        /// </summary>
        void DisConnect();
        /// <summary>
        /// 批量写增加参数
        /// </summary>
        /// <param name="id"></param>
        /// <param name="par"></param>
        /// <returns></returns>
        bool DoAddParWriteMultiple(int id, string par);
        /// <summary>
        /// 换型
        /// </summary>
        /// <param name="_ProductLineName"></param>
        /// <param name="_ProductName"></param>
        /// <param name="errMsg"></param>
        /// <param name="_WorkStationName"></param>
        /// <returns></returns>
        bool DoDoChangeModel(string _ProductLineName, string _ProductName, out string errMsg, string _WorkStationName = null);
        /// <summary>
        /// 批量读值
        /// </summary>
        /// <param name="_Id"></param>
        /// <returns></returns>
        string[,] DoFctReadMultiple(int[] _Id);
        /// <summary>
        /// 单一读值
        /// </summary>
        /// <param name="_name"></param>
        /// <param name="result"></param>
        /// <param name="errMsg"></param>
        /// <returns></returns>
        bool DoFctReadSingle(string _name, out string result, out string errMsg);
        /// <summary>
        /// 单一写值
        /// </summary>
        /// <param name="errMsg"></param>
        /// <returns></returns>
        bool DoFctWriteMultiple(out string errMsg);
        /// <summary>
        /// 写单一值
        /// </summary>
        /// <param name="_name"></param>
        /// <param name="_value"></param>
        /// <param name="errMsg"></param>
        /// <returns></returns>
        bool DoFctWriteSingle(string _name, string _value, out string errMsg);
        /// <summary>
        /// mqtt发布消息
        /// </summary>
        /// <param name="topic">主题</param>
        /// <param name="_value">值</param>
        /// <param name="errMsg">错误信息></param>
        /// <returns></returns>
        bool DoMqttPublish(string topic, string _value, out string errMsg);
        /// <summary>
        ///
        /// </summary>
        /// <param name="_user"></param>
        /// <param name="_password"></param>
        /// <param name="errMsg"></param>
        /// <returns></returns>
        bool DoLogin(string _user, string _password, out string errMsg);
        /// <summary>
        /// 重启DataServer服务器
        /// </summary>
        /// <param name="errMsg"></param>
        /// <returns></returns>
        bool DoReSETDataServer(out string errMsg);
        /// <summary>
        ///  获取动态IP
        /// </summary>
        /// <returns></returns>
        string getLocalIP();
        /// <summary>
        ///
        /// </summary>
        /// <param name="_paraMPairs"></param>
        /// <param name="errMsg"></param>
        /// <returns></returns>
        short[,] GetLowerComputerStatus(short[] _paraMPairs, out string errMsg);
        /// <summary>
        /// 初始化批量写参数
        /// </summary>
        void InitParWriteMultiple();
        /// <summary>
        /// 检查与LTSCADA是否断开连接了
        /// </summary>
        /// <param name="errMsg"></param>
        /// <returns></returns>
        bool IsConnected(out string errMsg);
        /// <summary>
        /// 批量写
        /// </summary>
        /// <param name="_paraMPairs">参数列表</param>
        /// <param name="errMsg">错误信息</param>
        /// <returns>处理结果</returns>
        bool ToFctWriteMultiple(Dictionary<int, string> _paraMPairs, out string errMsg);
    }
}
ConnectionUtil/TcpClient.cs
New file
@@ -0,0 +1,254 @@
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ConnectionUtil
{
    /// <summary>
    /// TCP客户端包装类
    /// </summary>
    /// <summary>
    /// TCP客户端包装类
    /// </summary>
    public class TcpClientWrapper : IDisposable
    {
        private System.Net.Sockets.TcpClient _client;
        private NetworkStream _stream;
        private readonly string _host;
        private readonly int _port;
        private bool _isConnected;
        private readonly CancellationTokenSource _cts;
        private Task _heartbeatTask;
        private readonly SemaphoreSlim _sendReceiveLock;
        /// <summary>
        /// 初始化TCP客户端
        /// </summary>
        /// <param name="host">服务器地址</param>
        /// <param name="port">服务器端口</param>
        public TcpClientWrapper(string host = "127.0.0.1", int port = 6543)
        {
            _host = host;
            _port = port;
            _cts = new CancellationTokenSource();
            _sendReceiveLock = new SemaphoreSlim(1, 1);
        }
        /// <summary>
        /// 异步连接到服务器
        /// </summary>
        /// <returns>连接是否成功</returns>
        public async Task<bool> ConnectAsync()
        {
            try
            {
                _client = new System.Net.Sockets.TcpClient();
                await _client.ConnectAsync(_host, _port);
                _stream = _client.GetStream();
                _isConnected = true;
                // 启动心跳任务
                _heartbeatTask = StartHeartbeatAsync(_cts.Token);
                Console.WriteLine($"成功连接到服务器 {_host}:{_port}");
                return true;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"连接服务器失败:{ex.Message}");
                return false;
            }
        }
        /// <summary>
        /// 同步连接到服务器
        /// </summary>
        /// <returns>连接是否成功</returns>
        public bool Connect()
        {
            try
            {
                _client = new System.Net.Sockets.TcpClient();
                _client.Connect(_host, _port);
                _stream = _client.GetStream();
                _isConnected = true;
                // 启动心跳任务
                _heartbeatTask = StartHeartbeatAsync(_cts.Token);
                return true;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"连接服务器失败:{ex.Message}");
                return false;
            }
        }
        /// <summary>
        /// 启动心跳检测
        /// </summary>
        private async Task StartHeartbeatAsync(CancellationToken token)
        {
            while (!token.IsCancellationRequested && _isConnected)
            {
                try
                {
                    await Task.Delay(20000, token); // 20秒发送一次心跳
                    if (_isConnected)
                    {
                        await SendMessageAsync("ping");
                    }
                }
                catch (OperationCanceledException)
                {
                    break;
                }
                catch (Exception)
                {
                    _isConnected = false;
                    break;
                }
            }
        }
        /// <summary>
        /// 发送消息并接收响应
        /// </summary>
        /// <typeparam name="T">响应数据类型</typeparam>
        /// <param name="data">要发送的数据对象</param>
        /// <param name="timeout">超时时间</param>
        /// <returns>服务器响应数据</returns>
        public async Task<string> SendMessageAsync(string message, TimeSpan? timeout = null)
        {
            if (!_isConnected)
            {
                throw new InvalidOperationException("未连接到服务器");
            }
            TimeSpan actualTimeout = timeout ?? TimeSpan.FromSeconds(30);
            if (!await _sendReceiveLock.WaitAsync(actualTimeout))
            {
                throw new TimeoutException("等待发送超时");
            }
            try
            {
                byte[] messageBytes = Encoding.UTF8.GetBytes(message + "\r\n");
                // 发送消息
                await _stream.WriteAsync(messageBytes, 0, messageBytes.Length);
                // 接收响应
                byte[] buffer = new byte[1024];
                StringBuilder response = new StringBuilder();
                int bytesRead;
                CancellationTokenSource cts = new CancellationTokenSource(actualTimeout);
                try
                {
                    // 读取直到遇到换行符
                    while ((bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
                    {
                        string received = Encoding.UTF8.GetString(buffer, 0, bytesRead);
                        response.Append(received);
                        if (received.EndsWith("\r\n"))
                        {
                            break;
                        }
                        if (cts.Token.IsCancellationRequested)
                        {
                            throw new TimeoutException("接收响应超时");
                        }
                    }
                }
                finally
                {
                    cts.Dispose();
                }
                string jsonResponse = response.ToString().TrimEnd('\r', '\n');
                Console.WriteLine($"收到服务器响应:{jsonResponse}");
                // 反序列化响应
                return jsonResponse;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"发送消息失败:{ex.Message}");
                _isConnected = false;
                throw;
            }
            finally
            {
                _sendReceiveLock.Release();
            }
        }
        /// <summary>
        /// 同步发送消息并接收响应
        /// </summary>
        /// <typeparam name="T">响应数据类型</typeparam>
        /// <param name="data">要发送的数据对象</param>
        /// <param name="timeout">超时时间</param>
        /// <returns>服务器响应数据</returns>
        public string SendMessage(string message, TimeSpan? timeout = null)
        {
            timeout = timeout ?? TimeSpan.FromSeconds(30);
            try
            {
                // 使用 Task.Run 来执行异步操作,并等待结果
                return Task.Run(() => SendMessageAsync(message, timeout)).Result;
            }
            catch (AggregateException ae)
            {
                // 解包异常
                throw ae.InnerException;
            }
        }
        /// <summary>
        /// 检查是否已连接
        /// </summary>
        public bool IsConnected
        {
            get { return _isConnected; }
        }
        /// <summary>
        /// 释放资源
        /// </summary>
        public void Dispose()
        {
            _cts.Cancel();
            _isConnected = false;
            if (_stream != null)
            {
                _stream.Dispose();
            }
            if (_client != null)
            {
                _client.Dispose();
            }
            _cts.Dispose();
            if (_sendReceiveLock != null)
            {
                _sendReceiveLock.Dispose();
            }
        }
        /// <summary>
        /// 关闭连接
        /// </summary>
        public void Close()
        {
            Dispose();
        }
    }
}
ConsoleTest/App.config
New file
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
    </startup>
</configuration>
ConsoleTest/ConsoleTest.csproj
New file
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{2BDDB984-199B-4913-96BB-39C252E101F2}</ProjectGuid>
    <OutputType>Exe</OutputType>
    <RootNamespace>ConsoleTest</RootNamespace>
    <AssemblyName>ConsoleTest</AssemblyName>
    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
    <DebugSymbols>true</DebugSymbols>
    <OutputPath>bin\x64\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <DebugType>full</DebugType>
    <PlatformTarget>x64</PlatformTarget>
    <LangVersion>7.3</LangVersion>
    <ErrorReport>prompt</ErrorReport>
    <Prefer32Bit>true</Prefer32Bit>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
    <OutputPath>bin\x64\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <Optimize>true</Optimize>
    <DebugType>pdbonly</DebugType>
    <PlatformTarget>x64</PlatformTarget>
    <LangVersion>7.3</LangVersion>
    <ErrorReport>prompt</ErrorReport>
    <Prefer32Bit>true</Prefer32Bit>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Data" />
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Program.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
  </ItemGroup>
  <ItemGroup>
    <None Include="App.config" />
    <None Include="Properties\app.manifest" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\ModBusTcpController\ModBusTcpController.csproj">
      <Project>{49db0c9b-b239-4608-a91e-f5ed43afaf96}</Project>
      <Name>ModBusTcpController</Name>
    </ProjectReference>
    <ProjectReference Include="..\SimpleUtil\DIOController\DIOController.csproj">
      <Project>{34ffbafe-478f-4b6e-95e7-92a949011a9b}</Project>
      <Name>DIOController</Name>
    </ProjectReference>
    <ProjectReference Include="..\SimpleUtil\HMI.Util.Base\HMI.Util.Base.csproj">
      <Project>{d59e25d1-9b27-46a1-8438-92661dd23fae}</Project>
      <Name>HMI.Util.Base</Name>
    </ProjectReference>
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
ConsoleTest/Program.cs
New file
@@ -0,0 +1,78 @@
using HMI.Util.Base;
using ModBusTcp;
using System;
using System.Text;
namespace ConsoleTest
{
    class Program
    {
        static void Main(string[] args)
        {
            string msg;
            ModBusTcpController controller = new ModBusTcpController("127.0.0.1", 502);
            if (controller.Start(out msg))
            {
                bool res;
                int? target;
                controller.WriteBool("1", true, out msg);
                controller.ReadDOBool("1", out res, out msg);
                Console.WriteLine(res);
                controller.ReadDOInt("1", out target, out msg);
                Console.WriteLine(target);
                controller.WriteInt("1", 1, out msg);
                controller.ReadDOInt("1", out target, out msg);
                Console.WriteLine(target);
            }
            controller.Close();
            //AlarmUtil.GetProcessIdByName("TestPlatform");
            /// ModbusTcp
            //IDIOController controller = DIOControllerCreator
            //    .CreateController("C:\\Workspace\\VisualStudio\\WPFProject\\SimpleUtil\\DIOController\\bin\\Release\\modbus.ini");
            //bool s;
            //controller.Start(out string msg);
            //controller.ReadDIBool("0", out bool res, out msg);
            //controller.ReadDOBool("0", out res, out msg);
            //controller.WriteBool("0", true, out msg);
            //s = controller.WriteBool("100", new bool[] { false, false }, out msg);
            //s = controller.ReadDOBool("100", 2, out bool[] ress, out msg);
            //controller.Dispose();
            ///// 7432
            //controller = controller = DIOControllerCreator
            //    .CreateController("C:\\Workspace\\VisualStudio\\WPFProject\\SimpleUtil\\DIOController\\bin\\Release\\7432.ini");
            //controller.Start(out msg);
            //controller.ReadDIBool("0", out res, out msg);
            //controller.ReadDOBool("0", out res, out msg);
            //controller.WriteBool("0", true, out msg);
            //controller.Dispose();
            //string recvstr = "62 FD 82 39 39 39 37 32 32 36 36 30 38 34 33 39 35 33 38 53 38 44 41 30 30 32 32";
            //string recvData;
            //if (recvstr.Contains("FF"))
            //{
            //    recvData = recvstr.Replace(" ", "").Substring(6);
            //}
            //else
            //{
            //    byte[] bus = HexToByteArray(recvstr.Replace(" ", "").Substring(6));
            //    recvData = Encoding.Default.GetString(bus);
            //}
        }
        public static byte[] HexToByteArray(string hexString)
        {
            hexString = hexString.Replace(" ", "");
            byte[] buffer = new byte[hexString.Length / 2];
            for (int i = 0; i < buffer.Length; i++)
            {
                buffer[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
            }
            return buffer;
        }
    }
}
ConsoleTest/Properties/AssemblyInfo.cs
New file
@@ -0,0 +1,33 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// 有关程序集的一般信息由以下
// 控制。更改这些特性值可修改
// 与程序集关联的信息。
[assembly: AssemblyTitle("ConsoleTest")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ConsoleTest")]
[assembly: AssemblyCopyright("Copyright ©  2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 将 ComVisible 设置为 false 会使此程序集中的类型
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
//请将此类型的 ComVisible 特性设置为 true。
[assembly: ComVisible(false)]
// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID
[assembly: Guid("2bddb984-199b-4913-96bb-39c252e101f2")]
// 程序集的版本信息由下列四个值组成:
//
//      主版本
//      次版本
//      生成号
//      修订号
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
ConsoleTest/Properties/app.manifest
New file
@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
    <assemblyIdentity version="1.0.0.0" name="MyApplication.app" />
    <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
        <security>
            <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
                <!-- UAC 清单选项
             如果想要更改 Windows 用户帐户控制级别,请使用
             以下节点之一替换 requestedExecutionLevel 节点。
        <requestedExecutionLevel  level="asInvoker" uiAccess="false" />
        <requestedExecutionLevel  level="requireAdministrator" uiAccess="false" />
        <requestedExecutionLevel  level="highestAvailable" uiAccess="false" />
            指定 requestedExecutionLevel 元素将禁用文件和注册表虚拟化。
            如果你的应用程序需要此虚拟化来实现向后兼容性,则移除此
            元素。
        -->
                <requestedExecutionLevel  level="requireAdministrator" uiAccess="false" />
            </requestedPrivileges>
            <applicationRequestMinimum>
                <defaultAssemblyRequest permissionSetReference="Custom" />
                <PermissionSet class="System.Security.PermissionSet" version="1" ID="Custom" SameSite="site" Unrestricted="true" />
            </applicationRequestMinimum>
        </security>
    </trustInfo>
    <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
        <application>
            <!-- 设计此应用程序与其一起工作且已针对此应用程序进行测试的
           Windows 版本的列表。取消评论适当的元素,
           Windows 将自动选择最兼容的环境。 -->
            <!-- Windows Vista -->
            <!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
            <!-- Windows 7 -->
            <!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
            <!-- Windows 8 -->
            <!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
            <!-- Windows 8.1 -->
            <!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
            <!-- Windows 10 -->
            <!--<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />-->
        </application>
    </compatibility>
    <!-- 指示该应用程序可感知 DPI 且 Windows 在 DPI 较高时将不会对其进行
       自动缩放。Windows Presentation Foundation (WPF)应用程序自动感知 DPI,无需
       选择加入。选择加入此设置的 Windows 窗体应用程序(面向 .NET Framework 4.6)还应
       在其 app.config 中将 "EnableWindowsFormsHighDpiAutoResizing" 设置设置为 "true"。
       将应用程序设为感知长路径。请参阅 https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation -->
    <!--
  <application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
      <longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
    </windowsSettings>
  </application>
  -->
    <!-- 启用 Windows 公共控件和对话框的主题(Windows XP 和更高版本) -->
    <!--
  <dependency>
    <dependentAssembly>
      <assemblyIdentity
          type="win32"
          name="Microsoft.Windows.Common-Controls"
          version="6.0.0.0"
          processorArchitecture="*"
          publicKeyToken="6595b64144ccf1df"
          language="*"
        />
    </dependentAssembly>
  </dependency>
  -->
</assembly>
HMI.AlermInfoManage/AlarmInfoManage.xaml
New file
@@ -0,0 +1,265 @@
<hb:PluginControl x:Class="HMI.AlarmInfoManage.AlarmInfoManage"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base"
             xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
             xmlns:local="clr-namespace:HMI.AlarmInfoManage"
             xmlns:uic="clr-namespace:HMI.UI.Converter;assembly=HMI.UI"
             xmlns:pw="https://opensource.panuon.com/wpf"
             xmlns:pu="https://opensource.panuon.com/wpf-ui"
             mc:Ignorable="d"
             Background="{DynamicResource WorkspaceBackground}"
             Loaded="UserControl_Loaded"
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/HMI.UI;component/Style/PanuonListView.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </UserControl.Resources>
    <Border Style="{StaticResource CardBoarder}">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <DockPanel Grid.Row="0" Margin="0,0,0,10">
                <StackPanel DockPanel.Dock="Right" Style="{StaticResource ButtonGroup}">
                    <Button Style="{StaticResource SettingBtn}" x:Name="settingBtn" Click="Button_Click"/>
                    <Button Style="{StaticResource RfhBtn}" x:Name="refreshBtn" Click="Button_Click"/>
                    <!--<Button Style="{StaticResource ImportBtn}" x:Name="importBtn" Click="Button_Click"/>-->
                </StackPanel>
                <!--<TextBlock DockPanel.Dock="Left" FontSize="{StaticResource TitleFontSize}"
                           VerticalAlignment="Center" HorizontalAlignment="Left" Text="{DynamicResource Alm.Title}"/>-->
                <TextBlock DockPanel.Dock="Left" FontSize="{StaticResource TitleFontSize}"
           VerticalAlignment="Center" HorizontalAlignment="Left" Text="{DynamicResource Alm.Record}"/>
            </DockPanel>
            <DockPanel Grid.Row="1">
                <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right">
                    <pu:Pagination
                                Height="36" Margin="10"
    HorizontalAlignment="Left"
    Foreground="Black"
    ItemsBackground="Transparent"
    ItemsCornerRadius="12"
    ItemsSelectedBackground="{DynamicResource SecondPrimaryColor}"
                                x:Name="page"
                                CurrentPage="{Binding CurrentPage}"
                                            MaxPage="{Binding MaxPage}"/>
                </StackPanel>
                <!--<ListView x:Name="table" Style="{StaticResource DefaultListTable}"
                                  VirtualizingPanel.VirtualizationMode="Standard"
                                  ItemsSource="{Binding Datasource}">
                    <ListView.View>
                        <GridView>
                            <GridViewColumn Width="50">
                                <GridViewColumn.Header>
                                    <TextBlock Text="#"/>
                                </GridViewColumn.Header>
                                <GridViewColumn.CellTemplate>
                                    <DataTemplate>
                                        <TextBlock Text="{Binding Converter={StaticResource lvRowNumber},
RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ListViewItem,AncestorLevel=1}}"/>
                                    </DataTemplate>
                                </GridViewColumn.CellTemplate>
                            </GridViewColumn>
                            <GridViewColumn Width="300">
                                <GridViewColumn.Header>
                                    <TextBlock Text="{DynamicResource Alm.Source}"/>
                                </GridViewColumn.Header>
                                <GridViewColumn.CellTemplate>
                                    <DataTemplate>
                                        <Border Style="{StaticResource CellBorder}">
                                            <TextBlock VerticalAlignment="Center" Text="{Binding Source}"/>
                                        </Border>
                                    </DataTemplate>
                                </GridViewColumn.CellTemplate>
                            </GridViewColumn>
                            <GridViewColumn Width="500">
                                <GridViewColumn.Header>
                                    <TextBlock Text="{DynamicResource Alm.Code}"/>
                                </GridViewColumn.Header>
                                <GridViewColumn.CellTemplate>
                                    <DataTemplate>
                                        <Border Style="{StaticResource CellBorder}">
                                            <TextBlock VerticalAlignment="Center" Text="{Binding AlarmCode}"/>
                                        </Border>
                                    </DataTemplate>
                                </GridViewColumn.CellTemplate>
                            </GridViewColumn>
                            <GridViewColumn Width="800">
                                <GridViewColumn.Header>
                                    <TextBlock Text="{DynamicResource Alm.Info}"/>
                                </GridViewColumn.Header>
                                <GridViewColumn.CellTemplate>
                                    <DataTemplate>
                                        <Border Style="{StaticResource CellBorder}">
                                            <TextBlock VerticalAlignment="Center" Text="{Binding AlarmInfo}"/>
                                        </Border>
                                    </DataTemplate>
                                </GridViewColumn.CellTemplate>
                            </GridViewColumn>
                            <GridViewColumn Width="200">
                                <GridViewColumn.Header>
                                    <TextBlock Text="{DynamicResource Alm.Time}"/>
                                </GridViewColumn.Header>
                                <GridViewColumn.CellTemplate>
                                    <DataTemplate>
                                        <Border Style="{StaticResource CellBorder}">
                                            <TextBlock VerticalAlignment="Center" Text="{Binding Time}"/>
                                        </Border>
                                    </DataTemplate>
                                </GridViewColumn.CellTemplate>
                            </GridViewColumn>
                        </GridView>
                    </ListView.View>
                </ListView>-->
                <DataGrid x:Name="table" Style="{StaticResource DefaultDataGrid}"
                            dp:DataGridHelper.ShowRowNumber="True"
                                  VirtualizingPanel.VirtualizationMode="Standard"
                                  ItemsSource="{Binding Datasource}">
                    <DataGrid.Columns>
                        <DataGridTextColumn Header="#"
                    Binding="{Binding Header, RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}, Mode=FindAncestor}}"
    Width="40"/>
                        <DataGridTextColumn Width="200" Binding="{Binding Source}">
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource Alm.Source}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                        <DataGridTextColumn Width="200" Binding="{Binding AlarmCode}">
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource Alm.Code}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                        <DataGridTextColumn Width="*" Binding="{Binding AlarmInfo}">
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource Alm.Info}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                        <DataGridTextColumn Width="200" Binding="{Binding Time}">
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource Alm.Time}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                    </DataGrid.Columns>
                </DataGrid>
                <!--<DataGrid x:Name="table" Style="{StaticResource DefaultDataGrid}" ItemsSource="{Binding Datasource}">
                            <DataGrid.Columns>
                                <DataGridTemplateColumn Header="" Width="Auto" CanUserResize="False">
                                    <DataGridTemplateColumn.CellTemplate>
                                        <DataTemplate>
                                            <CheckBox VerticalAlignment="Center" IsChecked="{Binding Selected,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
                                        </DataTemplate>
                                    </DataGridTemplateColumn.CellTemplate>
                                </DataGridTemplateColumn>
                                <DataGridTemplateColumn Header="#" CanUserResize="False">
                                    <DataGridTemplateColumn.CellTemplate>
                                        <DataTemplate>
                                            <TextBlock Text="{Binding Converter={StaticResource rowNumber},
        RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DataGridRow},AncestorLevel=1}}"/>
                                        </DataTemplate>
                                    </DataGridTemplateColumn.CellTemplate>
                                </DataGridTemplateColumn>
                                <DataGridTextColumn Width="*" Binding="{Binding Source}">
                                    <DataGridTextColumn.Header>
                                        <TextBlock Text="{DynamicResource Alm.Source}"/>
                                    </DataGridTextColumn.Header>
                                </DataGridTextColumn>
                                <DataGridTextColumn Width="*" Binding="{Binding AlarmCode}">
                                    <DataGridTextColumn.Header>
                                        <TextBlock Text="{DynamicResource Alm.Code}"/>
                                    </DataGridTextColumn.Header>
                                </DataGridTextColumn>
                                <DataGridTextColumn Width="*" Binding="{Binding AlarmInfo}">
                                    <DataGridTextColumn.Header>
                                        <TextBlock Text="{DynamicResource Alm.Info}"/>
                                    </DataGridTextColumn.Header>
                                </DataGridTextColumn>
                                <DataGridTextColumn Width="*" Binding="{Binding Time}">
                                    <DataGridTextColumn.Header>
                                        <TextBlock Text="{DynamicResource Alm.Time}"/>
                                    </DataGridTextColumn.Header>
                                </DataGridTextColumn>
                            </DataGrid.Columns>
                        </DataGrid>-->
            </DockPanel>
            <!--<TabControl Grid.Row="1" Style="{StaticResource HeaderTabControl}">
                <TabItem Header="">
                </TabItem>
                <TabItem Header="报警信息">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition/>
                        </Grid.RowDefinitions>
                        <DataGrid Grid.Row="1" x:Name="alarmTextTable" Visibility="Hidden" Style="{x:Null}">
                            <DataGrid.Columns>
                                <DataGridTemplateColumn Header="" Width="50"/>
                                <DataGridTextColumn x:Name="sb1" Width="*" Binding="{Binding AlarmCode}"/>
                                <DataGridTextColumn x:Name="sb2" Width="*" Binding="{Binding MAlarmText_CN}"/>
                            </DataGrid.Columns>
                        </DataGrid>
                        <ListView Grid.Row="1" Style="{StaticResource DefaultListTable}" ItemsSource="{Binding AlarmTexts}">
                            <ListView.View>
                                <GridView>
                                    <GridViewColumn Width="50">
                                        <GridViewColumn.Header>
                                            <TextBlock Text="#"/>
                                        </GridViewColumn.Header>
                                        <GridViewColumn.CellTemplate>
                                            <DataTemplate>
                                                <TextBlock Text="{Binding Converter={StaticResource lvRowNumber},
RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ListViewItem,AncestorLevel=1}}"/>
                                            </DataTemplate>
                                        </GridViewColumn.CellTemplate>
                                    </GridViewColumn>
                                    <GridViewColumn Width="{Binding ActualWidth,ElementName=sb1}">
                                        <GridViewColumn.Header>
                                            <TextBlock Text="{DynamicResource Alm.Code}"/>
                                        </GridViewColumn.Header>
                                        <GridViewColumn.CellTemplate>
                                            <DataTemplate>
                                                <Border Style="{StaticResource CellBorder}">
                                                    <TextBlock VerticalAlignment="Center" Text="{Binding AlarmCode}"/>
                                                </Border>
                                            </DataTemplate>
                                        </GridViewColumn.CellTemplate>
                                    </GridViewColumn>
                                    <GridViewColumn Width="{Binding ActualWidth,Converter={StaticResource sbC},ConverterParameter=50,ElementName=sb2}">
                                        <GridViewColumn.Header>
                                            <TextBlock Text="{DynamicResource Alm.Info}"/>
                                        </GridViewColumn.Header>
                                        <GridViewColumn.CellTemplate>
                                            <DataTemplate>
                                                <Border Style="{StaticResource CellBorder}">
                                                    <TextBlock VerticalAlignment="Center" Text="{Binding MAlarmText_CN}"/>
                                                </Border>
                                            </DataTemplate>
                                        </GridViewColumn.CellTemplate>
                                    </GridViewColumn>
                                </GridView>
                            </ListView.View>
                        </ListView>
                    </Grid>
                </TabItem>
            </TabControl>-->
        </Grid>
    </Border>
</hb:PluginControl>
HMI.AlermInfoManage/AlarmInfoManage.xaml.cs
New file
@@ -0,0 +1,424 @@
using HMI.Base;
using HMI.Core.Entity;
using HMI.Core.Util;
using HMI.UI.Util;
using HMI.Util;
using HMI.Util.Alarm;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data;
using System.IO;
using System.Windows;
using System.Windows.Controls;
namespace HMI.AlarmInfoManage
{
    /// <summary>
    /// UserControl1.xaml 的交互逻辑
    /// </summary>
    public partial class AlarmInfoManage : PluginControl
    {
        private string alarmTextPath;
        private string dbPath;
        private SqliteHelper sqliteHelper;
        //private string opcIP = "127.0.0.1";
        //private int opcPort = 6543;
        //private int localPort = 18888;
        //private SCADAApi lTSCADAApi = new OPCClient();
        private ObservableCollection<AlarmText> datasource = new ObservableCollection<AlarmText>();
        public ObservableCollection<AlarmText> Datasource
        {
            get { return datasource; }
            set { Set(ref datasource, value); }
        }
        /// <summary>
        /// 报警文本列表
        /// </summary>
        private ObservableCollection<AlarmText> alarmTexts = new ObservableCollection<AlarmText>();
        public ObservableCollection<AlarmText> AlarmTexts
        {
            get { return alarmTexts; }
            set { Set(ref alarmTexts, value); }
        }
        private double size = 30;
        private int currentPage = 0;
        public int CurrentPage
        {
            get => currentPage;
            set
            {
                Set(ref currentPage, value);
                RefreshData();
            }
        }
        private int maxPage = 0;
        public int MaxPage
        {
            get => maxPage;
            set
            {
                Set(ref maxPage, value);
            }
        }
        public AlarmInfoManage()
        {
            DataContext = this;
            InitializeComponent();
        }
        public override void SetBaseFolder(string folder)
        {
            base.SetBaseFolder(folder);
            dbPath = Path.Combine(BaseFolder, "alarm.db");
            alarmTextPath = Path.Combine(BaseFolder, "AlarmText.data");
        }
        public override void InitFinish()
        {
            if (!File.Exists(dbPath))
            {
                SqliteHelper.CreateDatabase(dbPath);
            }
            sqliteHelper = new SqliteHelper(dbPath);
            sqliteHelper.ExecuteSql(@"CREATE TABLE IF NOT EXISTS Alarm (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    source TEXT NOT NULL,
                    alarmcode TEXT,
                    alarminfo TEXT,
                    time DATETIME);");
            LoadAlarmText();
            AlarmUtil.OnAlarm += AlarmUtil_OnAlarm;
            //if (!lTSCADAApi.Connect2(opcIP, opcPort, localPort, out string errMsg))
            //{
            //}
            //else
            //{
            //    this.MonitorTimer.Start();
            //}
        }
        private void AlarmUtil_OnAlarm(object sender, List<AlarmText> e)
        {
            foreach (AlarmText alarm in e)
            {
                var sql = string.Empty;
                if (alarm.IsPLCAlarm)
                {
                    var currentInfo = alarm.AlarmInfo;
                    sql = string.Format("INSERT INTO Alarm (source, alarmcode, alarminfo, time) VALUES('{0}','{1}','{2}','{3}');",
                        alarm.Source, alarm.AlarmCode, currentInfo, alarm.Time);
                }
                else
                {
                    sql = string.Format("INSERT INTO Alarm (source, alarmcode, alarminfo, time) VALUES('{0}','{1}','{2}','{3}');",
                    alarm.Source, alarm.AlarmCode, alarm.AlarmInfo, alarm.Time);
                }
                sqliteHelper.ExecuteSql(sql);
            }
        }
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Button btn = sender as Button;
            switch (btn.Name)
            {
                case "settingBtn":
                    OpenSettingWindow();
                    break;
                case "refreshBtn":
                    LoadAlarmText();
                    RefreshPage(CurrentPage);
                    break;
                case "importBtn":
                    ImportText();
                    break;
                default:
                    break;
            }
        }
        private void OpenSettingWindow()
        {
            AlarmSettingList listModal = new AlarmSettingList(log)
            {
                Owner = Window.GetWindow(this)
            };
            listModal.ShowDialog();
        }
        private void RefreshPage(int? page = null)
        {
            var sql = "SELECT COUNT (*) as num FROM Alarm;";
            var dt = sqliteHelper.GetDataTable(sql);
            if (dt.Rows.Count <= 0)
            {
                CurrentPage = 1;
                MaxPage = 1;
                return;
            }
            int count = int.Parse(dt.Rows[0]["num"].ToString());
            MaxPage = (int)Math.Ceiling(count / size);
            if (page != null)
            {
                CurrentPage = page.Value;
            }
            else if (CurrentPage > MaxPage)
            {
                CurrentPage = MaxPage;
            }
        }
        private void RefreshData()
        {
            var list = new ObservableCollection<AlarmText>();
            var sql = $"SELECT source, alarmcode, alarminfo, time FROM Alarm ORDER BY time DESC LIMIT ({CurrentPage - 1} * {size}), {size};";
            var dt = sqliteHelper.GetDataTable(sql);
            if (dt.Rows.Count > 0)
            {
                foreach (DataRow dr in dt.Rows)
                {
                    var at = new AlarmText
                    {
                        Source = dr["source"].ToString(),
                        AlarmCode = dr["alarmcode"].ToString(),
                        AlarmInfo = dr["alarminfo"].ToString(),
                        Time = dr["time"].ToString(),
                    };
                    list.Add(at);
                }
            }
            Datasource = list;
        }
        private void ImportText()
        {
            try
            {
                OpenFileDialog openDialog = new OpenFileDialog();
                openDialog.Filter = "Files (*.xlsm)|*.xlsm";
                if (openDialog.ShowDialog() == true)
                {
                    this.ImportExcelAlarmText(openDialog.FileName);
                }
            }
            catch (Exception ex)
            {
                log.Error("AlarmText Import Failed.", ex);
                ToastHelper.Error(Window.GetWindow(this), $"{this.lang("App.ImportFailed")}: {ex.Message}");
            }
        }
        /// <summary>
        /// 导入报警代码文本
        /// </summary>
        /// <param name="excelFile">电子设计表格文件</param>
        //private void ImportExcelAlarmText(string excelFile)
        //{
        //    int index = 0;
        //    try
        //    {
        //        if (excelFile.IndexOf(".xlsm") < 0)
        //        {
        //            this.Warning(this.lang("Alm.OnlySupportXlsm"));
        //            return;
        //        }
        //        this.alarmTexts.Clear();
        //        XSSFWorkbook workbook = new XSSFWorkbook(excelFile);
        //        XSSFSheet sheet = (XSSFSheet)workbook.GetSheet("sheet1");
        //        XSSFRow row = null;
        //        for (int i = 2; i < (sheet.LastRowNum + 1); i++)
        //        {
        //            index = i;
        //            row = (XSSFRow)sheet.GetRow(i);
        //            if (row != null)
        //            {
        //                AlarmText alarm = new AlarmText();
        //                alarm.AlarmCode = row.Cells[0].StringCellValue;
        //                alarm.OpNo = row.Cells[1].StringCellValue;
        //                alarm.ModuleNo = row.Cells[2].StringCellValue;
        //                alarm.ModuleName_CN = row.Cells[3].StringCellValue;
        //                alarm.ModuleName_EN = row.Cells[4].StringCellValue;
        //                alarm.MAlarmCode = row.Cells[5].StringCellValue;
        //                alarm.MAlarmText_CN = row.Cells[6].StringCellValue;
        //                alarm.MAlarmText_EN = row.Cells[7].StringCellValue;
        //                this.alarmTexts.Add(alarm);
        //            }
        //        }
        //        if (sheet.LastRowNum > 0)
        //        {
        //            XmlHelper.SerializeToXml(textSettingPath, this.alarmTexts);
        //        }
        //        this.Success(this.lang("App.ImportSuccess"));
        //    }
        //    catch (Exception ex)
        //    {
        //        log.Error($"Row {index} AlarmText Import Failed.", ex);
        //        this.Error($"{this.lang("Alm.RowImportFailed", index)}: {ex.Message}");
        //    }
        //}
        /// <summary>
        /// 导入报警代码文本
        /// </summary>
        /// <param name="excelFile">电子设计表格文件</param>
        private void ImportExcelAlarmText(string excelFile)
        {
            int index = 0;
            try
            {
                if (excelFile.IndexOf(".xlsm") < 0)
                {
                    this.Warning(this.lang("Alm.OnlySupportXlsm"));
                    return;
                }
                this.alarmTexts = new ObservableCollection<AlarmText>
                    (ExcelHelper.ReadFromExcel<AlarmText>(excelFile));
                if (alarmTexts.Count > 0)
                {
                    XmlHelper.SerializeToXml(alarmTextPath, this.alarmTexts);
                }
                LoadAlarmText();
                this.Success(this.lang("App.ImportSuccess"));
            }
            catch (Exception ex)
            {
                log.Error($"Row {index} AlarmText Import Failed.", ex);
                this.Error($"{this.lang("Alm.RowImportFailed", index)}: {ex.Message}");
            }
        }
        private void LoadAlarmText()
        {
            try
            {
                if (string.IsNullOrEmpty(alarmTextPath) || !File.Exists(alarmTextPath))
                {
                    AlarmTexts = new ObservableCollection<AlarmText>();
                }
                else
                {
                    AlarmTexts = XmlHelper.DeserializeFromXml<ObservableCollection<AlarmText>>(alarmTextPath);
                }
            }
            catch (Exception ex)
            {
                log.Error("LoadAlarmText Error.", ex);
                AlarmTexts = new ObservableCollection<AlarmText>();
            }
        }
        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            //if (!string.IsNullOrEmpty(systemSetting.PlcIp))
            //{
            //    if (this.plc.isConnection)
            //    {
            //        this.plc.Simens_S7_ConnectPLC(systemSetting.PlcIp, 102, 0, 1, string.Empty);
            //    }
            //}
            RefreshPage(1);
            Loaded -= UserControl_Loaded;
        }
        public override void DoClose(object sender, EventArgs e)
        {
            // AlarmUtil.OnAlarm -= AlarmUtil_OnAlarm;
        }
        //private void MonitorTimer_Elapsed_bak(object sender, ElapsedEventArgs e)
        //{
        //    try
        //    {
        //        this.MonitorTimer.Enabled = false;
        //        isHaveAlarm = false;
        //        foreach (var item in signAlarmSetting)
        //        {
        //            //isHaveAlarm = isHaveAlarm ||
        //            //    this.plc.Simens_S7_ReadBool_DB(item.DBAddress, item.StartAddress, item.Length);
        //        }
        //        ////有报警
        //        if (isHaveAlarm)
        //        {
        //            this.isNewAlarm = false;
        //            this.currentAlarmCachePlc.Clear();
        //            RefreshDBSetting();
        //            for (int i = 0; i < ((int)alarmCodeBytes.Length / 4); i++)
        //            {
        //                int code = S7.GetDIntAt(alarmCodeBytes, 4 * i);
        //                ////有报警
        //                if (code != 0)
        //                {
        //                    string acode = string.Format("{0:d10}", code);
        //                    alarm = AlarmTexts.FirstOrDefault(item => item.AlarmCode == acode);
        //                    if (alarm != null)
        //                    {
        //                        alarm.Source = "PLC";
        //                        alarm.Time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        //                        this.currentAlarmCachePlc.Add(alarm);
        //                        ////有新报警
        //                        if (this.preAlarmCachePlc.Find(alarm => alarm.AlarmCode == acode) == null)
        //                        {
        //                            this.isNewAlarm = true;
        //                            this.SendLogMsg(alarm.ModuleName_CN + alarm.MAlarmText_CN);
        //                        }
        //                    }
        //                }
        //            }
        //            ////有新报警和报警减少时刷新报警表
        //            if (this.isNewAlarm || (this.currentAlarmCachePlc.Count != this.preAlarmCachePlc.Count))
        //            {
        //                this.currentAlarmCachePlcToMain = XmlHelper.DeepClone(this.currentAlarmCachePlc);
        //                this.RefershAlarm();
        //            }
        //            this.preAlarmCachePlc.Clear();
        //            this.preAlarmCachePlc = XmlHelper.DeepClone(this.currentAlarmCachePlc);
        //        }
        //        else
        //        {
        //            if (this.preAlarmCachePlc.Count != 0 || this.currentAlarmCachePlcToMain.Count != 0 || this.currentAlarmCachePlc.Count != 0)
        //            {
        //                this.preAlarmCachePlc.Clear();
        //                this.currentAlarmCachePlcToMain.Clear();
        //                this.currentAlarmCachePlc.Clear();
        //                this.RefershAlarm();
        //            }
        //        }
        //    }
        //    catch (Exception ex)
        //    {
        //        log.Error("sdadsa", ex);
        //    }
        //    finally
        //    {
        //        this.MonitorTimer.Enabled = true;
        //    }
        //}
    }
}
HMI.AlermInfoManage/AlarmInfoManage_SettingA.xaml
New file
@@ -0,0 +1,27 @@
<UserControl x:Class="HMI.AlarmInfoManage.AlarmInfoManage_SettingA"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:HMI.AlarmInfoManage"
            xmlns:pw="https://opensource.panuon.com/wpf"
            xmlns:pu="https://opensource.panuon.com/wpf-ui"
             Loaded="UserControl_Loaded"
             mc:Ignorable="d"
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <StackPanel>
            <GroupBox Header="PLC设置">
                <StackPanel>
                    <pu:FormGroup Header="PLC IP" GroupName="MyFormGroup">
                        <TextBox Text="{Binding PlcIp}"/>
                    </pu:FormGroup>
                    <pu:FormGroup Header="启用PLC" GroupName="MyFormGroup">
                        <CheckBox IsChecked="{Binding PlcEnabled,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
                    </pu:FormGroup>
                </StackPanel>
            </GroupBox>
        </StackPanel>
    </Grid>
</UserControl>
HMI.AlermInfoManage/AlarmInfoManage_SettingA.xaml.cs
New file
@@ -0,0 +1,43 @@
using HMI.AlarmInfoManage.Entity;
using HMI.Core.Api;
using HMI.Core.Util;
using System.Windows;
using System.Windows.Controls;
namespace HMI.AlarmInfoManage
{
    /// <summary>
    /// AlarmInfoManage_Setting.xaml 的交互逻辑
    /// </summary>
    public partial class AlarmInfoManage_SettingA : UserControl, IConfigManager
    {
        public AlarmInfoManage_SettingA()
        {
            InitializeComponent();
        }
        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            ResetConfig();
        }
        public object GetConfig()
        {
            return AimSetting.Instance;
        }
        public void SaveConfig()
        {
            AimSettingModel model = (AimSettingModel)DataContext;
            AimSetting newSetting = AimSetting.FromModel(model);
            XmlHelper.SerializeToXml(PathHelper.ConfigFolder + "\\AimSetting.xml", model);
        }
        public void ResetConfig()
        {
            DataContext = AimSetting.Instance.GetModel();
        }
    }
}
HMI.AlermInfoManage/AlarmSettingList.xaml
New file
@@ -0,0 +1,461 @@
<pu:WindowX x:Class="HMI.AlarmInfoManage.AlarmSettingList"
            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:local="clr-namespace:HMI.AlarmInfoManage"
            xmlns:pw="https://opensource.panuon.com/wpf"
            xmlns:pu="https://opensource.panuon.com/wpf-ui"
            xmlns:ui="clr-namespace:HMI.UI;assembly=HMI.UI"
            xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
            mc:Ignorable="d"
            Title="{Binding SelectedValue,Mode=OneWay}"
            pu:WindowXModalDialog.Buttons="OK"
            pu:WindowXModalDialog.OKButtonContent="{DynamicResource App.Close}"
            Width="1000"
            Height="600"
            InteropOwnersMask="False"
            Loaded="WindowX_Loaded"
            WindowStartupLocation="CenterOwner"
            Style="{StaticResource ModalWindow}">
    <pu:WindowX.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/HMI.UI;component/Style/PanuonListView.xaml"/>
            </ResourceDictionary.MergedDictionaries>
            <Style x:Key="EditLink" TargetType="Button" BasedOn="{StaticResource RowLink}">
                <Setter Property="Content" Value="{DynamicResource App.Edit}"/>
                <Setter Property="Foreground" Value="{DynamicResource PrimaryColor}"/>
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Path=IsMouseOver,
                RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ListViewItem,AncestorLevel=1}}"
         Value="True">
                        <Setter Property="Foreground" Value="#FFF"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding Path=IsSelected,
       RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ListViewItem,AncestorLevel=1}}"
Value="True">
                        <Setter Property="Foreground" Value="#FFF"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </ResourceDictionary>
    </pu:WindowX.Resources>
    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition />
        </Grid.RowDefinitions>
        <StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Right"
            Margin="0,0,0,10"
                    dp:StackPanelHelper.ChildSpace="10">
            <Button Style="{StaticResource AddBtn}" x:Name="addBtn" Click="Button_Click"/>
            <Button Style="{StaticResource RfhBtn}" x:Name="refreshBtn" Click="Button_Click"/>
            <Button Style="{StaticResource ExportBtn}" x:Name="exportBtn" Click="Button_Click"/>
            <Button Style="{StaticResource ImportBtn}" x:Name="importBtn" Click="Button_Click"/>
            <Button Style="{StaticResource DelBtn}" x:Name="delBatchBtn" Click="Button_Click"/>
        </StackPanel>
        <Grid Grid.Row="1">
            <TabControl Style="{StaticResource HeaderTabControl}" SelectedIndex="{Binding ItemIndex}" x:Name="tab">
                <TabItem Header="DB块报警">
                    <!--<ListView Style="{StaticResource DefaultListTable}" ItemsSource="{Binding Datasource}">
                        <ListView.View>
                            <GridView AllowsColumnReorder="False">
                                <GridViewColumn Width="30">
                                    <GridViewColumn.CellTemplate>
                                        <DataTemplate>
                                            <CheckBox VerticalAlignment="Center"
IsChecked="{Binding IsSelected,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
                                        </DataTemplate>
                                    </GridViewColumn.CellTemplate>
                                </GridViewColumn>
                                <GridViewColumn Width="60" Header="#">
                                    <GridViewColumn.CellTemplate>
                                        <DataTemplate>
                                            <Border Style="{StaticResource CellBorder}">
                                                <TextBlock Style="{StaticResource ListTableTextBlock}"  Text="{Binding Converter={StaticResource lvRowNumber},
RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ListViewItem,AncestorLevel=1}}"/>
                                            </Border>
                                        </DataTemplate>
                                    </GridViewColumn.CellTemplate>
                                </GridViewColumn>
                                <GridViewColumn Width="100">
                                    <GridViewColumn.Header>
                                        <TextBlock Text="{DynamicResource Alm.DbAddress}"/>
                                    </GridViewColumn.Header>
                                    <GridViewColumn.CellTemplate>
                                        <DataTemplate>
                                            <Border Style="{StaticResource CellBorder}">
                                                <TextBlock Style="{StaticResource ListTableTextBlock}"
                                                           Text="{Binding DBAddress}"/>
                                            </Border>
                                        </DataTemplate>
                                    </GridViewColumn.CellTemplate>
                                </GridViewColumn>
                                <GridViewColumn Width="150">
                                    <GridViewColumn.Header>
                                        <TextBlock Text="{DynamicResource Alm.StartAddress}"/>
                                    </GridViewColumn.Header>
                                    <GridViewColumn.CellTemplate>
                                        <DataTemplate>
                                            <Border Style="{StaticResource CellBorder}">
                                                <TextBlock Style="{StaticResource ListTableTextBlock}"
   Text="{Binding StartAddress}"/>
                                            </Border>
                                        </DataTemplate>
                                    </GridViewColumn.CellTemplate>
                                </GridViewColumn>
                                <GridViewColumn Width="100">
                                    <GridViewColumn.Header>
                                        <TextBlock Text="{DynamicResource Alm.DbLength}"/>
                                    </GridViewColumn.Header>
                                    <GridViewColumn.CellTemplate>
                                        <DataTemplate>
                                            <Border Style="{StaticResource CellBorder}">
                                                <TextBlock Style="{StaticResource ListTableTextBlock}"
                                                   Text="{Binding Length}"/>
                                            </Border>
                                        </DataTemplate>
                                    </GridViewColumn.CellTemplate>
                                </GridViewColumn>
                                <GridViewColumn Width="150">
                                    <GridViewColumn.Header>
                                        <TextBlock Text="报警代码"/>
                                    </GridViewColumn.Header>
                                    <GridViewColumn.CellTemplate>
                                        <DataTemplate>
                                            <Border Style="{StaticResource CellBorder}">
                                                <TextBlock Style="{StaticResource ListTableTextBlock}"
                   Text="{Binding AlarmCode}"/>
                                            </Border>
                                        </DataTemplate>
                                    </GridViewColumn.CellTemplate>
                                </GridViewColumn>
                                <GridViewColumn Width="250">
                                    <GridViewColumn.Header>
                                        <TextBlock Text="{DynamicResource Alm.Description}"/>
                                    </GridViewColumn.Header>
                                    <GridViewColumn.CellTemplate>
                                        <DataTemplate>
                                            <Border Style="{StaticResource CellBorder}">
                                                <TextBlock Style="{StaticResource ListTableTextBlock}"
Text="{Binding Description}"/>
                                            </Border>
                                        </DataTemplate>
                                    </GridViewColumn.CellTemplate>
                                </GridViewColumn>
                                <GridViewColumn Width="120">
                                    <GridViewColumn.Header>
                                        <TextBlock Text="{DynamicResource App.Opt}"/>
                                    </GridViewColumn.Header>
                                    <GridViewColumn.CellTemplate>
                                        <DataTemplate>
                                            <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="10">
                                                <Button x:Name="editBtn" Style="{StaticResource EditLink}"
                Click="Button_Click" Tag="{Binding }"/>
                                                <Button x:Name="delBtn" Style="{StaticResource DeleteLink}"
                Click="Button_Click" Tag="{Binding }"/>
                                            </StackPanel>
                                        </DataTemplate>
                                    </GridViewColumn.CellTemplate>
                                </GridViewColumn>
                            </GridView>
                        </ListView.View>
                    </ListView>-->
                    <DataGrid Style="{StaticResource DefaultDataGrid}"
                              dp:DataGridHelper.ShowRowNumber="True"
                              ItemsSource="{Binding BlockDatasource}">
                        <DataGrid.Columns>
                            <DataGridTemplateColumn Width="20" CanUserResize="False">
                                <DataGridTemplateColumn.CellTemplate>
                                    <DataTemplate>
                                        <CheckBox VerticalAlignment="Center"
HorizontalAlignment="Center" IsChecked="{Binding IsSelected,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
                                    </DataTemplate>
                                </DataGridTemplateColumn.CellTemplate>
                            </DataGridTemplateColumn>
                            <DataGridTextColumn Header="#"
                Binding="{Binding Header, RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}, Mode=FindAncestor}}"
                                                Width="40"/>
                            <DataGridTextColumn Width="100" Binding="{Binding DBAddress}">
                                <DataGridTextColumn.Header>
                                    <TextBlock Text="{DynamicResource Alm.DbAddress}"/>
                                </DataGridTextColumn.Header>
                            </DataGridTextColumn>
                            <DataGridTextColumn Width="150" Binding="{Binding StartAddress}">
                                <DataGridTextColumn.Header>
                                    <TextBlock Text="{DynamicResource Alm.StartAddress}"/>
                                </DataGridTextColumn.Header>
                            </DataGridTextColumn>
                            <DataGridTextColumn Width="100" Binding="{Binding Length}">
                                <DataGridTextColumn.Header>
                                    <TextBlock Text="{DynamicResource Alm.DbLength}"/>
                                </DataGridTextColumn.Header>
                            </DataGridTextColumn>
                            <DataGridTextColumn Width="150" Binding="{Binding AlarmCode}">
                                <DataGridTextColumn.Header>
                                    <TextBlock Text="报警代码"/>
                                </DataGridTextColumn.Header>
                            </DataGridTextColumn>
                            <DataGridTextColumn Width="*" Binding="{Binding Level}">
                                <DataGridTextColumn.Header>
                                    <TextBlock Text="级别"/>
                                </DataGridTextColumn.Header>
                            </DataGridTextColumn>
                            <DataGridTextColumn Width="*" Binding="{Binding Description}">
                                <DataGridTextColumn.Header>
                                    <TextBlock Text="{DynamicResource Alm.Description}"/>
                                </DataGridTextColumn.Header>
                            </DataGridTextColumn>
                            <DataGridTemplateColumn Width="120">
                                <DataGridTemplateColumn.Header>
                                    <TextBlock Text="{DynamicResource App.Opt}"/>
                                </DataGridTemplateColumn.Header>
                                <DataGridTemplateColumn.CellTemplate>
                                    <DataTemplate>
                                        <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="10">
                                            <Button x:Name="editBtn" Style="{StaticResource EditLink}"
Click="Button_Click" Tag="{Binding }"/>
                                            <Button x:Name="delBtn" Style="{StaticResource DeleteLink}"
Click="Button_Click" Tag="{Binding }"/>
                                        </StackPanel>
                                    </DataTemplate>
                                </DataGridTemplateColumn.CellTemplate>
                            </DataGridTemplateColumn>
                        </DataGrid.Columns>
                    </DataGrid>
                </TabItem>
                <TabItem Header="标志位报警">
                    <DataGrid Style="{StaticResource DefaultDataGrid}"
                              dp:DataGridHelper.ShowRowNumber="True"
                              ItemsSource="{Binding MarkDatasource}">
                        <DataGrid.Columns>
                            <DataGridTemplateColumn Width="Auto" CanUserResize="False">
                                <DataGridTemplateColumn.CellTemplate>
                                    <DataTemplate>
                                        <CheckBox VerticalAlignment="Center"
HorizontalAlignment="Center" IsChecked="{Binding IsSelected,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
                                    </DataTemplate>
                                </DataGridTemplateColumn.CellTemplate>
                            </DataGridTemplateColumn>
                            <DataGridTextColumn Header="#"
                Binding="{Binding Header, RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}, Mode=FindAncestor}}" Width="40"/>
                            <DataGridTextColumn Width="100" Binding="{Binding DBAddress}">
                                <DataGridTextColumn.Header>
                                    <TextBlock Text="{DynamicResource Alm.DbAddress}"/>
                                </DataGridTextColumn.Header>
                            </DataGridTextColumn>
                            <DataGridTextColumn Width="150" Binding="{Binding StartAddress}">
                                <DataGridTextColumn.Header>
                                    <TextBlock Text="{DynamicResource Alm.StartAddress}"/>
                                </DataGridTextColumn.Header>
                            </DataGridTextColumn>
                            <DataGridTextColumn Width="100" Binding="{Binding Length}">
                                <DataGridTextColumn.Header>
                                    <TextBlock Text="{DynamicResource Alm.DbLength}"/>
                                </DataGridTextColumn.Header>
                            </DataGridTextColumn>
                            <DataGridTextColumn Width="*" Binding="{Binding Description}">
                                <DataGridTextColumn.Header>
                                    <TextBlock Text="{DynamicResource Alm.Description}"/>
                                </DataGridTextColumn.Header>
                            </DataGridTextColumn>
                            <DataGridTemplateColumn Width="120">
                                <DataGridTemplateColumn.Header>
                                    <TextBlock Text="{DynamicResource App.Opt}"/>
                                </DataGridTemplateColumn.Header>
                                <DataGridTemplateColumn.CellTemplate>
                                    <DataTemplate>
                                        <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="10">
                                            <Button x:Name="editBtn" Style="{StaticResource EditLink}"
Click="Button_Click" Tag="{Binding }"/>
                                            <Button x:Name="delBtn" Style="{StaticResource DeleteLink}"
Click="Button_Click" Tag="{Binding }"/>
                                        </StackPanel>
                                    </DataTemplate>
                                </DataGridTemplateColumn.CellTemplate>
                            </DataGridTemplateColumn>
                        </DataGrid.Columns>
                    </DataGrid>
                    <!--<ListView Style="{StaticResource DefaultListTable}" ItemsSource="{Binding MarkDatasource}">
                        <ListView.View>
                            <GridView AllowsColumnReorder="False">
                                <GridViewColumn Width="30">
                                    <GridViewColumn.CellTemplate>
                                        <DataTemplate>
                                            <CheckBox VerticalAlignment="Center"
IsChecked="{Binding IsSelected,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
                                        </DataTemplate>
                                    </GridViewColumn.CellTemplate>
                                </GridViewColumn>
                                <GridViewColumn Width="60" Header="#">
                                    <GridViewColumn.CellTemplate>
                                        <DataTemplate>
                                            <Border Style="{StaticResource CellBorder}">
                                                <TextBlock Style="{StaticResource ListTableTextBlock}"  Text="{Binding Converter={StaticResource lvRowNumber},
RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ListViewItem,AncestorLevel=1}}"/>
                                            </Border>
                                        </DataTemplate>
                                    </GridViewColumn.CellTemplate>
                                </GridViewColumn>
                                <GridViewColumn Width="220">
                                    <GridViewColumn.Header>
                                        <TextBlock Text="{DynamicResource Alm.DbAddress}"/>
                                    </GridViewColumn.Header>
                                    <GridViewColumn.CellTemplate>
                                        <DataTemplate>
                                            <Border Style="{StaticResource CellBorder}">
                                                <TextBlock Style="{StaticResource ListTableTextBlock}"
                                                           Text="{Binding DBAddress}"/>
                                            </Border>
                                        </DataTemplate>
                                    </GridViewColumn.CellTemplate>
                                </GridViewColumn>
                                <GridViewColumn Width="180">
                                    <GridViewColumn.Header>
                                        <TextBlock Text="{DynamicResource Alm.StartAddress}"/>
                                    </GridViewColumn.Header>
                                    <GridViewColumn.CellTemplate>
                                        <DataTemplate>
                                            <Border Style="{StaticResource CellBorder}">
                                                <TextBlock Style="{StaticResource ListTableTextBlock}"
   Text="{Binding StartAddress}"/>
                                            </Border>
                                        </DataTemplate>
                                    </GridViewColumn.CellTemplate>
                                </GridViewColumn>
                                <GridViewColumn Width="150">
                                    <GridViewColumn.Header>
                                        <TextBlock Text="{DynamicResource Alm.DbLength}"/>
                                    </GridViewColumn.Header>
                                    <GridViewColumn.CellTemplate>
                                        <DataTemplate>
                                            <Border Style="{StaticResource CellBorder}">
                                                <TextBlock Style="{StaticResource ListTableTextBlock}"
                                                   Text="{Binding Length}"/>
                                            </Border>
                                        </DataTemplate>
                                    </GridViewColumn.CellTemplate>
                                </GridViewColumn>
                                <GridViewColumn Width="200">
                                    <GridViewColumn.Header>
                                        <TextBlock Text="{DynamicResource Alm.Description}"/>
                                    </GridViewColumn.Header>
                                    <GridViewColumn.CellTemplate>
                                        <DataTemplate>
                                            <Border Style="{StaticResource CellBorder}">
                                                <TextBlock Style="{StaticResource ListTableTextBlock}"
                   Text="{Binding Description}"/>
                                            </Border>
                                        </DataTemplate>
                                    </GridViewColumn.CellTemplate>
                                </GridViewColumn>
                                <GridViewColumn Width="120">
                                    <GridViewColumn.Header>
                                        <TextBlock Text="{DynamicResource App.Opt}"/>
                                    </GridViewColumn.Header>
                                    <GridViewColumn.CellTemplate>
                                        <DataTemplate>
                                            <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="10">
                                                <Button x:Name="editBtn" Style="{StaticResource EditLink}"
                Click="Button_Click" Tag="{Binding }"/>
                                                <Button x:Name="delBtn" Style="{StaticResource DeleteLink}"
                Click="Button_Click" Tag="{Binding }"/>
                                            </StackPanel>
                                        </DataTemplate>
                                    </GridViewColumn.CellTemplate>
                                </GridViewColumn>
                            </GridView>
                        </ListView.View>
                    </ListView>-->
                </TabItem>
            </TabControl>
            <!--<Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition />
                </Grid.RowDefinitions>
                <ListBox ItemsSource="{Binding Items}" SelectedIndex="{Binding ItemIndex}" BorderThickness="0">
                    <ListBox.ItemsPanel>
                        <ItemsPanelTemplate>
                            <StackPanel Orientation="Horizontal"/>
                        </ItemsPanelTemplate>
                    </ListBox.ItemsPanel>
                    <ListBox.ItemContainerStyle>
                        <Style TargetType="ListBoxItem">
                            <Setter Property="Margin" Value="0"/>
                            <Setter Property="Padding" Value="0"/>
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate TargetType="ListBoxItem">
                                        <Border x:Name="border" Background="LightGray"
Padding="10" Margin="0" BorderThickness="0">
                                            -->
            <!-- 内容呈现器 -->
            <!--
                                            <StackPanel>
                                                <ContentPresenter  Content="{TemplateBinding Content}"
                   ContentStringFormat="{TemplateBinding ContentStringFormat}"
                   ContentTemplate="{TemplateBinding ContentTemplate}"  />
                                            </StackPanel>
                                        </Border>
                                        <ControlTemplate.Triggers>
                                            <Trigger Property="IsSelected" Value="True">
                                                <Setter TargetName="border" Property="Background" Value="White"/>
                                            </Trigger>
                                        </ControlTemplate.Triggers>
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </ListBox.ItemContainerStyle>
                </ListBox>
                <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right"
                            Margin="0,0,0,10"
                                    dp:StackPanelHelper.ChildSpace="10">
                    <Button Style="{StaticResource AddBtn}" x:Name="addBtn" Click="Button_Click"/>
                    <Button Style="{StaticResource RfhBtn}" x:Name="refreshBtn" Click="Button_Click"/>
                    <Button Style="{StaticResource DelBtn}" x:Name="delBatchBtn" Click="Button_Click"/>
                </StackPanel>
            </Grid>-->
        </Grid>
    </Grid>
</pu:WindowX>
HMI.AlermInfoManage/AlarmSettingList.xaml.cs
New file
@@ -0,0 +1,269 @@
using HMI.Core.Entity;
using HMI.Core.Util;
using HMI.UI.Util;
using HMI.Util;
using HMI.Util.Alarm;
using log4net;
using Microsoft.Win32;
using Panuon.WPF.UI;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace HMI.AlarmInfoManage
{
    /// <summary>
    /// AlarmSettingList.xaml 的交互逻辑
    /// </summary>
    public partial class AlarmSettingList : WindowX
    {
        private ILog log;
        public AlarmSettingList(ILog log)
        {
            BlockDatasource = AlarmUtil.BlockSettings;
            MarkDatasource = AlarmUtil.MarkSettings;
            this.log = log;
            DataContext = this;
            InitializeComponent();
        }
        private ObservableCollection<AlarmSetting> blockDatasource;
        public ObservableCollection<AlarmSetting> BlockDatasource
        {
            get => blockDatasource;
            set { Set(ref blockDatasource, value); }
        }
        private ObservableCollection<AlarmSetting> markDatasource;
        public ObservableCollection<AlarmSetting> MarkDatasource
        {
            get => markDatasource;
            set { Set(ref markDatasource, value); }
        }
        private ObservableCollection<string> items =
            new ObservableCollection<string> { LangUtil.Get("Alm.DbBlockAlarm"), LangUtil.Get("Alm.TagAlarm") };
        public ObservableCollection<string> Items
        {
            get => items;
            set { Set(ref items, value); }
        }
        private int itemIndex;
        public int ItemIndex
        {
            get => itemIndex;
            set
            {
                Set(ref itemIndex, value);
                LoadData();
            }
        }
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Button btn = sender as Button;
            AlarmSetting data = btn?.Tag as AlarmSetting;
            switch (btn.Name)
            {
                case "refreshBtn":
                    LoadData();
                    break;
                case "addBtn":
                    AddData(new AlarmSetting());
                    break;
                case "delBatchBtn":
                    DeleteBatchData();
                    break;
                case "editBtn":
                    EditData(ItemIndex == 0 ? BlockDatasource.IndexOf(data) : MarkDatasource.IndexOf(data), data);
                    break;
                case "exportBtn":
                    ExportData();
                    break;
                case "importBtn":
                    ImportData();
                    break;
                case "delBtn":
                    DeleteData(ItemIndex == 0 ? BlockDatasource.IndexOf(data) : MarkDatasource.IndexOf(data));
                    break;
                default:
                    break;
            }
        }
        private void LoadData()
        {
            BlockDatasource = AlarmUtil.BlockSettings;
            MarkDatasource = AlarmUtil.MarkSettings;
        }
        private void AddData(AlarmSetting data)
        {
            try
            {
                var temp = ItemIndex == 0 ? this.lang("Alm.DbBlockAlarm") : this.lang("Alm.TagAlarm");
                var modal = new AlarmSettingModal(log)
                {
                    Title = $"{this.lang("App.Add")}{temp}",
                    Owner = Window.GetWindow(this),
                };
                if (modal.Open(-1, data, ItemIndex == 0 ? BlockDatasource : MarkDatasource, ItemIndex == 0) == true)
                {
                    LoadData();
                }
            }
            catch (Exception ex)
            {
                log.Error("Add AlarmSetting Failed.", ex);
                ToastHelper.Error(this, $"{this.lang("App.AddFailed")}: {ex.Message}");
            }
        }
        private void EditData(int index, AlarmSetting data)
        {
            try
            {
                var temp = ItemIndex == 0 ? this.lang("Alm.DbBlockAlarm") : this.lang("Alm.TagAlarm");
                var modal = new AlarmSettingModal(log)
                {
                    Title = $"{this.lang("App.Edit")}{temp}",
                    Owner = Window.GetWindow(this),
                };
                if (modal.Open(index, data, ItemIndex == 0 ? BlockDatasource : MarkDatasource, ItemIndex == 0) == true)
                {
                    LoadData();
                }
            }
            catch (Exception ex)
            {
                log.Error("Edit AlarmSetting Failed.", ex);
                ToastHelper.Error(this, $"{this.lang("App.EditFailed")}: {ex.Message}");
            }
        }
        private void ExportData()
        {
            try
            {
                SaveFileDialog sfd = new SaveFileDialog
                {
                    FileName = ItemIndex == 0 ? "DB块报警.xls" : "标志位报警.xls",
                    Filter = "Excel|*.xls"
                };
                if (sfd.ShowDialog() == true)
                {
                    ExcelHelper.ExportExcel(sfd.FileName, ItemIndex == 0 ? BlockDatasource : MarkDatasource);
                }
            }
            catch (Exception ex)
            {
                log.Error("Export Failed.", ex);
                ToastHelper.Error(this, $"{this.lang("App.ExportFailed")}: {ex.Message}");
            }
        }
        private void ImportData()
        {
            try
            {
                OpenFileDialog ofd = new OpenFileDialog
                {
                    Filter = "Excel|*.xls"
                };
                if (ofd.ShowDialog() == true)
                {
                    var datasource = new ObservableCollection<AlarmSetting>(
                        ExcelHelper.ReadFromExcel<AlarmSetting>(ofd.FileName));
                    if (ItemIndex == 0)
                    {
                        AlarmUtil.UpdateBlockSettings(datasource);
                        BlockDatasource = AlarmUtil.BlockSettings;
                    }
                    else
                    {
                        AlarmUtil.UpdateMarkSettings(datasource);
                        MarkDatasource = AlarmUtil.MarkSettings;
                    }
                }
            }
            catch (Exception ex)
            {
                log.Error("Import Failed.", ex);
                ToastHelper.Error(this, $"{this.lang("App.ImportFailed")}: {ex.Message}");
            }
        }
        private void DeleteData(int index)
        {
            try
            {
                if (MsgBoxHelper.WarnMessage(Window.GetWindow(this), this.lang("HMI.MakeSureDeleteThis")) == MessageBoxResult.OK)
                {
                    bool isBlock = ItemIndex == 0;
                    var temp = XmlHelper.DeepClone(isBlock ? BlockDatasource : MarkDatasource);
                    temp.RemoveAt(index);
                    if (isBlock)
                    {
                        AlarmUtil.UpdateBlockSettings(temp);
                    }
                    else
                    {
                        AlarmUtil.UpdateMarkSettings(temp);
                    }
                    LoadData();
                }
            }
            catch (Exception ex)
            {
                log.Error("Delete AlarmSetting Failed.", ex);
                ToastHelper.Error(this, $"{this.lang("App.DeleteFailed")}: {ex.Message}");
            }
        }
        private void DeleteBatchData()
        {
            try
            {
                var temp = (ItemIndex == 0 ? BlockDatasource : MarkDatasource).Where(e => e.IsSelected).ToList();
                if (temp == null || temp.Count <= 0)
                {
                    ToastHelper.Warning(Window.GetWindow(this), this.lang("HMI.NoDeleteData"));
                    return;
                }
                if (MsgBoxHelper.WarnMessage(Window.GetWindow(this), this.lang("HMI.MakeSureDeletes", temp.Count))
                    == MessageBoxResult.OK)
                {
                    bool isBlock = ItemIndex == 0;
                    temp = (isBlock ? BlockDatasource : MarkDatasource).Where(e => !e.IsSelected).ToList();
                    if (isBlock)
                    {
                        AlarmUtil.UpdateBlockSettings(temp);
                    }
                    else
                    {
                        AlarmUtil.UpdateMarkSettings(temp);
                    }
                    LoadData();
                }
            }
            catch (Exception ex)
            {
                log.Error("Delete Batch AlarmSetting Failed.", ex);
                ToastHelper.Error(this, $"{this.lang("App.DeleteFailed")}: {ex.Message}");
            }
        }
        private void WindowX_Loaded(object sender, RoutedEventArgs e)
        {
            ItemIndex = 0;
            Loaded -= WindowX_Loaded;
        }
    }
}
HMI.AlermInfoManage/Entity/AimSetting.cs
New file
@@ -0,0 +1,65 @@
using HMI.Core.Entity;
using HMI.Core.Util;
namespace HMI.AlarmInfoManage.Entity
{
    public class AimSetting
    {
        public static AimSetting Instance { get; private set; }
        public string PlcIp { get; private set; }
        public bool PlcEnabled { get; private set; }
        private AimSetting() { }
        static AimSetting()
        {
            LoadFromXml();
        }
        public static void LoadFromXml()
        {
            try
            {
                string path = PathHelper.ConfigFolder + "\\AimSetting.xml";
                AimSettingModel model = XmlHelper.DeserializeFromXml<AimSettingModel>(path);
                Instance = FromModel(model);
            }
            catch
            {
                Instance = new AimSetting();
            }
        }
        public AimSettingModel GetModel()
        {
            return new AimSettingModel() { PlcIp = PlcIp, PlcEnabled = PlcEnabled };
        }
        public static AimSetting FromModel(AimSettingModel model)
        {
            return new AimSetting() { PlcIp = model.PlcIp, PlcEnabled = model.PlcEnabled };
        }
    }
    public class AimSettingModel : NotifyBase
    {
        private string plcIp;
        public string PlcIp
        {
            get => plcIp;
            set
            {
                Set(ref plcIp, value);
            }
        }
        private bool plcEnabled;
        public bool PlcEnabled
        {
            get { return plcEnabled; }
            set { Set(ref plcEnabled, value); }
        }
    }
}
HMI.AlermInfoManage/HMI.AlarmInfoManage.csproj
New file
@@ -0,0 +1,195 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{945C2577-4558-45E3-891A-096CB3047304}</ProjectGuid>
    <OutputType>library</OutputType>
    <RootNamespace>HMI.AlarmInfoManage</RootNamespace>
    <AssemblyName>HMI.AlarmInfoManage</AssemblyName>
    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
    <WarningLevel>4</WarningLevel>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
    <DebugSymbols>true</DebugSymbols>
    <OutputPath>bin\x64\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <DebugType>full</DebugType>
    <PlatformTarget>x64</PlatformTarget>
    <LangVersion>7.3</LangVersion>
    <ErrorReport>prompt</ErrorReport>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
    <OutputPath>bin\x64\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <Optimize>true</Optimize>
    <DebugType>pdbonly</DebugType>
    <PlatformTarget>x64</PlatformTarget>
    <LangVersion>7.3</LangVersion>
    <ErrorReport>prompt</ErrorReport>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="ICSharpCode.SharpZipLib, Version=1.0.0.999, Culture=neutral, PublicKeyToken=1b03e6acf1164f73, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\SharpZipLib.1.0.0\lib\net45\ICSharpCode.SharpZipLib.dll</HintPath>
    </Reference>
    <Reference Include="log4net, Version=3.0.4.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\log4net.3.0.4\lib\net462\log4net.dll</HintPath>
    </Reference>
    <Reference Include="NPOI, Version=2.4.1.0, Culture=neutral, PublicKeyToken=0df73ec7942b34e1, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\NPOI.2.4.1\lib\net45\NPOI.dll</HintPath>
    </Reference>
    <Reference Include="NPOI.OOXML, Version=2.4.1.0, Culture=neutral, PublicKeyToken=0df73ec7942b34e1, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\NPOI.2.4.1\lib\net45\NPOI.OOXML.dll</HintPath>
    </Reference>
    <Reference Include="NPOI.OpenXml4Net, Version=2.4.1.0, Culture=neutral, PublicKeyToken=0df73ec7942b34e1, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\NPOI.2.4.1\lib\net45\NPOI.OpenXml4Net.dll</HintPath>
    </Reference>
    <Reference Include="NPOI.OpenXmlFormats, Version=2.4.1.0, Culture=neutral, PublicKeyToken=0df73ec7942b34e1, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\NPOI.2.4.1\lib\net45\NPOI.OpenXmlFormats.dll</HintPath>
    </Reference>
    <Reference Include="Panuon.WPF, Version=1.1.2.2, Culture=neutral, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\Panuon.WPF.1.1.3\lib\net472\Panuon.WPF.dll</HintPath>
    </Reference>
    <Reference Include="Panuon.WPF.UI, Version=1.3.0.2, Culture=neutral, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\Panuon.WPF.UI.1.3.0.2\lib\net472\Panuon.WPF.UI.dll</HintPath>
    </Reference>
    <Reference Include="System" />
    <Reference Include="System.Configuration" />
    <Reference Include="System.Data" />
    <Reference Include="System.Web" />
    <Reference Include="System.Xml" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Xaml">
      <RequiredTargetFramework>4.0</RequiredTargetFramework>
    </Reference>
    <Reference Include="WindowsBase" />
    <Reference Include="PresentationCore" />
    <Reference Include="PresentationFramework" />
  </ItemGroup>
  <ItemGroup>
    <Page Include="SimpleAlarm.xaml">
      <Generator>MSBuild:Compile</Generator>
      <SubType>Designer</SubType>
    </Page>
    <Page Include="AlarmInfoManage.xaml">
      <Generator>MSBuild:Compile</Generator>
      <SubType>Designer</SubType>
    </Page>
    <Compile Include="SimpleAlarm.xaml.cs">
      <DependentUpon>SimpleAlarm.xaml</DependentUpon>
    </Compile>
    <Compile Include="AlarmInfoManage.xaml.cs">
      <DependentUpon>AlarmInfoManage.xaml</DependentUpon>
      <SubType>Code</SubType>
    </Compile>
    <Page Include="AlarmInfoManage_SettingA.xaml">
      <SubType>Designer</SubType>
      <Generator>MSBuild:Compile</Generator>
    </Page>
    <Page Include="AlarmSettingList.xaml">
      <SubType>Designer</SubType>
      <Generator>MSBuild:Compile</Generator>
    </Page>
    <Page Include="Modal\AlarmSettingModal.xaml">
      <Generator>MSBuild:Compile</Generator>
      <SubType>Designer</SubType>
    </Page>
  </ItemGroup>
  <ItemGroup>
    <Compile Include="AlarmInfoManage_SettingA.xaml.cs">
      <DependentUpon>AlarmInfoManage_SettingA.xaml</DependentUpon>
    </Compile>
    <Compile Include="AlarmSettingList.xaml.cs">
      <DependentUpon>AlarmSettingList.xaml</DependentUpon>
    </Compile>
    <Compile Include="Entity\AimSetting.cs" />
    <Compile Include="Modal\AlarmSettingModal.xaml.cs">
      <DependentUpon>AlarmSettingModal.xaml</DependentUpon>
    </Compile>
    <Compile Include="Model\XmlModel.cs" />
    <Compile Include="Model\UIModel.cs" />
    <Compile Include="Properties\AssemblyInfo.cs">
      <SubType>Code</SubType>
    </Compile>
    <Compile Include="Properties\Resources.Designer.cs">
      <AutoGen>True</AutoGen>
      <DesignTime>True</DesignTime>
      <DependentUpon>Resources.resx</DependentUpon>
    </Compile>
    <Compile Include="Properties\Settings.Designer.cs">
      <AutoGen>True</AutoGen>
      <DependentUpon>Settings.settings</DependentUpon>
      <DesignTimeSharedInput>True</DesignTimeSharedInput>
    </Compile>
    <EmbeddedResource Include="Properties\Resources.resx">
      <Generator>ResXFileCodeGenerator</Generator>
      <LastGenOutput>Resources.Designer.cs</LastGenOutput>
    </EmbeddedResource>
    <None Include="packages.config" />
    <None Include="Properties\Settings.settings">
      <Generator>SettingsSingleFileGenerator</Generator>
      <LastGenOutput>Settings.Designer.cs</LastGenOutput>
    </None>
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\S7Driver\S7Driver.csproj">
      <Project>{628ef12b-9ed2-4b20-950f-dc3ecc3fd15f}</Project>
      <Name>S7Driver</Name>
    </ProjectReference>
    <ProjectReference Include="..\ConnectionUtil\ConnectionUtil.csproj">
      <Project>{d0806fa6-3f6d-4304-81b8-ead9da7cb5b2}</Project>
      <Name>ConnectionUtil</Name>
    </ProjectReference>
    <ProjectReference Include="..\HMI.Base\HMI.Base.csproj">
      <Project>{2a6f9720-a890-4036-9d73-41a0dae28341}</Project>
      <Name>HMI.Base</Name>
    </ProjectReference>
    <ProjectReference Include="..\HMI.Core\HMI.Core.csproj">
      <Project>{3e3bf44a-0b69-45c5-9512-528cb48a7106}</Project>
      <Name>HMI.Core</Name>
    </ProjectReference>
    <ProjectReference Include="..\HMI.UI\HMI.UI.csproj">
      <Project>{f17de6b5-af1d-48c6-94a0-6b763a6546a6}</Project>
      <Name>HMI.UI</Name>
    </ProjectReference>
    <ProjectReference Include="..\HMI.Util\HMI.Util.csproj">
      <Project>{AFEA100A-9223-448D-AB10-3015D93DD98C}</Project>
      <Name>HMI.Util</Name>
    </ProjectReference>
    <ProjectReference Include="..\OPCClient\OPCClient.csproj">
      <Project>{E5740BB5-9FC6-4A20-B7C2-CC90C281ED05}</Project>
      <Name>OPCClient</Name>
    </ProjectReference>
  </ItemGroup>
  <ItemGroup />
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  <PropertyGroup>
    <PostBuildEvent>
    </PostBuildEvent>
  </PropertyGroup>
</Project>
HMI.AlermInfoManage/Modal/AlarmSettingModal.xaml
New file
@@ -0,0 +1,58 @@
<pu:WindowX x:Class="HMI.AlarmInfoManage.AlarmSettingModal"
            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:local="clr-namespace:HMI.AlarmInfoManage"
            xmlns:pw="https://opensource.panuon.com/wpf"
            xmlns:pu="https://opensource.panuon.com/wpf-ui"
            xmlns:uic="clr-namespace:HMI.UI.Converter;assembly=HMI.UI"
            xmlns:ui="clr-namespace:HMI.UI;assembly=HMI.UI"
            xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
            mc:Ignorable="d"
            Title="Form"
            SizeToContent="Height"
            Width="600"
            WindowStartupLocation="CenterOwner"
            BorderBrush="#3E3E45"
            BorderThickness="1"
            Foreground="Black"
            Style="{StaticResource ModalWindow}">
    <Grid Margin="10">
        <StackPanel x:Name="formPanel">
            <pu:FormGroup Header="{DynamicResource Alm.DbAddress}"
                dp:FormGroupHelper.IsRequired="True"
                                dp:FormGroupHelper.ValidateMethod="ValidateDBAddress"
                           GroupName="MyFormGroup">
                <TextBox Style="{StaticResource EditorTextBoxStyle}"
                                x:Name="dbAddress"/>
            </pu:FormGroup>
            <pu:FormGroup Header="{DynamicResource Alm.StartAddress}" dp:FormGroupHelper.IsRequired="True"
                          GroupName="MyFormGroup">
                <TextBox Style="{StaticResource EditorTextBoxStyle}"
                         x:Name="startAddress"/>
            </pu:FormGroup>
            <pu:FormGroup Header="{DynamicResource Alm.DbLength}" dp:FormGroupHelper.IsRequired="True"
                          GroupName="MyFormGroup">
                <pu:NumberInput Style="{StaticResource FormNumberInput}" x:Name="length" Interval="1"/>
            </pu:FormGroup>
            <pu:FormGroup Header="报警代码" dp:FormGroupHelper.IsRequired="True" x:Name="FgCode"
              GroupName="MyFormGroup">
                <TextBox Style="{StaticResource EditorTextBoxStyle}" x:Name="TbCode"/>
            </pu:FormGroup>
            <pu:FormGroup Header="级别" dp:FormGroupHelper.IsRequired="True" x:Name="FgLevel"
   GroupName="MyFormGroup">
                <pu:NumberInput Style="{StaticResource FormNumberInput}" x:Name="NiLevel"/>
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgDesc" Header="{DynamicResource Alm.Description}" dp:FormGroupHelper.IsRequired="True"
              GroupName="MyFormGroup">
                <TextBox Style="{StaticResource EditorTextBoxStyle}" x:Name="TbDesc"/>
            </pu:FormGroup>
        </StackPanel>
    </Grid>
</pu:WindowX>
HMI.AlermInfoManage/Modal/AlarmSettingModal.xaml.cs
New file
@@ -0,0 +1,156 @@
using HMI.Core.Entity;
using HMI.Core.Util;
using HMI.UI.DpUtil;
using HMI.UI.Util;
using HMI.Util.Alarm;
using log4net;
using Panuon.WPF.UI;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows;
namespace HMI.AlarmInfoManage
{
    /// <summary>
    /// ModalWindow.xaml 的交互逻辑
    /// </summary>
    public partial class AlarmSettingModal : WindowX
    {
        public AlarmSettingModal(ILog log)
        {
            this.log = log;
            this.DataContext = this;
            InitializeComponent();
        }
        private ILog log;
        private bool isBlock;
        public int DataIndex { get; private set; }
        public AlarmSetting Data { get; private set; }
        public ObservableCollection<AlarmSetting> Datasource { get; private set; }
        private bool _isProcessing;
        protected override void OnClosing(CancelEventArgs e)
        {
            //Press Continue Button
            if (DialogResult == true)
            {
                if (_isProcessing)
                {
                    e.Cancel = true;
                    return;
                }
                var okButton = WindowXModalDialog.GetOKButton(this);
                try
                {
                    _isProcessing = true;
                    ButtonHelper.SetIsPending(okButton, true);
                    if (!ValidateForm())
                    {
                        e.Cancel = true;
                        return;
                    }
                    SaveOrUpdateData();
                }
                catch (Exception ex)
                {
                    log.Error("AlarmSetting Form Submit Failed.", ex);
                    ToastHelper.Error(this, $"Error: {ex.Message}");
                    e.Cancel = true;
                    return;
                }
                finally
                {
                    _isProcessing = false;
                    ButtonHelper.SetIsPending(okButton, false);
                }
            }
            base.OnClosing(e);
        }
        public bool ValidateDBAddress(object value, out string err)
        {
            err = null;
            var target = Datasource?.FirstOrDefault(e => e.DBAddress == value);
            if (target != null && target != Data)
            {
                err = this.lang("App.MustBeUnique", this.lang("Alm.DbAddress"));
                return false;
            }
            return true;
        }
        private bool ValidateForm()
        {
            bool flag = FormGroupHelper.ValidateForm(formPanel);
            if (flag)
            {
                Data.DBAddress = dbAddress.Text;
                Data.StartAddress = startAddress.Text;
                Data.Length = Convert.ToInt32(length.Value);
                Data.AlarmCode = TbCode.Text;
                Data.Level = NiLevel.Value ?? 0d;
                Data.Description = TbDesc.Text;
            }
            return flag;
        }
        public bool? Open(int index, AlarmSetting data,
            ObservableCollection<AlarmSetting> dataSource, bool isBlock)
        {
            this.isBlock = isBlock;
            FgCode.Visibility = isBlock ? Visibility.Visible : Visibility.Collapsed;
            DataIndex = index;
            Data = data;
            Datasource = dataSource;
            dbAddress.Text = Data.DBAddress;
            startAddress.Text = Data.StartAddress;
            length.Value = Data.Length;
            TbCode.Text = Data.AlarmCode;
            NiLevel.Value = Data.Level;
            TbDesc.Text = Data.Description;
            return this.ShowDialog();
        }
        private void SaveOrUpdateData()
        {
            if (DataIndex == -1)
            {
                InsertData(Data);
            }
            else
            {
                UpdateData(DataIndex, Data);
            }
        }
        public void InsertData(AlarmSetting data)
        {
            Datasource.Add(data);
            if (this.isBlock)
            {
                AlarmUtil.UpdateBlockSettings(Datasource);
            }
            else
            {
                AlarmUtil.UpdateMarkSettings(Datasource);
            }
        }
        public void UpdateData(int index, AlarmSetting data)
        {
            Datasource[index] = data;
            if (this.isBlock)
            {
                AlarmUtil.UpdateBlockSettings(Datasource);
            }
            else
            {
                AlarmUtil.UpdateMarkSettings(Datasource);
            }
        }
    }
}
HMI.AlermInfoManage/Model/UIModel.cs
New file
@@ -0,0 +1,37 @@
using HMI.AlarmInfoManage.Entity;
using HMI.Core.Entity;
using HMI.Core.Util;
using System.Collections.ObjectModel;
using System.Data;
using System.IO;
namespace HMI.AlarmInfoManage.Model
{
    public class UIModel : NotifyBase
    {
        public SqliteHelper sqliteHelper;
        private ObservableCollection<AlarmSetting> datasource;
        public ObservableCollection<AlarmSetting> Datasource
        {
            get { return datasource; }
            set { Set(ref datasource, value); }
        }
        public UIModel(string filePath)
        {
            if (!File.Exists(filePath))
            {
                SqliteHelper.CreateDatabase(filePath);
            }
            sqliteHelper = new SqliteHelper(filePath);
        }
        public void LoadData() {
            ObservableCollection<AlarmSetting> list = new ObservableCollection<AlarmSetting>();
            DataTable dt = sqliteHelper.GetDataTable("SELECT * FROM alerm_setting;");
        }
    }
}
HMI.AlermInfoManage/Model/XmlModel.cs
New file
@@ -0,0 +1,21 @@
using HMI.Core.Entity;
using System.IO;
namespace HMI.AlarmInfoManage.Model
{
    public class XmlUIModel : NotifyBase
    {
        public string FileFolder { get; private set; }
        public string AlermTextPath { get; private set; }
        public string AlermSettingPath { get; private set; }
        public XmlUIModel(string FileFolder)
        {
            this.FileFolder = FileFolder;
            this.AlermTextPath = Path.Combine(FileFolder, "AlermText.xml");
            this.AlermSettingPath = Path.Combine(FileFolder, "AlermSetting.xml");
        }
    }
}
HMI.AlermInfoManage/Properties/AssemblyInfo.cs
New file
@@ -0,0 +1,54 @@
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
using HMI.Core.Annotation;
// 有关程序集的一般信息由以下
// 控制。更改这些特性值可修改
// 与程序集关联的信息。
[assembly: AssemblyTitle("HMI.AlarmInfoManage")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Organization")]
[assembly: AssemblyProduct("HMI.AlarmInfoManage")]
[assembly: AssemblyCopyright("Copyright © Organization 2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 将 ComVisible 设置为 false 会使此程序集中的类型
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
//请将此类型的 ComVisible 特性设置为 true。
[assembly: ComVisible(false)]
//若要开始生成可本地化的应用程序,请设置
//.csproj 文件中的 <UICulture>CultureYouAreCodingWith</UICulture>
//在 <PropertyGroup> 中。例如,如果你使用的是美国英语。
//使用的是美国英语,请将 <UICulture> 设置为 en-US。  然后取消
//对以下 NeutralResourceLanguage 特性的注释。  更新
//以下行中的“en-US”以匹配项目文件中的 UICulture 设置。
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
[assembly:ThemeInfo(
    ResourceDictionaryLocation.None, //主题特定资源词典所处位置
                             //(未在页面中找到资源时使用,
                             //或应用程序资源字典中找到时使用)
    ResourceDictionaryLocation.SourceAssembly //常规资源词典所处位置
                                      //(未在页面中找到资源时使用,
                                      //、应用程序或任何主题专用资源字典中找到时使用)
)]
// 程序集的版本信息由下列四个值组成:
//
//      主版本
//      次版本
//      生成号
//      修订号
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: PluginClassName("HMI.AlarmInfoManage.AlarmInfoManage")]
HMI.AlermInfoManage/Properties/Resources.Designer.cs
New file
@@ -0,0 +1,63 @@
//------------------------------------------------------------------------------
// <auto-generated>
//     此代码由工具生成。
//     运行时版本:4.0.30319.42000
//
//     对此文件的更改可能会导致不正确的行为,并且如果
//     重新生成代码,这些更改将会丢失。
// </auto-generated>
//------------------------------------------------------------------------------
namespace HMI.AlarmInfoManage.Properties {
    using System;
    /// <summary>
    ///   一个强类型的资源类,用于查找本地化的字符串等。
    /// </summary>
    // 此类是由 StronglyTypedResourceBuilder
    // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
    // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
    // (以 /str 作为命令选项),或重新生成 VS 项目。
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
    internal class Resources {
        private static global::System.Resources.ResourceManager resourceMan;
        private static global::System.Globalization.CultureInfo resourceCulture;
        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        internal Resources() {
        }
        /// <summary>
        ///   返回此类使用的缓存的 ResourceManager 实例。
        /// </summary>
        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
        internal static global::System.Resources.ResourceManager ResourceManager {
            get {
                if (object.ReferenceEquals(resourceMan, null)) {
                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("HMI.AlarmInfoManage.Properties.Resources", typeof(Resources).Assembly);
                    resourceMan = temp;
                }
                return resourceMan;
            }
        }
        /// <summary>
        ///   重写当前线程的 CurrentUICulture 属性,对
        ///   使用此强类型资源类的所有资源查找执行重写。
        /// </summary>
        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
        internal static global::System.Globalization.CultureInfo Culture {
            get {
                return resourceCulture;
            }
            set {
                resourceCulture = value;
            }
        }
    }
}
HMI.AlermInfoManage/Properties/Resources.resx
New file
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
  <!--
    Microsoft ResX Schema
    Version 2.0
    The primary goals of this format is to allow a simple XML format
    that is mostly human readable. The generation and parsing of the
    various data types are done through the TypeConverter classes
    associated with the data types.
    Example:
    ... ado.net/XML headers & schema ...
    <resheader name="resmimetype">text/microsoft-resx</resheader>
    <resheader name="version">2.0</resheader>
    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
        <value>[base64 mime encoded serialized .NET Framework object]</value>
    </data>
    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
        <comment>This is a comment</comment>
    </data>
    There are any number of "resheader" rows that contain simple
    name/value pairs.
    Each data row contains a name, and value. The row also contains a
    type or mimetype. Type corresponds to a .NET class that support
    text/value conversion through the TypeConverter architecture.
    Classes that don't support this are serialized and stored with the
    mimetype set.
    The mimetype is used for serialized objects, and tells the
    ResXResourceReader how to depersist the object. This is currently not
    extensible. For a given mimetype the value must be set accordingly:
    Note - application/x-microsoft.net.object.binary.base64 is the format
    that the ResXResourceWriter will generate, however the reader can
    read any of the formats listed below.
    mimetype: application/x-microsoft.net.object.binary.base64
    value   : The object must be serialized with
            : System.Serialization.Formatters.Binary.BinaryFormatter
            : and then encoded with base64 encoding.
    mimetype: application/x-microsoft.net.object.soap.base64
    value   : The object must be serialized with
            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
            : and then encoded with base64 encoding.
    mimetype: application/x-microsoft.net.object.bytearray.base64
    value   : The object must be serialized into a byte array
            : using a System.ComponentModel.TypeConverter
            : and then encoded with base64 encoding.
    -->
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" />
              <xsd:attribute name="type" type="xsd:string" />
              <xsd:attribute name="mimetype" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string" />
              <xsd:attribute name="name" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" />
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
</root>
HMI.AlermInfoManage/Properties/Settings.Designer.cs
New file
@@ -0,0 +1,26 @@
//------------------------------------------------------------------------------
// <auto-generated>
//     此代码由工具生成。
//     运行时版本:4.0.30319.42000
//
//     对此文件的更改可能会导致不正确的行为,并且如果
//     重新生成代码,这些更改将会丢失。
// </auto-generated>
//------------------------------------------------------------------------------
namespace HMI.AlarmInfoManage.Properties {
    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.13.0.0")]
    internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
        private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
        public static Settings Default {
            get {
                return defaultInstance;
            }
        }
    }
}
HMI.AlermInfoManage/Properties/Settings.settings
New file
@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
  <Profiles>
    <Profile Name="(Default)" />
  </Profiles>
  <Settings />
</SettingsFile>
HMI.AlermInfoManage/SimpleAlarm.xaml
New file
@@ -0,0 +1,96 @@
<hb:PluginControl x:Class="HMI.AlarmInfoManage.SimpleAlarm"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base"
             xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
             xmlns:local="clr-namespace:HMI.AlarmInfoManage"
             xmlns:uic="clr-namespace:HMI.UI.Converter;assembly=HMI.UI"
             xmlns:pw="https://opensource.panuon.com/wpf"
             xmlns:pu="https://opensource.panuon.com/wpf-ui"
             mc:Ignorable="d"
             Background="{DynamicResource WorkspaceBackground}"
             Loaded="UserControl_Loaded"
             d:DesignHeight="450" d:DesignWidth="800">
    <Border Style="{StaticResource CardBoarder}">
        <DockPanel>
            <DockPanel Margin="0,0,0,10" DockPanel.Dock="Top">
                <StackPanel DockPanel.Dock="Right" Style="{StaticResource ButtonGroup}">
                    <Button Style="{StaticResource RfhBtn}" x:Name="refreshBtn" Click="Button_Click"/>
                    <Button Style="{StaticResource ExportBtn}" x:Name="exportBtn" Click="Button_Click"/>
                </StackPanel>
                <TextBlock DockPanel.Dock="Left" FontSize="{StaticResource TitleFontSize}"
                           VerticalAlignment="Center" HorizontalAlignment="Left" Text="{DynamicResource Alm.Title}"/>
            </DockPanel>
            <pu:Pagination DockPanel.Dock="Bottom" Margin="0,10,0,0" Height="30"
                       FontSize="14"
HorizontalAlignment="Left"
Foreground="Black"
ItemsBackground="Transparent"
ItemsCornerRadius="12"
ItemsSelectedBackground="#36AAF7" CurrentPage="1"  MaxPage="10"/>
            <DataGrid x:Name="table" Style="{StaticResource DefaultDataGrid}" ItemsSource="{Binding Datasource}">
                <DataGrid.Columns>
                    <DataGridTemplateColumn Header="" Width="Auto" CanUserResize="False">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <CheckBox VerticalAlignment="Center" IsChecked="{Binding Selected,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTemplateColumn Header="#" CanUserResize="False">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <TextBlock Text="{Binding Converter={StaticResource rowNumber},
                    RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DataGridRow},AncestorLevel=1}}"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTextColumn Width="*" Binding="{Binding Source}">
                        <DataGridTextColumn.Header>
                            <TextBlock Text="{DynamicResource Alm.Source}"/>
                        </DataGridTextColumn.Header>
                    </DataGridTextColumn>
                    <DataGridTextColumn Width="*" Binding="{Binding Code}">
                        <DataGridTextColumn.Header>
                            <TextBlock Text="{DynamicResource Alm.Code}"/>
                        </DataGridTextColumn.Header>
                    </DataGridTextColumn>
                    <DataGridTextColumn Width="*" Binding="{Binding Info}">
                        <DataGridTextColumn.Header>
                            <TextBlock Text="{DynamicResource Alm.Info}"/>
                        </DataGridTextColumn.Header>
                    </DataGridTextColumn>
                    <DataGridTextColumn Width="*" Binding="{Binding Time}">
                        <DataGridTextColumn.Header>
                            <TextBlock Text="{DynamicResource Alm.Time}"/>
                        </DataGridTextColumn.Header>
                    </DataGridTextColumn>
                    <!--<DataGridTemplateColumn Width="100" IsReadOnly="True" CanUserResize="False">
                        <DataGridTemplateColumn.Header>
                            <TextBlock Text="{DynamicResource App.Opt}"/>
                        </DataGridTemplateColumn.Header>
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <StackPanel Orientation="Horizontal">
                                    <Button x:Name="editBtn" Style="{StaticResource EditLink}"
                            Margin="0,0,10,0"
                            Click="Button_Click"
                            DataContext="{Binding }"/>
                                    <Button x:Name="delBtn" Style="{StaticResource DeleteLink}"
                            DataContext="{Binding }"
                            Click="Button_Click"/>
                                </StackPanel>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>-->
                </DataGrid.Columns>
            </DataGrid>
        </DockPanel>
    </Border>
</hb:PluginControl>
HMI.AlermInfoManage/SimpleAlarm.xaml.cs
New file
@@ -0,0 +1,100 @@
using HMI.Base;
using HMI.Core.Util;
using System;
using System.IO;
using System.Timers;
using System.Windows;
using System.Windows.Controls;
namespace HMI.AlarmInfoManage
{
    /// <summary>
    /// UserControl1.xaml 的交互逻辑
    /// </summary>
    public partial class SimpleAlarm : PluginControl
    {
        private string dbPath;
        private SqliteHelper sqliteHelper;
        private Timer MonitorTimer;
        public SimpleAlarm()
        {
            DataContext = this;
            InitializeComponent();
        }
        public override void InitFinish()
        {
            dbPath = Path.Combine(BaseFolder, "alarm.db");
            if (!File.Exists(dbPath))
            {
                SqliteHelper.CreateDatabase(dbPath);
                sqliteHelper = new SqliteHelper(dbPath);
                sqliteHelper.ExecuteSql(@"CREATE TABLE IF NOT EXISTS Alarm (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    source TEXT NOT NULL,
                    alarmcode TEXT,
                    alarminfo TEXT,
                    time DATETIME);");
            }
        }
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Button btn = sender as Button;
            switch (btn.Name)
            {
                case "refreshBtn":
                    //Model.
                    break;
                case "exportBtn":
                    //ImportText();
                    break;
                default:
                    break;
            }
        }
        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            MonitorTimer = new Timer
            {
                Interval = 100,
            };
            MonitorTimer.Elapsed += MonitorTimer_Elapsed;
            Loaded -= UserControl_Loaded;
        }
        private void MonitorTimer_Elapsed(object sender, ElapsedEventArgs e)
        {
            try
            {
                this.MonitorTimer.Enabled = false;
                this.RefershAlarm();
            }
            catch (Exception ex)
            {
                log.Error("sdadsa", ex);
            }
            finally
            {
                this.MonitorTimer.Enabled = true;
            }
        }
        /// <summary>
        /// 刷新报警信息
        /// </summary>
        private void RefershAlarm()
        {
        }
        public override void SetBaseFolder(string folder)
        {
            base.SetBaseFolder(folder);
        }
    }
}
HMI.AlermInfoManage/packages.config
New file
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="log4net" version="3.0.4" targetFramework="net472" />
  <package id="NPOI" version="2.4.1" targetFramework="net472" />
  <package id="Panuon.WPF" version="1.1.3" targetFramework="net472" />
  <package id="Panuon.WPF.UI" version="1.3.0.2" targetFramework="net472" />
  <package id="SharpZipLib" version="1.0.0" targetFramework="net472" />
</packages>
HMI.Base/DtConverter.cs
New file
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Reflection;
namespace HMI.Base
{
    public class DtConverter
    {
        public static void DoConvert<T>(DataTable dataTable, ICollection<T> target) where T : new()
        {
            var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
            foreach (DataRow row in dataTable.Rows)
            {
                var item = new T();
                foreach (var property in properties)
                {
                    if (dataTable.Columns.Contains(property.Name) && row[property.Name] != DBNull.Value)
                    {
                        property.SetValue(item, Convert.ChangeType(row[property.Name], property.PropertyType));
                    }
                }
                target.Add(item);
            }
        }
    }
}
HMI.Base/HMI.Base.csproj
New file
@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{2A6F9720-A890-4036-9D73-41A0DAE28341}</ProjectGuid>
    <OutputType>library</OutputType>
    <RootNamespace>HMI.Base</RootNamespace>
    <AssemblyName>HMI.Base</AssemblyName>
    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
    <WarningLevel>4</WarningLevel>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
    <DebugSymbols>true</DebugSymbols>
    <OutputPath>bin\x64\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <DebugType>full</DebugType>
    <PlatformTarget>x64</PlatformTarget>
    <LangVersion>7.3</LangVersion>
    <ErrorReport>prompt</ErrorReport>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
    <OutputPath>bin\x64\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <Optimize>true</Optimize>
    <DebugType>pdbonly</DebugType>
    <PlatformTarget>x64</PlatformTarget>
    <LangVersion>7.3</LangVersion>
    <ErrorReport>prompt</ErrorReport>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="log4net, Version=3.0.4.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\log4net.3.0.4\lib\net462\log4net.dll</HintPath>
    </Reference>
    <Reference Include="System" />
    <Reference Include="System.Configuration" />
    <Reference Include="System.Data" />
    <Reference Include="System.Web" />
    <Reference Include="System.Xml" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Xaml">
      <RequiredTargetFramework>4.0</RequiredTargetFramework>
    </Reference>
    <Reference Include="WindowsBase" />
    <Reference Include="PresentationCore" />
    <Reference Include="PresentationFramework" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="DtConverter.cs" />
    <Compile Include="PluginMaskControl.cs" />
    <Compile Include="NotifyControl.cs" />
    <Compile Include="SimpleControl.cs" />
    <Compile Include="PluginControl.cs" />
    <Compile Include="PluginInfo.cs" />
    <Compile Include="PluginLoader.cs" />
    <Compile Include="Properties\AssemblyInfo.cs">
      <SubType>Code</SubType>
    </Compile>
    <Compile Include="Properties\Resources.Designer.cs">
      <AutoGen>True</AutoGen>
      <DesignTime>True</DesignTime>
      <DependentUpon>Resources.resx</DependentUpon>
    </Compile>
    <Compile Include="Properties\Settings.Designer.cs">
      <AutoGen>True</AutoGen>
      <DependentUpon>Settings.settings</DependentUpon>
      <DesignTimeSharedInput>True</DesignTimeSharedInput>
    </Compile>
    <Compile Include="SimpleMaskControl.cs" />
    <EmbeddedResource Include="Properties\Resources.resx">
      <Generator>ResXFileCodeGenerator</Generator>
      <LastGenOutput>Resources.Designer.cs</LastGenOutput>
    </EmbeddedResource>
    <None Include="packages.config" />
    <None Include="Properties\Settings.settings">
      <Generator>SettingsSingleFileGenerator</Generator>
      <LastGenOutput>Settings.Designer.cs</LastGenOutput>
    </None>
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\HMI.Core\HMI.Core.csproj">
      <Project>{3e3bf44a-0b69-45c5-9512-528cb48a7106}</Project>
      <Name>HMI.Core</Name>
    </ProjectReference>
    <ProjectReference Include="..\HMI.UI\HMI.UI.csproj">
      <Project>{f17de6b5-af1d-48c6-94a0-6b763a6546a6}</Project>
      <Name>HMI.UI</Name>
    </ProjectReference>
  </ItemGroup>
  <ItemGroup>
    <Page Include="MaskControl.xaml">
      <SubType>Designer</SubType>
      <Generator>MSBuild:Compile</Generator>
    </Page>
    <Page Include="Themes\Generic.xaml">
      <Generator>MSBuild:Compile</Generator>
      <SubType>Designer</SubType>
    </Page>
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
HMI.Base/MaskControl.xaml
New file
@@ -0,0 +1,32 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:ui="clr-namespace:HMI.UI;assembly=HMI.UI"
             xmlns:local="clr-namespace:HMI.Base">
    <Style x:Key="simpleMaskControl" TargetType="local:SimpleMaskControl">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:SimpleMaskControl">
                    <Grid>
                        <ContentPresenter Content="{TemplateBinding Content}"/>
                        <ui:MaskControl x:Name="mask"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <Style x:Key="pluginMaskControl" TargetType="local:PluginMaskControl">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:PluginMaskControl">
                    <Grid>
                        <ContentPresenter Content="{TemplateBinding Content}"/>
                        <ui:MaskControl x:Name="mask"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>
HMI.Base/NotifyControl.cs
New file
@@ -0,0 +1,28 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Controls;
namespace HMI.Base
{
    public class NotifyControl : UserControl, INotifyPropertyChanged
    {
        #region ...
        public event PropertyChangedEventHandler PropertyChanged;
        protected void Set([CallerMemberName] string name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
        protected void Set<T>(ref T old, T @new, [CallerMemberName] string propertyName = null)
        {
            old = @new;
            if (propertyName != null)
            {
                Set(propertyName);
            }
        }
        #endregion
    }
}
HMI.Base/PluginControl.cs
New file
@@ -0,0 +1,89 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Controls;
using HMI.Core.Api;
using log4net;
namespace HMI.Base
{
    public class PluginControl : UserControl, IPlugin, INotifyPropertyChanged
    {
        #region ...
        public event PropertyChangedEventHandler PropertyChanged;
        protected void Set([CallerMemberName] string name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
        protected void Set<T>(ref T old, T @new, [CallerMemberName] string propertyName = null)
        {
            old = @new;
            if (propertyName != null)
            {
                Set(propertyName);
            }
        }
        #endregion
        protected ILog log;
        protected string BaseFolder;
        public virtual void Active()
        {
        }
        public virtual void DeActive()
        {
        }
        public virtual void Init()
        {
        }
        public virtual void OnTabChange(object sender, object args)
        {
        }
        public virtual void SetBaseFolder(string folder)
        {
            BaseFolder = folder;
        }
        public virtual void SetLogger(ILog logger)
        {
            log = logger;
        }
        public virtual void InitFinish()
        {
        }
        public virtual void OnClose(object sender, CancelEventArgs e)
        {
        }
        public virtual void DoClose(object sender, EventArgs e)
        {
        }
        public virtual bool CanActive()
        {
            return true;
        }
        public virtual bool CanDeActive()
        {
            return true;
        }
    }
}
HMI.Base/PluginInfo.cs
New file
@@ -0,0 +1,28 @@
using HMI.Core.Api;
using System.Collections.ObjectModel;
using System.Windows.Media.Imaging;
namespace HMI.Base
{
    public class PluginInfo
    {
        public PluginInfo()
        {
        }
        public bool IsFont { get; set; }
        public string Code { get; set; }
        public string Name { get; set; }
        public string Type { get; set; }
        public string ParentCode { get; set; }
        public string DllPath { get; set; }
        public string ClassName { get; set; }
        public ObservableCollection<PluginInfo> Children { get; set; } = new ObservableCollection<PluginInfo>();
        public string IconFont { get; set; }
        public BitmapImage _Icon { get; set; }
        public object Content;
        public object SettingControl;
    }
}
HMI.Base/PluginLoader.cs
New file
@@ -0,0 +1,234 @@
using HMI.Core.Api;
using HMI.Core.Entity;
using HMI.Core.Util;
using log4net;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Threading;
using System.Windows;
using System.Windows.Threading;
namespace HMI.Base
{
    public class PluginLoader
    {
        private static ILog log = LogManager.GetLogger("PluginLoader");
        private static string PluginFolder = PathHelper.PluginFolder();
        public SqliteHelper sqliteHelper;
        public List<PluginInfo> Menus { get; private set; }
        public List<PluginInfo> Plugins { get; private set; }
        public static PluginLoader Instance { get; private set; }
        public event EventHandler<LoadMsg> LoadMessageAppend;
        private PluginLoader()
        {
            sqliteHelper = new SqliteHelper(PathHelper.DatabasePath);
        }
        public static void Reset()
        {
            if (Instance != null)
            {
                Instance = null;
            }
            GC.Collect();
        }
        public static PluginLoader GetLoader()
        {
            if (Instance == null)
            {
                Instance = new PluginLoader();
            }
            return Instance;
        }
        private IPluginContainer pluginContainer;
        public void LoadMenuAndPlugin(IPluginContainer container, Dispatcher dispatcher)
        {
            pluginContainer = container;
            if (Menus == null)
            {
                Plugins = new List<PluginInfo>();
                Menus = LoadTreeMenu(dispatcher);
                sqliteHelper.Close();
                LoadMessageAppend?.Invoke(this, new LoadMsg { Type = LoadMsgType.FINISHED });
            }
            pluginContainer.DoCloseEvent += PluginContainer_DoCloseEvent;
        }
        private void PluginContainer_DoCloseEvent(object sender, EventArgs e)
        {
            Reset();
        }
        private void LoadChildrenMenu()
        {
            foreach (var menu in Plugins)
            {
                CountdownEvent c = new CountdownEvent(1);
                Thread.Sleep(50);
                Application.Current.Dispatcher.Invoke(new Action(() =>
                {
                    try
                    {
                        var start = DateTime.Now;
                        log.Info(this.lang("App.IsLoadingMenu", menu.Name));
                        LoadMessageAppend?.Invoke(this,
                            new LoadMsg
                            {
                                Type = LoadMsgType.LOADING,
                                Message = this.lang("App.IsLoadingMenu", menu.Name)
                            }
                        );
                        if (!string.IsNullOrEmpty(menu.DllPath))
                        {
                            string dllPath = Path.Combine(PluginFolder, menu.DllPath);
                            Assembly ass = Assembly.LoadFrom(dllPath);
                            var tss = ass.GetTypes();
                            Type ClassType = ass.GetType(menu.ClassName);
                            object Instance = Activator.CreateInstance(ClassType);
                            menu.Content = Instance;
                            if (Instance is IPlugin plugin)
                            {
                            }
                            else
                            {
                                plugin = new PluginPackage(Instance);
                            }
                            plugin.SetLogger(LogManager.GetLogger(menu.Code));
                            string configFolder = Path.Combine(PathHelper.ConfigFolder, menu.Code);
                            if (!Directory.Exists(configFolder))
                            {
                                Directory.CreateDirectory(configFolder);
                            }
                            plugin.SetBaseFolder(configFolder);
                            plugin.InitFinish();
                            pluginContainer.OnCloseEvent += plugin.OnClose;
                            pluginContainer.DoCloseEvent += plugin.DoClose;
                            Type SettingType = ass.GetType($"{menu.ClassName}_Setting");
                            if (SettingType != null)
                            {
                                object SettingInstance = Activator.CreateInstance(SettingType);
                                if (SettingInstance is IFolderOwner fo)
                                {
                                    fo.SetBaseFolder(configFolder);
                                }
                                menu.SettingControl = SettingInstance;
                            }
                        }
                        log.Info($"{this.lang("App.MenuLoadSuccess", menu.Name)} Cost: {(DateTime.Now - start).TotalMilliseconds}ms");
                        LoadMessageAppend?.Invoke(this,
                            new LoadMsg
                            {
                                Type = LoadMsgType.LOADING,
                                Result = true,
                                Message = this.lang("App.MenuLoadSuccess", menu.Name),
                            });
                    }
                    catch (Exception ex)
                    {
                        log.Error($"Load Plugin [{menu.Name}] Failed.", ex);
                        LoadMessageAppend?.Invoke(this, new LoadMsg
                        {
                            Type = LoadMsgType.LOADING,
                            Result = false,
                            Message = this.lang("App.MenuLoadFailed", menu.Name, ex.Message, ex.StackTrace)
                        });
                    }
                    finally
                    {
                        c.Signal();
                    }
                }));
                c.Wait();
            }
        }
        private PluginInfo ConvertPermission2PluginInfo(PermissionData data)
        {
            if (data.Type == "权限") return null;
            PluginInfo info = new PluginInfo
            {
                Code = data.Code,
                Name = data.Name,
                Type = data.Type,
                ParentCode = data.ParentCode,
                DllPath = data.DllPath,
                ClassName = data.ClassName,
                IconFont = data.IconFont
                //Icon = new BitmapImage(new Uri(Path.Combine(PathHelper.IconFolder, dr["iconPath"].ToString())))
            };
            if (data.Children != null && data.Children.Count > 0)
            {
                foreach (var child in data.Children)
                {
                    var childInfo = ConvertPermission2PluginInfo(child);
                    if (childInfo != null) info.Children.Add(childInfo);
                }
            }
            return info;
        }
        public List<PluginInfo> LoadTreeMenu(Dispatcher dispatcher)
        {
            List<PluginInfo> menus = new List<PluginInfo>();
            Dictionary<string, PluginInfo> cache = new Dictionary<string, PluginInfo>();
            try
            {
                List<PermissionData> allPermissions = UserInfo.User?.AllPermissions;
                if (allPermissions != null)
                {
                    Application.Current.Dispatcher.Invoke(new Action(() =>
                    {
                        log.Info(this.lang("App.MenuCount", allPermissions.Count));
                        LoadMessageAppend?.Invoke(this,
                          new LoadMsg
                          {
                              Type = LoadMsgType.INIT,
                              NumData = allPermissions.Count,
                              Message = this.lang("App.MenuCount", allPermissions.Count),
                          });
                        foreach (PermissionData data in allPermissions)
                        {
                            if (data.Type == "Permission") continue;
                            var menu = new PluginInfo
                            {
                                Code = data.Code,
                                Name = data.Name,
                                Type = data.Type,
                                ParentCode = data.ParentCode,
                                DllPath = data.DllPath,
                                ClassName = data.ClassName,
                                IconFont = data.IconFont
                            };
                            cache[menu.Code] = menu;
                            if (!cache.ContainsKey(menu.ParentCode))
                            {
                                menus.Add(menu);
                            }
                            else
                            {
                                cache[menu.ParentCode].Children.Add(menu);
                            }
                            Plugins.Add(menu);
                        }
                    }));
                    LoadChildrenMenu();
                }
            }
            catch (Exception ex)
            {
                //LoadMessageAppend?.Invoke(this, ex.Message);
            }
            return menus;
        }
    }
}
HMI.Base/PluginMaskControl.cs
New file
@@ -0,0 +1,110 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using HMI.Core.Api;
using HMI.UI;
using log4net;
namespace HMI.Base
{
    /// <summary>
    /// PluginMaskControl.xaml 的交互逻辑
    /// </summary>
    public partial class PluginMaskControl : UserControl, IPlugin, INotifyPropertyChanged
    {
        #region ...
        public event PropertyChangedEventHandler PropertyChanged;
        protected void Set([CallerMemberName] string name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
        protected void Set<T>(ref T old, T @new, [CallerMemberName] string propertyName = null)
        {
            old = @new;
            if (propertyName != null)
            {
                Set(propertyName);
            }
        }
        #endregion
        static PluginMaskControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(PluginMaskControl),
                new FrameworkPropertyMetadata(typeof(PluginMaskControl)));
        }
        protected ILog log;
        protected string BaseFolder;
        public virtual void Active()
        {
        }
        public virtual void DeActive()
        {
        }
        public virtual void Init()
        {
        }
        public virtual void OnTabChange(object sender, object args)
        {
        }
        public virtual void SetBaseFolder(string folder)
        {
            BaseFolder = folder;
        }
        public virtual void SetLogger(ILog logger)
        {
            log = logger;
        }
        public virtual void InitFinish()
        {
        }
        public virtual void OnClose(object sender, CancelEventArgs e)
        {
        }
        public virtual void DoClose(object sender, EventArgs e)
        {
        }
        public void ShowMask(Action action, Action<Exception> reject = null, string title = null, string content = null)
        {
            MaskControl mask = this.Template.FindName("mask", this) as MaskControl;
            if (mask != null)
            {
                mask.ShowMask(this, action, reject, title, content);
            }
        }
        public virtual bool CanActive()
        {
            return true;
        }
        public virtual bool CanDeActive()
        {
            return true;
        }
    }
}
HMI.Base/Properties/AssemblyInfo.cs
New file
@@ -0,0 +1,52 @@
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
// 有关程序集的一般信息由以下
// 控制。更改这些特性值可修改
// 与程序集关联的信息。
[assembly: AssemblyTitle("HMI.Base")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Organization")]
[assembly: AssemblyProduct("HMI.Base")]
[assembly: AssemblyCopyright("Copyright © Organization 2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 将 ComVisible 设置为 false 会使此程序集中的类型
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
//请将此类型的 ComVisible 特性设置为 true。
[assembly: ComVisible(false)]
//若要开始生成可本地化的应用程序,请设置
//.csproj 文件中的 <UICulture>CultureYouAreCodingWith</UICulture>
//在 <PropertyGroup> 中。例如,如果你使用的是美国英语。
//使用的是美国英语,请将 <UICulture> 设置为 en-US。  然后取消
//对以下 NeutralResourceLanguage 特性的注释。  更新
//以下行中的“en-US”以匹配项目文件中的 UICulture 设置。
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
[assembly:ThemeInfo(
    ResourceDictionaryLocation.None, //主题特定资源词典所处位置
                             //(未在页面中找到资源时使用,
                             //或应用程序资源字典中找到时使用)
    ResourceDictionaryLocation.SourceAssembly //常规资源词典所处位置
                                      //(未在页面中找到资源时使用,
                                      //、应用程序或任何主题专用资源字典中找到时使用)
)]
// 程序集的版本信息由下列四个值组成:
//
//      主版本
//      次版本
//      生成号
//      修订号
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
HMI.Base/Properties/Resources.Designer.cs
New file
@@ -0,0 +1,62 @@
//------------------------------------------------------------------------------
// <auto-generated>
//     此代码由工具生成。
//     运行时版本: 4.0.30319.42000
//
//     对此文件的更改可能导致不正确的行为,如果
//     重新生成代码,则所做更改将丢失。
// </auto-generated>
//------------------------------------------------------------------------------
namespace HMI.Base.Properties {
    /// <summary>
    ///   强类型资源类,用于查找本地化字符串等。
    /// </summary>
    // 此类是由 StronglyTypedResourceBuilder
    // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
    // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
    // (以 /str 作为命令选项),或重新生成 VS 项目。
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
    internal class Resources {
        private static global::System.Resources.ResourceManager resourceMan;
        private static global::System.Globalization.CultureInfo resourceCulture;
        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        internal Resources() {
        }
        /// <summary>
        ///   返回此类使用的缓存 ResourceManager 实例。
        /// </summary>
        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
        internal static global::System.Resources.ResourceManager ResourceManager {
            get {
                if ((resourceMan == null)) {
                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("HMI.Base.Properties.Resources", typeof(Resources).Assembly);
                    resourceMan = temp;
                }
                return resourceMan;
            }
        }
        /// <summary>
        ///   重写当前线程的 CurrentUICulture 属性,对
        ///   使用此强类型资源类的所有资源查找执行重写。
        /// </summary>
        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
        internal static global::System.Globalization.CultureInfo Culture {
            get {
                return resourceCulture;
            }
            set {
                resourceCulture = value;
            }
        }
    }
}
HMI.Base/Properties/Resources.resx
New file
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
  <!--
    Microsoft ResX Schema
    Version 2.0
    The primary goals of this format is to allow a simple XML format
    that is mostly human readable. The generation and parsing of the
    various data types are done through the TypeConverter classes
    associated with the data types.
    Example:
    ... ado.net/XML headers & schema ...
    <resheader name="resmimetype">text/microsoft-resx</resheader>
    <resheader name="version">2.0</resheader>
    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
        <value>[base64 mime encoded serialized .NET Framework object]</value>
    </data>
    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
        <comment>This is a comment</comment>
    </data>
    There are any number of "resheader" rows that contain simple
    name/value pairs.
    Each data row contains a name, and value. The row also contains a
    type or mimetype. Type corresponds to a .NET class that support
    text/value conversion through the TypeConverter architecture.
    Classes that don't support this are serialized and stored with the
    mimetype set.
    The mimetype is used for serialized objects, and tells the
    ResXResourceReader how to depersist the object. This is currently not
    extensible. For a given mimetype the value must be set accordingly:
    Note - application/x-microsoft.net.object.binary.base64 is the format
    that the ResXResourceWriter will generate, however the reader can
    read any of the formats listed below.
    mimetype: application/x-microsoft.net.object.binary.base64
    value   : The object must be serialized with
            : System.Serialization.Formatters.Binary.BinaryFormatter
            : and then encoded with base64 encoding.
    mimetype: application/x-microsoft.net.object.soap.base64
    value   : The object must be serialized with
            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
            : and then encoded with base64 encoding.
    mimetype: application/x-microsoft.net.object.bytearray.base64
    value   : The object must be serialized into a byte array
            : using a System.ComponentModel.TypeConverter
            : and then encoded with base64 encoding.
    -->
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" />
              <xsd:attribute name="type" type="xsd:string" />
              <xsd:attribute name="mimetype" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string" />
              <xsd:attribute name="name" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" />
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
</root>
HMI.Base/Properties/Settings.Designer.cs
New file
@@ -0,0 +1,30 @@
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.42000
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace HMI.Base.Properties
{
    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
    internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
    {
        private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
        public static Settings Default
        {
            get
            {
                return defaultInstance;
            }
        }
    }
}
HMI.Base/Properties/Settings.settings
New file
@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
  <Profiles>
    <Profile Name="(Default)" />
  </Profiles>
  <Settings />
</SettingsFile>
HMI.Base/SimpleControl.cs
New file
@@ -0,0 +1,69 @@
using System;
using System.ComponentModel;
using System.Windows.Controls;
using HMI.Core.Api;
using log4net;
namespace HMI.Base
{
    public class SimpleControl : UserControl, IPlugin
    {
        protected ILog log;
        protected string BaseFolder;
        public virtual void Active()
        {
        }
        public virtual void DeActive()
        {
        }
        public virtual void Init()
        {
        }
        public virtual void OnTabChange(object sender, object args)
        {
        }
        public virtual void SetBaseFolder(string folder)
        {
            BaseFolder = folder;
        }
        public virtual void SetLogger(ILog logger)
        {
            log = logger;
        }
        public virtual void InitFinish()
        {
        }
        public virtual void OnClose(object sender, CancelEventArgs e)
        {
        }
        public virtual void DoClose(object sender, EventArgs e)
        {
        }
        public virtual bool CanActive()
        {
            return true;
        }
        public virtual bool CanDeActive()
        {
            return true;
        }
    }
}
HMI.Base/SimpleMaskControl.cs
New file
@@ -0,0 +1,89 @@
using HMI.Core.Api;
using HMI.UI;
using log4net;
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
namespace HMI.Base
{
    /// <summary>
    /// PluginMaskControl.xaml 的交互逻辑
    /// </summary>
    public partial class SimpleMaskControl : UserControl, IPlugin
    {
        static SimpleMaskControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(SimpleMaskControl),
                new FrameworkPropertyMetadata(typeof(SimpleMaskControl)));
        }
        protected ILog log;
        protected string BaseFolder;
        public virtual void Active()
        {
        }
        public virtual void DeActive()
        {
        }
        public virtual void Init()
        {
        }
        public virtual void OnTabChange(object sender, object args)
        {
        }
        public virtual void SetBaseFolder(string folder)
        {
            BaseFolder = folder;
        }
        public virtual void SetLogger(ILog logger)
        {
            log = logger;
        }
        public virtual void InitFinish()
        {
        }
        public virtual void OnClose(object sender, CancelEventArgs e)
        {
        }
        public virtual void DoClose(object sender, EventArgs e)
        {
        }
        public void ShowMask(Action action, Action<Exception> reject = null, string title = null, string content = null)
        {
            MaskControl mask = this.Template.FindName("mask", this) as MaskControl;
            if (mask != null)
            {
                mask.ShowMask(this, action, reject, title, content);
            }
        }
        public virtual bool CanActive()
        {
            return true;
        }
        public virtual bool CanDeActive()
        {
            return true;
        }
    }
}
HMI.Base/Themes/Generic.xaml
New file
@@ -0,0 +1,31 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:HMI.Base"
                    xmlns:ui="clr-namespace:HMI.UI;assembly=HMI.UI">
    <Style TargetType="local:SimpleMaskControl">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:SimpleMaskControl">
                    <Grid Background="{TemplateBinding Background}">
                        <ContentPresenter Content="{TemplateBinding Content}"/>
                        <ui:MaskControl x:Name="mask"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <Style TargetType="local:PluginMaskControl">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:PluginMaskControl">
                    <Grid>
                        <ContentPresenter Content="{TemplateBinding Content}" Margin="{TemplateBinding Padding}"/>
                        <ui:MaskControl x:Name="mask"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>
HMI.Base/packages.config
New file
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="log4net" version="3.0.4" targetFramework="net472" />
</packages>
HMI.Core/Annotation/ExcelColumn.cs
New file
@@ -0,0 +1,19 @@
using System;
namespace HMI.Core
{
    [AttributeUsage(AttributeTargets.Property)]
    public class ExcelColumnAttribute : Attribute
    {
        public string Name { get; private set; }
        public bool IgnoreExport { get; private set; }
        public bool IgnoreImport { get; private set; }
        public ExcelColumnAttribute(string name, bool ignoreExport = false, bool ignoreImport = false)
        {
            this.Name = name;
            this.IgnoreExport = ignoreExport;
            this.IgnoreImport = ignoreImport;
        }
    }
}
HMI.Core/Annotation/InitAttribute.cs
New file
@@ -0,0 +1,18 @@
using System;
using System.Runtime.InteropServices;
namespace HMI.Core.Annotation
{
    [AttributeUsage(AttributeTargets.Assembly, Inherited = false)]
    [ComVisible(true)]
    public class PluginClassNameAttribute : Attribute
    {
        public string ClassName { get; private set; }
        public PluginClassNameAttribute(string ClassName)
        {
            this.ClassName = ClassName;
        }
    }
}
HMI.Core/Api/AlarmShowHelper.cs
New file
@@ -0,0 +1,65 @@
using HMI.Core.Entity;
using System;
using System.Collections.Generic;
namespace HMI.Core.Api
{
    public class AlarmShowHelper
    {
        private static List<IAlarmHandler> handlers;
        private static IAlarmShow shower;
        public static void Register(IAlarmShow shower)
        {
            if (AlarmShowHelper.shower == null)
            {
                AlarmShowHelper.shower = shower;
            }
        }
        public static void UnRegister(IAlarmShow shower)
        {
            if (AlarmShowHelper.shower == shower)
            {
                AlarmShowHelper.shower = null;
            }
        }
        public static void AddHandler(IAlarmHandler handler)
        {
            if (handlers == null)
            {
                handlers = new List<IAlarmHandler>();
            }
            if (!handlers.Contains(handler))
            {
                handlers.Add(handler);
            }
        }
        public static bool ShowAlarm(AlarmText alarm)
        {
            if (handlers != null)
            {
                foreach (var handler in handlers)
                {
                    try
                    {
                        handler.HandleAlarm(alarm);
                    }
                    catch (Exception ex)
                    {
                    }
                }
            }
            if (shower != null)
            {
                return shower.ShowAlarm(alarm);
            }
            return false;
        }
    }
}
HMI.Core/Api/IAlarmShow.cs
New file
@@ -0,0 +1,19 @@
using HMI.Core.Entity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace HMI.Core.Api
{
    public interface IAlarmShow
    {
        bool ShowAlarm(AlarmText alarm);
    }
    public interface IAlarmHandler
    {
        void HandleAlarm(AlarmText alarm);
    }
}
HMI.Core/Api/IConfigManager.cs
New file
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace HMI.Core.Api
{
    public interface IConfigManager
    {
        object GetConfig();
        void SaveConfig();
        void ResetConfig();
    }
}
HMI.Core/Api/IConfigRefresh.cs
New file
@@ -0,0 +1,7 @@
namespace HMI.Core.Api
{
    public interface IConfigRefresh
    {
        void OnConfigRefrsh(object sender, object setting);
    }
}
HMI.Core/Api/IFolderOwner.cs
New file
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace HMI.Core.Api
{
    public interface IFolderOwner
    {
        /// <summary>
        /// 插件加载完之后会分配一个路径,可以存放一些独立的文件
        /// </summary>
        /// <param name="folder"></param>
        void SetBaseFolder(string folder);
    }
}
HMI.Core/Api/ILogWriter.cs
New file
@@ -0,0 +1,10 @@
namespace HMI.Core.Api
{
    public interface ILogWriter
    {
        void Debug(string source, string content);
        void Info(string source, string content);
        void Warn(string source, string content);
        void Error(string source, string content);
    }
}
HMI.Core/Api/IPlugin.cs
New file
@@ -0,0 +1,65 @@
using log4net;
using System;
using System.ComponentModel;
namespace HMI.Core.Api
{
    public interface IPlugin : IFolderOwner
    {
        /// <summary>
        /// 初始化
        /// </summary>
        /// <returns></returns>
        void Init();
        /// <summary>
        /// 初始化完成(执行完构造器,并且设置好logger folder等资源之后执行)
        /// </summary>
        void InitFinish();
        /// <summary>
        /// 插件被激活时的操作
        /// </summary>
        /// <returns></returns>
        void Active();
        /// <summary>
        /// 插件被激活时的操作
        /// </summary>
        /// <returns></returns>
        bool CanActive();
        /// <summary>
        /// 失活时的操作
        /// </summary>
        /// <returns></returns>
        void DeActive();
        bool CanDeActive();
        /// <summary>
        /// 分配日志logger(log4net)
        /// </summary>
        /// <param name="logger"></param>
        void SetLogger(ILog logger);
        /// <summary>
        /// 将要关闭程序事件
        /// </summary>
        void OnClose(object sender, CancelEventArgs e);
        /// <summary>
        /// 关闭程序事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void DoClose(object sender, EventArgs e);
        /// <summary>
        /// 面板切换事件(预留)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        void OnTabChange(object sender, object args);
    }
}
HMI.Core/Api/IPluginContainer.cs
New file
@@ -0,0 +1,12 @@
using System;
using System.ComponentModel;
namespace HMI.Core.Api
{
    public interface IPluginContainer
    {
        event CancelEventHandler OnCloseEvent;
        event EventHandler DoCloseEvent;
        event EventHandler LoadComplateEvent;
    }
}
HMI.Core/Api/PluginPackage.cs
New file
@@ -0,0 +1,110 @@
using log4net;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
namespace HMI.Core.Api
{
    public class PluginPackage : IPlugin
    {
        public static Dictionary<object, IPlugin> Cache = new Dictionary<object, IPlugin>();
        public static IPlugin Get(object obj)
        {
            if (obj is null) return null;
            if (obj is IPlugin plugin)
            {
                return plugin;
            }
            if (Cache.ContainsKey(obj)) return Cache[obj];
            PluginPackage pluginPackage = new PluginPackage(obj);
            Cache.Add(obj, pluginPackage);
            return pluginPackage;
        }
        private object Content;
        private MethodInfo ActiveMethod;
        private MethodInfo DeActiveMethod;
        private MethodInfo DoCloseMethod;
        private MethodInfo InitMethod;
        private MethodInfo InitFinishMethod;
        private MethodInfo OnCloseMethod;
        private MethodInfo OnTabChangeMethod;
        private MethodInfo SetBaseFolderMethod;
        private MethodInfo SetLoggerMethod;
        private MethodInfo CanDeActiveMethod;
        private MethodInfo CanActiveMethod;
        public PluginPackage(object content)
        {
            this.Content = content;
            ActiveMethod = content.GetType().GetMethod("Active");
            DeActiveMethod = content.GetType().GetMethod("DeActive");
            DoCloseMethod = content.GetType().GetMethod("DoClose", new Type[] { typeof(object), typeof(EventArgs) });
            InitMethod = content.GetType().GetMethod("Init");
            InitFinishMethod = content.GetType().GetMethod("InitFinish");
            OnCloseMethod = content.GetType().GetMethod("OnClose", new Type[] { typeof(object), typeof(CancelEventArgs) });
            OnTabChangeMethod = content.GetType().GetMethod("OnTabChange", new Type[] { typeof(object), typeof(object) });
            SetBaseFolderMethod = content.GetType().GetMethod("SetBaseFolder", new Type[] { typeof(string) });
            SetLoggerMethod = content.GetType().GetMethod("SetLogger", new Type[] { typeof(ILog) });
            CanDeActiveMethod = content.GetType().GetMethod("CanDeActive");
            CanActiveMethod = content.GetType().GetMethod("CanActive");
        }
        public void Active()
        {
            ActiveMethod?.Invoke(Content, null);
        }
        public void DeActive()
        {
            DeActiveMethod?.Invoke(Content, null);
        }
        public void DoClose(object sender, EventArgs e)
        {
            DoCloseMethod?.Invoke(Content, new object[] { sender, e });
        }
        public void Init()
        {
            InitMethod?.Invoke(Content, null);
        }
        public void InitFinish()
        {
            InitFinishMethod?.Invoke(Content, null);
        }
        public void OnClose(object sender, CancelEventArgs e)
        {
            OnCloseMethod?.Invoke(Content, new object[] { sender, e });
        }
        public void OnTabChange(object sender, object args)
        {
            OnTabChangeMethod?.Invoke(Content, new object[] { sender, args });
        }
        public void SetBaseFolder(string folder)
        {
            SetBaseFolderMethod?.Invoke(Content, new object[] { folder });
        }
        public void SetLogger(ILog logger)
        {
            SetLoggerMethod?.Invoke(Content, new object[] { logger });
        }
        public bool CanActive()
        {
            return (bool)CanActiveMethod?.Invoke(Content, null);
        }
        public bool CanDeActive()
        {
            return (bool)CanDeActiveMethod?.Invoke(Content, null);
        }
    }
}
HMI.Core/DpUtil/PermissionHelper.cs
New file
@@ -0,0 +1,47 @@
using HMI.Core.Entity;
using System.Linq;
using System.Windows;
namespace HMI.Core.DpUtil
{
    public class PermissionHelper
    {
        public static string GetRequiredPermission(UIElement element)
        {
            return (string)element.GetValue(RequiredPermissionProperty);
        }
        public static void SetRequiredPermission(UIElement element, string requiredPermission)
        {
            element.SetValue(RequiredPermissionProperty, requiredPermission);
        }
        public static readonly DependencyProperty RequiredPermissionProperty =
            DependencyProperty.RegisterAttached("RequiredPermission", typeof(string), typeof(UIElement),
            new PropertyMetadata(string.Empty, OnRequiredPermissionChanged));
        private static void OnRequiredPermissionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is UIElement element)
            {
                if (e.Property == RequiredPermissionProperty)
                {
                    var requiredPermission = (string)e.NewValue;
                    if (string.IsNullOrEmpty(requiredPermission)) return;
                    if (UserInfo.User == null)
                    {
                        element.Visibility = Visibility.Collapsed;
                        return;
                    }
                    if (UserInfo.User?.AllPermissions == null)
                    {
                        element.Visibility = Visibility.Collapsed;
                        return;
                    }
                    if (UserInfo.User.AllPermissions.FirstOrDefault(p => p.Code == requiredPermission) != null) return;
                    element.Visibility = Visibility.Collapsed;
                }
            }
        }
    }
}
HMI.Core/Entity/AlarmSetting.cs
New file
@@ -0,0 +1,34 @@
namespace HMI.Core.Entity
{
    public class AlarmSetting : SelectBase
    {
        public static readonly string BLOCK_TYPE = "BLOCK";
        public static readonly string MARK_TYPE = "MARK";
        /// <summary>
        /// 开始DB地址
        /// </summary>
        [ExcelColumn("DB地址")]
        public string DBAddress { get; set; }
        /// <summary>
        /// 起始Byte位
        /// </summary>
        [ExcelColumn("起始位")]
        public string StartAddress { get; set; }
        /// <summary>
        /// 长度
        /// </summary>
        [ExcelColumn("长度")]
        public int Length { get; set; }
        [ExcelColumn("报警代码")]
        public string AlarmCode { get; set; }
        [ExcelColumn("级别")]
        public double Level { get; set; }
        [ExcelColumn("报警描述")]
        public string Description { get; set; }
    }
}
HMI.Core/Entity/AlarmText.cs
New file
@@ -0,0 +1,83 @@
using System.Xml.Serialization;
namespace HMI.Core.Entity
{
    public class AlarmText
    {
        /// <summary>
        /// 报警代码
        /// </summary>
        [ExcelColumn("组合报警代码")]
        public string AlarmCode { get; set; }
        public string VirtualId { get; set; }
        /// <summary>
        /// 工位编号
        /// </summary>
        [ExcelColumn("设备号")]
        public string OpNo { get; set; }
        /// <summary>
        /// 模块编号
        /// </summary>
        [ExcelColumn("模块号")]
        public string ModuleNo { get; set; }
        ///// <summary>
        ///// 模块名称(CN)
        ///// </summary>
        //[ExcelColumn("模块名称-CN")]
        //public string ModuleName_CN { get; set; }
        ///// <summary>
        ///// 模块名称(EN)
        ///// </summary>
        //[ExcelColumn("模块名称-US")]
        //public string ModuleName_EN { get; set; }
        ///// <summary>
        ///// 模块报警文本(CN)
        ///// </summary>
        //[ExcelColumn("报警信息文本-CN")]
        //public string MAlarmText_CN { get; set; }
        ///// <summary>
        ///// 模块报警文本(EN)
        ///// </summary>
        //[ExcelColumn("报警信息文本-US")]
        //public string MAlarmText_EN { get; set; }
        /// <summary>
        /// 模块报警代码
        /// </summary>
        [ExcelColumn("报警代码")]
        public string MAlarmCode { get; set; }
        /// <summary>
        /// 报警时间
        /// </summary>
        public string Time { get; set; }
        [XmlIgnore]
        public string AlarmInfo { get; set; }
        /// <summary>
        /// 报警来源
        /// </summary>
        [XmlIgnore]
        public string Source { get; set; }
        [XmlIgnore]
        public string PLC_DB_BLOCK { get; set; }
        [XmlIgnore]
        public double Level { get; set; }
        [XmlIgnore]
        public bool IsPLCAlarm { get; set; }
    }
}
HMI.Core/Entity/LangInfo.cs
New file
@@ -0,0 +1,14 @@
namespace HMI.Core.Entity
{
    public class LangInfo
    {
        public LangInfo(string code, string name)
        {
            Code = code;
            Name = name;
        }
        public string Code { get; private set; }
        public string Name { get; private set; }
    }
}
HMI.Core/Entity/LoadMsg.cs
New file
@@ -0,0 +1,15 @@
namespace HMI.Core.Entity
{
    public enum LoadMsgType
    {
        INIT, LOADING, FINISHED
    }
    public class LoadMsg
    {
        public string Message { get; set; }
        public bool? Result { get; set; }
        public LoadMsgType Type { get; set; }
        public int NumData { get; set; }
    }
}
HMI.Core/Entity/NotifyBase.cs
New file
@@ -0,0 +1,96 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace HMI.Core.Entity
{
    public class NotifyBase : INotifyPropertyChanged
    {
        #region ...
        public virtual event PropertyChangedEventHandler PropertyChanged;
        protected virtual void Set([CallerMemberName] string name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
        protected void Set<T>(ref T old, T @new, [CallerMemberName] string propertyName = null)
        {
            old = @new;
            if (propertyName != null)
            {
                Set(propertyName);
            }
        }
        #endregion
    }
    public class RelayCommand<T> : ICommand
    {
        /// <summary>
        /// 命令能否执行
        /// </summary>
        readonly Func<bool> _canExecute;
        /// <summary>
        /// 命令执行的方法
        /// </summary>
        readonly Action<T> _execute;
        /// <summary>
        /// 命令的构造函数
        /// </summary>
        /// <param name="action">命令需执行的方法</param>
        /// <param name="canExecute">命令是否可以执行的方法</param>
        public RelayCommand(Action<T> action, Func<bool> canExecute)
        {
            _execute = action;
            _canExecute = canExecute;
        }
        public RelayCommand(Action<T> action)
        {
            _execute = action;
        }
        /// <summary>
        /// 判断命令是否可以执行
        /// </summary>
        /// <param name="parameter"></param>
        /// <returns></returns>
        public bool CanExecute(Object parameter)
        {
            if (_canExecute == null)
                return true;
            return _canExecute();
        }
        /// <summary>
        /// 执行命令
        /// </summary>
        /// <param name="parameter"></param>
        public void Execute(Object parameter)
        {
            _execute((T)parameter);
        }
        /// <summary>
        /// 事件追加、移除
        /// </summary>
        public event EventHandler CanExecuteChanged
        {
            add
            {
                if (_canExecute != null)
                    CommandManager.RequerySuggested += value;
            }
            remove
            {
                if (_canExecute != null)
                    CommandManager.RequerySuggested -= value;
            }
        }
    }
}
HMI.Core/Entity/ParameterData.cs
New file
@@ -0,0 +1,41 @@
using System.Xml.Serialization;
namespace HMI.Core.Entity
{
    public class ParameterData : SelectBase
    {
        public string Id { get; set; }
        [ExcelColumn("设置项名称")]
        public string Name { get; set; }
        [ExcelColumn("设置项类型")]
        public string Type { get; set; }
        [ExcelColumn("是否为数组")]
        public bool IsArray { get; set; } = false;
        [ExcelColumn("数组长度")]
        public int Length { get; set; } = 1;
        [ExcelColumn("值")]
        public string Value { get; set; }
        [ExcelColumn("单位")]
        public string Unit { get; set; }
        [ExcelColumn("描述")]
        public string Description { get; set; }
        [ExcelColumn("分组")]
        public string PGroup { get; set; }
        [ExcelColumn("来源")]
        public string Source { get; set; }
        public string ProductName { get; set; }
        [XmlIgnore]
        public string ShowGroup
        {
            get => string.IsNullOrEmpty(PGroup) ? "暂无" : PGroup;
            set
            {
            }
        }
    }
}
HMI.Core/Entity/PermissionData.cs
New file
@@ -0,0 +1,24 @@
using System.Collections.Generic;
namespace HMI.Core.Entity
{
    public class PermissionData
    {
        public string Id { get; set; }
        public string Code { get; set; }
        public string Name { get; set; }
        public string IconPath { get; set; }
        public string Icon { get; set; }
        public string Level { get; set; }
        public string Type { get; set; }
        public string ParentCode { get; set; }
        public string ParentName { get; set; }
        public string DllPath { get; set; }
        public string ClassName { get; set; }
        public string IconFont { get; set; }
        public double? Sort { get; set; }
        public List<PermissionData> Children { get; set; } = new List<PermissionData>();
    }
}
HMI.Core/Entity/ProductData.cs
New file
@@ -0,0 +1,14 @@
using System.Xml.Serialization;
namespace HMI.Core.Entity
{
    public class ProductData:SelectBase
    {
        [XmlIgnore]
        public string Id { get; set; }
        public string Name { get; set; }
        public bool IsMaster { get; set; }
        public string ParentId { get; set; }
        public string dir;
    }
}
HMI.Core/Entity/SelectBase.cs
New file
@@ -0,0 +1,15 @@
using System.Xml.Serialization;
namespace HMI.Core.Entity
{
    public class SelectBase : NotifyBase
    {
        private bool isSelected;
        [XmlIgnore]
        public virtual bool IsSelected
        {
            get => isSelected;
            set { Set(ref isSelected, value); }
        }
    }
}
HMI.Core/Entity/SortParam.cs
New file
@@ -0,0 +1,18 @@
using System.ComponentModel;
namespace HMI.Core.Entity
{
    public class SortParam
    {
        public string field { get; set; }
        public ListSortDirection direction { get; set; }
        public string sort { get; set; }
    }
    public class PageParam
    {
        public int limit { get; set; }
        public int offset { get; set; }
    }
}
HMI.Core/Entity/TestItemData.cs
New file
@@ -0,0 +1,64 @@
namespace HMI.Core.Entity
{
    public class TestItemData : SelectBase
    {
        /// <summary>
        /// 参数ID
        /// </summary>
        public string ID { get; set; } = string.Empty;
        /// <summary>
        /// 测试项名称
        /// </summary>
        [ExcelColumn("测试项名称")]
        public string Name { get; set; }
        /// <summary>
        /// 测试项类型
        /// </summary>
        [ExcelColumn("测试项类型")]
        public string Type { get; set; } = "NumberTest";
        /// <summary>
        /// 字符串型数据
        /// </summary>
        [ExcelColumn("字符串")]
        public string StrValue { get; set; }
        /// <summary>
        /// 测试项下限
        /// </summary>
        [ExcelColumn("下限")]
        public double LimitDown { get; set; }
        /// <summary>
        /// 测试项上限
        /// </summary>
        [ExcelColumn("上限")]
        public double LimitUp { get; set; }
        /// <summary>
        /// 使能
        /// </summary>
        [ExcelColumn("使能")]
        public bool Enable { get; set; }
        /// <summary>
        /// 补偿值
        /// </summary>
        [ExcelColumn("补偿值")]
        public double Offset { get; set; }
        /// <summary>
        /// 来源
        /// </summary>
        [ExcelColumn("来源")]
        public string Source { get; set; } = string.Empty;
        /// <summary>
        /// 分组
        /// </summary>
        public string Group { get; set; } = string.Empty;
    }
}
HMI.Core/Entity/UserInfo.cs
New file
@@ -0,0 +1,40 @@
using System.Collections.Generic;
namespace HMI.Core.Entity
{
    public class UserInfo
    {
        public static UserInfo User { get; private set; }
        public UserInfo(string id, string code, string username, string name,
            List<PermissionData> permissions,
            List<PermissionData> allPermissions,
            string password = null)
        {
            Id = id;
            Code = code;
            Username = username;
            Name = name;
            Password = password;
            Permissions = permissions;
            AllPermissions = allPermissions;
        }
        public string Id { get; private set; }
        public string Code { get; private set; }
        public string Username { get; private set; }
        public string Name { get; private set; }
        public string Password { get; private set; }
        public List<PermissionData> Permissions { get; private set; }
        public List<PermissionData> AllPermissions { get; private set; }
        public static void Login(UserInfo info)
        {
            if (User == null) User = info;
        }
        public static void Logout()
        {
            User = null;
        }
    }
}
HMI.Core/Event/TabChange.cs
New file
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace HMI.Core.Api
{
    public class TabChangeEventArgs
    {
    }
}
HMI.Core/Exceptions/FormValidateException.cs
New file
@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace HMI.Core.Exceptions
{
    public class FormValidateException : Exception
    {
        public readonly List<FormGroupValidateResult> Result;
        public bool Success
        {
            get => Result == null || Result.Count == 0;
        }
        public FormValidateException()
        {
            Result = new List<FormGroupValidateResult>();
        }
    }
    public class FormGroupValidateResult
    {
        public readonly string Message;
        public readonly object Sender;
        public FormGroupValidateResult(string message, object sender)
        {
            Message = message;
            Sender = sender;
        }
    }
}
HMI.Core/FontSizeConverter.cs
New file
@@ -0,0 +1,88 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace HMI.Core
{
    public class WidthSizeConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            /// "{size},{基准值},{最小值},{最大值}"
            if (parameter == null) return 15;
            string[] ps = parameter.ToString().Split(',');
            double size = double.Parse(ps[0]);
            double baseSize = double.Parse(ps[1]);
            double min = -1, max = -1;
            if (ps.Length >= 3)
            {
                min = double.Parse(ps[2]);
            }
            if (ps.Length >= 4)
            {
                max = double.Parse(ps[3]);
            }
            if (value is double width)
            {
                double res = CountFontSize(size, width, baseSize);
                res = min > 0 && res < min ? min : res;
                res = max > 0 && res > max ? max : res;
                return res;
            }
            return 15;
        }
        public static double CountFontSize(double size, double limit, double baseSize)
        {
            return size * limit / baseSize;
        }
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    public class HeightSizeConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (parameter == null) return 15;
            string[] ps = parameter.ToString().Split(',');
            int size = int.Parse(ps[0]);
            int baseSize = int.Parse(ps[1]);
            double min = -1, max = -1;
            if (ps.Length >= 3)
            {
                min = double.Parse(ps[2]);
            }
            if (ps.Length >= 4)
            {
                max = double.Parse(ps[3]);
            }
            if (value is double height)
            {
                double res = CountFontSize(size, height, baseSize);
                res = min > 0 && res < min ? min : res;
                res = max > 0 && res > max ? max : res;
                return res;
            }
            return 15;
        }
        public static double CountFontSize(double size, double limit, double baseSize)
        {
            return size * limit / baseSize;
        }
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}
HMI.Core/HMI.Core.csproj
New file
@@ -0,0 +1,153 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{3E3BF44A-0B69-45C5-9512-528CB48A7106}</ProjectGuid>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>HMI.Core</RootNamespace>
    <AssemblyName>HMI.Core</AssemblyName>
    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <Deterministic>true</Deterministic>
    <NuGetPackageImportStamp>
    </NuGetPackageImportStamp>
    <TargetFrameworkProfile />
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
    <DebugSymbols>true</DebugSymbols>
    <OutputPath>bin\x64\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <DebugType>full</DebugType>
    <PlatformTarget>x64</PlatformTarget>
    <LangVersion>7.3</LangVersion>
    <ErrorReport>prompt</ErrorReport>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
    <OutputPath>bin\x64\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <Optimize>true</Optimize>
    <DebugType>pdbonly</DebugType>
    <PlatformTarget>x64</PlatformTarget>
    <LangVersion>7.3</LangVersion>
    <ErrorReport>prompt</ErrorReport>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
    <DebugSymbols>true</DebugSymbols>
    <OutputPath>bin\x86\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <DebugType>full</DebugType>
    <PlatformTarget>x86</PlatformTarget>
    <LangVersion>7.3</LangVersion>
    <ErrorReport>prompt</ErrorReport>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
    <OutputPath>bin\x86\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <Optimize>true</Optimize>
    <DebugType>pdbonly</DebugType>
    <PlatformTarget>x86</PlatformTarget>
    <LangVersion>7.3</LangVersion>
    <ErrorReport>prompt</ErrorReport>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="log4net, Version=3.0.4.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\log4net.3.0.4\lib\net462\log4net.dll</HintPath>
    </Reference>
    <Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
    </Reference>
    <Reference Include="PresentationCore" />
    <Reference Include="PresentationFramework" />
    <Reference Include="System" />
    <Reference Include="System.ComponentModel.DataAnnotations" />
    <Reference Include="System.Configuration" />
    <Reference Include="System.Core" />
    <Reference Include="System.Data.SQLite">
      <HintPath>..\Libs\System.Data.SQLite.dll</HintPath>
    </Reference>
    <Reference Include="System.Messaging" />
    <Reference Include="System.Numerics" />
    <Reference Include="System.Web" />
    <Reference Include="System.Xaml" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Data" />
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Xml" />
    <Reference Include="WindowsBase" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Annotation\ExcelColumn.cs" />
    <Compile Include="Api\AlarmShowHelper.cs" />
    <Compile Include="Api\IAlarmShow.cs" />
    <Compile Include="Api\IConfigManager.cs" />
    <Compile Include="Api\IConfigRefresh.cs" />
    <Compile Include="Api\IFolderOwner.cs" />
    <Compile Include="Api\ILogWriter.cs" />
    <Compile Include="Api\IPlugin.cs" />
    <Compile Include="Api\IPluginContainer.cs" />
    <Compile Include="Annotation\InitAttribute.cs" />
    <Compile Include="Api\PluginPackage.cs" />
    <Compile Include="DpUtil\PermissionHelper.cs" />
    <Compile Include="Entity\AlarmSetting.cs" />
    <Compile Include="Entity\AlarmText.cs" />
    <Compile Include="Entity\LangInfo.cs" />
    <Compile Include="Entity\LoadMsg.cs" />
    <Compile Include="Entity\NotifyBase.cs" />
    <Compile Include="Entity\ParameterData.cs" />
    <Compile Include="Entity\PermissionData.cs" />
    <Compile Include="Entity\ProductData.cs" />
    <Compile Include="Entity\SelectBase.cs" />
    <Compile Include="Entity\SortParam.cs" />
    <Compile Include="Entity\TestItemData.cs" />
    <Compile Include="Entity\UserInfo.cs" />
    <Compile Include="Event\TabChange.cs" />
    <Compile Include="Exceptions\FormValidateException.cs" />
    <Compile Include="FontSizeConverter.cs" />
    <Compile Include="LangConverter.cs" />
    <Compile Include="Manager\TestItemManager.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
    <Compile Include="Setting\ProductSetting.cs" />
    <Compile Include="Setting\ParamSetting.cs" />
    <Compile Include="Setting\SystemSetting.cs" />
    <Compile Include="Util\BeanUtil.cs" />
    <Compile Include="Util\BindingProxy.cs" />
    <Compile Include="Util\DllHelper.cs" />
    <Compile Include="Util\HexUtil.cs" />
    <Compile Include="Util\JsonUtil.cs" />
    <Compile Include="Util\LangUtil.cs" />
    <Compile Include="Util\LoggerHolder.cs" />
    <Compile Include="Util\MSMQHelper.cs" />
    <Compile Include="Manager\ParameterManager.cs" />
    <Compile Include="Util\PathHelper.cs" />
    <Compile Include="Manager\ProductManager.cs" />
    <Compile Include="Util\Sqlite3Helper.cs" />
    <Compile Include="Util\SqliteHelper.cs" />
    <Compile Include="Util\UtilContainer.cs" />
    <Compile Include="Util\XmlHelper.cs" />
  </ItemGroup>
  <ItemGroup>
    <None Include="packages.config" />
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
HMI.Core/LangConverter.cs
New file
@@ -0,0 +1,20 @@
using HMI.Core.Util;
using System;
using System.Globalization;
using System.Windows.Data;
namespace HMI.Core
{
    public class LangConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return LangUtil.Get(parameter.ToString());
        }
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return null;
        }
    }
}
HMI.Core/Manager/ParameterManager.cs
New file
@@ -0,0 +1,136 @@
using HMI.Core.Entity;
using HMI.Core.Util;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace HMI.Core.Manager
{
    /// <summary>
    /// TODO 优化数据源,需要支持sqlite和xml
    /// </summary>
    public class ParameterManager
    {
        /// <summary>
        /// 获取通用参数
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public static T GetCommonParameter<T>() where T : IList<ParameterData>
        {
            string path = PathHelper.ParameterDataPath;
            if (!File.Exists(path)) return default(T);
            T datas = XmlHelper.DeserializeFromXml<T>(path);
            return datas;
        }
        /// <summary>
        /// 获取通用参数
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public static ParameterData GetCommonParameterByName(string name)
        {
            string path = PathHelper.ParameterDataPath;
            if (!File.Exists(path)) return null;
            List<ParameterData> datas = XmlHelper.DeserializeFromXml<List<ParameterData>>(path);
            return datas.FirstOrDefault(d => d.Name == name);
        }
        public static T GetCommonParameterByName<T>(string name, T defaultValue)
        {
            try
            {
                string path = PathHelper.ParameterDataPath;
                if (!File.Exists(path)) return defaultValue;
                List<ParameterData> datas = XmlHelper.DeserializeFromXml<List<ParameterData>>(path);
                var data = datas.FirstOrDefault(d => d.Name == name);
                if (data == null)
                {
                    return defaultValue;
                }
                return (T)Convert.ChangeType(data.Value, typeof(T));
            }
            catch
            {
                return defaultValue;
            }
        }
        /// <summary>
        /// 保存通用参数
        /// </summary>
        /// <param name="datas"></param>
        /// <exception cref="Exception"></exception>
        public static void SaveCommonParameter(IList<ParameterData> datas)
        {
            if (datas == null)
            {
                throw new Exception("不能为null");
            }
            string path = PathHelper.ParameterDataPath;
            XmlHelper.SerializeToXml(path, datas);
        }
        public static string GetCommonParameterValue(string path, string name)
        {
            if (!File.Exists(path)) return null;
            List<ParameterData> datas = XmlHelper.DeserializeFromXml<List<ParameterData>>(path);
            ParameterData data = datas.FirstOrDefault(d => d.Name == name);
            if (data == null) return null;
            return data.Value;
        }
        public static bool UpdateCommonParameter(string name, object value, out string errMsg)
        {
            errMsg = string.Empty;
            try
            {
                string path = PathHelper.ParameterDataPath;
                if (!File.Exists(path))
                {
                    errMsg = "配置文件不存在";
                    return false;
                }
                List<ParameterData> datas = XmlHelper.DeserializeFromXml<List<ParameterData>>(path);
                ParameterData data = datas.FirstOrDefault(d => d.Name == name);
                if (data != null)
                {
                    data.Value = value.ToString();
                    XmlHelper.SerializeToXml(path, datas);
                    return true;
                }
                string type = "String";
                if (value is bool)
                {
                    type = "Bool";
                }
                else if (value is int)
                {
                    type = "Int";
                }
                else if (value is double)
                {
                    type = "Double";
                }
                data = new ParameterData
                {
                    Name = name,
                    Type = type,
                    Value = value.ToString()
                };
                datas.Add(data);
                XmlHelper.SerializeToXml(path, datas);
                return true;
            }
            catch (Exception ex)
            {
                errMsg = ex.Message;
                return false;
            }
        }
    }
}
HMI.Core/Manager/ProductManager.cs
New file
@@ -0,0 +1,82 @@
using HMI.Core.Entity;
using HMI.Core.Setting;
using HMI.Core.Util;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace HMI.Core.Manager
{
    /// <summary>
    /// TODO 优化数据源,需要支持sqlite和xml
    /// </summary>
    public class ProductManager
    {
        public static string GetFolder()
        {
            if (ParamSetting.ShareConfig != null)
            {
                return Path.Combine(ParamSetting.ShareConfig, "Product");
            }
            return Path.Combine(PathHelper.ConfigFolder, "Product");
        }
        public static string GetDataFilePath()
        {
            return Path.Combine(GetFolder(), "Product.data");
        }
        public static string GetParamFolderByProductName(string productName)
        {
            return Path.Combine(GetFolder(), "Parameter", $"Parameter_{productName}.data");
        }
        /// <summary>
        /// 获取保存的产品
        /// </summary>
        /// <returns></returns>
        public static T GetProducts<T>() where T : IList<ProductData>
        {
            string path = GetDataFilePath();
            if (!File.Exists(path)) return default(T);
            T datas = XmlHelper.DeserializeFromXml<T>(path);
            return datas;
        }
        /// <summary>
        /// 获取保存的产品
        /// </summary>
        /// <returns></returns>
        public static T GetParametersByProductName<T>(string productName) where T : IList<ParameterData>
        {
            string path = GetParamFolderByProductName(productName);
            if (!File.Exists(path)) return default(T);
            T datas = XmlHelper.DeserializeFromXml<T>(path);
            return datas;
        }
        public static List<ProductData> GetProducts()
        {
            List<ProductData> datas = GetProducts<List<ProductData>>();
            return datas ?? new List<ProductData>();
        }
        /// <summary>
        /// 根据产品名称获取产品参数
        /// </summary>
        /// <param name="productName"></param>
        /// <returns></returns>
        public static List<ParameterData> GetParametersByProductName(string productName)
        {
            List<ParameterData> datas = GetParametersByProductName<List<ParameterData>>(productName);
            return datas ?? new List<ParameterData>();
        }
        public static ParameterData GetParameterByProductAndName(string product, string name)
        {
            List<ParameterData> datas = GetParametersByProductName<List<ParameterData>>(product);
            return datas?.FirstOrDefault(e => e.Name == name);
        }
    }
}
HMI.Core/Manager/TestItemManager.cs
New file
@@ -0,0 +1,43 @@
using HMI.Core.Entity;
using HMI.Core.Util;
using System;
using System.Collections.Generic;
using System.IO;
namespace HMI.Core.Manager
{
    /// <summary>
    /// TODO 优化数据源,需要支持sqlite和xml
    /// </summary>
    public class TestItemManager
    {
        /// <summary>
        /// 根据产品获取测试项
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public static T GetTestItemByProduct<T>(string product) where T : IList<TestItemData>
        {
            string path = Path.Combine(PathHelper.TestItemDataFolder, $"TestItem_{product}.setting");
            if (!File.Exists(path)) return default(T);
            T datas = XmlHelper.DeserializeFromXml<T>(path);
            return datas;
        }
        /// <summary>
        /// 根据产品保存测试项
        /// </summary>
        /// <param name="datas"></param>
        /// <exception cref="Exception"></exception>
        public static void SaveTestItemByProduct(string product, IList<TestItemData> datas)
        {
            if (datas == null)
            {
                throw new Exception("不能为null");
            }
            string path = Path.Combine(PathHelper.TestItemDataFolder, $"TestItem_{product}.setting");
            XmlHelper.SerializeToXml(path, datas);
        }
    }
}
HMI.Core/Properties/AssemblyInfo.cs
New file
@@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// 有关程序集的一般信息由以下
// 控制。更改这些特性值可修改
// 与程序集关联的信息。
[assembly: AssemblyTitle("HMI.Core")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("HP Inc.")]
[assembly: AssemblyProduct("HMI.Core")]
[assembly: AssemblyCopyright("Copyright © HP Inc. 2024")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 将 ComVisible 设置为 false 会使此程序集中的类型
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
//请将此类型的 ComVisible 特性设置为 true。
[assembly: ComVisible(false)]
// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID
[assembly: Guid("3e3bf44a-0b69-45c5-9512-528cb48a7106")]
// 程序集的版本信息由下列四个值组成:
//
//      主版本
//      次版本
//      生成号
//      修订号
//
//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值
//通过使用 "*",如下所示:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.1")]
[assembly: AssemblyFileVersion("1.0.0.1")]
HMI.Core/Setting/ParamSetting.cs
New file
@@ -0,0 +1,241 @@
using HMI.Core.Manager;
using HMI.Core.Util;
using System.Collections;
using System.Configuration;
using System.IO;
using System.Runtime.CompilerServices;
namespace HMI.Core.Setting
{
    public class ParamSetting
    {
        private static Hashtable customConfig = (Hashtable)ConfigurationManager.GetSection("TestResultConfig");
        private static string GetValue([CallerMemberName] string propertyName = null)
        {
            return (customConfig != null && customConfig.ContainsKey(propertyName)) ? customConfig[propertyName].ToString() : null;
        }
        /// <summary>
        /// MesAddress
        /// </summary>
        public static string MesAddress { get; private set; } = "127.0.0.1:8080";
        /// <summary>
        /// 老化导出行转列
        /// </summary>
        public static bool ReverseCSV { get; private set; } = false;
        /// <summary>
        /// 客户编码
        /// </summary>
        public static string CustomerCode { get; private set; }
        /// <summary>
        /// 生产数据DB存放目录
        /// </summary>
        public static string DataLogFolder { get; private set; } = null;
        /// <summary>
        /// 是否开启水冷机设置
        /// </summary>
        public static bool ShowCoolerSetting { get; private set; } = false;
        public static bool ShowWaterChart { get; private set; } = false;
        /// <summary>
        /// 按型号计算探针
        /// </summary>
        public static bool ProbeByModel { get; private set; } = false;
        public static int ReverseCSVSkipRow { get; private set; } = 3;
        /// <summary>
        /// 操作是否需要密码
        /// </summary>
        public static bool IsOptRequirePassword { get; private set; } = false;
        /// <summary>
        /// 启用扫码换型
        /// </summary>
        public static bool EnableAutoModel { get; private set; } = false;
        public const string ShareConfigKey = "ShareConfig";
        public static string ShareConfig { get; private set; } = null;
        public static bool ShowBurnInAbort { get; private set; } = true;
        public static string SocketNames { get; private set; } = string.Empty;
        public static string StationName { get; private set; } = string.Empty;
        public static string MainSeqPath { get; private set; } = string.Empty;
        public static string EngineType
        {
            get
            {
                var data = GetValue();
                return data == null ? "TestStand" : data.ToString();
            }
        }
        static ParamSetting()
        {
            try
            {
                var value = ParameterManager.GetCommonParameterByName("MesAddress");
                if (value != null)
                {
                    MesAddress = value.Value;
                }
            }
            catch
            {
            }
            try
            {
                var value = ParameterManager.GetCommonParameterByName("ReverseCSV");
                if (value != null)
                {
                    ReverseCSV = bool.Parse(value.Value);
                }
            }
            catch
            {
            }
            try
            {
                var value = ParameterManager.GetCommonParameterByName("TS_DataLogPath");
                DataLogFolder = value?.Value ?? Path.Combine(PathHelper.ConfigFolder, "DataBoard");
            }
            catch
            {
                DataLogFolder = Path.Combine(PathHelper.ConfigFolder, "DataBoard");
            }
            try
            {
                var value = ParameterManager.GetCommonParameterByName("ShowWaterChart");
                if (value != null)
                {
                    ShowWaterChart = bool.Parse(value.Value);
                }
            }
            catch
            {
            }
            try
            {
                var value = ParameterManager.GetCommonParameterByName("ShowCoolerSetting");
                if (value != null)
                {
                    ShowCoolerSetting = bool.Parse(value.Value);
                }
            }
            catch
            {
            }
            try
            {
                var value = ParameterManager.GetCommonParameterByName("CustomerCode");
                if (value != null)
                {
                    CustomerCode = value.Value;
                }
            }
            catch
            {
            }
            try
            {
                var value = ParameterManager.GetCommonParameterByName("ProbeByModel");
                if (value != null)
                {
                    ProbeByModel = bool.Parse(value.Value);
                }
            }
            catch
            {
            }
            try
            {
                var value = ParameterManager.GetCommonParameterByName("ReverseCSVSkipRow");
                if (value != null)
                {
                    ReverseCSVSkipRow = int.Parse(value.Value);
                }
            }
            catch
            {
            }
            try
            {
                IsOptRequirePassword = ParameterManager.GetCommonParameterByName<bool>("IsOptRequirePassword", false);
            }
            catch
            {
            }
            try
            {
                EnableAutoModel = ParameterManager.GetCommonParameterByName<bool>("EnableAutoModel", false);
            }
            catch
            {
            }
            try
            {
                ShareConfig = ParameterManager.GetCommonParameterByName<string>(ShareConfigKey, null);
            }
            catch
            {
            }
            try
            {
                ShowBurnInAbort = ParameterManager.GetCommonParameterByName<bool>("ShowBurnInAbort", true);
            }
            catch
            {
            }
            try
            {
                SocketNames = ParameterManager.GetCommonParameterByName<string>("SocketNames", string.Empty);
            }
            catch
            {
            }
            try
            {
                StationName = ParameterManager.GetCommonParameterByName<string>("StationName", string.Empty);
            }
            catch
            {
            }
            try
            {
                MainSeqPath = ParameterManager.GetCommonParameterByName<string>("BurnInMainSeq", string.Empty);
            }
            catch
            {
            }
        }
    }
}
HMI.Core/Setting/ProductSetting.cs
New file
@@ -0,0 +1,10 @@
using HMI.Core.Manager;
using HMI.Core.Util;
using System.IO;
namespace HMI.Core.Setting
{
    public class ProductSetting
    {
    }
}
HMI.Core/Setting/SystemSetting.cs
New file
@@ -0,0 +1,134 @@
using System;
using System.IO;
using HMI.Core.Entity;
using HMI.Core.Util;
namespace HMI.Core.Setting
{
    public class SystemSetting
    {
        public static SystemSetting Instance { get; private set; }
        /// <summary>
        /// 系统设置改变事件
        /// </summary>
        public static event EventHandler<SystemSetting> SystemSettingChanged;
        private static string FilePath = Path.Combine(PathHelper.SettingFolder, "SystemConfig.xml");
        static SystemSetting()
        {
            LoadFromXml();
        }
        private SystemSetting()
        {
        }
        public string LogoPath { get; private set; } = ".\\Logo.png";
        public string LoginLogoPath { get; private set; } = ".\\LoginLogo.png";
        public string WindowTitle { get; private set; } = "产品测试平台";
        public string PlcIp { get; private set; } = "127.0.0.1";
        public int PlcPort { get; private set; } = 102;
        public bool PlcEnabled { get; private set; }
        public string CurrentLanguage { get; private set; } = "zh-CN";
        public int LogExpiredDays { get; private set; } = 7;
        private static void LoadFromXml()
        {
            if (Instance != null) return;
            if (!File.Exists(FilePath))
            {
                Instance = new SystemSetting();
            }
            else
            {
                SystemSettingModel model = XmlHelper.DeserializeFromXml<SystemSettingModel>(FilePath);
                SystemSetting setting = new SystemSetting();
                BeanUtil.Copy(model, setting);
                Instance = setting;
            }
        }
        public static SystemSettingModel GetModel()
        {
            return XmlHelper.DeserializeFromXml<SystemSettingModel>(FilePath);
        }
        public static void ReLoad()
        {
            if (!File.Exists(FilePath))
            {
                return;
            }
            else
            {
                SystemSettingModel model = XmlHelper.DeserializeFromXml<SystemSettingModel>(FilePath);
                SystemSetting setting = new SystemSetting();
                BeanUtil.Copy(model, setting);
                Instance = setting;
            }
            SystemSettingChanged?.Invoke(null, Instance);
        }
    }
    public class SystemSettingModel : NotifyBase
    {
        private string logoPath;
        public string LogoPath
        {
            get => logoPath;
            set { Set(ref logoPath, value); }
        }
        private string loginLogoPath;
        public string LoginLogoPath
        {
            get => loginLogoPath;
            set { Set(ref loginLogoPath, value); }
        }
        private string windowTitle;
        public string WindowTitle
        {
            get => windowTitle;
            set { Set(ref windowTitle, value); }
        }
        private string plcIp;
        public string PlcIp
        {
            get => plcIp;
            set { Set(ref plcIp, value); }
        }
        private int plcPort;
        public int PlcPort
        {
            get => plcPort;
            set { Set(ref plcPort, value); }
        }
        private bool plcEnabled;
        public bool PlcEnabled
        {
            get => plcEnabled;
            set { Set(ref plcEnabled, value); }
        }
        private string currentLanguage;
        public string CurrentLanguage
        {
            get => currentLanguage;
            set { Set(ref currentLanguage, value); }
        }
        private int logExpiredDays;
        public int LogExpiredDays
        {
            get => logExpiredDays;
            set { Set(ref logExpiredDays, value); }
        }
    }
}
HMI.Core/Util/BeanUtil.cs
New file
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Reflection;
namespace HMI.Core.Util
{
    public class BeanUtil
    {
        public static void Copy<S, T>(S source, T target)
        {
            Type sourceType = typeof(S);
            Type targetType = typeof(T);
            PropertyInfo[] fieldArray = targetType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
            foreach (PropertyInfo field in fieldArray)
            {
                string fileName = field.Name;
                PropertyInfo sourceField = sourceType.GetProperty(fileName);
                if (sourceField == null) continue;
                field.SetValue(target, sourceField.GetValue(source));
            }
        }
        public static List<T> ReadBeanFromDataTable<T>(DataTable table)
        {
            if (table == null || table.Rows.Count <= 0) return new List<T>();
            List<T> list = new List<T>();
            foreach (DataRow row in table.Rows)
            {
                T bean = Activator.CreateInstance<T>();
                foreach (var prop in typeof(T).GetProperties())
                {
                    if (table.Columns.Contains(prop.Name))
                    {
                        var value = row[prop.Name];
                        if (value != DBNull.Value)
                        {
                            prop.SetValue(bean, Convert.ChangeType(value, prop.PropertyType));
                        }
                    }
                }
                list.Add(bean);
            }
            return list;
        }
    }
}
HMI.Core/Util/BindingProxy.cs
New file
@@ -0,0 +1,22 @@
using System.Windows;
namespace HMI.Core.Util
{
    public class BindingProxy : Freezable
    {
        protected override Freezable CreateInstanceCore()
        {
            return new BindingProxy();
        }
        public object Data
        {
            get { return (object)GetValue(DataProperty); }
            set { SetValue(DataProperty, value); }
        }
        public static readonly DependencyProperty DataProperty =
            DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy),
                                        new UIPropertyMetadata(null));
    }
}
HMI.Core/Util/DllHelper.cs
New file
@@ -0,0 +1,197 @@
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CSharp;
namespace HMI.Core.Util
{
    /// <summary>
    /// DLL文件操作类
    /// </summary>
    public class DllHelper
    {
        /// <summary>
        /// 获取当前DLL所处的路径
        /// </summary>
        /// <returns>Dll路径</returns>
        public static string GetAssemblyPath()
        {
            string CodeBase = System.Reflection.Assembly.GetExecutingAssembly().CodeBase;
            CodeBase = CodeBase.Substring(8, CodeBase.Length - 8);    // 8是 file:// 的长度
            string[] arrSection = CodeBase.Split(new char[] { '/' });
            string FolderPath = string.Empty;
            for (int i = 0; i < arrSection.Length - 1; i++)
            {
                FolderPath += arrSection[i] + "\\";
            }
            return FolderPath;
        }
        /// <summary>
        /// 导入DLL(要导入的DLL要放在程序的根目录中)
        /// </summary>
        /// <param name="DLLName">要导入的DLL名称</param>
        public static void ImportDLL(string DllPath)
        {
            try
            {
                //string DllPath = GetAssemblyPath() + DLLName;
                Assembly[] assemblyArray = AppDomain.CurrentDomain.GetAssemblies();
                foreach (Assembly dll in assemblyArray)
                {
                    if (dll.Location == DllPath)
                    {
                        return;
                    }
                }
                Assembly.LoadFrom(DllPath);
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }
        }
        /// <summary>
        /// 导入DLL
        /// </summary>
        /// <param name="DllPath">要导入的DLL文件路径</param>
        public static void ImportDLL2(string DllPath)
        {
            try
            {
                Assembly[] assemblyArray = AppDomain.CurrentDomain.GetAssemblies();
                foreach (Assembly dll in assemblyArray)
                {
                    if (!dll.FullName.Contains("Microsoft.GeneratedCode"))
                    {
                        if (dll.Location == DllPath)
                        {
                            return;
                        }
                    }
                }
                Assembly.LoadFile(DllPath);
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }
        }
        /// <summary>
        /// 生成无参数的构造函数的实例
        /// </summary>
        /// <param name="NameSpaceStr">命名空间</param>
        /// <param name="ClassStr">包含命名控件的类名</param>
        /// <returns>生成的对应类的实例</returns>
        public static object CreateInstance(string NameSpaceStr, string ClassStr)
        {
            try
            {
                Type DllType = Assembly.Load(NameSpaceStr).GetType(ClassStr); // 生成类型
                object Instance = Activator.CreateInstance(DllType); // 生成实例
                return Instance;
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }
        }
        /// <summary>
        /// 动态编译和执行语句
        /// </summary>
        /// <param name="Expression">要编译执行的语句</param>
        /// <returns>执行的结果</returns>
        public static object GetValue(string Expression)
        {
            object Rtn = null;
            // 1.CSharpCodePrivoder
            CSharpCodeProvider objCSharpCodePrivoder = new CSharpCodeProvider();
            // 2.ICodeComplier
            ICodeCompiler objICodeCompiler = objCSharpCodePrivoder.CreateCompiler();
            // 3.CompilerParameters
            CompilerParameters objCompilerParameters = new CompilerParameters();
            objCompilerParameters.ReferencedAssemblies.Add("System.dll");
            objCompilerParameters.GenerateExecutable = false;
            objCompilerParameters.GenerateInMemory = true;
            // 4.CompilerResults
            CompilerResults cr = objICodeCompiler.CompileAssemblyFromSource(objCompilerParameters, GenerateCode(Expression));
            if (cr.Errors.HasErrors)
            {
                throw new Exception("编译错误:" + cr.Errors[0].ErrorText);
            }
            else
            {
                // 通过反射,调用HelloWorld的实例
                Assembly objAssembly = cr.CompiledAssembly;
                object Obj = objAssembly.CreateInstance("CommonLibs.Eval");
                MethodInfo objMI = Obj.GetType().GetMethod("GetValue");
                Rtn = objMI.Invoke(Obj, null);
            }
            return Rtn;
        }
        /// <summary>
        /// 生成编译语句
        /// </summary>
        /// <param name="Expression">需要编译的语句</param>
        /// <returns>编译执行后的结果</returns>
        private static string GenerateCode(string Expression)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("using System;");
            sb.Append(Environment.NewLine);
            sb.Append("namespace CommonLibs");
            sb.Append(Environment.NewLine);
            sb.Append("{");
            sb.Append(Environment.NewLine);
            sb.Append("    public class Eval");
            sb.Append(Environment.NewLine);
            sb.Append("    {");
            sb.Append(Environment.NewLine);
            sb.Append("        public object GetValue()");
            sb.Append(Environment.NewLine);
            sb.Append("        {");
            sb.Append(Environment.NewLine);
            sb.Append("             return " + Expression + " ;");
            sb.Append(Environment.NewLine);
            sb.Append("        }");
            sb.Append(Environment.NewLine);
            sb.Append("    }");
            sb.Append(Environment.NewLine);
            sb.Append("}");
            string code = sb.ToString();
            return code;
        }
        /// <summary>
        ///
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="obj"></param>
        /// <returns></returns>
        public static T DeepCopy<T>(T obj)
        {
            //如果是字符串或值类型则直接返回
            if (obj is string || obj.GetType().IsValueType) return obj;
            object retval = Activator.CreateInstance(obj.GetType());
            FieldInfo[] fields = obj.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
            foreach (FieldInfo field in fields)
            {
                try { field.SetValue(retval, DeepCopy(field.GetValue(obj))); }
                catch { }
            }
            return (T)retval;
        }
    }
}
HMI.Core/Util/HexUtil.cs
New file
@@ -0,0 +1,16 @@
using System;
namespace HMI.Core.Util
{
    //public class HexUtil
    //{
    //    public static byte[] ToBytesFromHexString(string hexString)
    //    {
    //        //以 ' ' 分割字符串,并去掉空字符
    //        string[] chars = hexString.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
    //        byte[] returnBytes;
    //        returnBytes = BitConverter.GetBytes(Convert.ToInt32(chars[0]));
    //        return returnBytes;
    //    }
    //}
}
HMI.Core/Util/JsonUtil.cs
New file
@@ -0,0 +1,26 @@
using Newtonsoft.Json;
namespace HMI.Core.Util
{
    public class JsonUtil
    {
        static readonly JsonSerializerSettings JsSetting = new JsonSerializerSettings()
        {
            NullValueHandling = NullValueHandling.Ignore
        };
        public static T Deserialize<T>(string jsonStr)
        {
            return JsonConvert.DeserializeObject<T>(jsonStr);
        }
        public static string Serialize(object obj)
        {
            return JsonConvert.SerializeObject(obj);
        }
        public static string Stringfiy(object obj)
        {
            return JsonConvert.SerializeObject(obj, Formatting.Indented, JsSetting);
        }
    }
}
HMI.Core/Util/LangUtil.cs
New file
@@ -0,0 +1,177 @@
using HMI.Core.Entity;
using HMI.Core.Setting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Windows;
using System.Windows.Markup;
namespace HMI.Core.Util
{
    public class LangUtil
    {
        public const string DefaultLang = "zh-CN";
        public static List<LangInfo> langs = new List<LangInfo>() {
            new LangInfo(DefaultLang,"中文"),
            new LangInfo("en-US","English")
        };
        private static ResourceDictionary currentResource;
        private static Dictionary<string, ResourceDictionary> ResourceMap
            = new Dictionary<string, ResourceDictionary>();
        private static Dictionary<string, JObject> JsonMap = new Dictionary<string, JObject>();
        static LangUtil()
        {
        }
        private static string GetLang()
        {
            var lang = SystemSetting.Instance.CurrentLanguage;
            if (string.IsNullOrEmpty(lang)) lang = "zh-CN";
            return lang;
        }
        public static void LoadLang()
        {
            var lang = GetLang();
            if (!ResourceMap.ContainsKey(lang)) return;
            var newLang = ResourceMap[lang];
            if (currentResource == newLang)
            {
                return;
            }
            if (currentResource != null)
            {
                Application.Current.Resources.MergedDictionaries.Remove(currentResource);
            }
            currentResource = newLang;
            Application.Current.Resources.MergedDictionaries.Add(currentResource);
        }
        public static void Load()
        {
            try
            {
                LoadAllData();
                LoadLang();
            }
            finally
            {
            }
        }
        public static string Get(string key)
        {
            var lang = GetLang();
            if (!JsonMap.ContainsKey(lang)) return key;
            if (!JsonMap[lang].ContainsKey(key)) return key;
            return JsonMap[lang][key].ToString();
        }
        private static void LoadDataByJson(string lang)
        {
            string langFolder = PathHelper.LangFolder;
            string folder = Path.Combine(langFolder, lang);
            if (!Directory.Exists(folder))
            {
                return;
            }
            string[] files = Directory.GetFiles(folder, "*.json", SearchOption.AllDirectories);
            foreach (var path in files)
            {
                try
                {
                    using (FileStream stream = File.Open(path, FileMode.Open))
                    {
                        using (StreamReader streamReader = new StreamReader(stream))
                        {
                            JsonTextReader jsonTextReader = new JsonTextReader(streamReader);
                            JObject jsonObject = (JObject)JToken.ReadFrom(jsonTextReader);
                            if (!JsonMap.ContainsKey(lang))
                            {
                                JsonMap[lang] = jsonObject;
                            }
                            else
                            {
                                foreach (var item in jsonObject)
                                {
                                    JsonMap[lang][item.Key] = item.Value;
                                }
                            }
                        }
                    }
                }
                catch
                {
                }
            }
        }
        private static void LoadXamlFromJson(string lang)
        {
            LoadDataByJson(lang);
            if (!JsonMap.ContainsKey(lang)) return;
            JObject json = JsonMap[lang];
            var resource = new ResourceDictionary();
            foreach (var item in json)
            {
                resource.Add(item.Key, item.Value.ToString());
            }
            ResourceMap[lang] = resource;
        }
        private static void LoadXamlFromJson2(string lang)
        {
            LoadDataByJson(lang);
            if (!JsonMap.ContainsKey(lang)) return;
            JObject json = JsonMap[lang];
            var resource = new ResourceDictionary();
            foreach (var item in json)
            {
                resource.Add(item.Key, item.Value.ToString());
            }
            ResourceMap[lang] = resource;
            if (lang != DefaultLang)
            {
                var defaultMap = new ResourceDictionary();
                foreach (var item in json)
                {
                    defaultMap.Add(item.Key, item.Key);
                }
                ResourceMap[DefaultLang] = defaultMap;
            }
        }
        private static void LoadAllData()
        {
            foreach (var lang in langs)
            {
                LoadXamlFromJson(lang.Code);
            }
        }
        public static string Format(string key, params object[] datas)
        {
            return string.Format(Get(key), datas);
        }
    }
    public static class LangReader
    {
        public static string lang(this object dp, string key, params object[] datas)
        {
            return string.Format(LangUtil.Get(key), datas);
        }
    }
}
HMI.Core/Util/LoggerHolder.cs
New file
@@ -0,0 +1,37 @@
using HMI.Core.Api;
namespace HMI.Core.Util
{
    public class LoggerHelper
    {
        public static ILogWriter Logger { get; private set; }
        public static void Register(ILogWriter logger)
        {
            if (Logger == null)
            {
                Logger = logger;
            }
        }
        public static void Debug(string source, string content)
        {
            Logger?.Debug(source, content);
        }
        public static void Info(string source, string content)
        {
            Logger?.Info(source, content);
        }
        public static void Warn(string source, string content)
        {
            Logger?.Warn(source, content);
        }
        public static void Error(string source, string content)
        {
            Logger?.Error(source, content);
        }
    }
}
HMI.Core/Util/MSMQHelper.cs
New file
@@ -0,0 +1,144 @@
//-----------------------------------------------------------------------
// <copyright file="MSMQHelper.cs" company="iPilotWhale">
//      Suzhou Linktron Systems Co.,Ltd . All rights reserved.
// </copyright>
// <author>温向超</author>
// <description>消息队列操作辅助类</description>
//-----------------------------------------------------------------------
using System;
using System.Messaging;
namespace HMI.Core.Util
{
    /// <summary>
    /// 消息队列操作类
    /// </summary>
    public class MSMQHelperCCC
    {
        /// <summary>
        /// 消息队列名称
        /// </summary>
        private string msmqName = string.Empty;
        /// <summary>
        /// 消息队列路径
        /// </summary>
        private string msmqPath = string.Empty;
        /// <summary>
        /// 要操作的消息队列
        /// </summary>
        private MessageQueue msgQueue = null;
        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="Name">消息队列名称</param>
        public MSMQHelperCCC(string Name)
        {
            try
            {
                this.msmqName = Name;
                this.msmqPath = ".\\private$\\" + this.msmqName;
                if (MessageQueue.Exists(this.msmqPath))
                {
                    // 如果当前消息队列已经存在
                    this.msgQueue = new MessageQueue(this.msmqPath);
                }
                else
                {
                    // 如果消息队列不存在,则创建消息对垒
                    this.msgQueue = MessageQueue.Create(this.msmqPath);
                }
                this.msgQueue.Formatter = new XmlMessageFormatter(new Type[] { typeof(string) });
            }
            catch(Exception ex)
            {
                throw new Exception(ex.Message);
            }
        }
        /// <summary>
        /// 发送消息
        /// </summary>
        /// <param name="MessageID">消息ID</param>
        /// <param name="MsgString">要发送的字符串</param>
        public void Send(string MessageID,string MsgString)
        {
            try
            {
                this.msgQueue.Send(MsgString, MessageID);
            }
            catch(Exception ex)
            {
                throw new Exception(ex.Message);
            }
        }
        /// <summary>
        /// 接收消息队列中的消息
        /// </summary>
        /// <param name="TimeOut">超时时间(ms)</param>
        /// <param name="MessageID">消息ID</param>
        /// <param name="MsgString">消息内容</param>
        public void Receive(long TimeOut,out string MessageID,out string MsgString)
        {
            try
            {
                MessageID = string.Empty;
                MsgString = string.Empty;
                TimeSpan Time = new TimeSpan(TimeOut);
                Message MsgReceived = null;
                try
                {
                    MsgReceived = this.msgQueue.Receive(Time);
                }
                catch
                {
                    // do nothing
                }
                if(MsgReceived!=null)
                {
                    MessageID = MsgReceived.Label;
                    MsgString = MsgReceived.Body.ToString();
                }
            }
            catch(Exception ex)
            {
                throw new Exception("接收消息失败:" + ex.Message);
            }
        }
        /// <summary>
        /// 清除消息队列中的消息
        /// </summary>
        public void Clear()
        {
            try
            {
                this.msgQueue.Purge();
            }
            catch(Exception ex)
            {
                throw new Exception("清除消息队列失败:" + ex.Message);
            }
        }
        /// <summary>
        /// 关闭当前队列
        /// </summary>
        public void Close()
        {
            this.msgQueue.Close();
        }
        /// <summary>
        /// 删除当前队列
        /// </summary>
        public void Delete()
        {
            MessageQueue.Delete(this.msmqPath);
        }
    }
}
HMI.Core/Util/PathHelper.cs
New file
@@ -0,0 +1,188 @@
using System;
using System.IO;
namespace HMI.Core.Util
{
    public class PathHelper
    {
        /// <summary>
        /// 程序目录
        /// </summary>
        public static string AppFolder { get; private set; } = RootPath();
        /// <summary>
        /// 图标目录
        /// </summary>
        public static string IconFolder { get; private set; }
        /// <summary>
        /// Config目录
        /// </summary>
        public static string ConfigFolder { get; private set; }
        /// <summary>
        /// Sqlite数据库目录
        /// </summary>
        public static string DatabaseFolder { get; private set; }
        /// <summary>
        /// Sqlite数据库Path
        /// </summary>
        public static string DatabasePath { get; private set; }
        /// <summary>
        /// 多语言目录
        /// </summary>
        public static string LangFolder { get; private set; }
        /// <summary>
        /// 系统设置目录
        /// </summary>
        public static string SettingFolder { get; private set; }
        /// <summary>
        /// 参数目录
        /// </summary>
        public static string ParameterFolder { get; private set; }
        /// <summary>
        /// 参数数据源文件
        /// </summary>
        public static string ParameterDataPath { get; private set; }
        /// <summary>
        /// 测试项数据目录
        /// </summary>
        public static string TestItemDataFolder { get; private set; }
        /// <summary>
        /// 测试项数据源文件
        /// </summary>
        public static string TestItemDataFile { get; private set; }
        /// <summary>
        /// sqlite日志数据库文件夹
        /// </summary>
        public static string LogViewerDir { get; private set; }
        /// <summary>
        /// 应用程序的根目录
        /// </summary>
        /// <returns></returns>
        private static string RootPath()
        {
            string DllPath = AppDomain.CurrentDomain.BaseDirectory;
            string path = Path.GetFullPath(".");
            IconFolder = InitFolder(path + "\\Icon");
            ConfigFolder = InitFolder(path + "\\Config");
            DatabaseFolder = InitFolder(ConfigFolder + "\\Database");
            DatabasePath = DatabaseFolder + "\\database.db";
            LangFolder = InitFolder(path + "\\Lang");
            SettingFolder = InitFolder(ConfigFolder + "\\SystemSetting");
            ParameterFolder = InitFolder(ConfigFolder + "\\Parameter");
            ParameterDataPath = ParameterFolder + "\\CommonParameter.data";
            //ProductDataFolder = InitFolder(ConfigFolder + "\\Product");
            //ProductParameterFolder = InitFolder(ProductDataFolder + "\\Parameter");
            //ProductDataFile = ProductDataFolder + "\\Product.data";
            // TestItemDataFolder = InitFolder(ProductDataFolder + "\\TestItem");
            TestItemDataFile = TestItemDataFolder + "\\TestItem.data";
            LogViewerDir = InitFolder(ConfigFolder + "\\LogViewer");
            return path;
        }
        /// <summary>
        /// 系统配置信息存储文件夹路径
        /// </summary>
        /// <returns>系统配置信息文件夹</returns>
        public static string SystemSettingFolder()
        {
            string Path = RootPath() + "\\Config\\SystemSetting";
            return Path;
        }
        /// <summary>
        /// 系统配置文件
        /// </summary>
        /// <returns>系统配置文件路径</returns>
        public static string SystemSettingFile()
        {
            string Path = RootPath() + "\\Config\\SystemSetting\\system.setting";
            return Path;
        }
        // <summary>
        /// 菜单配置文件
        /// </summary>
        /// <returns>菜单配置文件</returns>
        public static string MenuFile()
        {
            string Path = RootPath() + "\\Config\\SystemSetting\\menu.setting";
            return Path;
        }
        /// <summary>
        /// 生产配置文件夹
        /// </summary>
        /// <returns>生产配置文件夹</returns>
        public static string ProductionFolder()
        {
            string Path = RootPath() + "\\Config\\ProductionSetting";
            return Path;
        }
        /// <summary>
        /// 测试项信息存储文件夹
        /// </summary>
        /// <returns>测试项存储文件夹</returns>
        public static string TestItemFolder()
        {
            string Path = ProductionFolder() + "\\TestItem";
            return Path;
        }
        public static string BlockAlarmSettingPath()
        {
            string Path = RootPath() + "\\Config\\AlarmInfo\\BlockSetting.xml";
            return Path;
        }
        public static string MarkAlarmSettingPath()
        {
            string Path = RootPath() + "\\Config\\AlarmInfo\\MarkSetting.xml";
            return Path;
        }
        /// <summary>
        /// 插件存储目录
        /// </summary>
        /// <returns>插件存储目录</returns>
        public static string PluginFolder()
        {
            string Path = RootPath() + "\\Plugin";
            return Path;
        }
        public static string InitFolder(string path)
        {
            try
            {
                if (!Directory.Exists(path))
                {
                    Directory.CreateDirectory(path);
                }
            }
            catch
            {
            }
            return path;
        }
    }
}
HMI.Core/Util/Sqlite3Helper.cs
New file
@@ -0,0 +1,270 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SQLite;
namespace HMI.Core.Util
{
    public class Sqlite3Helper : IDisposable
    {
        public string DataBaseFile { get; private set; } = string.Empty;
        private string ConnectString = string.Empty;
        public const int MAX_ATTACHED = 30;
        private SQLiteConnection conn = null;
        public Sqlite3Helper(string DBFile)
        {
            DataBaseFile = DBFile;
            ConnectString = "DataSource=" + DataBaseFile + ";Version=3;Pooling=False;Max Pool Size=100;";
            conn = new SQLiteConnection(ConnectString);
            Connect();
        }
        public static bool CreateDatabase(string DataBaseFile)
        {
            try
            {
                SQLiteConnection.CreateFile(DataBaseFile);
                return true;
            }
            catch (Exception ex)
            {
                throw new Exception($"Create Database [${DataBaseFile}] Failed.", ex);
            }
        }
        public int Execute(string Sql)
        {
            try
            {
                using (var cmd = new SQLiteCommand(conn))
                {
                    cmd.CommandType = CommandType.Text;
                    cmd.CommandText = Sql;
                    return cmd.ExecuteNonQuery();
                }
            }
            catch (Exception ex)
            {
                string message = "ExecuteSql Error:" + ex.Message;
                throw new Exception(message);
            }
        }
        public long InsertWithId(string Sql)
        {
            try
            {
                if (Sql.EndsWith(";"))
                {
                    Sql += "SELECT last_insert_rowid();";
                }
                else
                {
                    Sql += ";SELECT last_insert_rowid();";
                }
                using (var cmd = new SQLiteCommand(Sql, conn))
                {
                    return (long)cmd.ExecuteScalar();
                }
            }
            catch (Exception ex)
            {
                string message = "ExecuteSql Error:" + ex.Message;
                throw new Exception(message);
            }
        }
        public object SelectOne(string Sql)
        {
            try
            {
                using (var cmd = new SQLiteCommand(Sql, conn))
                {
                    return cmd.ExecuteScalar();
                }
            }
            catch (Exception ex)
            {
                string message = "ExecuteSql Error:" + ex.Message;
                throw new Exception(message);
            }
        }
        public int ExecuteBatch(IEnumerable<string> sqls)
        {
            SQLiteTransaction tr = null;
            int res = 0;
            try
            {
                Connect();
                tr = conn.BeginTransaction();
                using (var cmd = new SQLiteCommand(conn))
                {
                    cmd.CommandType = CommandType.Text;
                    foreach (string sql in sqls)
                    {
                        if (string.IsNullOrEmpty(sql) || string.IsNullOrWhiteSpace(sql))
                            continue;
                        cmd.CommandText = sql;
                        res += cmd.ExecuteNonQuery();
                    }
                }
                tr.Commit();
                return res;
            }
            catch (Exception ex)
            {
                tr?.Rollback();
                throw ex;
            }
        }
        public DataTable GetDataTable(string Sql)
        {
            DataTable dataTable = new DataTable();
            try
            {
                Connect();
                using (var cmd = new SQLiteCommand(conn)
                {
                    CommandType = CommandType.Text,
                    CommandText = Sql
                })
                using (SQLiteDataAdapter sQLiteDataAdapter = new SQLiteDataAdapter()
                {
                    SelectCommand = cmd
                })
                {
                    sQLiteDataAdapter.Fill(dataTable);
                }
            }
            catch (Exception ex)
            {
                string message = "ExecuteSql Error:" + ex.Message;
                throw new Exception(message);
            }
            return dataTable;
        }
        public string[,] GetDataTableToArray(string Sql)
        {
            try
            {
                DataTable dataTable = GetDataTable(Sql);
                string[,] array = new string[dataTable.Rows.Count, dataTable.Columns.Count];
                int num = 0;
                int num2 = 0;
                foreach (DataRow row in dataTable.Rows)
                {
                    num2 = 0;
                    object[] itemArray = row.ItemArray;
                    foreach (object obj in itemArray)
                    {
                        array[num, num2] = obj.ToString();
                        num2++;
                    }
                    num++;
                }
                return array;
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }
        }
        public void Close()
        {
            try
            {
                if (conn.State == ConnectionState.Open)
                {
                    conn.Close();
                }
                if (conn != null)
                {
                    conn.Dispose();
                }
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }
        }
        public bool HasData(string sql)
        {
            DataTable dataTable = GetDataTable(sql);
            return dataTable.Rows.Count > 0;
        }
        public string[] GetColumnNameFromTable(string TableName)
        {
            try
            {
                string sql = $"PRAGMA table_info('{TableName}')";
                DataTable dataTable = GetDataTable(sql);
                string[] array = new string[dataTable.Rows.Count];
                int num = 0;
                foreach (DataRow row in dataTable.Rows)
                {
                    array[num] = row.ItemArray[1].ToString();
                    num++;
                }
                return array;
            }
            catch (Exception ex)
            {
                throw new Exception("获取表字段名称失败:" + ex.Message);
            }
        }
        public string[] GetAllTableInFile()
        {
            try
            {
                string sql = "select name from sqlite_master where type = 'table' order by name";
                DataTable dataTable = GetDataTable(sql);
                string[] array = new string[dataTable.Rows.Count];
                int num = 0;
                foreach (DataRow row in dataTable.Rows)
                {
                    array[num] = row.ItemArray[0].ToString();
                    num++;
                }
                return array;
            }
            catch (Exception ex)
            {
                throw new Exception("获取表名失败:" + ex.Message);
            }
        }
        private void Connect()
        {
            if (conn.State == ConnectionState.Closed)
            {
                conn.ConnectionString = ConnectString;
                conn.Open();
            }
        }
        private void DisConnect()
        {
            if (conn.State == ConnectionState.Open)
            {
                conn.Close();
            }
        }
        public void Dispose()
        {
            this.Close();
        }
    }
}
HMI.Core/Util/SqliteHelper.cs
New file
@@ -0,0 +1,238 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SQLite;
namespace HMI.Core.Util
{
    public class SqliteHelper:IDisposable
    {
        public string DataBaseFile { get; private set; } = string.Empty;
        private string ConnectString = string.Empty;
        private SQLiteConnection conn = null;
        private SQLiteCommand cmd = null;
        public SqliteHelper(string DBFile)
        {
            DataBaseFile = DBFile;
            ConnectString = "DataSource=" + DataBaseFile + ";Version=3;Pooling=False;Max Pool Size=100;";
            conn = new SQLiteConnection(ConnectString);
            cmd = new SQLiteCommand(conn);
        }
        public static bool CreateDatabase(string DataBaseFile)
        {
            try
            {
                SQLiteConnection.CreateFile(DataBaseFile);
                return true;
            }
            catch (Exception ex)
            {
                throw new Exception($"Create Database [${DataBaseFile}] Failed.", ex);
            }
        }
        public int ExecuteSql(string Sql)
        {
            try
            {
                Connect();
                cmd.CommandType = CommandType.Text;
                cmd.CommandText = Sql;
                return cmd.ExecuteNonQuery();
            }
            catch (Exception ex)
            {
                string message = "ExecuteSql Error:" + ex.Message;
                throw new Exception(message);
            }
            finally
            {
                DisConnect();
            }
        }
        public void ExecuteBatchSql(IEnumerable<string> sqls)
        {
            SQLiteTransaction tr = null;
            try
            {
                Connect();
                tr = conn.BeginTransaction();
                cmd.CommandType = CommandType.Text;
                foreach (string sql in sqls)
                {
                    if (string.IsNullOrEmpty(sql) || string.IsNullOrWhiteSpace(sql)) continue;
                    cmd.CommandText = sql;
                    cmd.ExecuteNonQuery();
                }
                tr.Commit();
            }
            catch (Exception ex)
            {
                tr?.Rollback();
                throw ex;
            }
            finally
            {
                DisConnect();
            }
        }
        public DataTable GetDataTable(string Sql)
        {
            DataTable dataTable = new DataTable();
            SQLiteDataAdapter sQLiteDataAdapter = new SQLiteDataAdapter();
            try
            {
                Connect();
                cmd.CommandType = CommandType.Text;
                cmd.CommandText = Sql;
                sQLiteDataAdapter.SelectCommand = cmd;
                sQLiteDataAdapter.Fill(dataTable);
            }
            catch (Exception ex)
            {
                string message = "ExecuteSql Error:" + ex.Message;
                throw new Exception(message);
            }
            finally
            {
                DisConnect();
            }
            return dataTable;
        }
        public string[,] GetDataTableToArray(string Sql)
        {
            try
            {
                DataTable dataTable = GetDataTable(Sql);
                string[,] array = new string[dataTable.Rows.Count, dataTable.Columns.Count];
                int num = 0;
                int num2 = 0;
                foreach (DataRow row in dataTable.Rows)
                {
                    num2 = 0;
                    object[] itemArray = row.ItemArray;
                    foreach (object obj in itemArray)
                    {
                        array[num, num2] = obj.ToString();
                        num2++;
                    }
                    num++;
                }
                return array;
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }
        }
        public void Close()
        {
            try
            {
                if (conn.State == ConnectionState.Open)
                {
                    conn.Close();
                }
                if (cmd != null)
                {
                    cmd.Dispose();
                }
                if (conn != null)
                {
                    conn.Dispose();
                }
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }
        }
        public bool HasData(string sql)
        {
            DataTable dataTable = GetDataTable(sql);
            return dataTable.Rows.Count > 0;
        }
        public string[] GetColumnNameFromTable(string TableName)
        {
            try
            {
                string sql = $"PRAGMA table_info('{TableName}')";
                DataTable dataTable = GetDataTable(sql);
                string[] array = new string[dataTable.Rows.Count];
                int num = 0;
                foreach (DataRow row in dataTable.Rows)
                {
                    array[num] = row.ItemArray[1].ToString();
                    num++;
                }
                return array;
            }
            catch (Exception ex)
            {
                throw new Exception("获取表字段名称失败:" + ex.Message);
            }
        }
        public string[] GetAllTableInFile()
        {
            try
            {
                string sql = "select name from sqlite_master where type = 'table' order by name";
                DataTable dataTable = GetDataTable(sql);
                string[] array = new string[dataTable.Rows.Count];
                int num = 0;
                foreach (DataRow row in dataTable.Rows)
                {
                    array[num] = row.ItemArray[0].ToString();
                    num++;
                }
                return array;
            }
            catch (Exception ex)
            {
                throw new Exception("获取表名失败:" + ex.Message);
            }
        }
        private void Connect()
        {
            if (conn.State == ConnectionState.Closed)
            {
                conn.ConnectionString = ConnectString;
                cmd.Connection = conn;
                conn.Open();
            }
        }
        private void DisConnect()
        {
            if (conn.State == ConnectionState.Open)
            {
                conn.Close();
            }
        }
        public void Dispose()
        {
            this.Close();
        }
    }
}
HMI.Core/Util/UtilContainer.cs
New file
@@ -0,0 +1,22 @@
using System.Collections.Generic;
namespace HMI.Core.Util
{
    public class UtilContainer
    {
        private static Dictionary<string, object> Map = new Dictionary<string, object>();
        public static bool RegisterUtil(string name, object value)
        {
            if (Map.ContainsKey(name)) return false;
            Map[name] = value;
            return true;
        }
        public T GetUtil<T>(string name)
        {
            if (!Map.ContainsKey(name)) return default(T);
            return (T)Map[name];
        }
    }
}
HMI.Core/Util/XmlHelper.cs
New file
@@ -0,0 +1,93 @@
using System;
using System.IO;
using System.Reflection;
using System.Xml;
using System.Xml.Serialization;
namespace HMI.Core.Util
{
    public static class XmlHelper
    {
        static readonly XmlSerializerNamespaces DEFAULT_NS = new XmlSerializerNamespaces();
        static XmlHelper()
        {
            DEFAULT_NS.Add("", "");
        }
        /// <summary>
        /// serialize object to xml file.
        /// </summary>
        /// <param name="path">the path to save the xml file</param>
        /// <param name="obj">the object you want to serialize</param>
        public static void SerializeToXml(string path, object obj)
        {
            XmlSerializer serializer = new XmlSerializer(obj.GetType());
            string content = string.Empty;
            //serialize
            using (StringWriter writer = new StringWriter())
            {
                serializer.Serialize(writer, obj, DEFAULT_NS);
                content = writer.ToString();
            }
            //save to file
            using (StreamWriter stream_writer = new StreamWriter(path))
            {
                stream_writer.Write(content);
            }
        }
        /// <summary>
        /// xml 文件反序列化为对象
        /// </summary>
        /// <typeparam name="T">序列化类型</typeparam>
        /// <param name="path">the path of the xml file</param>
        /// <returns>对象</returns>
        public static T DeserializeFromXml<T>(string path)
        {
            XmlSerializer serializer = new XmlSerializer(typeof(T));
            using (StreamReader reader = new StreamReader(path))
            {
                return (T)serializer.Deserialize(reader);
            }
        }
        public static bool ReadFromXml<T>(T obj, string path)
        {
            XmlDocument xd = new XmlDocument();
            xd.Load(path);
            Type type = typeof(T);
            XmlNode root = xd.SelectSingleNode(type.Name);
            if (root == null)
            {
                return false;
            }
            //获取 RefHero 中所有的实例私有字段
            FieldInfo[] fieldArray = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
            foreach (FieldInfo filed in fieldArray)
            {
                string fileName = filed.Name;
                XmlNode node = root.SelectSingleNode(fileName);
                if (node == null)
                {
                    continue;
                }
                filed.SetValue(obj, node.InnerText);
            }
            return true;
        }
        public static T DeepClone<T>(T data)
        {
            using (MemoryStream stream = new MemoryStream())
            {
                XmlSerializer serializer = new XmlSerializer(typeof(T));
                serializer.Serialize(stream, data);
                stream.Seek(0, SeekOrigin.Begin);
                return (T)serializer.Deserialize(stream);
            }
        }
    }
}
HMI.Core/packages.config
New file
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="log4net" version="3.0.4" targetFramework="net472" />
  <package id="Newtonsoft.Json" version="13.0.3" targetFramework="net472" />
</packages>
HMI.DataViewer/DataViewer.xaml
New file
@@ -0,0 +1,267 @@
<hb:PluginControl x:Class="HMI.DataViewer.DataViewer"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base"
             xmlns:local="clr-namespace:HMI.DataViewer"
             mc:Ignorable="d"
             xmlns:uic="clr-namespace:HMI.UI.Converter;assembly=HMI.UI"
             xmlns:pw="https://opensource.panuon.com/wpf"
             xmlns:pu="https://opensource.panuon.com/wpf-ui"
             xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
             Background="{DynamicResource WorkspaceBackground}"
             Loaded="PluginControl_Loaded"
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </UserControl.Resources>
    <Border Style="{StaticResource CardBoarder}">
        <Grid dp:GridHelper.Space="10">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="7*"/>
                <ColumnDefinition Width="3*"/>
            </Grid.ColumnDefinitions>
            <DockPanel Grid.ColumnSpan="2">
                <StackPanel DockPanel.Dock="Right" Style="{StaticResource ButtonGroup}">
                    <Button Style="{StaticResource AddBtn}" x:Name="addBtn" Click="Button_Click"/>
                    <Button Style="{StaticResource EditBtn}" x:Name="editBtn" Click="Button_Click"/>
                    <Button Style="{StaticResource RfhBtn}" x:Name="rfhBtn" Click="Button_Click"/>
                    <Button Style="{StaticResource DelBtn}" x:Name="delBtn" Click="Button_Click"/>
                </StackPanel>
                <TextBlock Style="{StaticResource TitleBox}" Text="{DynamicResource Dv.Title}"/>
            </DockPanel>
            <!--<Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>
            </Grid>-->
            <StackPanel Grid.Row="1" Grid.ColumnSpan="2" HorizontalAlignment="Left" Style="{StaticResource ButtonGroup}">
                <TextBlock Style="{StaticResource TitleBox}" FontSize="{StaticResource ContentFontSize2}"
                           Text="{DynamicResource Dv.SelectItem}"/>
                <ComboBox x:Name="programInput"
                          MinWidth="180"
                          SelectionChanged="programInput_SelectionChanged"/>
                <TextBlock Style="{StaticResource TitleBox}" FontSize="{StaticResource ContentFontSize2}"
                           Text="{DynamicResource App.StartTime}"/>
                <pu:DateTimePicker Style="{StaticResource TimePicker}"
                                   SelectedDateTime="{x:Null}" x:Name="startTimeInput"/>
                <TextBlock Style="{StaticResource TitleBox}" FontSize="{StaticResource ContentFontSize2}"
                           Text="{DynamicResource App.EndTime}"/>
                <pu:DateTimePicker Style="{StaticResource TimePicker}"
                                   SelectedDateTime="{x:Null}" x:Name="endTimeInput"/>
                <Button Style="{StaticResource QueryBtn}"
                        x:Name="srhBtn" Click="Button_Click"/>
                <Button Style="{StaticResource ResetBtn}"
                        x:Name="resetBtn" Click="Button_Click"/>
                <Button Style="{StaticResource ExportBtn}"
                        x:Name="exportBtn" Click="Button_Click"/>
                <!--<Menu x:Name="anyBtn" Style="{StaticResource CommonMenu}">
                    <MenuItem Header="分析">
                        <MenuItem Header="CPK" Click="MenuItem_Click"/>
                        <MenuItem Header="RandR" />
                    </MenuItem>
                </Menu>-->
            </StackPanel>
            <DockPanel Grid.Row="2" Grid.ColumnSpan="2" Margin="0,10,0,0">
                <pu:Drawer DockPanel.Dock="Left" x:Name="DrwMenu" MinWidth="35" MaxWidth="400"
                           Padding="0"
                           ShadowColor="{x:Null}"
                           AnimationDuration="0" BorderThickness="2,0,0,2" BorderBrush="{DynamicResource CommonBoarder}">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition/>
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>
                        <Border Grid.ColumnSpan="2" BorderThickness="2" BorderBrush="{DynamicResource CommonBoarder}" Background="{DynamicResource VerBar}">
                            <DockPanel Height="35">
                                <ToggleButton x:Name="drawerBtn" BorderThickness="0"
                  DockPanel.Dock="Right"
                  Background="Transparent"
                  Width="35" Height="35" HorizontalAlignment="Right"
                  RenderTransformOrigin="0.5,0.5"
                                              Foreground="{DynamicResource PrimaryColorContract}"
                  FontFamily="{StaticResource iconfont}"
                  Content="&#xe60e;" IsChecked="{Binding IsOpen, ElementName=DrwMenu, Mode=TwoWay}">
                                    <ToggleButton.Style>
                                        <Style TargetType="ToggleButton" BasedOn="{StaticResource CommonToggle}">
                                            <Style.Triggers>
                                                <DataTrigger Binding="{Binding IsOpen, ElementName=DrwMenu}" Value="False">
                                                    <Setter Property="RenderTransform">
                                                        <Setter.Value>
                                                            <RotateTransform Angle="-90"/>
                                                        </Setter.Value>
                                                    </Setter>
                                                </DataTrigger>
                                                <DataTrigger Binding="{Binding IsOpen, ElementName=DrwMenu}" Value="True">
                                                    <Setter Property="Background" Value="Red"/>
                                                    <Setter Property="RenderTransform">
                                                        <Setter.Value>
                                                            <RotateTransform Angle="90"/>
                                                        </Setter.Value>
                                                    </Setter>
                                                </DataTrigger>
                                                <Trigger Property="IsChecked" Value="True">
                                                    <Setter Property="Background" Value="Red"/>
                                                </Trigger>
                                            </Style.Triggers>
                                        </Style>
                                    </ToggleButton.Style>
                                </ToggleButton>
                                <TextBlock Text="{DynamicResource Dv.Advanced}" FontSize="16"
                                           Foreground="{DynamicResource PrimaryColorContract}"
                                           VerticalAlignment="Center" HorizontalAlignment="Center"/>
                            </DockPanel>
                        </Border>
                        <Grid Grid.Row="1">
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto"/>
                                <RowDefinition/>
                            </Grid.RowDefinitions>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition/>
                            </Grid.ColumnDefinitions>
                            <!--<DockPanel Margin="10,10,10,0" Grid.ColumnSpan="2">
                                <Label Content="{DynamicResource Dv.SelectItem}"/>
                                <ComboBox Grid.Column="1" x:Name="programInput" SelectionChanged="programInput_SelectionChanged"/>
                            </DockPanel>-->
                            <DataGrid Grid.Row="1" Grid.ColumnSpan="2" Style="{StaticResource DefaultDataGrid}"
                                      IsReadOnly="False"
                                      SelectionMode="Single"
                                      BorderThickness="0"
                                      ItemsSource="{Binding Datas}">
                                <DataGrid.Columns>
                                    <DataGridTemplateColumn Header="#" IsReadOnly="True" CanUserResize="False">
                                        <DataGridTemplateColumn.CellTemplate>
                                            <DataTemplate>
                                                <TextBlock Text="{Binding Converter={StaticResource rowNumber},
RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DataGridRow},AncestorLevel=1}}"/>
                                            </DataTemplate>
                                        </DataGridTemplateColumn.CellTemplate>
                                    </DataGridTemplateColumn>
                                    <DataGridTextColumn IsReadOnly="True" Width="80" Binding="{Binding Name}">
                                        <DataGridTextColumn.Header>
                                            <TextBlock Text="{DynamicResource Dv.Field}"/>
                                        </DataGridTextColumn.Header>
                                    </DataGridTextColumn>
                                    <DataGridTemplateColumn Width="60">
                                        <DataGridTemplateColumn.Header>
                                            <TextBlock Text="{DynamicResource Dv.Condition}"/>
                                        </DataGridTemplateColumn.Header>
                                        <DataGridTemplateColumn.CellTemplate>
                                            <DataTemplate>
                                                <TextBlock Text="{Binding Condition}"/>
                                            </DataTemplate>
                                        </DataGridTemplateColumn.CellTemplate>
                                        <DataGridTemplateColumn.CellEditingTemplate>
                                            <DataTemplate>
                                                <ComboBox SelectedValue="{Binding Condition}"
                                                          SelectedValuePath="Content" >
                                                    <ComboBoxItem Content="="/>
                                                    <ComboBoxItem Content="&lt;"/>
                                                    <ComboBoxItem Content="&gt;"/>
                                                    <ComboBoxItem Content="&lt;="/>
                                                    <ComboBoxItem Content="&gt;="/>
                                                </ComboBox>
                                            </DataTemplate>
                                        </DataGridTemplateColumn.CellEditingTemplate>
                                    </DataGridTemplateColumn>
                                    <DataGridTextColumn Width="*" Binding="{Binding Value}" >
                                        <DataGridTextColumn.Header>
                                            <TextBlock Text="{DynamicResource Dv.Value}"/>
                                        </DataGridTextColumn.Header>
                                        <DataGridTextColumn.HeaderTemplate>
                                            <DataTemplate>
                                                <StackPanel Orientation="Horizontal">
                                                    <TextBlock Text="*" Foreground="Red" />
                                                    <ContentControl Content="{Binding }"/>
                                                </StackPanel>
                                            </DataTemplate>
                                        </DataGridTextColumn.HeaderTemplate>
                                    </DataGridTextColumn>
                                </DataGrid.Columns>
                            </DataGrid>
                        </Grid>
                        <Label Grid.Row="1" Grid.Column="1"  Width="100" Height="34"
                               VerticalContentAlignment="Center"
                               HorizontalContentAlignment="Center"
                                   Content="{DynamicResource Dv.Advanced}"
                                   FontSize="16">
                            <Label.LayoutTransform>
                                <RotateTransform CenterX="25" CenterY="25" Angle="270"/>
                            </Label.LayoutTransform>
                            <Label.Style>
                                <Style TargetType="Label">
                                    <Style.Triggers>
                                        <DataTrigger Binding="{Binding IsOpen, ElementName=DrwMenu,Mode=OneWay}" Value="True">
                                            <Setter Property="Visibility" Value="Collapsed"/>
                                        </DataTrigger>
                                        <DataTrigger Binding="{Binding IsOpen, ElementName=DrwMenu}" Value="False">
                                            <Setter Property="Visibility" Value="Visible"/>
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </Label.Style>
                        </Label>
                    </Grid>
                </pu:Drawer>
                <!-- SelectionUnit="CellOrRowHeader"
SelectionMode="Extended" -->
                <DataGrid BorderThickness="2" BorderBrush="{DynamicResource CommonBoarder}" x:Name="dataTable"
                          Style="{StaticResource DefaultDataGrid}"
                          EnableRowVirtualization="True"
                          PreviewMouseLeftButtonDown="dataTable_PreviewMouseLeftButtonDown">
                    <DataGrid.Columns>
                        <DataGridTemplateColumn Header="#" IsReadOnly="True" CanUserResize="False">
                            <DataGridTemplateColumn.CellTemplate>
                                <DataTemplate>
                                    <TextBlock Text="{Binding Converter={StaticResource rowNumber},
RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DataGridRow},AncestorLevel=1}}"/>
                                </DataTemplate>
                            </DataGridTemplateColumn.CellTemplate>
                        </DataGridTemplateColumn>
                    </DataGrid.Columns>
                </DataGrid>
            </DockPanel>
        </Grid>
    </Border>
</hb:PluginControl>
HMI.DataViewer/DataViewer.xaml.cs
New file
@@ -0,0 +1,547 @@
using HMI.Base;
using HMI.Core.Util;
using HMI.DataViewer.Entity;
using HMI.UI.Converter;
using HMI.UI.Util;
using NPOI.SS.Formula.Functions;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Forms;
namespace HMI.DataViewer
{
    /// <summary>
    /// UserControl1.xaml 的交互逻辑
    /// </summary>
    public partial class DataViewer : PluginControl
    {
        public DataViewer()
        {
            DataContext = this;
            InitializeComponent();
            DefaultDate();
        }
        private string SourceFilePath;
        private ObservableCollection<DataBase_Setting> settings = new ObservableCollection<DataBase_Setting>();
        public ObservableCollection<DataBase_Setting> Settings
        {
            get => settings;
            set { Set(ref settings, value); }
        }
        private ObservableCollection<QueryData> datas = new ObservableCollection<QueryData>();
        public ObservableCollection<QueryData> Datas
        {
            get => datas;
            set { Set(ref datas, value); }
        }
        private Dictionary<string, DataBase_Setting> Map = new Dictionary<string, DataBase_Setting>();
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            System.Windows.Controls.Button btn = sender as System.Windows.Controls.Button;
            switch (btn.Name)
            {
                case "addBtn":
                    OpenSettingModal(-1, new DataBase_Setting());
                    break;
                case "editBtn":
                    EditSettingModal();
                    break;
                case "rfhBtn":
                    LoadData();
                    break;
                case "delBtn":
                    DeleteDatasource();
                    break;
                case "srhBtn":
                    SearchData();
                    break;
                case "resetBtn":
                    ResetDataTable();
                    break;
                case "exportBtn":
                    ExportData();
                    break;
                default:
                    break;
            }
        }
        private void DeleteDatasource()
        {
            try
            {
                var key = programInput.Text;
                if (string.IsNullOrEmpty(key))
                {
                    ToastHelper.Warning(Window.GetWindow(this), this.lang("Dv.SelectDatasourceFirst"));
                    return;
                }
                if (!Map.ContainsKey(key))
                {
                    ToastHelper.Error(Window.GetWindow(this), this.lang("App.ParameterException"));
                    return;
                }
                DataBase_Setting setting = Map[key];
                if (MsgBoxHelper.WarnMessage(Window.GetWindow(this), this.lang("HMI.MakeSureDeleteSingle", setting.Program))
                    == MessageBoxResult.OK)
                {
                    var temp = XmlHelper.DeepClone(Settings);
                    temp.RemoveAt(Settings.IndexOf(setting));
                    XmlHelper.SerializeToXml(SourceFilePath, temp);
                    ToastHelper.Success(Window.GetWindow(this), this.lang("App.DeleteSuccess"));
                    LoadData();
                }
            }
            catch (Exception ex)
            {
                log.Error("Delete Datasource Failed.", ex);
                ToastHelper.Success(Window.GetWindow(this), $"{this.lang("App.DeleteFailed")}: {ex.Message}");
            }
        }
        private void EditSettingModal()
        {
            var key = programInput.Text;
            if (string.IsNullOrEmpty(key))
            {
                ToastHelper.Warning(Window.GetWindow(this), this.lang("Dv.SelectDatasourceFirst"));
                return;
            }
            if (!Map.ContainsKey(key))
            {
                ToastHelper.Error(Window.GetWindow(this), this.lang("App.ParameterException"));
                return;
            }
            DataBase_Setting setting = Map[key];
            OpenSettingModal(Settings.IndexOf(setting), setting);
        }
        private void OpenSettingModal(int index, DataBase_Setting data)
        {
            DbSettingModal modal = new DbSettingModal(log)
            {
                Title = this.lang("Dv.AddDatasource"),
                Owner = Window.GetWindow(this)
            };
            if (modal.Open(index, data, Settings, SourceFilePath) == true)
            {
                LoadData();
            }
        }
        private void DefaultDate()
        {
            var now = DateTime.Now;
            startTimeInput.SelectedDateTime = DateTime.Parse(now.ToString("yyyy-MM-dd"));
            endTimeInput.SelectedDateTime = DateTime.Parse(now.AddDays(1).ToString("yyyy-MM-dd"));
        }
        private void ResetDataTable()
        {
            DefaultDate();
            dataTable.ItemsSource = null;
            foreach (var data in Datas)
            {
                data.Value = null;
            }
        }
        public override void SetBaseFolder(string folder)
        {
            base.SetBaseFolder(folder);
            SourceFilePath = Path.Combine(BaseFolder, "SourceData.setting");
        }
        private void PluginControl_Loaded(object sender, RoutedEventArgs e)
        {
            LoadData();
            Loaded -= PluginControl_Loaded;
        }
        private void LoadData()
        {
            ObservableCollection<DataBase_Setting> settings = new ObservableCollection<DataBase_Setting>();
            if (File.Exists(SourceFilePath))
            {
                settings = XmlHelper.DeserializeFromXml<ObservableCollection<DataBase_Setting>>(SourceFilePath);
            }
            List<string> names = new List<string>();
            Map.Clear();
            foreach (var data in settings)
            {
                Map[data.Program] = data;
                names.Add(data.Program);
            }
            programInput.ItemsSource = names;
            //ResetInput();
            UpdateAdvanced();
            Settings = settings;
        }
        private void ResetInput()
        {
            programInput.Text = string.Empty;
            Datas.Clear();
            var temp = dataTable.Columns[0];
            HMI.UI.DpUtil.DataGridHelper.SetSelectedColumn(dataTable, null);
            dataTable.Columns.Clear();
            dataTable.Columns.Add(temp);
            currentColumns?.Clear();
        }
        private void UpdateAdvanced()
        {
            var key = programInput.Text;
            if (!Map.ContainsKey(key))
            {
                ResetInput();
                return;
            }
            DataBase_Setting setting = Map[key];
            Datas = new ObservableCollection<QueryData>(
                setting?.Condition?.Select(d => new QueryData { Key = d.Key, Name = d.Desc, Condition = "=" }).ToList());
            var temp = dataTable.Columns[0];
            HMI.UI.DpUtil.DataGridHelper.SetSelectedColumn(dataTable, null);
            dataTable.Columns.Clear();
            dataTable.Columns.Add(temp);
            List<DataGridTextColumn> columns = new List<DataGridTextColumn>();
            foreach (var data in Datas)
            {
                columns.Add(AddDataGridTextColumn(data.Name, data.Key));
            }
            currentColumns = columns;
        }
        private void programInput_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (e.AddedItems == null || e.AddedItems.Count <= 0) return;
            var key = e.AddedItems[0].ToString();
            if (!Map.ContainsKey(key))
            {
                ToastHelper.Error(Window.GetWindow(this), this.lang("App.ParameterException"));
                programInput.Text = string.Empty;
                return;
            }
            DataBase_Setting setting = Map[key];
            Datas = new ObservableCollection<QueryData>(
                setting?.Condition?.Select(d => new QueryData { Key = d.Key, Name = d.Desc, Condition = "=" }).ToList());
            var temp = dataTable.Columns[0];
            HMI.UI.DpUtil.DataGridHelper.SetSelectedColumn(dataTable, null);
            dataTable.Columns.Clear();
            dataTable.Columns.Add(temp);
            List<DataGridTextColumn> columns = new List<DataGridTextColumn>();
            foreach (var data in Datas)
            {
                columns.Add(AddDataGridTextColumn(data.Name, data.Key));
            }
            currentColumns = columns;
        }
        private List<DataGridTextColumn> currentColumns;
        private DataGridTextColumn AddDataGridTextColumn(string header, string bindingPath)
        {
            DataGridTextColumn textColumn = new DataGridTextColumn
            {
                Header = header,
                Binding = new System.Windows.Data.Binding(bindingPath)
                {
                    Converter = DateStrConverter.Instance
                },
                Width = new DataGridLength(1, DataGridLengthUnitType.Star)
            };
            dataTable.Columns.Add(textColumn);
            return textColumn;
        }
        private void dataTable_PreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            //// 获取点击的元素
            //DependencyObject dep = (DependencyObject)e.OriginalSource;
            //// 遍历找到 DataGridColumnHeader
            //while (dep != null && !(dep is DataGridColumnHeader) && !(dep is DataGridRow))
            //{
            //    dep = VisualTreeHelper.GetParent(dep);
            //}
            //HMI.UI.DpUtil.DataGridHelper.SetSelectedColumn(dataTable, null);
            //if (dep is DataGridColumnHeader columnHeader)
            //{
            //    // 获取列索引
            //    int columnIndex = columnHeader.Column.DisplayIndex;
            //    // 清除已有选择
            //    dataTable.SelectedCells.Clear();
            //    // 遍历所有行,选中该列的所有单元格
            //    foreach (var item in dataTable.Items)
            //    {
            //        DataGridCellInfo cellInfo = new DataGridCellInfo(item, columnHeader.Column);
            //        dataTable.SelectedCells.Add(cellInfo);
            //    }
            //    HMI.UI.DpUtil.DataGridHelper.SetSelectedColumn(dataTable, columnHeader.Column);
            //    e.Handled = true; // 防止默认行为
            //}
            //else if (dep is DataGridRow row)
            //{
            //    // 清除已有选择
            //    dataTable.SelectedCells.Clear();
            //    dataTable.SelectedItem = row.Item;
            //    e.Handled = true;
            //}
        }
        private DataTable DoSearchData(DataBase_Setting setting, List<QueryData> datas)
        {
            string conditon = "WHERE 1=1 ";
            string fields = string.Join(",", Datas?.Select(d => $"[{d.Key}]").ToList());
            if (datas != null && datas.Count > 0)
            {
                conditon = "WHERE " + string.Join(" AND ", datas.Select(d => $"[{d.Key}] {d.Condition} '{d.Value}'").ToList());
            }
            string timeCol = setting.TimeCol;
            if (string.IsNullOrEmpty(timeCol))
            {
                timeCol = "time";
            }
            if (!string.IsNullOrEmpty(startTimeInput.Text))
            {
                conditon += $" AND {timeCol} >= '{startTimeInput.Text}'";
            }
            if (!string.IsNullOrEmpty(endTimeInput.Text))
            {
                conditon += $" AND {timeCol} < '{endTimeInput.Text}'";
            }
            string sql = $"SELECT {fields} FROM {setting.TableName} {conditon}";
            DataTable dt = new DataTable();
            if (string.IsNullOrEmpty(setting.Pattern))
            {
                try
                {
                    using (var helper = new Sqlite3Helper(setting.DBFilePath))
                    {
                        dt = helper.GetDataTable(sql);
                    }
                }
                catch (Exception ex)
                {
                    log.Error($"Select Data Failed. SQL: {sql}", ex);
                    throw ex;
                }
            }
            else
            {
                // 通配符
                try
                {
                    string dir = Path.GetDirectoryName(setting.DBFilePath);
                    string startName = $"Data_{startTimeInput.SelectedDateTime?.ToString("yyyyMMdd")}";
                    string endName = $"Data_{endTimeInput.SelectedDateTime?.ToString("yyyyMMdd")}";
                    var oldFiles = Directory.GetFiles(dir, "Data_*.db");
                    oldFiles = oldFiles.Where(file =>
                    {
                        var fileName = Path.GetFileNameWithoutExtension(file);
                        bool flag = true;
                        flag &= (string.Compare(fileName, startName) == 1 || fileName == startName);
                        flag &= (string.Compare(endName, fileName) == 1 || fileName == endName);
                        return flag;
                    }).ToArray();
                    if (oldFiles == null || oldFiles.Length <= 0)
                    {
                        return dt;
                    }
                    foreach (string file in oldFiles)
                    {
                        try
                        {
                            using (var helper = new SqliteHelper(file))
                            {
                                var temp = helper.GetDataTable(sql);
                                dt.Merge(temp);
                            }
                        }
                        catch
                        {
                        }
                    }
                    return dt;
                }
                catch (Exception ex)
                {
                    log.Error($"Select Data Failed. SQL: {sql}", ex);
                    throw ex;
                }
            }
            //HMI.UI.DpUtil.DataGridHelper.SetSelectedColumn(dataTable, null);
            //dt.
            return dt;
        }
        private void SearchData()
        {
            string program = programInput.Text;
            if (!Map.ContainsKey(program))
            {
                ToastHelper.Error(Window.GetWindow(this), this.lang("App.ParameterException"));
                return;
            }
            var datas = Datas?.Where(d => !string.IsNullOrEmpty(d.Value)).ToList();
            if ((datas == null || datas.Count <= 0) &&
                (string.IsNullOrEmpty(startTimeInput.Text) || string.IsNullOrEmpty(endTimeInput.Text)))
            {
                ToastHelper.Warning(Window.GetWindow(this), this.lang("Dv.QueryCantBeEmpty"));
                return;
            }
            DataBase_Setting setting = Map[program];
            try
            {
                dataTable.ItemsSource = DoSearchData(setting, datas).DefaultView;
            }
            catch (Exception ex)
            {
                ToastHelper.Error(Window.GetWindow(this), $"{this.lang("App.QueryFailed")}: {ex.Message}");
            }
        }
        private void ExportData()
        {
            int maxRow = 1048575;
            try
            {
                string program = programInput.Text;
                if (string.IsNullOrEmpty(program))
                {
                    ToastHelper.Warning(Window.GetWindow(this), this.lang("Dv.SelectDatasourceFirst"));
                    return;
                }
                if (!Map.ContainsKey(program))
                {
                    ToastHelper.Error(Window.GetWindow(this), this.lang("App.ParameterException"));
                    return;
                }
                var datas = Datas?.Where(d => !string.IsNullOrEmpty(d.Value)).ToList();
                if ((datas == null || datas.Count <= 0) &&
                    (string.IsNullOrEmpty(startTimeInput.Text) || string.IsNullOrEmpty(endTimeInput.Text)))
                {
                    ToastHelper.Warning(Window.GetWindow(this), this.lang("Dv.QueryCantBeEmpty"));
                    return;
                }
                DataBase_Setting setting = Map[program];
                DataTable dt;
                try
                {
                    dt = DoSearchData(setting, datas);
                }
                catch (Exception ex)
                {
                    ToastHelper.Error(Window.GetWindow(this), $"{this.lang("App.QueryFailed")}: {ex.Message}");
                    return;
                }
                if (currentColumns == null) return;
                var dataView = dt?.DefaultView;
                if (dataView == null) return;
                string fname = string.Empty;
                SaveFileDialog sfd = new SaveFileDialog();
                sfd.Filter = "";
                sfd.DefaultExt = "xlsx";
                sfd.FileName = setting.Program;
                if (sfd.ShowDialog() == DialogResult.OK)
                {
                    fname = sfd.FileName;
                }
                /*导出当前dataGridView中的所有数据到xls文件
                1.引入库文件,新建lib文件夹,复制相关文件
                2.在项目中添加对这几个dll的引用
                3.在内存中建立 excel表文件  */
                using (FileStream fs = new FileStream(fname, FileMode.Create))
                {
                    XSSFWorkbook workbook = new XSSFWorkbook();
                    int row = 1;
                    for (int i = 0; i < dataView.Count; i++)
                    {
                        int sheetIndex = sheetIndex = i / maxRow;
                        string sheetName = $"sheet{sheetIndex + 1}";
                        var sheet = (XSSFSheet)workbook.GetSheet(sheetName);
                        if (sheet == null)
                        {
                            sheet = (XSSFSheet)workbook.CreateSheet(sheetName);
                            IRow row1 = sheet.CreateRow(0);
                            for (int j = 0; j < currentColumns.Count; j++)
                            {
                                row1.CreateCell(j).SetCellValue(currentColumns[j].Header.ToString());
                            }
                            row = 1;
                        }
                        IRow rowTemp = sheet.CreateRow(row++);
                        for (int j = 0; j < currentColumns.Count; j++)
                        {
                            string name = ((System.Windows.Data.Binding)currentColumns[j].Binding).Path.Path;
                            var value = dataView[i][name].ToString();
                            rowTemp.CreateCell(j).SetCellValue(value);
                        }
                    }
                    workbook.Write(fs);
                }
                ToastHelper.Success(Window.GetWindow(this), this.lang("App.ExportSuccess"));
            }
            catch (Exception ex)
            {
                log.Error("Export Data Failed.", ex);
                ToastHelper.Error(Window.GetWindow(this), $"{this.lang("App.ExportFailed")}: {ex.Message}");
            }
        }
        private void MenuItem_Click(object sender, RoutedEventArgs e)
        {
            DataGridColumn column = HMI.UI.DpUtil.DataGridHelper.GetSelectedColumn(dataTable);
        }
    }
}
HMI.DataViewer/DbSettingModal.xaml
New file
@@ -0,0 +1,137 @@
<pu:WindowX x:Class="HMI.DataViewer.DbSettingModal"
            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:local="clr-namespace:HMI.DataViewer"
            xmlns:pw="https://opensource.panuon.com/wpf"
            xmlns:pu="https://opensource.panuon.com/wpf-ui"
            xmlns:uic="clr-namespace:HMI.UI.Converter;assembly=HMI.UI"
            xmlns:ui="clr-namespace:HMI.UI;assembly=HMI.UI"
            xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
            mc:Ignorable="d"
            Title="Form"
            SizeToContent="Height"
            Width="800"
            WindowStartupLocation="CenterOwner"
            BorderBrush="#3E3E45"
            BorderThickness="1"
            Foreground="Black"
            Style="{StaticResource ModalWindow}">
    <Grid Margin="10">
        <StackPanel x:Name="formPanel">
            <pu:FormGroup Header="{DynamicResource Dv.Datasource.Name}"
                          dp:FormGroupHelper.IsRequired="True"
                          dp:FormGroupHelper.ValidateMethod="ValidateName"
                          Style="{StaticResource InputFormGroupStyle}"
                          GroupName="MyFormGroup">
                <TextBox Style="{StaticResource EditorTextBoxStyle}" x:Name="programInput"/>
            </pu:FormGroup>
            <pu:FormGroup Header="{DynamicResource Dv.Datasource.File}"
                          dp:FormGroupHelper.IsRequired="True"
                          Style="{StaticResource InputFormGroupStyle}"
                          GroupName="MyFormGroup">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition/>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>
                    <TextBox Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="False" x:Name="dbFilePathInput"/>
                    <Button Style="{StaticResource SearchButton}" x:Name="searchBtn" Click="Button_Click" Grid.Column="1"/>
                </Grid>
            </pu:FormGroup>
            <pu:FormGroup Header="{DynamicResource Dv.Datasource.Table}"
                          dp:FormGroupHelper.IsRequired="True"
                          Style="{StaticResource InputFormGroupStyle}"
                          GroupName="MyFormGroup">
                <ComboBox Style="{StaticResource FormComboxStyle}" SelectedValuePath="Content"
                          x:Name="tableNameInput" SelectionChanged="ComboxSelectionChanged"/>
            </pu:FormGroup>
            <pu:FormGroup Header="通配符" Style="{StaticResource InputFormGroupStyle}" GroupName="MyFormGroup">
                <TextBox Style="{StaticResource EditorTextBoxStyle}" x:Name="TbPattern"/>
            </pu:FormGroup>
            <pu:FormGroup Header="{DynamicResource Dv.Datasource.Fields}" Orientation="Vertical" GroupName="MyFormGroup">
                <Grid dp:GridHelper.Space="10">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition/>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition/>
                    </Grid.ColumnDefinitions>
                    <DataGrid Style="{StaticResource DefaultDataGrid}"
                              BorderThickness="1"
                              SelectionMode="Extended"
                              ItemsSource="{Binding Columns}" x:Name="columnTable">
                        <DataGrid.Columns>
                            <DataGridTemplateColumn Header="#" CanUserResize="False">
                                <DataGridTemplateColumn.CellTemplate>
                                    <DataTemplate>
                                        <TextBlock Text="{Binding Converter={StaticResource rowNumber},
RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DataGridRow},AncestorLevel=1}}"/>
                                    </DataTemplate>
                                </DataGridTemplateColumn.CellTemplate>
                            </DataGridTemplateColumn>
                            <DataGridTextColumn Width="*" Binding="{Binding }">
                                <DataGridTextColumn.Header>
                                    <TextBlock Text="{DynamicResource Dv.Datasource.FieldName}"/>
                                </DataGridTextColumn.Header>
                            </DataGridTextColumn>
                        </DataGrid.Columns>
                    </DataGrid>
                    <StackPanel Grid.Column="1" VerticalAlignment="Stretch" dp:StackPanelHelper.ChildSpace="10">
                        <Button Style="{StaticResource PrimaryButton}" x:Name="selectAllBtn" Click="Button_Click"
                                Content="{DynamicResource App.SelectAll}"
                                />
                        <Button Style="{StaticResource PrimaryButton}" x:Name="addBtn" Click="Button_Click"
                                Content="{DynamicResource App.AddTo}"/>
                        <Button Style="{StaticResource PrimaryButton}" x:Name="delBtn" Click="Button_Click"
                                Content="{DynamicResource App.Delete}" />
                    </StackPanel>
                    <DataGrid Grid.Column="2" Style="{StaticResource DefaultDataGrid}"
                              BorderThickness="1"
                              IsReadOnly="False"
                              SelectionMode="Extended"
                              ItemsSource="{Binding Conditions}" x:Name="conditionTable">
                        <DataGrid.Columns>
                            <DataGridTemplateColumn Header="#" CanUserResize="False" IsReadOnly="True">
                                <DataGridTemplateColumn.CellTemplate>
                                    <DataTemplate>
                                        <TextBlock Text="{Binding Converter={StaticResource rowNumber},
RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DataGridRow},AncestorLevel=1}}"/>
                                    </DataTemplate>
                                </DataGridTemplateColumn.CellTemplate>
                            </DataGridTemplateColumn>
                            <DataGridTextColumn Width="*" IsReadOnly="True" Binding="{Binding Key}">
                                <DataGridTextColumn.Header>
                                    <TextBlock Text="{DynamicResource Dv.Datasource.FieldName}"/>
                                </DataGridTextColumn.Header>
                            </DataGridTextColumn>
                            <DataGridTextColumn Width="*" Binding="{Binding Desc}">
                                <DataGridTextColumn.Header>
                                    <TextBlock Text="{DynamicResource Dv.Datasource.Desc}"/>
                                </DataGridTextColumn.Header>
                            </DataGridTextColumn>
                            <DataGridTemplateColumn Width="*" IsReadOnly="True">
                                <DataGridTemplateColumn.Header>
                                    <TextBlock Text="{DynamicResource Dv.Datasource.TimeField}"/>
                                </DataGridTemplateColumn.Header>
                                <DataGridTemplateColumn.CellTemplate>
                                    <DataTemplate>
                                        <RadioButton IsChecked="{Binding IsTimeCol,Mode=OneWay}"
                                                     Click="RadioButton_Click"/>
                                    </DataTemplate>
                                </DataGridTemplateColumn.CellTemplate>
                            </DataGridTemplateColumn>
                        </DataGrid.Columns>
                    </DataGrid>
                </Grid>
            </pu:FormGroup>
        </StackPanel>
    </Grid>
</pu:WindowX>
HMI.DataViewer/DbSettingModal.xaml.cs
New file
@@ -0,0 +1,297 @@
using HMI.Core.Util;
using HMI.DataViewer.Entity;
using HMI.UI.DpUtil;
using HMI.UI.Util;
using log4net;
using Microsoft.Win32;
using Panuon.WPF.UI;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Windows.Controls;
namespace HMI.DataViewer
{
    /// <summary>
    /// ModalWindow.xaml 的交互逻辑
    /// </summary>
    public partial class DbSettingModal : WindowX
    {
        public DbSettingModal(ILog log)
        {
            this.log = log;
            this.DataContext = this;
            InitializeComponent();
        }
        private ILog log;
        public string FilePath { get; private set; }
        public int DataIndex { get; private set; }
        public DataBase_Setting Data { get; private set; }
        public ObservableCollection<DataBase_Setting> Datasource { get; private set; }
        private SqliteHelper helper;
        public SqliteHelper Helper
        {
            get => helper;
            set
            {
                if (helper == value) return;
                helper?.Close();
                helper = value;
            }
        }
        private ObservableCollection<string> columns = new ObservableCollection<string>();
        public ObservableCollection<string> Columns
        {
            get => columns;
            set { Set(ref columns, value); }
        }
        private ObservableCollection<QueryCondition> conditions =
            new ObservableCollection<QueryCondition>();
        public ObservableCollection<QueryCondition> Conditions
        {
            get => conditions;
            set { Set(ref conditions, value); }
        }
        private bool _isProcessing;
        protected override void OnClosing(CancelEventArgs e)
        {
            //Press Continue Button
            if (DialogResult == true)
            {
                if (_isProcessing)
                {
                    e.Cancel = true;
                    return;
                }
                var okButton = WindowXModalDialog.GetOKButton(this);
                try
                {
                    _isProcessing = true;
                    ButtonHelper.SetIsPending(okButton, true);
                    if (!ValidateForm())
                    {
                        e.Cancel = true;
                        return;
                    }
                    SaveOrUpdateData();
                    Helper?.Close();
                }
                catch (Exception ex)
                {
                    log.Error("IPManage Form Submit Failed.", ex);
                    ToastHelper.Error(this, $"{this.lang("App.App.UnhandledException")}: {ex.Message}");
                    e.Cancel = true;
                    return;
                }
                finally
                {
                    _isProcessing = false;
                    ButtonHelper.SetIsPending(okButton, false);
                }
            }
            base.OnClosing(e);
        }
        public bool ValidateName(object value, out string err)
        {
            err = null;
            var target = Datasource.FirstOrDefault(e => e.Program == value.ToString());
            if (target != null && target != Data)
            {
                err = this.lang("App.MustBeUnique", LangUtil.Get("Dv.Datasource.Name")); ;
                return false;
            }
            return true;
        }
        private bool ValidateForm()
        {
            bool flag = FormGroupHelper.ValidateForm(formPanel);
            if (flag)
            {
                Data.Program = programInput.Text;
                Data.DBFilePath = dbFilePathInput.Text;
                Data.TableName = tableNameInput.Text;
                Data.Pattern = TbPattern.Text;
                Data.TimeCol = Conditions.FirstOrDefault(c => c.IsTimeCol)?.Key;
                Data.Condition = Conditions;
            }
            return flag;
        }
        public bool? Open(int index, DataBase_Setting data,
            ObservableCollection<DataBase_Setting> dataSource,
            string filePath)
        {
            FilePath = filePath;
            DataIndex = index;
            Data = data;
            Datasource = dataSource;
            programInput.Text = Data.Program;
            dbFilePathInput.Text = Data.DBFilePath;
            tableNameInput.Text = Data.TableName;
            TbPattern.Text = Data.Pattern;
            if (!string.IsNullOrEmpty(Data.DBFilePath) && File.Exists(Data.DBFilePath))
            {
                UpdateDbHelper(Data.DBFilePath);
                UpdateTableSource();
                tableNameInput.SelectedItem = Data.TableName;
                UpdateColumnSource(Data.TableName);
            }
            Conditions = data?.Condition ?? new ObservableCollection<QueryCondition>();
            return this.ShowDialog();
        }
        private void SaveOrUpdateData()
        {
            if (DataIndex == -1)
            {
                InsertData(Data);
            }
            else
            {
                UpdateData(DataIndex, Data);
            }
        }
        public void InsertData(DataBase_Setting data)
        {
            Datasource.Add(data);
            XmlHelper.SerializeToXml(FilePath, Datasource);
        }
        public void UpdateData(int index, DataBase_Setting data)
        {
            Datasource[index] = data;
            XmlHelper.SerializeToXml(FilePath, Datasource);
        }
        private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
        {
            Button btn = sender as Button;
            switch (btn.Name)
            {
                case "searchBtn":
                    OpenSelectDbDialog();
                    break;
                case "selectAllBtn":
                    SelectAll();
                    break;
                case "addBtn":
                    AddCondition();
                    break;
                case "delBtn":
                    RemoveCondition();
                    break;
                default:
                    break;
            }
        }
        private void UpdateDbHelper(string filePath)
        {
            if (Helper == null)
            {
                Helper = new SqliteHelper(filePath);
            }
            else if (Helper.DataBaseFile != filePath)
            {
                //Helper.Close();
                Helper = new SqliteHelper(filePath);
            }
        }
        private void UpdateTableSource()
        {
            string[] tableNames = Helper.GetAllTableInFile();
            tableNameInput.ItemsSource = tableNames;
        }
        private void UpdateColumnSource(string tableName)
        {
            string[] cols = Helper.GetColumnNameFromTable(tableName);
            Columns = new ObservableCollection<string>(cols);
        }
        private void OpenSelectDbDialog()
        {
            string BasePath = PathHelper.ConfigFolder;
            OpenFileDialog openFileDialog = new OpenFileDialog();
            openFileDialog.Filter = "(*.db)|*.db";
            openFileDialog.InitialDirectory = BasePath;
            openFileDialog.Title = this.lang("Dv.SelectDbFile");
            openFileDialog.Multiselect = false;
            if (openFileDialog.ShowDialog() == true)
            {
                string FileName = openFileDialog.FileName;
                dbFilePathInput.Text = FileName;
                UpdateDbHelper(FileName);
                UpdateTableSource();
            }
        }
        private void SelectAll()
        {
            columnTable.SelectAll();
            AddCondition();
        }
        private void AddCondition()
        {
            if (columnTable.SelectedItems == null || columnTable.SelectedItems.Count <= 0) return;
            var temp = new ObservableCollection<QueryCondition>();
            foreach (var column in columnTable.SelectedItems)
            {
                temp.Add(new QueryCondition { Key = column.ToString(), Desc = column.ToString() });
            }
            Conditions = temp;
        }
        private void RemoveCondition()
        {
            if (conditionTable.SelectedItems == null || conditionTable.SelectedItems.Count <= 0) return;
            Conditions = new ObservableCollection<QueryCondition>(
                Conditions.Where(d => !conditionTable.SelectedItems.Contains(d)).ToList());
        }
        private void ComboxSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            UpdateColumnSource(e.AddedItems[0].ToString());
            //Conditions = new ObservableCollection<QueryCondition>();
        }
        private void RadioButton_Click(object sender, System.Windows.RoutedEventArgs e)
        {
            RadioButton btn = sender as RadioButton;
            var condition = btn.DataContext as QueryCondition;
            if (condition.IsTimeCol == true)
            {
                condition.IsTimeCol = false;
            }
            else
            {
                condition.IsTimeCol = true;
                foreach (var c in Conditions)
                {
                    if (c != condition) c.IsTimeCol = false;
                }
            }
        }
    }
}
HMI.DataViewer/Entity/DataBase_Setting.cs
New file
@@ -0,0 +1,116 @@
using HMI.Core.Entity;
using HMI.Core.Util;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
namespace HMI.DataViewer.Entity
{
    /// <summary>
    /// 数据库配置类
    /// </summary>
    [Serializable]
    public class DataBase_Setting : NotifyBase
    {
        /// <summary>
        /// 数据库文件路径
        /// </summary>
        private string dBFilePath = string.Empty;
        /// <summary>
        /// 数据库操作类
        /// </summary>
        private SqliteHelper sql = null;
        /// <summary>
        /// 数据表名称
        /// </summary>
        private string tableName = string.Empty;
        /// <summary>
        /// 设定程序名
        /// </summary>
        private string program = string.Empty;
        private string timeCol = string.Empty;
        /// <summary>
        /// 高级查询条件
        /// </summary>
        private ObservableCollection<QueryCondition> condition = new ObservableCollection<QueryCondition>();
        /// <summary>
        /// 构造函数
        /// </summary>
        public DataBase_Setting()
        {
            // do nothing
        }
        /// <summary>
        /// 数据库文件的存储路径
        /// </summary>
        public string DBFilePath
        {
            get => dBFilePath;
            set { Set(ref dBFilePath, value); }
        }
        private string pattern;
        public string Pattern
        {
            get => pattern;
            set
            {
                Set(ref pattern, value);
            }
        }
        public string TimeCol
        {
            get => timeCol;
            set { Set(ref timeCol, value); }
        }
        /// <summary>
        /// 数据库操作句柄
        /// </summary>
        [XmlIgnore]
        public SqliteHelper SQL
        {
            get { return this.sql; }
            set { this.sql = value; }
        }
        /// <summary>
        /// 高级查询条件
        /// </summary>
        public ObservableCollection<QueryCondition> Condition
        {
            get { return this.condition; }
            set { Set(ref condition, value); }
        }
        /// <summary>
        /// 数据表名称
        /// </summary>
        public string TableName
        {
            get { return this.tableName; }
            set { Set(ref tableName, value); }
        }
        /// <summary>
        /// 设定程序名
        /// </summary>
        public string Program
        {
            get { return this.program; }
            set { Set(ref program, value); }
        }
    }
}
HMI.DataViewer/Entity/QueryCondition.cs
New file
@@ -0,0 +1,50 @@
using HMI.Core.Entity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace HMI.DataViewer.Entity
{
    /// <summary>
    /// 筛选条件类
    /// </summary>
    public class QueryCondition : NotifyBase
    {
        /// <summary>
        /// 查询的键值
        /// </summary>
        private string key = string.Empty;
        /// <summary>
        /// 键值描述
        /// </summary>
        private string desc = string.Empty;
        /// <summary>
        /// 查询的键值
        /// </summary>
        public string Key
        {
            get { return this.key; }
            set { Set(ref key, value); }
        }
        /// <summary>
        /// 键值描述
        /// </summary>
        public string Desc
        {
            get { return this.desc; }
            set { Set(ref desc, value); }
        }
        private bool isTimeCol;
        public bool IsTimeCol
        {
            get => isTimeCol;
            set { Set(ref isTimeCol, value); }
        }
    }
}
HMI.DataViewer/Entity/QueryData.cs
New file
@@ -0,0 +1,36 @@
using HMI.Core.Entity;
namespace HMI.DataViewer.Entity
{
    public class QueryData : NotifyBase
    {
        private string key;
        private string name;
        private string condition;
        private string _value;
        public string Key
        {
            get => key;
            set => Set(ref key, value);
        }
        public string Name
        {
            get => name;
            set => Set(ref name, value);
        }
        public string Condition
        {
            get => condition;
            set => Set(ref condition, value);
        }
        public string Value
        {
            get => _value;
            set => Set(ref _value, value);
        }
    }
}
HMI.DataViewer/HMI.DataViewer.csproj
New file
@@ -0,0 +1,160 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{A580DE4A-34E7-4EFE-AD2C-0871396000E9}</ProjectGuid>
    <OutputType>library</OutputType>
    <RootNamespace>HMI.DataViewer</RootNamespace>
    <AssemblyName>HMI.DataViewer</AssemblyName>
    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
    <WarningLevel>4</WarningLevel>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
    <DebugSymbols>true</DebugSymbols>
    <OutputPath>bin\x64\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <DebugType>full</DebugType>
    <PlatformTarget>x64</PlatformTarget>
    <LangVersion>7.3</LangVersion>
    <ErrorReport>prompt</ErrorReport>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
    <OutputPath>bin\x64\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <Optimize>true</Optimize>
    <DebugType>pdbonly</DebugType>
    <PlatformTarget>x64</PlatformTarget>
    <LangVersion>7.3</LangVersion>
    <ErrorReport>prompt</ErrorReport>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="ICSharpCode.SharpZipLib, Version=1.0.0.999, Culture=neutral, PublicKeyToken=1b03e6acf1164f73, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\SharpZipLib.1.0.0\lib\net45\ICSharpCode.SharpZipLib.dll</HintPath>
    </Reference>
    <Reference Include="log4net, Version=3.0.4.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\log4net.3.0.4\lib\net462\log4net.dll</HintPath>
    </Reference>
    <Reference Include="NPOI, Version=2.4.1.0, Culture=neutral, PublicKeyToken=0df73ec7942b34e1, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\NPOI.2.4.1\lib\net45\NPOI.dll</HintPath>
    </Reference>
    <Reference Include="NPOI.OOXML, Version=2.4.1.0, Culture=neutral, PublicKeyToken=0df73ec7942b34e1, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\NPOI.2.4.1\lib\net45\NPOI.OOXML.dll</HintPath>
    </Reference>
    <Reference Include="NPOI.OpenXml4Net, Version=2.4.1.0, Culture=neutral, PublicKeyToken=0df73ec7942b34e1, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\NPOI.2.4.1\lib\net45\NPOI.OpenXml4Net.dll</HintPath>
    </Reference>
    <Reference Include="NPOI.OpenXmlFormats, Version=2.4.1.0, Culture=neutral, PublicKeyToken=0df73ec7942b34e1, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\NPOI.2.4.1\lib\net45\NPOI.OpenXmlFormats.dll</HintPath>
    </Reference>
    <Reference Include="Panuon.WPF, Version=1.1.2.2, Culture=neutral, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\Panuon.WPF.1.1.3\lib\net472\Panuon.WPF.dll</HintPath>
    </Reference>
    <Reference Include="Panuon.WPF.UI, Version=1.3.0.2, Culture=neutral, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\Panuon.WPF.UI.1.3.0.2\lib\net472\Panuon.WPF.UI.dll</HintPath>
    </Reference>
    <Reference Include="System" />
    <Reference Include="System.Configuration" />
    <Reference Include="System.Data" />
    <Reference Include="System.Web" />
    <Reference Include="System.Windows.Forms" />
    <Reference Include="System.Xml" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Xaml">
      <RequiredTargetFramework>4.0</RequiredTargetFramework>
    </Reference>
    <Reference Include="WindowsBase" />
    <Reference Include="PresentationCore">
      <Private>False</Private>
    </Reference>
    <Reference Include="PresentationFramework" />
  </ItemGroup>
  <ItemGroup>
    <Page Include="DataViewer.xaml">
      <Generator>MSBuild:Compile</Generator>
      <SubType>Designer</SubType>
    </Page>
    <Compile Include="DataViewer.xaml.cs">
      <DependentUpon>DataViewer.xaml</DependentUpon>
      <SubType>Code</SubType>
    </Compile>
    <Page Include="DbSettingModal.xaml">
      <Generator>MSBuild:Compile</Generator>
      <SubType>Designer</SubType>
    </Page>
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Entity\QueryCondition.cs" />
    <Compile Include="Entity\DataBase_Setting.cs" />
    <Compile Include="DbSettingModal.xaml.cs">
      <DependentUpon>DbSettingModal.xaml</DependentUpon>
    </Compile>
    <Compile Include="Entity\QueryData.cs" />
    <Compile Include="Properties\AssemblyInfo.cs">
      <SubType>Code</SubType>
    </Compile>
    <Compile Include="Properties\Resources.Designer.cs">
      <AutoGen>True</AutoGen>
      <DesignTime>True</DesignTime>
      <DependentUpon>Resources.resx</DependentUpon>
    </Compile>
    <Compile Include="Properties\Settings.Designer.cs">
      <AutoGen>True</AutoGen>
      <DependentUpon>Settings.settings</DependentUpon>
      <DesignTimeSharedInput>True</DesignTimeSharedInput>
    </Compile>
    <EmbeddedResource Include="Properties\Resources.resx">
      <Generator>ResXFileCodeGenerator</Generator>
      <LastGenOutput>Resources.Designer.cs</LastGenOutput>
    </EmbeddedResource>
    <None Include="packages.config" />
    <None Include="Properties\Settings.settings">
      <Generator>SettingsSingleFileGenerator</Generator>
      <LastGenOutput>Settings.Designer.cs</LastGenOutput>
    </None>
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\HMI.Base\HMI.Base.csproj">
      <Project>{2a6f9720-a890-4036-9d73-41a0dae28341}</Project>
      <Name>HMI.Base</Name>
    </ProjectReference>
    <ProjectReference Include="..\HMI.Core\HMI.Core.csproj">
      <Project>{3e3bf44a-0b69-45c5-9512-528cb48a7106}</Project>
      <Name>HMI.Core</Name>
    </ProjectReference>
    <ProjectReference Include="..\HMI.UI\HMI.UI.csproj">
      <Project>{f17de6b5-af1d-48c6-94a0-6b763a6546a6}</Project>
      <Name>HMI.UI</Name>
    </ProjectReference>
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  <PropertyGroup>
    <PostBuildEvent>
    </PostBuildEvent>
  </PropertyGroup>
</Project>
HMI.DataViewer/Properties/AssemblyInfo.cs
New file
@@ -0,0 +1,52 @@
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
// 有关程序集的一般信息由以下
// 控制。更改这些特性值可修改
// 与程序集关联的信息。
[assembly: AssemblyTitle("HMI.DataViewer")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Organization")]
[assembly: AssemblyProduct("HMI.DataViewer")]
[assembly: AssemblyCopyright("Copyright © Organization 2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 将 ComVisible 设置为 false 会使此程序集中的类型
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
//请将此类型的 ComVisible 特性设置为 true。
[assembly: ComVisible(false)]
//若要开始生成可本地化的应用程序,请设置
//.csproj 文件中的 <UICulture>CultureYouAreCodingWith</UICulture>
//在 <PropertyGroup> 中。例如,如果你使用的是美国英语。
//使用的是美国英语,请将 <UICulture> 设置为 en-US。  然后取消
//对以下 NeutralResourceLanguage 特性的注释。  更新
//以下行中的“en-US”以匹配项目文件中的 UICulture 设置。
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
[assembly:ThemeInfo(
    ResourceDictionaryLocation.None, //主题特定资源词典所处位置
                             //(未在页面中找到资源时使用,
                             //或应用程序资源字典中找到时使用)
    ResourceDictionaryLocation.SourceAssembly //常规资源词典所处位置
                                      //(未在页面中找到资源时使用,
                                      //、应用程序或任何主题专用资源字典中找到时使用)
)]
// 程序集的版本信息由下列四个值组成:
//
//      主版本
//      次版本
//      生成号
//      修订号
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
HMI.DataViewer/Properties/Resources.Designer.cs
New file
@@ -0,0 +1,62 @@
//------------------------------------------------------------------------------
// <auto-generated>
//     此代码由工具生成。
//     运行时版本: 4.0.30319.42000
//
//     对此文件的更改可能导致不正确的行为,如果
//     重新生成代码,则所做更改将丢失。
// </auto-generated>
//------------------------------------------------------------------------------
namespace HMI.DataViewer.Properties {
    /// <summary>
    ///   强类型资源类,用于查找本地化字符串等。
    /// </summary>
    // 此类是由 StronglyTypedResourceBuilder
    // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
    // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
    // (以 /str 作为命令选项),或重新生成 VS 项目。
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
    internal class Resources {
        private static global::System.Resources.ResourceManager resourceMan;
        private static global::System.Globalization.CultureInfo resourceCulture;
        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        internal Resources() {
        }
        /// <summary>
        ///   返回此类使用的缓存 ResourceManager 实例。
        /// </summary>
        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
        internal static global::System.Resources.ResourceManager ResourceManager {
            get {
                if ((resourceMan == null)) {
                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("HMI.DataViewer.Properties.Resources", typeof(Resources).Assembly);
                    resourceMan = temp;
                }
                return resourceMan;
            }
        }
        /// <summary>
        ///   重写当前线程的 CurrentUICulture 属性,对
        ///   使用此强类型资源类的所有资源查找执行重写。
        /// </summary>
        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
        internal static global::System.Globalization.CultureInfo Culture {
            get {
                return resourceCulture;
            }
            set {
                resourceCulture = value;
            }
        }
    }
}
HMI.DataViewer/Properties/Resources.resx
New file
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
  <!--
    Microsoft ResX Schema
    Version 2.0
    The primary goals of this format is to allow a simple XML format
    that is mostly human readable. The generation and parsing of the
    various data types are done through the TypeConverter classes
    associated with the data types.
    Example:
    ... ado.net/XML headers & schema ...
    <resheader name="resmimetype">text/microsoft-resx</resheader>
    <resheader name="version">2.0</resheader>
    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
        <value>[base64 mime encoded serialized .NET Framework object]</value>
    </data>
    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
        <comment>This is a comment</comment>
    </data>
    There are any number of "resheader" rows that contain simple
    name/value pairs.
    Each data row contains a name, and value. The row also contains a
    type or mimetype. Type corresponds to a .NET class that support
    text/value conversion through the TypeConverter architecture.
    Classes that don't support this are serialized and stored with the
    mimetype set.
    The mimetype is used for serialized objects, and tells the
    ResXResourceReader how to depersist the object. This is currently not
    extensible. For a given mimetype the value must be set accordingly:
    Note - application/x-microsoft.net.object.binary.base64 is the format
    that the ResXResourceWriter will generate, however the reader can
    read any of the formats listed below.
    mimetype: application/x-microsoft.net.object.binary.base64
    value   : The object must be serialized with
            : System.Serialization.Formatters.Binary.BinaryFormatter
            : and then encoded with base64 encoding.
    mimetype: application/x-microsoft.net.object.soap.base64
    value   : The object must be serialized with
            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
            : and then encoded with base64 encoding.
    mimetype: application/x-microsoft.net.object.bytearray.base64
    value   : The object must be serialized into a byte array
            : using a System.ComponentModel.TypeConverter
            : and then encoded with base64 encoding.
    -->
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" />
              <xsd:attribute name="type" type="xsd:string" />
              <xsd:attribute name="mimetype" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string" />
              <xsd:attribute name="name" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" />
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
</root>
HMI.DataViewer/Properties/Settings.Designer.cs
New file
@@ -0,0 +1,30 @@
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.42000
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace HMI.DataViewer.Properties
{
    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
    internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
    {
        private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
        public static Settings Default
        {
            get
            {
                return defaultInstance;
            }
        }
    }
}
HMI.DataViewer/Properties/Settings.settings
New file
@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
  <Profiles>
    <Profile Name="(Default)" />
  </Profiles>
  <Settings />
</SettingsFile>
HMI.DataViewer/packages.config
New file
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="log4net" version="3.0.4" targetFramework="net472" />
  <package id="NPOI" version="2.4.1" targetFramework="net472" />
  <package id="Panuon.WPF" version="1.1.3" targetFramework="net472" />
  <package id="Panuon.WPF.UI" version="1.3.0.2" targetFramework="net472" />
  <package id="SharpZipLib" version="1.0.0" targetFramework="net472" />
</packages>
HMI.LogViewer/HMI.LogViewer.csproj
New file
@@ -0,0 +1,137 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{20C23602-B029-4E21-8C11-B9E5F2223237}</ProjectGuid>
    <OutputType>library</OutputType>
    <RootNamespace>HMI.LogViewer</RootNamespace>
    <AssemblyName>HMI.LogViewer</AssemblyName>
    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
    <WarningLevel>4</WarningLevel>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
    <DebugSymbols>true</DebugSymbols>
    <OutputPath>bin\x64\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <DebugType>full</DebugType>
    <PlatformTarget>x64</PlatformTarget>
    <LangVersion>7.3</LangVersion>
    <ErrorReport>prompt</ErrorReport>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
    <OutputPath>bin\x64\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <Optimize>true</Optimize>
    <DebugType>pdbonly</DebugType>
    <PlatformTarget>x64</PlatformTarget>
    <LangVersion>7.3</LangVersion>
    <ErrorReport>prompt</ErrorReport>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="log4net, Version=3.0.4.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\log4net.3.0.4\lib\net462\log4net.dll</HintPath>
    </Reference>
    <Reference Include="Panuon.WPF, Version=1.1.2.2, Culture=neutral, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\Panuon.WPF.1.1.3\lib\net472\Panuon.WPF.dll</HintPath>
    </Reference>
    <Reference Include="Panuon.WPF.UI, Version=1.3.0.2, Culture=neutral, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\Panuon.WPF.UI.1.3.0.2\lib\net472\Panuon.WPF.UI.dll</HintPath>
    </Reference>
    <Reference Include="System" />
    <Reference Include="System.Configuration" />
    <Reference Include="System.Data" />
    <Reference Include="System.Web" />
    <Reference Include="System.Xml" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Xaml">
      <RequiredTargetFramework>4.0</RequiredTargetFramework>
    </Reference>
    <Reference Include="WindowsBase" />
    <Reference Include="PresentationCore" />
    <Reference Include="PresentationFramework" />
  </ItemGroup>
  <ItemGroup>
    <Page Include="LogViewer.xaml">
      <Generator>MSBuild:Compile</Generator>
      <SubType>Designer</SubType>
    </Page>
    <Compile Include="LogData.cs" />
    <Compile Include="LogViewer.xaml.cs">
      <DependentUpon>LogViewer.xaml</DependentUpon>
      <SubType>Code</SubType>
    </Compile>
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Properties\AssemblyInfo.cs">
      <SubType>Code</SubType>
    </Compile>
    <Compile Include="Properties\Resources.Designer.cs">
      <AutoGen>True</AutoGen>
      <DesignTime>True</DesignTime>
      <DependentUpon>Resources.resx</DependentUpon>
    </Compile>
    <Compile Include="Properties\Settings.Designer.cs">
      <AutoGen>True</AutoGen>
      <DependentUpon>Settings.settings</DependentUpon>
      <DesignTimeSharedInput>True</DesignTimeSharedInput>
    </Compile>
    <EmbeddedResource Include="Properties\Resources.resx">
      <Generator>ResXFileCodeGenerator</Generator>
      <LastGenOutput>Resources.Designer.cs</LastGenOutput>
    </EmbeddedResource>
    <None Include="packages.config" />
    <None Include="Properties\Settings.settings">
      <Generator>SettingsSingleFileGenerator</Generator>
      <LastGenOutput>Settings.Designer.cs</LastGenOutput>
    </None>
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\HMI.Base\HMI.Base.csproj">
      <Project>{2a6f9720-a890-4036-9d73-41a0dae28341}</Project>
      <Name>HMI.Base</Name>
    </ProjectReference>
    <ProjectReference Include="..\HMI.Core\HMI.Core.csproj">
      <Project>{3e3bf44a-0b69-45c5-9512-528cb48a7106}</Project>
      <Name>HMI.Core</Name>
    </ProjectReference>
    <ProjectReference Include="..\HMI.UI\HMI.UI.csproj">
      <Project>{f17de6b5-af1d-48c6-94a0-6b763a6546a6}</Project>
      <Name>HMI.UI</Name>
    </ProjectReference>
    <ProjectReference Include="..\HMI.Util\HMI.Util.csproj">
      <Project>{afea100a-9223-448d-ab10-3015d93dd98c}</Project>
      <Name>HMI.Util</Name>
    </ProjectReference>
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  <PropertyGroup>
    <PostBuildEvent>
    </PostBuildEvent>
  </PropertyGroup>
</Project>
HMI.LogViewer/LogData.cs
New file
@@ -0,0 +1,19 @@
using HMI.Core;
namespace HMI.LogViewer
{
    public class LogData
    {
        [ExcelColumn("级别")]
        public string level { get; set; }
        [ExcelColumn("时间")]
        public string time { get; set; }
        [ExcelColumn("来源")]
        public string source { get; set; }
        [ExcelColumn("内容")]
        public string text { get; set; }
    }
}
HMI.LogViewer/LogViewer.xaml
New file
@@ -0,0 +1,380 @@
<hb:PluginControl x:Class="HMI.LogViewer.LogViewer"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:HMI.LogViewer"
             mc:Ignorable="d"
             xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base"
             xmlns:uic="clr-namespace:HMI.UI.Converter;assembly=HMI.UI"
             xmlns:pw="https://opensource.panuon.com/wpf"
             xmlns:pu="https://opensource.panuon.com/wpf-ui"
             xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
             Loaded="UserControl_Loaded"
             Background="{DynamicResource WorkspaceBackground}"
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/HMI.UI;component/Style/PanuonListView.xaml"/>
            </ResourceDictionary.MergedDictionaries>
            <Style x:Key="TableBlock" TargetType="TextBlock">
                <Setter Property="Padding" Value="0"/>
                <Setter Property="Margin" Value="0"/>
                <Setter Property="VerticalAlignment" Value="Center"/>
                <Setter Property="TextWrapping" Value="Wrap"/>
            </Style>
            <Style x:Key="TableBox" TargetType="TextBox">
                <Setter Property="Padding" Value="0"/>
                <Setter Property="Margin" Value="0"/>
                <Setter Property="VerticalAlignment" Value="Center"/>
                <Setter Property="VerticalContentAlignment" Value="Center"/>
                <Setter Property="TextWrapping" Value="Wrap"/>
                <Setter Property="BorderThickness" Value="0"/>
                <Setter Property="BorderBrush" Value="Transparent"/>
                <Setter Property="Background" Value="Transparent"/>
                <Setter Property="Foreground" Value="{Binding Foreground,RelativeSource={RelativeSource Mode=FindAncestor,
AncestorLevel=1,AncestorType=ListViewItem}}"/>
                <Style.Triggers>
                    <DataTrigger Binding="{Binding IsSelected,RelativeSource={RelativeSource Mode=FindAncestor,
                        AncestorLevel=1,AncestorType=ListViewItem}}" Value="True">
                        <Setter Property="Foreground" Value="#FFF"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding IsMouseOver,RelativeSource={RelativeSource Mode=FindAncestor,
    AncestorLevel=1,AncestorType=ListViewItem}}" Value="True">
                        <Setter Property="Foreground" Value="#FFF"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
            <Style x:Key="sbCell" TargetType="DataGridCell" BasedOn="{StaticResource {x:Static pu:StyleKeys.DataGridCellStyle}}">
                <Setter Property="Padding" Value="0"/>
                <Setter Property="Margin" Value="0"/>
            </Style>
        </ResourceDictionary>
    </UserControl.Resources>
    <Border Style="{StaticResource CardBoarder}">
        <Grid dp:GridHelper.Space="10" Cursor="">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <TextBlock FontSize="{StaticResource TitleFontSize}"
VerticalAlignment="Center" HorizontalAlignment="Left" Text="{DynamicResource log.title}"/>
            <DockPanel Grid.Row="1" Cursor="">
                <StackPanel DockPanel.Dock="Right" Style="{StaticResource ButtonGroup}">
                    <Button Style="{StaticResource QueryBtn}"
                            x:Name="queryBtn" Click="Button_Click"/>
                    <Button Style="{StaticResource ResetBtn}"
                            x:Name="resetBtn" Click="Button_Click"/>
                    <Button x:Name="exportBtn"
                            Style="{StaticResource ExportBtn}" Click="Button_Click"/>
                </StackPanel>
                <StackPanel Style="{StaticResource ButtonGroup}" HorizontalAlignment="Left">
                    <TextBlock Style="{StaticResource TitleBox}" FontSize="14" Text="{DynamicResource log.level}"/>
                    <ComboBox Style="{StaticResource FormComboxStyle}" x:Name="levelInput"
                              Width="180"
                              SelectedValuePath="Content">
                        <ComboBoxItem Content="Debug"/>
                        <ComboBoxItem Content="Info"/>
                        <ComboBoxItem Content="Warn"/>
                        <ComboBoxItem Content="Error"/>
                    </ComboBox>
                    <TextBlock Style="{StaticResource TitleBox}" FontSize="14" Text="{DynamicResource log.source}"/>
                    <TextBox Style="{StaticResource EditorTextBoxStyle}" x:Name="sourceInput" Width="180"/>
                    <TextBlock Style="{StaticResource TitleBox}" FontSize="14" Text="{DynamicResource App.StartTime}"/>
                    <pu:DateTimePicker Style="{StaticResource TimePicker}" IsEnabled="{Binding AutoRfh,Converter={StaticResource reverseBool}}"
                                       x:Name="startTimeInput"/>
                    <TextBlock Style="{StaticResource TitleBox}" FontSize="14" Text="{DynamicResource App.EndTime}"/>
                    <pu:DateTimePicker Style="{StaticResource TimePicker}" IsEnabled="{Binding AutoRfh,Converter={StaticResource reverseBool}}"
                                       x:Name="endTimeInput"/>
                    <TextBlock Style="{StaticResource TitleBox}" FontSize="14" Text="{DynamicResource log.autoRefresh}"/>
                    <CheckBox x:Name="autoRfhInput" HorizontalAlignment="Center" VerticalAlignment="Center"
                              IsChecked="{Binding AutoRfh,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
                </StackPanel>
            </DockPanel>
            <ListView Grid.Row="2" Style="{StaticResource DefaultListTable}" x:Name="table"
                                  VirtualizingPanel.VirtualizationMode="Standard" >
                <ListView.ItemContainerStyle>
                    <Style TargetType="ListViewItem" BasedOn="{StaticResource {x:Static pu:StyleKeys.ListViewItemStyle}}">
                        <Setter Property="Height" Value="{StaticResource nanD}" />
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding level}" Value="Warn">
                                <Setter Property="Foreground" Value="{DynamicResource WarningColor}"/>
                            </DataTrigger>
                            <DataTrigger Binding="{Binding level}" Value="Error">
                                <Setter Property="Foreground" Value="{DynamicResource DangerColor}"/>
                            </DataTrigger>
                            <Trigger Property="IsSelected" Value="True">
                                <Setter Property="Background" Value="Transparent"/>
                            </Trigger>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter Property="Background" Value="Transparent"/>
                            </Trigger>
                        </Style.Triggers>
                    </Style>
                </ListView.ItemContainerStyle>
                <ListView.View>
                    <GridView>
                        <GridViewColumn Width="50">
                            <GridViewColumn.Header>
                                <TextBlock Text="#"/>
                            </GridViewColumn.Header>
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <TextBlock Text="{Binding Converter={StaticResource lvRowNumber},
RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ListViewItem,AncestorLevel=1}}"/>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
                        <GridViewColumn Width="80">
                            <GridViewColumn.Header>
                                <TextBlock Text="{DynamicResource log.level}"/>
                            </GridViewColumn.Header>
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <Border Style="{StaticResource CellBorder}">
                                        <TextBlock VerticalAlignment="Center" Text="{Binding level}"/>
                                    </Border>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
                        <GridViewColumn Width="150">
                            <GridViewColumn.Header>
                                <TextBlock Text="{DynamicResource log.time}"/>
                            </GridViewColumn.Header>
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <Border Style="{StaticResource CellBorder}">
                                        <TextBlock VerticalAlignment="Center" Text="{Binding time,Converter={x:Static uic:DateStrConverter.Instance},ConverterParameter='yyyy-MM-dd HH:mm:ss.fff'}"/>
                                    </Border>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
                        <GridViewColumn Width="150">
                            <GridViewColumn.Header>
                                <TextBlock Text="{DynamicResource log.source}"/>
                            </GridViewColumn.Header>
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <Border Style="{StaticResource CellBorder}">
                                        <TextBlock VerticalAlignment="Center" Text="{Binding source}"/>
                                    </Border>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
                        <GridViewColumn Width="1420">
                            <GridViewColumn.Header>
                                <TextBlock Text="{DynamicResource log.text}"/>
                            </GridViewColumn.Header>
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <Border Style="{StaticResource CellBorder}">
                                        <TextBox IsReadOnly="True" Style="{StaticResource TableBox}"
                                                 Text="{Binding text}"/>
                                    </Border>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
                    </GridView>
                </ListView.View>
            </ListView>
            <StackPanel Grid.Row="3" HorizontalAlignment="Right">
                <pu:Pagination Style="{StaticResource DefaultPage}"
                               CurrentPage="{Binding CurrentPage}"
                               MaxPage="{Binding MaxPage}"/>
            </StackPanel>
            <!--<DataGrid Grid.Row="2" x:Name="table"
                      CellStyle="{StaticResource sbCell}"
                  RowHeight="{StaticResource nanD}" Style="{StaticResource DefaultDataGrid}">
                <DataGrid.RowStyle>
                    <Style TargetType="DataGridRow" BasedOn="{StaticResource {x:Static pu:StyleKeys.DataGridRowStyle}}">
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding level}" Value="Warn">
                                <Setter Property="Background" Value="{DynamicResource WarningColor}"/>
                                <Setter Property="Foreground" Value="Black"/>
                            </DataTrigger>
                            <DataTrigger Binding="{Binding level}" Value="Error">
                                <Setter Property="Background" Value="{DynamicResource DangerColor}"/>
                                <Setter Property="Foreground" Value="Black"/>
                            </DataTrigger>
                            <Trigger Property="IsSelected" Value="True">
                                <Setter Property="Background" Value="Transparent"/>
                            </Trigger>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter Property="Background" Value="Transparent"/>
                            </Trigger>
                        </Style.Triggers>
                    </Style>
                </DataGrid.RowStyle>
                <DataGrid.Columns>
                    <DataGridTemplateColumn Header="#" Width="60" CanUserResize="False">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <TextBlock Style="{StaticResource TableBlock}" Text="{Binding Converter={StaticResource rowNumber},
    RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DataGridRow},AncestorLevel=1}}"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTextColumn Width="80" Binding="{Binding level}"
                                        ElementStyle="{StaticResource TableBlock}">
                        <DataGridTextColumn.Header>
                            <TextBlock Text="{DynamicResource log.level}"/>
                        </DataGridTextColumn.Header>
                    </DataGridTextColumn>
                    <DataGridTextColumn Width="150" Binding="{Binding time}"
                                        ElementStyle="{StaticResource TableBlock}">
                        <DataGridTextColumn.Header>
                            <TextBlock Text="{DynamicResource log.time}"/>
                        </DataGridTextColumn.Header>
                    </DataGridTextColumn>
                    <DataGridTextColumn Width="150" Binding="{Binding source}"
                                        ElementStyle="{StaticResource TableBlock}">
                        <DataGridTextColumn.Header>
                            <TextBlock Text="{DynamicResource log.source}"/>
                        </DataGridTextColumn.Header>
                    </DataGridTextColumn>
                    <DataGridTemplateColumn Width="*">
                        <DataGridTemplateColumn.Header>
                            <TextBlock Text="{DynamicResource log.text}"/>
                        </DataGridTemplateColumn.Header>
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <TextBox IsReadOnly="True" Style="{StaticResource TableBox}" Text="{Binding text}"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                </DataGrid.Columns>
            </DataGrid>-->
            <!--<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right">
                <pu:Pagination
                            Height="36" Margin="10"
HorizontalAlignment="Left"
Foreground="Black"
ItemsBackground="Transparent"
ItemsCornerRadius="12"
ItemsSelectedBackground="{DynamicResource SecondPrimaryColor}"
                            x:Name="page"
                            CurrentPage="{Binding CurrentPage}"
                                        MaxPage="{Binding MaxPage}"/>
            </StackPanel>-->
            <!--<ListView Grid.Row="2" Style="{StaticResource DefaultListTable}" x:Name="table" VirtualizingPanel.IsContainerVirtualizable="True"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Standard">
                <ListView.ItemContainerStyle>
                    <Style TargetType="ListViewItem" BasedOn="{StaticResource {x:Static pu:StyleKeys.ListViewItemStyle}}">
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding level}" Value="Warn">
                                <Setter Property="Background" Value="{DynamicResource WarningColor}"/>
                                <Setter Property="Foreground" Value="Black"/>
                            </DataTrigger>
                            <DataTrigger Binding="{Binding level}" Value="Error">
                                <Setter Property="Background" Value="{DynamicResource DangerColor}"/>
                                <Setter Property="Foreground" Value="Black"/>
                            </DataTrigger>
                            <Trigger Property="IsSelected" Value="True">
                                <Setter Property="Background" Value="Transparent"/>
                            </Trigger>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter Property="Background" Value="Transparent"/>
                            </Trigger>
                        </Style.Triggers>
                    </Style>
                </ListView.ItemContainerStyle>
                <ListView.View>
                    <GridView AllowsColumnReorder="False">
                        <GridViewColumn Width="60" Header="#">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <TextBlock Text="{Binding Converter={StaticResource lvRowNumber},
RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ListViewItem,AncestorLevel=1}}"/>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
                        <GridViewColumn Width="80">
                            <GridViewColumn.Header>
                                <TextBlock Text="{DynamicResource log.level}"/>
                            </GridViewColumn.Header>
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <Border Style="{StaticResource CellBorder}">
                                        <TextBlock Style="{StaticResource ListTableTextBlock}"
                                                           Text="{Binding level}"/>
                                    </Border>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
                        <GridViewColumn Width="150">
                            <GridViewColumn.Header>
                                <TextBlock Text="{DynamicResource log.time}"/>
                            </GridViewColumn.Header>
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <Border Style="{StaticResource CellBorder}">
                                        <TextBlock Style="{StaticResource ListTableTextBlock}"
   Text="{Binding time}"/>
                                    </Border>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
                        <GridViewColumn Width="150">
                            <GridViewColumn.Header>
                                <TextBlock Text="{DynamicResource log.source}"/>
                            </GridViewColumn.Header>
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <Border Style="{StaticResource CellBorder}">
                                        <TextBlock Style="{StaticResource ListTableTextBlock}"
                                                   Text="{Binding source}"/>
                                    </Border>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
                        <GridViewColumn Width="1410">
                            <GridViewColumn.Header>
                                <TextBlock Text="{DynamicResource log.text}"/>
                            </GridViewColumn.Header>
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <Border Style="{StaticResource CellBorder}">
                                        <TextBlock Style="{StaticResource ListTableTextBlock}"
                                                   Text="{Binding text}"/>
                                    </Border>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
                    </GridView>
                </ListView.View>
            </ListView>-->
            <!--<pu:Pagination HorizontalContentAlignment="Right" Grid.Row="3" Margin="0,10,0,0" Height="30"
                       FontSize="14"
HorizontalAlignment="Left"
Foreground="Black"
ItemsBackground="Transparent"
ItemsCornerRadius="12"
ItemsSelectedBackground="#36AAF7" CurrentPage="1"  MaxPage="10"/>-->
        </Grid>
    </Border>
</hb:PluginControl>
HMI.LogViewer/LogViewer.xaml.cs
New file
@@ -0,0 +1,396 @@
using HMI.Base;
using HMI.Core.Util;
using HMI.UI.Util;
using HMI.Util;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Timers;
using System.Windows;
using System.Windows.Controls;
namespace HMI.LogViewer
{
    /// <summary>
    /// UserControl1.xaml 的交互逻辑
    /// </summary>
    public partial class LogViewer : PluginControl
    {
        private bool autoRfh;
        public bool AutoRfh
        {
            get { return autoRfh; }
            set
            {
                Set(ref autoRfh, value);
                if (autoRfhTimer != null) autoRfhTimer.Enabled = value;
            }
        }
        private Timer autoRfhTimer;
        private long pageSize = 50;
        private long currentPage = 1;
        public long CurrentPage
        {
            get => currentPage;
            set
            {
                Set(ref currentPage, value);
                Filtrate(value, pageSize);
            }
        }
        private long maxPage = 1;
        public long MaxPage
        {
            get => maxPage;
            set
            {
                Set(ref maxPage, value);
            }
        }
        public LogViewer()
        {
            DataContext = this;
            autoRfhTimer = new Timer
            {
                Enabled = false,
                Interval = 1500,
                AutoReset = false
            };
            autoRfhTimer.Elapsed += AutoRfhTimer_Elapsed;
            InitializeComponent();
            QueryReset();
        }
        public override void InitFinish()
        {
            //base.InitFinish();
            //string baseFile = Path.Combine(logDir, "log.db");
            //try
            //{
            //    if (!File.Exists(baseFile))
            //    {
            //        SqliteHelper.CreateDatabase(baseFile);
            //    }
            //    SqliteHelper sqliteHelper = new SqliteHelper(baseFile);
            //    sqliteHelper.ExecuteSql(@"CREATE TABLE IF NOT EXISTS log (
            //        id INTEGER PRIMARY KEY AUTOINCREMENT,
            //        level TEXT NOT NULL,
            //        text TEXT NOT NULL,
            //        source TEXT,
            //        time TIME
            //        );");
            //}
            //catch (Exception ex)
            //{
            //    log.Error("Create Log File Failed.", ex);
            //}
        }
        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            Loaded -= UserControl_Loaded;
        }
        private void AutoRfhTimer_Elapsed(object sender, ElapsedEventArgs e)
        {
            Refresh100items();
        }
        //private void RefreshTable() {
        //    if(CurrentPage == )
        //}
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Button btn = sender as Button;
            switch (btn.Name)
            {
                case "queryBtn":
                    QueryLog();
                    break;
                case "resetBtn":
                    QueryReset();
                    CurrentPage = 1;
                    break;
                case "exportBtn":
                    DoExport();
                    break;
                default:
                    break;
            }
        }
        private void DoExport()
        {
            try
            {
                SaveFileDialog sfd = new SaveFileDialog
                {
                    Filter = "Excel Files (*.xls)|*.xls",
                };
                if (sfd.ShowDialog() == true)
                {
                    var dt = GetDataTable(false, 1, 1, out long totalPage);
                    ExcelHelper.ExportExcel<LogData>(sfd.FileName, dt);
                    ToastHelper.Success(this.lang("App.ExportSuccess"));
                }
            }
            catch (Exception ex)
            {
                log.Error("Export Log Failed.", ex);
                MsgBoxHelper2.Error($"导出失败: {ex.Message}");
            }
        }
        private void QueryLog()
        {
            try
            {
                if (startTimeInput.SelectedDateTime == null && endTimeInput.SelectedDateTime == null)
                {
                    ToastHelper.Error(this.lang("log.timeCantBeNull"));
                    return;
                }
                if (endTimeInput.SelectedDateTime != null && startTimeInput.SelectedDateTime != null &&
                endTimeInput.SelectedDateTime < startTimeInput.SelectedDateTime)
                {
                    ToastHelper.Error(this.lang("log.timeRangeError"));
                    return;
                }
                AutoRfh = false;
                //queryBtn.IsEnabled = false;
                //autoRfhInput.IsEnabled = false;
                CurrentPage = 1;
            }
            catch (Exception ex)
            {
                log.Error("Log Query Failed.", ex);
                this.Error($"查询失败!{ex.Message}");
            }
            finally
            {
                queryBtn.IsEnabled = true;
                autoRfhInput.IsEnabled = true;
            }
        }
        private void QueryReset()
        {
            var today = DateTime.Now;
            var start = DateTime.Parse(today.ToString("yyyy-MM-dd 00:00:00")).AddDays(-7);
            var end = DateTime.Parse(today.ToString("yyyy-MM-dd 00:00:00")).AddDays(1);
            levelInput.Text = null;
            startTimeInput.SelectedDateTime = start;
            endTimeInput.SelectedDateTime = end;
        }
        public DataTable GetDataTable(bool usePage, long page, long size, out long totalPage)
        {
            totalPage = 0;
            var files = GetOldDbFiles(PathHelper.LogViewerDir, new DateTime?[] { startTimeInput.SelectedDateTime,
                endTimeInput.SelectedDateTime });
            if (files.Length <= 0)
            {
                return new DataTable();
            }
            using (Sqlite3Helper helper = new Sqlite3Helper(files[0]))
            {
                for (int i = 0; i < files.Length; i++)
                {
                    var attach = $"ATTACH DATABASE '{files[i]}' AS 'db{i}'";
                    helper.Execute(attach);
                }
                string sql = string.Join(" UNION ALL ", GetSqls(files));
                string sortName = "time", sortDirection = "DESC";
                string baseSql = $"SELECT level,time,source,text FROM ({sql}) as _temp";
                string countSql = $"SELECT COUNT(1) FROM ({sql}) as __temp";
                long total = (long)helper.SelectOne(countSql);
                totalPage = (total / pageSize) + ((total % pageSize) > 0 ? 1 : 0);
                if (usePage)
                {
                    sql = $"{baseSql} ORDER BY {sortName} {sortDirection} LIMIT {size} OFFSET {(page - 1) * size} ";
                }
                else
                {
                    sql = $"{baseSql} ORDER BY {sortName} {sortDirection}";
                }
                DataTable dt = helper.GetDataTable(sql);
                return dt;
            }
        }
        private void Filtrate(long page, long size)
        {
            try
            {
                //var files = GetOldDbFiles(PathHelper.LogViewerDir, new DateTime?[] { startTimeInput.SelectedDateTime,
                //endTimeInput.SelectedDateTime });
                //if (files.Length <= 0)
                //{
                //    table.ItemsSource = new DataTable().DefaultView;
                //    return;
                //}
                //using (Sqlite3Helper helper = new Sqlite3Helper(files[0]))
                //{
                //    for (int i = 0; i < files.Length; i++)
                //    {
                //        var attach = $"ATTACH DATABASE '{files[i]}' AS 'db{i}'";
                //        helper.Execute(attach);
                //    }
                //    string sql = string.Join(" UNION ALL ", GetSqls(files));
                //    string sortName = "time", sortDirection = "DESC";
                //    string baseSql = $"SELECT level,time,source,text FROM ({sql}) as _temp";
                //    string countSql = $"SELECT COUNT(1) FROM ({sql}) as __temp";
                //    long total = (long)helper.SelectOne(countSql);
                //    long pages = (total/pageSize)+ ((total % pageSize) > 0 ? 1 : 0);
                //    sql = $"{baseSql} ORDER BY {sortName} {sortDirection} LIMIT {size} OFFSET {(page - 1) * size} ";
                //    DataTable dt = helper.GetDataTable(sql);
                //    table.ItemsSource = dt.DefaultView;
                //    MaxPage = pages;
                //}
                table.ItemsSource = GetDataTable(true, page, size, out long totalPage).DefaultView;
                MaxPage = totalPage;
            }
            catch (Exception ex)
            {
                log.Error("Select Log Error.", ex);
                MsgBoxHelper2.Error($"查询失败: {ex.Message}");
            }
        }
        public static string[] GetOldDbFiles(string folderPath, DateTime?[] range)
        {
            if (!Directory.Exists(folderPath))
            {
                return new string[0];
            }
            // 获取所有.db文件并筛选出7天前更新的文件
            var oldFiles = Directory.GetFiles(folderPath, "*.db");
            try
            {
                oldFiles = oldFiles.Where(file =>
                {
                    var lastWriteTime = File.GetLastWriteTime(file);
                    bool flag = true;
                    if (range[0] != null) flag &= lastWriteTime >= range[0];
                    //if (range[1] != null) flag &= lastWriteTime <= range[1];
                    return flag;
                }).OrderByDescending(f => f).ToArray();
                return oldFiles;
            }
            catch (Exception ex)
            {
                return new string[0];
            }
        }
        /// <summary>
        ///
        /// </summary>
        /// <param name="files"></param>
        /// <returns></returns>
        private string[] GetSqls(string[] files)
        {
            string[] sqls = new string[files.Length];
            for (int i = 0; i < files.Length; i++)
            {
                string sql = $"SELECT level,time,source,text FROM db{i}.log";
                List<string> datas = new List<string>();
                if (!string.IsNullOrEmpty(levelInput.Text))
                {
                    datas.Add($"level='{levelInput.Text}'");
                }
                if (!string.IsNullOrEmpty(startTimeInput.Text))
                {
                    datas.Add($"time>='{startTimeInput.Text}'");
                }
                if (!string.IsNullOrEmpty(endTimeInput.Text))
                {
                    datas.Add($"time<'{endTimeInput.Text}'");
                }
                if (!string.IsNullOrEmpty(sourceInput.Text))
                {
                    datas.Add($"source='{sourceInput.Text}'");
                }
                string condition = string.Join(" AND ", datas);
                condition = string.IsNullOrEmpty(condition) ? string.Empty : $" WHERE {condition}";
                sql = $"{sql}{condition}";
                sqls[i] = sql;
            }
            return sqls;
        }
        /// <summary>
        /// 刷新100条数据
        /// </summary>
        private void Refresh100items()
        {
            autoRfhTimer.Enabled = false;
            Task.Run(() =>
            {
                try
                {
                    string todayLogFile = Path.Combine(PathHelper.LogViewerDir, $"log_{DateTime.Today:yyyy-MM-dd}.db");
                    if (!File.Exists(todayLogFile)) return;
                    string str = "select level,time,source,text from log order by time desc limit 100";
                    using (SqliteHelper sql = new SqliteHelper(todayLogFile))
                    {
                        var dt = sql.GetDataTable(str);
                        this.Dispatcher.BeginInvoke((Action)delegate ()
                        {
                            table.ItemsSource = dt.DefaultView;
                            MaxPage = 1;
                        });
                    }
                }
                catch (Exception ex)
                {
                    log.Error("Log Auto Refresh Failed.", ex);
                    this.Error($"自动刷新失败!{ex.Message}");
                }
                finally
                {
                    this.Dispatcher.BeginInvoke((Action)delegate ()
                    {
                        autoRfhTimer.Enabled = AutoRfh;
                    });
                }
            });
        }
    }
}
HMI.LogViewer/Properties/AssemblyInfo.cs
New file
@@ -0,0 +1,52 @@
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
// 有关程序集的一般信息由以下
// 控制。更改这些特性值可修改
// 与程序集关联的信息。
[assembly: AssemblyTitle("HMI.LogViewer")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("HMI.LogViewer")]
[assembly: AssemblyCopyright("Copyright ©  2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 将 ComVisible 设置为 false 会使此程序集中的类型
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
//请将此类型的 ComVisible 特性设置为 true。
[assembly: ComVisible(false)]
//若要开始生成可本地化的应用程序,请设置
//.csproj 文件中的 <UICulture>CultureYouAreCodingWith</UICulture>
//在 <PropertyGroup> 中。例如,如果你使用的是美国英语。
//使用的是美国英语,请将 <UICulture> 设置为 en-US。  然后取消
//对以下 NeutralResourceLanguage 特性的注释。  更新
//以下行中的“en-US”以匹配项目文件中的 UICulture 设置。
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
[assembly:ThemeInfo(
    ResourceDictionaryLocation.None, //主题特定资源词典所处位置
                             //(未在页面中找到资源时使用,
                             //或应用程序资源字典中找到时使用)
    ResourceDictionaryLocation.SourceAssembly //常规资源词典所处位置
                                      //(未在页面中找到资源时使用,
                                      //、应用程序或任何主题专用资源字典中找到时使用)
)]
// 程序集的版本信息由下列四个值组成:
//
//      主版本
//      次版本
//      生成号
//      修订号
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.5")]
HMI.LogViewer/Properties/Resources.Designer.cs
New file
@@ -0,0 +1,62 @@
//------------------------------------------------------------------------------
// <auto-generated>
//     此代码由工具生成。
//     运行时版本: 4.0.30319.42000
//
//     对此文件的更改可能导致不正确的行为,如果
//     重新生成代码,则所做更改将丢失。
// </auto-generated>
//------------------------------------------------------------------------------
namespace HMI.LogViewer.Properties {
    /// <summary>
    ///   强类型资源类,用于查找本地化字符串等。
    /// </summary>
    // 此类是由 StronglyTypedResourceBuilder
    // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
    // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
    // (以 /str 作为命令选项),或重新生成 VS 项目。
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
    internal class Resources {
        private static global::System.Resources.ResourceManager resourceMan;
        private static global::System.Globalization.CultureInfo resourceCulture;
        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        internal Resources() {
        }
        /// <summary>
        ///   返回此类使用的缓存 ResourceManager 实例。
        /// </summary>
        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
        internal static global::System.Resources.ResourceManager ResourceManager {
            get {
                if ((resourceMan == null)) {
                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("HMI.LogViewer.Properties.Resources", typeof(Resources).Assembly);
                    resourceMan = temp;
                }
                return resourceMan;
            }
        }
        /// <summary>
        ///   重写当前线程的 CurrentUICulture 属性,对
        ///   使用此强类型资源类的所有资源查找执行重写。
        /// </summary>
        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
        internal static global::System.Globalization.CultureInfo Culture {
            get {
                return resourceCulture;
            }
            set {
                resourceCulture = value;
            }
        }
    }
}
HMI.LogViewer/Properties/Resources.resx
New file
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
  <!--
    Microsoft ResX Schema
    Version 2.0
    The primary goals of this format is to allow a simple XML format
    that is mostly human readable. The generation and parsing of the
    various data types are done through the TypeConverter classes
    associated with the data types.
    Example:
    ... ado.net/XML headers & schema ...
    <resheader name="resmimetype">text/microsoft-resx</resheader>
    <resheader name="version">2.0</resheader>
    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
        <value>[base64 mime encoded serialized .NET Framework object]</value>
    </data>
    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
        <comment>This is a comment</comment>
    </data>
    There are any number of "resheader" rows that contain simple
    name/value pairs.
    Each data row contains a name, and value. The row also contains a
    type or mimetype. Type corresponds to a .NET class that support
    text/value conversion through the TypeConverter architecture.
    Classes that don't support this are serialized and stored with the
    mimetype set.
    The mimetype is used for serialized objects, and tells the
    ResXResourceReader how to depersist the object. This is currently not
    extensible. For a given mimetype the value must be set accordingly:
    Note - application/x-microsoft.net.object.binary.base64 is the format
    that the ResXResourceWriter will generate, however the reader can
    read any of the formats listed below.
    mimetype: application/x-microsoft.net.object.binary.base64
    value   : The object must be serialized with
            : System.Serialization.Formatters.Binary.BinaryFormatter
            : and then encoded with base64 encoding.
    mimetype: application/x-microsoft.net.object.soap.base64
    value   : The object must be serialized with
            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
            : and then encoded with base64 encoding.
    mimetype: application/x-microsoft.net.object.bytearray.base64
    value   : The object must be serialized into a byte array
            : using a System.ComponentModel.TypeConverter
            : and then encoded with base64 encoding.
    -->
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" />
              <xsd:attribute name="type" type="xsd:string" />
              <xsd:attribute name="mimetype" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string" />
              <xsd:attribute name="name" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" />
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
</root>
HMI.LogViewer/Properties/Settings.Designer.cs
New file
@@ -0,0 +1,30 @@
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.42000
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace HMI.LogViewer.Properties
{
    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
    internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
    {
        private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
        public static Settings Default
        {
            get
            {
                return defaultInstance;
            }
        }
    }
}
HMI.LogViewer/Properties/Settings.settings
New file
@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
  <Profiles>
    <Profile Name="(Default)" />
  </Profiles>
  <Settings />
</SettingsFile>
HMI.LogViewer/packages.config
New file
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="log4net" version="3.0.4" targetFramework="net472" />
  <package id="Panuon.WPF" version="1.1.3" targetFramework="net472" />
  <package id="Panuon.WPF.UI" version="1.3.0.2" targetFramework="net472" />
</packages>
HMI.MenuManage/Entity/IconHolder.cs
New file
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace HMI.MenuManage.Entity
{
    public class IconHolder
    {
        public List<IconMeta> glyphs {  get; set; }
    }
}
HMI.MenuManage/Entity/IconMeta.cs
New file
@@ -0,0 +1,18 @@
using System;
namespace HMI.MenuManage.Entity
{
    public class IconMeta
    {
        public string unicode { get; set; }
        public string Code
        {
            get
            {
                int temp = Convert.ToInt32(unicode, 16);
                return char.ConvertFromUtf32(temp);
            }
        }
    }
}
HMI.MenuManage/Entity/MenuData.cs
New file
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using HMI.Core.Entity;
namespace HMI.MenuManage.Entity
{
    public class MenuData : NotifyBase
    {
        public string Id { get; set; }
        public string Code { get; set; }
        public string Name { get; set; }
        public string IconPath { get; set; }
        public string Icon { get; set; }
        public string Level { get; set; }
        public string Type { get; set; }
        public string ParentCode { get; set; }
        public string ParentName { get; set; }
        public string DllPath { get; set; }
        public string ClassName { get; set; }
        public double? Sort { get; set; }
        private ObservableCollection<MenuData> _Children = new ObservableCollection<MenuData>();
        public ObservableCollection<MenuData> Children
        {
            get => _Children;
            set
            {
                Set(ref _Children, value);
            }
        }
        private bool _Selected;
        public bool Selected
        {
            get => _Selected;
            set
            {
                Set(ref _Selected, value);
            }
        }
    }
}
HMI.MenuManage/Entity/RoleData.cs
New file
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using HMI.Core.Entity;
namespace HMI.MenuManage.Entity
{
    public class RoleData : NotifyBase
    {
        public string Id { get; set; }
        public string Code { get; set; }
        public string Name { get; set; }
        private bool _Selected;
        public bool Selected
        {
            get => _Selected;
            set
            {
                if (_Selected != value)
                {
                    Set(ref _Selected, value);
                }
            }
        }
    }
}
HMI.MenuManage/Entity/RoleMenuRel.cs
New file
@@ -0,0 +1,64 @@
using System;
using System.Collections.ObjectModel;
using System.Windows.Input;
using HMI.Core.Entity;
namespace HMI.MenuManage.Entity
{
    public class RoleMenuRel : NotifyBase
    {
        private event EventHandler<bool> SelectedChanged;
        public string Id { get; set; }
        public string RoleCode { get; set; }
        public string RoleName { get; set; }
        public string MenuCode { get; set; }
        public string ParentCode { get; set; }
        public string MenuName { get; set; }
        public string MenuIcon { get; set; }
        private bool _Selected;
        public bool Selected
        {
            get => _Selected;
            set
            {
                if (_Selected != value)
                {
                    Set(ref _Selected, value);
                }
            }
        }
        public ICommand CheckCommand
        {
            get => new RelayCommand<bool>(ExcuteCheckCommand);
        }
        private void ExcuteCheckCommand(bool flag)
        {
            if (flag && Parent != null) Parent.Selected = flag;
            foreach (var item in Children)
            {
                item.Selected = flag;
            }
        }
        public RoleMenuRel Parent;
        private ObservableCollection<RoleMenuRel> _Children = new ObservableCollection<RoleMenuRel>();
        public ObservableCollection<RoleMenuRel> Children
        {
            get { return _Children; }
            set { Set(ref _Children, value); }
        }
        public void AddChild(RoleMenuRel child)
        {
            child.Parent = this;
            Children.Add(child);
        }
    }
}
HMI.MenuManage/Entity/UserData.cs
New file
@@ -0,0 +1,40 @@
using HMI.Core;
using HMI.Core.Entity;
namespace HMI.MenuManage.Entity
{
    public class UserData : NotifyBase
    {
        public string Id { get; set; }
        [ExcelColumn(name: "工号")]
        public string Code { get; set; }
        [ExcelColumn(name: "账号")]
        public string Username { get; set; }
        [ExcelColumn(name: "名称")]
        public string Name { get; set; }
        [ExcelColumn(name: "角色编码")]
        public string RoleCodes { get; set; }
        [ExcelColumn(name: "密码", ignoreExport: true)]
        public string Password { get; set; }
        public string RoleCode { get; set; }
        private bool _Selected;
        public bool Selected
        {
            get => _Selected;
            set
            {
                if (_Selected != value)
                {
                    Set(ref _Selected, value);
                }
            }
        }
    }
}
HMI.MenuManage/Entity/UserRoleRel.cs
New file
@@ -0,0 +1,26 @@
using System;
using System.Collections.ObjectModel;
using System.Windows.Input;
using HMI.Core.Entity;
namespace HMI.MenuManage.Entity
{
    public class UserRoleRel : NotifyBase
    {
        public string Id { get; set; }
        public string Username { get; set; }
        public string RealName { get; set; }
        public string RoleCode { get; set; }
        public string RoleName { get; set; }
        private bool _Selected;
        public bool Selected
        {
            get => _Selected;
            set
            {
                Set(ref _Selected, value);
            }
        }
    }
}
HMI.MenuManage/HMI.MenuManage.csproj
New file
@@ -0,0 +1,207 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{DE4205C6-5923-46FD-A4B9-65169BF61C75}</ProjectGuid>
    <OutputType>library</OutputType>
    <RootNamespace>HMI.MenuManage</RootNamespace>
    <AssemblyName>HMI.MenuManage</AssemblyName>
    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
    <WarningLevel>4</WarningLevel>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
    <DebugSymbols>true</DebugSymbols>
    <OutputPath>bin\x64\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <DebugType>full</DebugType>
    <PlatformTarget>x64</PlatformTarget>
    <LangVersion>7.3</LangVersion>
    <ErrorReport>prompt</ErrorReport>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
    <OutputPath>bin\x64\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <Optimize>true</Optimize>
    <DebugType>pdbonly</DebugType>
    <PlatformTarget>x64</PlatformTarget>
    <LangVersion>7.3</LangVersion>
    <ErrorReport>prompt</ErrorReport>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="DatabaseHelper">
      <HintPath>..\Libs\DatabaseHelper.dll</HintPath>
    </Reference>
    <Reference Include="log4net, Version=3.0.4.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\log4net.3.0.4\lib\net462\log4net.dll</HintPath>
    </Reference>
    <Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
    </Reference>
    <Reference Include="Panuon.WPF, Version=1.1.2.2, Culture=neutral, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\Panuon.WPF.1.1.3\lib\net472\Panuon.WPF.dll</HintPath>
    </Reference>
    <Reference Include="Panuon.WPF.UI, Version=1.3.0.2, Culture=neutral, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\Panuon.WPF.UI.1.3.0.2\lib\net472\Panuon.WPF.UI.dll</HintPath>
    </Reference>
    <Reference Include="System" />
    <Reference Include="System.Configuration" />
    <Reference Include="System.Data" />
    <Reference Include="System.Data.SQLite">
      <HintPath>..\Libs\System.Data.SQLite.dll</HintPath>
    </Reference>
    <Reference Include="System.Data.SQLite.EF6">
      <HintPath>..\Libs\System.Data.SQLite.EF6.dll</HintPath>
    </Reference>
    <Reference Include="System.Data.SQLite.Linq">
      <HintPath>..\Libs\System.Data.SQLite.Linq.dll</HintPath>
    </Reference>
    <Reference Include="System.Web" />
    <Reference Include="System.Windows.Forms" />
    <Reference Include="System.Xml" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Xaml">
      <RequiredTargetFramework>4.0</RequiredTargetFramework>
    </Reference>
    <Reference Include="WindowsBase" />
    <Reference Include="PresentationCore" />
    <Reference Include="PresentationFramework" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Entity\IconHolder.cs" />
    <Compile Include="Entity\IconMeta.cs" />
    <Compile Include="Entity\MenuData.cs" />
    <Compile Include="Entity\RoleData.cs" />
    <Compile Include="Entity\UserRoleRel.cs" />
    <Compile Include="Entity\RoleMenuRel.cs" />
    <Compile Include="Entity\UserData.cs" />
    <Compile Include="Modal\IconSelector.xaml.cs">
      <DependentUpon>IconSelector.xaml</DependentUpon>
    </Compile>
    <Compile Include="Modal\RoleModal.xaml.cs">
      <DependentUpon>RoleModal.xaml</DependentUpon>
    </Compile>
    <Compile Include="Model\RoleManageModel.cs" />
    <Compile Include="RoleManage.xaml.cs">
      <DependentUpon>RoleManage.xaml</DependentUpon>
    </Compile>
    <Compile Include="Modal\UserModal.xaml.cs">
      <DependentUpon>UserModal.xaml</DependentUpon>
    </Compile>
    <Compile Include="Model\UserManageModel.cs" />
    <Compile Include="UserManage.xaml.cs">
      <DependentUpon>UserManage.xaml</DependentUpon>
    </Compile>
    <Compile Include="Util\UserRoleRelHelper.cs" />
    <Compile Include="Util\RoleHelper.cs" />
    <Compile Include="Util\RoleMenuHelper.cs" />
    <Page Include="Modal\IconSelector.xaml">
      <Generator>MSBuild:Compile</Generator>
      <SubType>Designer</SubType>
    </Page>
    <Page Include="Modal\RoleModal.xaml">
      <Generator>MSBuild:Compile</Generator>
      <SubType>Designer</SubType>
    </Page>
    <Page Include="RoleManage.xaml">
      <Generator>MSBuild:Compile</Generator>
      <SubType>Designer</SubType>
    </Page>
    <Page Include="MenuManage.xaml">
      <Generator>MSBuild:Compile</Generator>
      <SubType>Designer</SubType>
    </Page>
    <Compile Include="MenuManage.xaml.cs">
      <DependentUpon>MenuManage.xaml</DependentUpon>
      <SubType>Code</SubType>
    </Compile>
    <Page Include="Modal\UserModal.xaml">
      <Generator>MSBuild:Compile</Generator>
      <SubType>Designer</SubType>
    </Page>
    <Page Include="Modal\MenuModal.xaml">
      <SubType>Designer</SubType>
      <Generator>MSBuild:Compile</Generator>
    </Page>
    <Page Include="UserManage.xaml">
      <SubType>Designer</SubType>
      <Generator>MSBuild:Compile</Generator>
    </Page>
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Modal\MenuModal.xaml.cs">
      <DependentUpon>MenuModal.xaml</DependentUpon>
    </Compile>
    <Compile Include="Model\MenuManageModel.cs" />
    <Compile Include="Properties\AssemblyInfo.cs">
      <SubType>Code</SubType>
    </Compile>
    <Compile Include="Properties\Resources.Designer.cs">
      <AutoGen>True</AutoGen>
      <DesignTime>True</DesignTime>
      <DependentUpon>Resources.resx</DependentUpon>
    </Compile>
    <Compile Include="Properties\Settings.Designer.cs">
      <AutoGen>True</AutoGen>
      <DependentUpon>Settings.settings</DependentUpon>
      <DesignTimeSharedInput>True</DesignTimeSharedInput>
    </Compile>
    <EmbeddedResource Include="Properties\Resources.resx">
      <Generator>ResXFileCodeGenerator</Generator>
      <LastGenOutput>Resources.Designer.cs</LastGenOutput>
    </EmbeddedResource>
    <None Include="packages.config" />
    <None Include="Properties\Settings.settings">
      <Generator>SettingsSingleFileGenerator</Generator>
      <LastGenOutput>Settings.Designer.cs</LastGenOutput>
    </None>
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\HMI.Base\HMI.Base.csproj">
      <Project>{2a6f9720-a890-4036-9d73-41a0dae28341}</Project>
      <Name>HMI.Base</Name>
    </ProjectReference>
    <ProjectReference Include="..\HMI.Core\HMI.Core.csproj">
      <Project>{3e3bf44a-0b69-45c5-9512-528cb48a7106}</Project>
      <Name>HMI.Core</Name>
    </ProjectReference>
    <ProjectReference Include="..\HMI.UI\HMI.UI.csproj">
      <Project>{f17de6b5-af1d-48c6-94a0-6b763a6546a6}</Project>
      <Name>HMI.UI</Name>
    </ProjectReference>
    <ProjectReference Include="..\HMI.Util\HMI.Util.csproj">
      <Project>{afea100a-9223-448d-ab10-3015d93dd98c}</Project>
      <Name>HMI.Util</Name>
    </ProjectReference>
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  <PropertyGroup>
    <PostBuildEvent>
    </PostBuildEvent>
  </PropertyGroup>
</Project>
HMI.MenuManage/MenuManage.xaml
New file
@@ -0,0 +1,146 @@
<hb:PluginControl x:Class="HMI.MenuManage.MenuManage"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:HMI.MenuManage"
             xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base"
             xmlns:uic="clr-namespace:HMI.UI.Converter;assembly=HMI.UI"
             xmlns:pw="https://opensource.panuon.com/wpf"
             xmlns:pu="https://opensource.panuon.com/wpf-ui"
             xmlns:dp="clr-namespace:HMI.UI;assembly=HMI.UI"
             xmlns:cc="clr-namespace:HMI.Core;assembly=HMI.Core"
             mc:Ignorable="d"
             Padding="10"
             Loaded="DataGrid_Loaded"
             Background="{DynamicResource WorkspaceBackground}"
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
    </UserControl.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="8*"/>
            <ColumnDefinition Width="2*"/>
        </Grid.ColumnDefinitions>
        <Border Style="{StaticResource CardBoarder}" Margin="0,0,2,0">
            <StackPanel>
                <DockPanel Margin="0,0,0,10">
                    <Button DockPanel.Dock="Right" Style="{StaticResource DelBtn}"
                            x:Name="deleteBatchBtn" Click="Button_Click"/>
                    <Button DockPanel.Dock="Right" Style="{StaticResource RfhBtn}"
                            Margin="0,0,10,0" x:Name="refreshBtn" Click="Button_Click"/>
                    <Button DockPanel.Dock="Right" Style="{StaticResource AddBtn}"
                            Margin="0,0,10,0" x:Name="addBtn" Click="Button_Click"/>
                    <TextBlock DockPanel.Dock="Left" FontSize="{StaticResource TitleFontSize}"
                           VerticalAlignment="Center" HorizontalAlignment="Left"
                               Text="{DynamicResource HMI.Menu.Title}"/>
                </DockPanel>
                <DataGrid Grid.Column="1" x:Name="table" Style="{StaticResource DefaultDataGrid}"
                          ItemsSource="{Binding DataSource}">
                    <DataGrid.Columns>
                        <DataGridTemplateColumn Header="" Width="Auto" CanUserResize="False">
                            <DataGridTemplateColumn.CellTemplate>
                                <DataTemplate>
                                    <CheckBox VerticalAlignment="Center" IsChecked="{Binding Selected,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
                                </DataTemplate>
                            </DataGridTemplateColumn.CellTemplate>
                        </DataGridTemplateColumn>
                        <DataGridTemplateColumn Header="#" CanUserResize="False">
                            <DataGridTemplateColumn.CellTemplate>
                                <DataTemplate>
                                    <TextBlock Text="{Binding Converter={StaticResource rowNumber},
                    RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DataGridRow},AncestorLevel=1}}"/>
                                </DataTemplate>
                            </DataGridTemplateColumn.CellTemplate>
                        </DataGridTemplateColumn>
                        <DataGridTextColumn Binding="{Binding Code}">
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource HMI.Menu.Code}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                        <DataGridTextColumn Binding="{Binding Name}">
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource HMI.Menu.Name}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                        <DataGridTextColumn Binding="{Binding Level}">
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource HMI.Menu.Level}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                        <DataGridTextColumn Binding="{Binding Type}">
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource HMI.Menu.Type}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                        <!--<DataGridTextColumn Binding="{Binding ParentCode}" Header="{DynamicResource HMI.Menu.ParentCode}"/>-->
                        <DataGridTextColumn Binding="{Binding ParentName}">
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource HMI.Menu.ParentName}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                        <DataGridTextColumn Width="*" Binding="{Binding DllPath}">
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource HMI.Menu.DllPath}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                        <DataGridTextColumn Width="*" Binding="{Binding ClassName}">
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource HMI.Menu.ClassName}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                        <DataGridTextColumn Width="50" Binding="{Binding Sort}">
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource HMI.Menu.Sort}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                        <DataGridTemplateColumn Width="110" IsReadOnly="True" CanUserResize="False">
                            <DataGridTemplateColumn.Header>
                                <TextBlock Text="{DynamicResource App.Opt}"/>
                            </DataGridTemplateColumn.Header>
                            <DataGridTemplateColumn.CellTemplate>
                                <DataTemplate>
                                    <StackPanel Orientation="Horizontal">
                                        <Button x:Name="editBtn" Style="{StaticResource EditLink}"
                            Click="Button_Click" Margin="0,0,10,0"
                            DataContext="{Binding }"/>
                                        <Button x:Name="delBtn" Style="{StaticResource DeleteLink}"
                            DataContext="{Binding }"
                            Click="Button_Click"/>
                                    </StackPanel>
                                </DataTemplate>
                            </DataGridTemplateColumn.CellTemplate>
                        </DataGridTemplateColumn>
                    </DataGrid.Columns>
                </DataGrid>
            </StackPanel>
        </Border>
        <Border Grid.Column="1" Style="{StaticResource CardBoarder}" Margin="2,0,0,0">
            <StackPanel>
                <DockPanel Margin="0,0,0,10" >
                    <TextBlock DockPanel.Dock="Left" FontSize="{StaticResource TitleFontSize}"
                   VerticalAlignment="Center" HorizontalAlignment="Left" Text="{DynamicResource HMI.Menu.TreeTitle}"/>
                </DockPanel>
                <TreeView x:Name="treeViewer" Style="{StaticResource DefaultTreeView}" ItemsSource="{Binding TreeDataSource}"
                          SelectedItemChanged="treeViewer_SelectedItemChanged">
                    <TreeView.ItemTemplate>
                        <HierarchicalDataTemplate ItemsSource="{Binding Children}">
                            <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
                                <Image Width="{StaticResource ContentFontSize}" Height="{StaticResource ContentFontSize}" Margin="5"
                                       Source="{Binding IconPath,Converter={StaticResource path2Image},Mode=OneWay}"/>
                                <TextBlock VerticalAlignment="Center" FontSize="{StaticResource ContentFontSize}" Text="{Binding Name}"/>
                            </StackPanel>
                        </HierarchicalDataTemplate>
                    </TreeView.ItemTemplate>
                </TreeView>
            </StackPanel>
        </Border>
    </Grid>
</hb:PluginControl>
HMI.MenuManage/MenuManage.xaml.cs
New file
@@ -0,0 +1,270 @@
using HMI.Base;
using HMI.Core.Util;
using HMI.MenuManage.Entity;
using HMI.MenuManage.Model;
using HMI.UI.Util;
using log4net;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace HMI.MenuManage
{
    /// <summary>
    /// UserControl1.xaml 的交互逻辑
    /// </summary>
    public partial class MenuManage : PluginControl
    {
        #region 属性
        public SqliteHelper sqliteHelper;
        public int DataSourceCount { get => DataSource?.Count ?? 0; }
        private ObservableCollection<MenuData> _DataSource;
        public ObservableCollection<MenuData> DataSource
        {
            get => _DataSource;
            set
            {
                Set(ref _DataSource, value);
                Set(nameof(DataSourceCount));
            }
        }
        private ObservableCollection<MenuData> _TreeDataSource;
        public ObservableCollection<MenuData> TreeDataSource
        {
            get => _TreeDataSource;
            set { Set(ref _TreeDataSource, value); }
        }
        #endregion
        public static ILog PluginLog { get; private set; }
        public MenuManage()
        {
            this.DataContext = this;
            InitializeComponent();
            if (!File.Exists(PathHelper.DatabasePath))
            {
                SqliteHelper.CreateDatabase(PathHelper.DatabasePath);
            }
            sqliteHelper = new SqliteHelper(PathHelper.DatabasePath);
            sqliteHelper.ExecuteSql(@"CREATE TABLE IF NOT EXISTS menu (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    code TEXT NOT NULL,
                    name TEXT NOT NULL,
                    icon TEXT,
                    iconPath TEXT,
                    level TEXT,
                    type TEXT,
                    parentCode TEXT,
                    dllPath TEXT,
                    className TEXT,
                    sort REAL,
                    UNIQUE(code)
                    );");
        }
        public override void SetLogger(ILog logger)
        {
            base.SetLogger(logger);
            PluginLog = logger;
        }
        public void LoadData()
        {
            ObservableCollection<MenuData> data = new ObservableCollection<MenuData>();
            ObservableCollection<MenuData> treeData = new ObservableCollection<MenuData>();
            DataTable dt = sqliteHelper.GetDataTable("SELECT m1.*,m2.name AS parentName FROM menu AS m1 LEFT JOIN menu AS m2 ON m1.parentCode = m2.code Order By IIF(m1.level=1,m1.sort,m2.sort),IIF(m1.level=1,0,m1.sort),m1.id;");
            Dictionary<string, MenuData> cache = new Dictionary<string, MenuData>();
            if (dt != null)
            {
                foreach (DataRow dr in dt.Rows)
                {
                    var menu = new MenuData
                    {
                        Id = dr["id"].ToString(),
                        Code = dr["code"].ToString(),
                        Name = dr["name"].ToString(),
                        Icon = dr["icon"].ToString(),
                        Level = dr["level"].ToString(),
                        Type = dr["type"].ToString(),
                        ParentCode = dr["parentCode"].ToString(),
                        ParentName = dr["parentName"].ToString(),
                        DllPath = dr["dllPath"].ToString(),
                        ClassName = dr["className"].ToString(),
                        Sort = dr["sort"] == DBNull.Value ? 0 : double.Parse(dr["sort"].ToString()),
                    };
                    data.Add(menu);
                    if (menu.Level == "1")
                    {
                        treeData.Add(menu);
                    }
                    cache.Add(menu.Code, menu);
                }
                foreach (var menu in data)
                {
                    if (!string.IsNullOrEmpty(menu.ParentCode) && menu.ParentCode != menu.Code)
                    {
                        cache[menu.ParentCode].Children.Add(menu);
                    }
                }
            }
            TreeDataSource = treeData;
            DataSource = data;
        }
        public void DeleteMenuById(string id)
        {
            string sql = $"DELETE FROM menu WHERE id = {id};";
            sqliteHelper.ExecuteSql(sql);
        }
        public void DeleteMenuByIds(IEnumerable<string> ids)
        {
            List<string> sqls = ids.Select(id => $"DELETE FROM menu WHERE id = {id};")
                .ToList();
            sqliteHelper.ExecuteBatchSql(sqls);
        }
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var btn = ((Button)sender);
            if (btn.Name == "addBtn")
            {
                try
                {
                    MenuModal window = new MenuModal
                    {
                        Title = $"{LangUtil.Get("HMI.Menu.Add")}",
                        sqliteHelper = sqliteHelper,
                        Owner = Window.GetWindow(this)
                    };
                    if (true == window.Open(new MenuData()))
                    {
                        MenuData data = window.Data;
                        LoadData();
                    }
                }
                catch (Exception ex)
                {
                    log.Error("Add Menu Failed.", ex);
                    ToastHelper.Error(Window.GetWindow(this), $"{LangUtil.Get("App.AddFailed")}: {ex.Message}");
                }
            }
            else if (btn.Name == "refreshBtn")
            {
                try
                {
                    LoadData();
                }
                catch (Exception ex)
                {
                    log.Error("Refresh Menu List Failed.", ex);
                    ToastHelper.Error(Window.GetWindow(this), $"{LangUtil.Get("App.RefreshFailed")}: {ex.Message}");
                }
            }
            else if (btn.Name == "deleteBatchBtn")
            {
                try
                {
                    List<string> ids = DataSource.Where(x => x.Selected).Select(x => x.Id).ToList();
                    if (ids == null || ids.Count <= 0)
                    {
                        ToastHelper.Warning(Window.GetWindow(this), LangUtil.Get("HMI.NoDeleteData"));
                        return;
                    }
                    var res = MsgBoxHelper.WarnMessage(Window.GetWindow(this),
                        string.Format(LangUtil.Get("HMI.MakeSureDeletes"), ids.Count));
                    if (res == MessageBoxResult.OK)
                    {
                        DeleteMenuByIds(ids);
                        LoadData();
                    }
                }
                catch (Exception ex)
                {
                    log.Error("Delete Menu Batch Failed.", ex);
                    ToastHelper.Error(Window.GetWindow(this), $"{LangUtil.Get("App.DeleteFailed")}: {ex.Message}");
                }
            }
            else if (btn.Name == "editBtn")
            {
                try
                {
                    var data = (MenuData)btn.DataContext;
                    MenuModal window = new MenuModal
                    {
                        Title = $"{LangUtil.Get("HMI.Menu.Edit")}",
                        sqliteHelper = sqliteHelper,
                        Owner = Window.GetWindow(this)
                    };
                    if (true == window.Open(data))
                    {
                        var newData = window.Data;
                        LoadData();
                    }
                }
                catch (Exception ex)
                {
                    log.Error("Edit Menu Failed.", ex);
                    ToastHelper.Error(Window.GetWindow(this), $"{LangUtil.Get("HMI.EditFailed")}: {ex.Message}");
                }
            }
            else if (btn.Name == "delBtn")
            {
                try
                {
                    var data = (MenuData)((Button)sender).DataContext;
                    var res = MsgBoxHelper.WarnMessage(Window.GetWindow(this),
                        string.Format(LangUtil.Get("HMI.MakeSureDeleteSingle"), data.Name));
                    if (res == MessageBoxResult.OK)
                    {
                        DeleteMenuById(data.Id);
                        LoadData();
                    }
                }
                catch (Exception ex)
                {
                    log.Error("Delete Menu Failed.", ex);
                    ToastHelper.Error(Window.GetWindow(this), $"{LangUtil.Get("HMI.DeleteFailed")}: {ex.Message}");
                }
            }
        }
        public override void DoClose(object sender, EventArgs e)
        {
            try
            {
                sqliteHelper?.Close();
            }
            catch (Exception ex)
            {
                log.Warn("Do Close Failed.", ex);
            }
        }
        private void DataGrid_Loaded(object sender, RoutedEventArgs e)
        {
            LoadData();
            Loaded -= DataGrid_Loaded;
        }
        private void treeViewer_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
        {
            table.SelectedItem = e.NewValue;
        }
    }
}
HMI.MenuManage/Modal/IconSelector.xaml
New file
@@ -0,0 +1,46 @@
<pu:WindowX x:Class="HMI.MenuManage.IconSelector"
            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:local="clr-namespace:HMI.MenuManage"
            xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
            xmlns:pw="https://opensource.panuon.com/wpf"
            xmlns:pu="https://opensource.panuon.com/wpf-ui"
            mc:Ignorable="d"
            Title="Form"
            SizeToContent="Height"
            Width="600"
            Style="{StaticResource ModalWindow}"
            WindowStartupLocation="CenterOwner"
            Foreground="Black">
    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/HMI.UI;component/Resources/Font/Font.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>
    <Grid>
        <ListBox Margin="10" x:Name="list"
                 FontSize="50"
                 SelectedValue="{Binding Data}"
                 ScrollViewer.HorizontalScrollBarVisibility="Disabled">
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel HorizontalAlignment="Center" Orientation="Horizontal" IsItemsHost="True">
                    </WrapPanel>
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Label FontFamily="{StaticResource iconfont}" Content="{Binding }"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</pu:WindowX>
HMI.MenuManage/Modal/IconSelector.xaml.cs
New file
@@ -0,0 +1,72 @@
using HMI.MenuManage.Entity;
using Newtonsoft.Json;
using Panuon.WPF.UI;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.IO;
using System.Linq;
using System.Reflection;
namespace HMI.MenuManage
{
    /// <summary>
    /// ModalWindow.xaml 的交互逻辑
    /// </summary>
    public partial class IconSelector : WindowX
    {
        private static readonly List<IconMeta> icons;
        private string data;
        public string Data
        {
            get => data;
            set { Set(ref data, value); }
        }
        static IconSelector()
        {
            try
            {
                Assembly assembly = Assembly.Load("HMI.UI");
                var serializer = new JsonSerializer();
                var sb = assembly.GetManifestResourceNames();
                using (Stream stream = assembly.GetManifestResourceStream("HMI.UI.Resources.Font.iconfont.json"))
                using (StreamReader streamReader = new StreamReader(stream))
                using (JsonTextReader jsonTextReader = new JsonTextReader(streamReader))
                {
                    IconHolder holder = serializer.Deserialize<IconHolder>(jsonTextReader);
                    icons = holder?.glyphs;
                }
            }
            catch (Exception ex)
            {
            }
        }
        public IconSelector()
        {
            DataContext = this;
            InitializeComponent();
        }
        private bool _isProcessing;
        protected override void OnClosing(CancelEventArgs e)
        {
            base.OnClosing(e);
        }
        public bool? Open(string currentValue)
        {
            list.ItemsSource = icons?.Select(i => i.Code).ToList();
            Data = currentValue;
            return this.ShowDialog();
        }
    }
}
HMI.MenuManage/Modal/MenuModal.xaml
New file
@@ -0,0 +1,107 @@
<pu:WindowX x:Class="HMI.MenuManage.MenuModal"
            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:local="clr-namespace:HMI.MenuManage"
            xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
            xmlns:pw="https://opensource.panuon.com/wpf"
            xmlns:pu="https://opensource.panuon.com/wpf-ui"
            mc:Ignorable="d"
            Title="Form"
            SizeToContent="Height"
            Width="600"
            Style="{StaticResource ModalWindow}"
            WindowStartupLocation="CenterOwner"
            Foreground="Black"
            >
    <Grid Margin="10">
        <StackPanel x:Name="formPanel">
            <pu:FormGroup x:Name="FgCode" Style="{StaticResource InputFormGroupStyle}"
                          dp:FormGroupHelper.ValidateMethod="ValidateCode"
                  Header="{DynamicResource HMI.Menu.Code}"
                  dp:FormGroupHelper.IsRequired="True" GroupName="MyFormGroup">
                <TextBox x:Name="TbCode" Style="{StaticResource EditorTextBoxStyle}" />
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgName" Style="{StaticResource InputFormGroupStyle}"
                          Header="{DynamicResource HMI.Menu.Name}" GroupName="MyFormGroup"
                          dp:FormGroupHelper.IsRequired="True"
dp:FormGroupHelper.ValidateMethod="ValidateName">
                <TextBox x:Name="TbName" Style="{StaticResource EditorTextBoxStyle}" />
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgIcon" Style="{StaticResource InputFormGroupStyle}"
                          Header="{DynamicResource HMI.Menu.Icon}" GroupName="MyFormGroup">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition />
                        <ColumnDefinition Width="30" />
                    </Grid.ColumnDefinitions>
                    <TextBox x:Name="TbIcon" IsReadOnly="True" FontFamily="{StaticResource iconfont}" Style="{StaticResource EditorTextBoxStyle}" />
                    <Button Grid.Column="1" BorderThickness="0,1,1,1" Style="{StaticResource SearchButton}"
                            Click="IconSelect_Click"/>
                </Grid>
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgLevel" Style="{StaticResource InputFormGroupStyle}"
                          Header="{DynamicResource HMI.Menu.Level}"
                          dp:FormGroupHelper.IsRequired="True" GroupName="MyFormGroup">
                <ComboBox x:Name="TbLevel" Style="{StaticResource FormComboxStyle}"
                          SelectionChanged="TbLevel_SelectionChanged">
                    <ComboBoxItem>1</ComboBoxItem>
                    <ComboBoxItem>2</ComboBoxItem>
                </ComboBox>
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgType"
  Style="{StaticResource InputFormGroupStyle}" Header="{DynamicResource HMI.Menu.Type}" GroupName="MyFormGroup">
                <ComboBox x:Name="TbType" Style="{StaticResource FormComboxStyle}">
                    <ComboBoxItem>Permission</ComboBoxItem>
                    <ComboBoxItem>WPF</ComboBoxItem>
                    <ComboBoxItem>WinForm</ComboBoxItem>
                    <ComboBoxItem>LV</ComboBoxItem>
                </ComboBox>
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgParentCode" Style="{StaticResource InputFormGroupStyle}"
                          Header="{DynamicResource HMI.Menu.ParentName}" GroupName="MyFormGroup">
                <!--<Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition />
                        <ColumnDefinition Width="30" />
                    </Grid.ColumnDefinitions>
                    <TextBox x:Name="TbParentCode" Style="{StaticResource EditorTextBoxStyle}" />
                    <Button Grid.Column="1" Style="{StaticResource SearchButton}"/>
                </Grid>-->
                <ComboBox x:Name="TbParentCode" Style="{StaticResource FormComboxStyle}"
                          DisplayMemberPath="Name"
                          SelectedValuePath="Code"
                          ItemsSource="{Binding ParentSource}"
                          ></ComboBox>
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgDllPath" Style="{StaticResource InputFormGroupStyle}"
                          Header="{DynamicResource HMI.Menu.DllPath}" GroupName="MyFormGroup">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition />
                        <ColumnDefinition Width="30" />
                    </Grid.ColumnDefinitions>
                    <TextBox x:Name="TbDllPath" IsReadOnly="True" Style="{StaticResource EditorTextBoxStyle}" />
                    <Button Grid.Column="1" Click="DllSelect_Click" Style="{StaticResource SearchButton}"/>
                </Grid>
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgClassName" Style="{StaticResource InputFormGroupStyle}"
                          Header="{DynamicResource HMI.Menu.ClassName}" GroupName="MyFormGroup">
                <TextBox x:Name="TbClassName" Style="{StaticResource EditorTextBoxStyle}" />
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgSort" Style="{StaticResource InputFormGroupStyle}"
              Header="{DynamicResource HMI.Menu.Sort}" GroupName="MyFormGroup">
                <pu:NumberInput x:Name="NiSort" Style="{StaticResource FormNumberInput}" />
            </pu:FormGroup>
        </StackPanel>
    </Grid>
</pu:WindowX>
HMI.MenuManage/Modal/MenuModal.xaml.cs
New file
@@ -0,0 +1,271 @@
using HMI.Core.Annotation;
using HMI.Core.Util;
using HMI.MenuManage.Entity;
using HMI.UI.DpUtil;
using HMI.UI.Util;
using Microsoft.Win32;
using Panuon.WPF.UI;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
namespace HMI.MenuManage
{
    /// <summary>
    /// ModalWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MenuModal : WindowX
    {
        public MenuModal()
        {
            DataContext = this;
            InitializeComponent();
        }
        public MenuData Data { get; set; }
        public SqliteHelper sqliteHelper { get; set; }
        private bool _isProcessing;
        private ObservableCollection<MenuData> parentSource;
        public ObservableCollection<MenuData> ParentSource
        {
            get { return parentSource; }
            set { Set(ref parentSource, value); }
        }
        protected override void OnClosing(CancelEventArgs e)
        {
            //Press Continue Button
            if (DialogResult == true)
            {
                if (_isProcessing)
                {
                    e.Cancel = true;
                    return;
                }
                var okButton = WindowXModalDialog.GetOKButton(this);
                try
                {
                    _isProcessing = true;
                    ButtonHelper.SetIsPending(okButton, true);
                    if (!ValidateForm())
                    {
                        e.Cancel = true;
                        return;
                    }
                    SaveOrUpdateData();
                }
                catch (Exception ex)
                {
                    MenuManage.PluginLog.Error("Modal Commit Error.", ex);
                    ToastHelper.Error(this, ex.Message);
                    e.Cancel = true;
                    return;
                }
                finally
                {
                    _isProcessing = false;
                    ButtonHelper.SetIsPending(okButton, false);
                }
            }
            base.OnClosing(e);
        }
        public bool ValidateCode(object value, out string err)
        {
            err = null;
            string segment = string.IsNullOrEmpty(Data.Id) ? "" : $" AND id!='{Data.Id}'";
            string sql = $"SELECT 1 FROM menu WHERE code='{TbCode.Text}'{segment};";
            if (sqliteHelper.HasData(sql))
            {
                err = this.lang("App.MustBeUnique", LangUtil.Get("HMI.Menu.Code")); ;
                return false;
            }
            return true;
        }
        public bool ValidateName(object value, out string err)
        {
            err = null;
            string segment = string.IsNullOrEmpty(Data.Id) ? "" : $" AND id!='{Data.Id}'";
            string sql = $"SELECT 1 FROM menu WHERE name='{TbName.Text}'{segment};";
            if (sqliteHelper.HasData(sql))
            {
                err = string.Format(LangUtil.Get("App.MustBeUnique"), LangUtil.Get("HMI.Menu.Name"));
                return false;
            }
            return true;
        }
        private bool ValidateForm()
        {
            //PreValidate();
            bool flag = FormGroupHelper.ValidateForm(formPanel);
            if (flag)
            {
                Data.Code = TbCode.Text;
                Data.Name = TbName.Text;
                Data.Icon = TbIcon.Text;
                Data.Level = TbLevel.Text;
                Data.Type = TbType.Text;
                Data.ParentCode = TbParentCode.SelectedValue?.ToString();
                Data.DllPath = TbDllPath.Text;
                Data.ClassName = TbClassName.Text;
                Data.Sort = NiSort.Value;
            }
            return flag;
        }
        private void RefreshParentCode()
        {
            ObservableCollection<MenuData> datas = new ObservableCollection<MenuData>();
            string sql = $"SELECT code,name FROM menu WHERE level = 1 ORDER By id;";
            DataTable dt = sqliteHelper.GetDataTable(sql);
            if (dt != null)
            {
                foreach (DataRow dr in dt.Rows)
                {
                    var menu = new MenuData
                    {
                        Code = dr["code"].ToString(),
                        Name = dr["name"].ToString(),
                    };
                    datas.Add(menu);
                }
            }
            ParentSource = datas;
        }
        public bool? Open(MenuData data)
        {
            RefreshParentCode();
            Data = data;
            TbCode.Text = Data.Code;
            TbName.Text = Data.Name;
            TbIcon.Text = Data.Icon;
            TbLevel.SelectedValue = Data.Level;
            TbType.SelectedValue = Data.Type;
            TbParentCode.SelectedValue = Data.ParentCode;
            TbDllPath.Text = Data.DllPath;
            TbClassName.Text = Data.ClassName;
            NiSort.Value = Data.Sort;
            return this.ShowDialog();
        }
        private void SaveOrUpdateData()
        {
            if (string.IsNullOrEmpty(Data.Id))
            {
                InsertMenu(Data);
            }
            else
            {
                UpdateMenu(Data);
            }
        }
        public void InsertMenu(MenuData data)
        {
            string sql = $@"INSERT INTO menu (code,name,icon,level,type,parentCode,dllPath,className,sort)
                VALUES
                ('{data.Code}','{data.Name}','{data.Icon}','{data.Level}','{data.Type}','{data.ParentCode}',
                '{data.DllPath}','{data.ClassName}',{data.Sort});";
            sqliteHelper.ExecuteSql(sql);
        }
        public void UpdateMenu(MenuData data)
        {
            string sql = $@"UPDATE menu SET code='{data.Code}',name='{data.Name}',
icon='{data.Icon}',level='{data.Level}',type='{data.Type}',parentCode='{data.ParentCode}',
dllPath='{data.DllPath}',className='{data.ClassName}',sort={data.Sort} WHERE id='{data.Id}';";
            sqliteHelper.ExecuteSql(sql);
        }
        private void IconSelect_Click(object sender, RoutedEventArgs e)
        {
            //string BasePath = PathHelper.IconFolder;
            //OpenFileDialog openFileDialog = new OpenFileDialog();
            //openFileDialog.Filter = "(*.*)|*.*";
            //openFileDialog.InitialDirectory = BasePath;
            //openFileDialog.Title = "选择图片";
            //openFileDialog.Multiselect = false;
            //if (openFileDialog.ShowDialog() == true)
            //{
            //    string FileName = openFileDialog.FileName;
            //    if (!FileName.StartsWith(BasePath))
            //    {
            //       (this, "请将图标移动至程序Icon目录下!", ToastPosition.Top);
            //    }
            //    else
            //    {
            //        TbIcon.Text = FileName.Replace(BasePath, ".");
            //    }
            //}
            IconSelector selector = new IconSelector
            {
                Title = LangUtil.Get("App.SelectIcon"),
                Owner = this
            };
            if (selector.Open(Data.Icon) == true)
            {
                TbIcon.Text = selector.Data;
            }
        }
        private void DllSelect_Click(object sender, RoutedEventArgs e)
        {
            string pluginPath = PathHelper.PluginFolder();
            OpenFileDialog openFileDialog = new OpenFileDialog();
            openFileDialog.Filter = "(*.dll)|*.dll";
            openFileDialog.InitialDirectory = pluginPath;
            openFileDialog.Title = LangUtil.Get("App.SelectFile");
            openFileDialog.Multiselect = false;
            if (openFileDialog.ShowDialog() == true)
            {
                string FileName = openFileDialog.FileName;
                if (!FileName.StartsWith(pluginPath))
                {
                    ToastHelper.Warning(this, LangUtil.Get("App.Msg.PluginMustInPluginFolder"));
                }
                else
                {
                    TbDllPath.Text = FileName.Replace(pluginPath, ".");
                    try
                    {
                        object[] infos = Assembly.LoadFrom(FileName).GetCustomAttributes(typeof(PluginClassNameAttribute), false);
                        if (infos != null && infos.Length > 0)
                        {
                            PluginClassNameAttribute attr = (PluginClassNameAttribute)infos[0];
                            TbClassName.Text = attr.ClassName;
                        }
                    }
                    catch (Exception ex)
                    {
                    }
                }
            }
        }
        private void TbLevel_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            bool flag = (((ComboBoxItem)e.AddedItems[0])?.Content?.ToString() == "2");
            FormGroupHelper.SetIsRequired(FgType, flag);
            FormGroupHelper.SetIsRequired(FgParentCode, flag);
            FormGroupHelper.SetIsRequired(FgDllPath, flag);
            FormGroupHelper.SetIsRequired(FgClassName, flag);
        }
    }
}
HMI.MenuManage/Modal/RoleModal.xaml
New file
@@ -0,0 +1,54 @@
<pu:WindowX x:Class="HMI.MenuManage.RoleModal"
            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:local="clr-namespace:HMI.MenuManage"
            xmlns:pw="https://opensource.panuon.com/wpf"
            xmlns:pu="https://opensource.panuon.com/wpf-ui"
            xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
            mc:Ignorable="d"
            Title="Form"
            Width="600"
            Height="800"
            WindowStartupLocation="CenterOwner"
            Style="{StaticResource ModalWindow}">
    <Grid Margin="10">
        <StackPanel x:Name="formPanel">
            <pu:FormGroup x:Name="FgCode" Style="{StaticResource InputFormGroupStyle}"
              Header="{DynamicResource HMI.Role.Code}"
                          dp:FormGroupHelper.IsRequired="True"
                          dp:FormGroupHelper.ValidateMethod="ValidateCode"
                          GroupName="MyFormGroup">
                <TextBox x:Name="TbCode" Style="{StaticResource EditorTextBoxStyle}" />
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgName" Style="{StaticResource InputFormGroupStyle}"
                              Header="{DynamicResource HMI.Role.Name}"
            dp:FormGroupHelper.IsRequired="True" GroupName="MyFormGroup">
                <TextBox x:Name="TbName" Style="{StaticResource EditorTextBoxStyle}" />
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgMenu" Orientation="Vertical" Style="{StaticResource RequiredInputFormGroupStyle}"
                              Header="{DynamicResource HMI.Role.Permission}" GroupName="MyFormGroup">
                <TreeView x:Name="menuTree" Grid.Column="1" Style="{StaticResource DefaultTreeView}"
                          MaxHeight="550"
                          ItemsSource="{Binding TreeDataSource}">
                    <TreeView.ItemTemplate>
                        <HierarchicalDataTemplate ItemsSource="{Binding Children}">
                            <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
                                <CheckBox VerticalAlignment="Center" IsChecked="{Binding Selected}"
Command="{Binding CheckCommand}"
CommandParameter="{Binding Path=IsChecked,RelativeSource={RelativeSource Mode=Self}}"/>
                                <Image Width="{StaticResource ContentFontSize}"
                                       Height="{StaticResource ContentFontSize}" Margin="5" Source="{Binding MenuIcon,Converter={StaticResource path2Image},Mode=OneWay}"/>
                                <TextBlock VerticalAlignment="Center" Text="{Binding MenuName}"/>
                            </StackPanel>
                        </HierarchicalDataTemplate>
                    </TreeView.ItemTemplate>
                </TreeView>
            </pu:FormGroup>
        </StackPanel>
    </Grid>
</pu:WindowX>
HMI.MenuManage/Modal/RoleModal.xaml.cs
New file
@@ -0,0 +1,135 @@
using HMI.Core.Util;
using HMI.MenuManage.Entity;
using HMI.MenuManage.Util;
using HMI.UI.DpUtil;
using HMI.UI.Util;
using Panuon.WPF.UI;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace HMI.MenuManage
{
    /// <summary>
    /// ModalWindow.xaml 的交互逻辑
    /// </summary>
    public partial class RoleModal : WindowX
    {
        public RoleModal()
        {
            this.DataContext = this;
            InitializeComponent();
        }
        public RoleData Data { get; set; }
        private ObservableCollection<RoleMenuRel> _TreeDataSource;
        public ObservableCollection<RoleMenuRel> TreeDataSource
        {
            get { return _TreeDataSource; }
            set { Set(ref _TreeDataSource, value); }
        }
        public SqliteHelper sqliteHelper { get; set; }
        private bool _isProcessing;
        protected override void OnClosing(CancelEventArgs e)
        {
            //Press Continue Button
            if (DialogResult == true)
            {
                if (_isProcessing)
                {
                    e.Cancel = true;
                    return;
                }
                var okButton = WindowXModalDialog.GetOKButton(this);
                try
                {
                    _isProcessing = true;
                    ButtonHelper.SetIsPending(okButton, true);
                    if (!ValidateForm())
                    {
                        e.Cancel = true;
                        return;
                    }
                    SaveOrUpdateData();
                }
                catch (Exception ex)
                {
                    ToastHelper.Error(this, $"Error!{ex.Message}");
                    e.Cancel = true;
                    return;
                }
                finally
                {
                    _isProcessing = false;
                    ButtonHelper.SetIsPending(okButton, false);
                }
            }
            base.OnClosing(e);
        }
        private bool ValidateCode(object value, out string err)
        {
            err = null;
            string segment = string.IsNullOrEmpty(Data.Id) ? "" : $" AND id!='{Data.Id}'";
            string sql = $"SELECT 1 FROM user WHERE username='{TbCode.Text}'{segment};";
            if (sqliteHelper.HasData(sql))
            {
                err = this.lang("App.MustUnique", this.lang("HMI.Role.Code"));
                return false;
            }
            return true;
        }
        private bool ValidateForm()
        {
            bool flag = FormGroupHelper.ValidateForm(formPanel);
            if (flag)
            {
                Data.Code = TbCode.Text;
                Data.Name = TbName.Text;
            }
            return flag;
        }
        public bool? Open(RoleData data)
        {
            Data = data;
            TbCode.Text = Data.Code;
            TbName.Text = Data.Name;
            TreeDataSource = RoleMenuHelper.SelectRoleMenuTreeData(sqliteHelper, Data.Code);
            return this.ShowDialog();
        }
        private void SaveOrUpdateData()
        {
            if (string.IsNullOrEmpty(Data.Id))
            {
                InsertData(Data);
            }
            else
            {
                UpdateData(Data);
            }
        }
        public void InsertData(RoleData data)
        {
            string sql = $@"INSERT INTO role (code,name)
                VALUES
                ('{data.Code}','{data.Name}');";
            RoleMenuHelper.SaveRelData(sqliteHelper, TreeDataSource, data.Code, sql, null);
        }
        public void UpdateData(RoleData data)
        {
            string sql = $@"UPDATE role SET code='{data.Code}',name='{data.Name}' WHERE id='{data.Id}';";
            RoleMenuHelper.SaveRelData(sqliteHelper, TreeDataSource, data.Code, sql, null);
        }
    }
}
HMI.MenuManage/Modal/UserModal.xaml
New file
@@ -0,0 +1,83 @@
<pu:WindowX x:Class="HMI.MenuManage.UserModal"
            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:local="clr-namespace:HMI.MenuManage"
            xmlns:pw="https://opensource.panuon.com/wpf"
            xmlns:pu="https://opensource.panuon.com/wpf-ui"
            xmlns:uic="clr-namespace:HMI.UI.Converter;assembly=HMI.UI"
            xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
            mc:Ignorable="d"
            Title="Form"
            SizeToContent="Height"
            Width="600"
            WindowStartupLocation="CenterOwner"
            Style="{StaticResource ModalWindow}">
    <Grid Margin="10">
        <StackPanel x:Name="formPanel">
            <pu:FormGroup x:Name="FgCode" Style="{StaticResource InputFormGroupStyle}"
                          dp:FormGroupHelper.ValidateMethod="ValidateCode"
              Header="{DynamicResource HMI.User.Code}"
              dp:FormGroupHelper.IsRequired="True" GroupName="MyFormGroup">
                <TextBox x:Name="TbCode" Style="{StaticResource EditorTextBoxStyle}" />
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgName" Style="{StaticResource InputFormGroupStyle}"
                          Header="{DynamicResource HMI.User.RealName}"
                          dp:FormGroupHelper.IsRequired="True" GroupName="MyFormGroup">
                <TextBox x:Name="TbName" Style="{StaticResource EditorTextBoxStyle}" />
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgUsername" Style="{StaticResource InputFormGroupStyle}"
                         dp:FormGroupHelper.ValidateMethod="ValidateUsername"
                         Header="{DynamicResource HMI.User.Account}"
                         dp:FormGroupHelper.IsRequired="True" GroupName="MyFormGroup">
                <TextBox x:Name="TbUsername" Style="{StaticResource EditorTextBoxStyle}" />
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgPassword" Style="{StaticResource InputFormGroupStyle}"
                          Header="{DynamicResource HMI.User.Password}"
                          dp:FormGroupHelper.IsRequired="True" GroupName="MyFormGroup">
                <PasswordBox x:Name="TbPassword" pu:PasswordBoxHelper.PlainButtonVisibility="VisibleOnHover"
                             Style="{StaticResource SignInPasswordBoxStyle}" />
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgRoleCode" Orientation="Vertical"
                          Style="{StaticResource InputFormGroupStyle}"
                          Header="{DynamicResource HMI.Role}" GroupName="MyFormGroup">
                <DataGrid x:Name="table" Style="{StaticResource DefaultDataGrid}" SelectionMode="Single"
           ItemsSource="{Binding Roles}">
                    <DataGrid.Columns>
                        <DataGridTemplateColumn Width="Auto" CanUserResize="False">
                            <DataGridTemplateColumn.Header>
                                <TextBlock Text="{DynamicResource App.Granted}"/>
                            </DataGridTemplateColumn.Header>
                            <DataGridTemplateColumn.CellTemplate>
                                <DataTemplate>
                                    <CheckBox VerticalAlignment="Center"
                               HorizontalAlignment="Center" IsChecked="{Binding Selected,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
                                </DataTemplate>
                            </DataGridTemplateColumn.CellTemplate>
                        </DataGridTemplateColumn>
                        <DataGridTextColumn Header="#"
             Binding="{Binding Converter={StaticResource rowNumber},
                     RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DataGridRow},AncestorLevel=1}}"
                             CanUserResize="False"/>
                        <DataGridTextColumn Width="*" Binding="{Binding RoleCode}">
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource HMI.Role.Code}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                        <DataGridTextColumn Width="*" Binding="{Binding RoleName}">
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource HMI.Role.Name}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                    </DataGrid.Columns>
                </DataGrid>
            </pu:FormGroup>
        </StackPanel>
    </Grid>
</pu:WindowX>
HMI.MenuManage/Modal/UserModal.xaml.cs
New file
@@ -0,0 +1,153 @@
using HMI.Core.Util;
using HMI.MenuManage.Entity;
using HMI.MenuManage.Util;
using HMI.UI.DpUtil;
using HMI.UI.Util;
using Panuon.WPF.UI;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace HMI.MenuManage
{
    /// <summary>
    /// ModalWindow.xaml 的交互逻辑
    /// </summary>
    public partial class UserModal : WindowX
    {
        public UserModal()
        {
            this.DataContext = this;
            InitializeComponent();
        }
        public UserData Data { get; set; }
        private ObservableCollection<UserRoleRel> _Roles;
        public ObservableCollection<UserRoleRel> Roles
        {
            get => _Roles;
            set { Set(ref _Roles, value); }
        }
        public SqliteHelper sqliteHelper { get; set; }
        private bool _isProcessing;
        protected override void OnClosing(CancelEventArgs e)
        {
            //Press Continue Button
            if (DialogResult == true)
            {
                if (_isProcessing)
                {
                    e.Cancel = true;
                    return;
                }
                var okButton = WindowXModalDialog.GetOKButton(this);
                try
                {
                    _isProcessing = true;
                    ButtonHelper.SetIsPending(okButton, true);
                    if (!ValidateForm())
                    {
                        e.Cancel = true;
                        return;
                    }
                    SaveOrUpdateData();
                }
                catch (Exception ex)
                {
                    ToastHelper.Error(this, $"Error!{ex.Message}");
                    e.Cancel = true;
                    return;
                }
                finally
                {
                    _isProcessing = false;
                    ButtonHelper.SetIsPending(okButton, false);
                }
            }
            base.OnClosing(e);
        }
        private bool ValidateUsername(object value, out string err)
        {
            err = null;
            string segment = string.IsNullOrEmpty(Data.Id) ? "" : $" AND id!='{Data.Id}'";
            string sql = $"SELECT 1 FROM user WHERE username='{TbUsername.Text}'{segment};";
            if (sqliteHelper.HasData(sql))
            {
                err = string.Format(LangUtil.Get("App.MustBeUnique"), LangUtil.Get("HMI.User.Account"));
                return false;
            }
            return true;
        }
        private bool ValidateCode(object value, out string err)
        {
            err = null;
            string segment = string.IsNullOrEmpty(Data.Id) ? "" : $" AND id!='{Data.Id}'";
            string sql = $"SELECT 1 FROM user WHERE code='{TbCode.Text}'{segment};";
            if (sqliteHelper.HasData(sql))
            {
                err = string.Format(LangUtil.Get("App.MustBeUnique"), LangUtil.Get("HMI.User.Code"));
                return false;
            }
            return true;
        }
        private bool ValidateForm()
        {
            bool flag = FormGroupHelper.ValidateForm(formPanel);
            if (flag)
            {
                Data.Code = TbCode.Text;
                Data.Username = TbUsername.Text;
                Data.Name = TbName.Text;
                Data.Password = TbPassword.Password;
            }
            return flag;
        }
        public bool? Open(UserData data)
        {
            Data = data;
            TbCode.Text = Data.Code;
            TbUsername.Text = Data.Username;
            TbName.Text = Data.Name;
            TbPassword.Password = Data.Password;
            Roles = UserRoleRelHelper.SelectUserRoleData(sqliteHelper, Data.Username);
            return this.ShowDialog();
        }
        private void SaveOrUpdateData()
        {
            if (string.IsNullOrEmpty(Data.Id))
            {
                InsertData(Data);
            }
            else
            {
                UpdateData(Data);
            }
        }
        public void InsertData(UserData data)
        {
            string sql = $@"INSERT INTO user (code, username,password,name)
                VALUES
                ('{data.Code}', '{data.Username}','{data.Password}','{data.Name}');";
            UserRoleRelHelper.SaveUserRoleRel(sqliteHelper, Roles, data, sql, null);
        }
        public void UpdateData(UserData data)
        {
            string sql = $@"UPDATE user SET code='{data.Code}', username='{data.Username}',password='{data.Password}',name='{data.Name}'
WHERE id='{data.Id}';";
            UserRoleRelHelper.SaveUserRoleRel(sqliteHelper, Roles, data, sql, null);
        }
    }
}
HMI.MenuManage/Model/MenuManageModel.cs
New file
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data;
using System.IO;
using System.Linq;
using HMI.Core.Entity;
using HMI.Core.Util;
using HMI.MenuManage.Entity;
namespace HMI.MenuManage.Model
{
    public class MenuManageModel : NotifyBase
    {
    }
}
HMI.MenuManage/Model/RoleManageModel.cs
New file
@@ -0,0 +1,122 @@
using HMI.Core.Entity;
using HMI.Core.Util;
using HMI.MenuManage.Entity;
using HMI.MenuManage.Util;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data;
using System.IO;
namespace HMI.MenuManage.Model
{
    public class RoleManageModel : NotifyBase
    {
        public SqliteHelper sqliteHelper;
        private ObservableCollection<RoleData> _DataSource;
        public ObservableCollection<RoleData> DataSource
        {
            get { return _DataSource; }
            set { Set(ref _DataSource, value); }
        }
        private RoleData _SelectedValue;
        public RoleData SelectedValue
        {
            get => _SelectedValue;
            set
            {
                Set(ref _SelectedValue, value);
                LoadRelData(value);
            }
        }
        private ObservableCollection<RoleMenuRel> _TreeDataSource;
        public ObservableCollection<RoleMenuRel> TreeDataSource
        {
            get { return _TreeDataSource; }
            set { Set(ref _TreeDataSource, value); }
        }
        public RoleManageModel()
        {
            if (!File.Exists(PathHelper.DatabasePath))
            {
                SqliteHelper.CreateDatabase(PathHelper.DatabasePath);
            }
            sqliteHelper = new SqliteHelper(PathHelper.DatabasePath);
            sqliteHelper.ExecuteSql(@"CREATE TABLE IF NOT EXISTS role (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    code TEXT NOT NULL,
                    name TEXT,
                    UNIQUE(code)
                    );
CREATE TABLE IF NOT EXISTS role_menu_rel (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    roleCode TEXT NOT NULL,
                    menuCode TEXT NOT NULL,
                    UNIQUE(roleCode,menuCode)
                    );
");
        }
        public void LoadData()
        {
            ObservableCollection<RoleData> data = new ObservableCollection<RoleData>();
            DataTable dt = sqliteHelper.GetDataTable("SELECT * FROM role Order By id;");
            if (dt != null)
            {
                foreach (DataRow dr in dt.Rows)
                {
                    var menu = new RoleData
                    {
                        Id = dr["id"].ToString(),
                        Code = dr["code"].ToString(),
                        Name = dr["name"].ToString()
                    };
                    data.Add(menu);
                }
            }
            DataSource = data;
        }
        public void LoadRelData(RoleData data)
        {
            if (data == null)
            {
                TreeDataSource.Clear();
                return;
            }
            TreeDataSource = RoleMenuHelper.SelectRoleMenuTreeData(sqliteHelper, data.Code);
        }
        public void DelSingleData(RoleData data)
        {
            sqliteHelper.ExecuteBatchSql(new List<string>{
            $"DELETE FROM role WHERE id = '{data.Id}'",
            $"DELETE FROM role_menu_rel WHERE roleCode = '{data.Code}'",
            $"DELETE FROM user_role_rel WHERE roleCode = '{data.Code}'"
            });
        }
        public void DeleteRoleByCodes(IEnumerable<string> codes)
        {
            List<string> sqls = new List<string>();
            foreach (string code in codes)
            {
                sqls.Add($"DELETE FROM role WHERE code = '{code}';");
                sqls.Add($"DELETE FROM role_menu_rel WHERE roleCode = '{code}';");
                sqls.Add($"DELETE FROM user_role_rel WHERE roleCode = '{code}';");
            }
            sqliteHelper.ExecuteBatchSql(sqls);
        }
        public void SaveRelData(RoleData role)
        {
            if (role == null) return;
            RoleMenuHelper.SaveRelData(sqliteHelper, TreeDataSource, role.Code, null, null);
        }
    }
}
HMI.MenuManage/Model/UserManageModel.cs
New file
@@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data;
using System.IO;
using HMI.Core.Util;
using HMI.MenuManage.Entity;
using HMI.MenuManage.Util;
using HMI.Core.Entity;
namespace HMI.MenuManage.Model
{
    public class UserManageModel : NotifyBase
    {
        public SqliteHelper sqliteHelper;
        private ObservableCollection<UserData> _DataSource;
        public ObservableCollection<UserData> DataSource
        {
            get { return _DataSource; }
            set { Set(ref _DataSource, value); }
        }
        private UserData _SelectedUser;
        public UserData SelectedUser
        {
            get { return _SelectedUser; }
            set
            {
                Set(ref _SelectedUser, value);
                LoadUserRole();
            }
        }
        private ObservableCollection<UserRoleRel> _Roles;
        public ObservableCollection<UserRoleRel> Roles
        {
            get => _Roles;
            set { Set(ref _Roles, value); }
        }
        public UserManageModel()
        {
            if (!File.Exists(PathHelper.DatabasePath))
            {
                SqliteHelper.CreateDatabase(PathHelper.DatabasePath);
            }
            sqliteHelper = new SqliteHelper(PathHelper.DatabasePath);
            sqliteHelper.ExecuteSql(@"CREATE TABLE IF NOT EXISTS user (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    code TEXT NOT NULL,
                    username TEXT NOT NULL,
                    password TEXT NOT NULL,
                    name TEXT,
                    UNIQUE(username)
                    );
CREATE TABLE IF NOT EXISTS user_role_rel (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    username TEXT NOT NULL,
                    roleCode TEXT NOT NULL,
                    UNIQUE(username,roleCode)
                    );
");
        }
        public void ImportUser(List<UserData> users)
        {
            List<string> batch = new List<string>();
            foreach (var user in users)
            {
                string sql = $"SELECT 1 FROM user WHERE username='{user.Username}' OR code='{user.Code}' OR name='{user.Name}'";
                if (sqliteHelper.HasData(sql))
                {
                    throw new Exception($"User {user.Username} already exists.");
                }
                string codes = "''";
                string roleCodes = user.RoleCodes?.Trim();
                if (!string.IsNullOrEmpty(roleCodes))
                {
                    codes = $"'{roleCodes.Replace(",", "','")}'";
                }
                batch.Add($"INSERT INTO user (code,username,name,password) VALUES ('{user.Code}', '{user.Username}', '{user.Name}', '{user.Password}');");
                batch.Add($"INSERT INTO user_role_rel (username, roleCode) SELECT '{user.Username}', code FROM role WHERE code IN ({codes});");
            }
            sqliteHelper.ExecuteBatchSql(batch);
        }
        public void LoadData()
        {
            ObservableCollection<UserData> data = new ObservableCollection<UserData>();
            DataTable dt = sqliteHelper.GetDataTable("SELECT * FROM user Order By id;");
            if (dt != null)
            {
                foreach (DataRow dr in dt.Rows)
                {
                    var menu = new UserData
                    {
                        Id = dr["id"].ToString(),
                        Code = dr["code"].ToString(),
                        Username = dr["username"].ToString(),
                        Password = dr["password"].ToString(),
                        Name = dr["name"].ToString()
                    };
                    DataTable roles = sqliteHelper.GetDataTable($@"SELECT r.code AS code FROM user_role_rel AS rel
INNER JOIN role AS r ON rel.roleCode = r.code AND rel.username = '{menu.Username}' Order By r.code;");
                    List<string> codes = new List<string>();
                    foreach (DataRow row in roles.Rows)
                    {
                        if (row["code"] != DBNull.Value)
                        {
                            codes.Add(row["code"].ToString());
                        }
                    }
                    menu.RoleCodes = string.Join(",", codes);
                    data.Add(menu);
                }
            }
            DataSource = data;
        }
        public void LoadUserRole()
        {
            Roles = SelectedUser == null ? new ObservableCollection<UserRoleRel>()
                : UserRoleRelHelper.SelectUserRoleData(sqliteHelper, SelectedUser.Username);
        }
        public void SaveUserRole()
        {
            UserRoleRelHelper.SaveUserRoleRel(sqliteHelper, Roles, SelectedUser, null, null);
        }
        public void DeleteUserByUsername(string username)
        {
            sqliteHelper.ExecuteBatchSql(new List<string> {
            $"DELETE FROM user WHERE username = '{username}';",
            $"DELETE FROM user_role_rel WHERE username = '{username}';"
            });
        }
        public void DeleteUserByUsernames(IEnumerable<string> usernames)
        {
            List<string> sqls = new List<string>();
            foreach (string username in usernames)
            {
                sqls.Add($"DELETE FROM user WHERE username = '{username}';");
                sqls.Add($"DELETE FROM user_role_rel WHERE username = '{username}';");
            }
            sqliteHelper.ExecuteBatchSql(sqls);
        }
    }
}
HMI.MenuManage/Properties/AssemblyInfo.cs
New file
@@ -0,0 +1,52 @@
using HMI.Core.Annotation;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows;
// 有关程序集的一般信息由以下
// 控制。更改这些特性值可修改
// 与程序集关联的信息。
[assembly: AssemblyTitle("HMI.MenuManage")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Organization")]
[assembly: AssemblyProduct("HMI.MenuManage")]
[assembly: AssemblyCopyright("Copyright © Organization 2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 将 ComVisible 设置为 false 会使此程序集中的类型
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
//请将此类型的 ComVisible 特性设置为 true。
[assembly: ComVisible(false)]
//若要开始生成可本地化的应用程序,请设置
//.csproj 文件中的 <UICulture>CultureYouAreCodingWith</UICulture>
//在 <PropertyGroup> 中。例如,如果你使用的是美国英语。
//使用的是美国英语,请将 <UICulture> 设置为 en-US。  然后取消
//对以下 NeutralResourceLanguage 特性的注释。  更新
//以下行中的“en-US”以匹配项目文件中的 UICulture 设置。
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
[assembly:ThemeInfo(
    ResourceDictionaryLocation.None, //主题特定资源词典所处位置
                             //(未在页面中找到资源时使用,
                             //或应用程序资源字典中找到时使用)
    ResourceDictionaryLocation.SourceAssembly //常规资源词典所处位置
                                      //(未在页面中找到资源时使用,
                                      //、应用程序或任何主题专用资源字典中找到时使用)
)]
// 程序集的版本信息由下列四个值组成:
//
//      主版本
//      次版本
//      生成号
//      修订号
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: PluginClassName("HMI.MenuManage.MenuManage")]
HMI.MenuManage/Properties/Resources.Designer.cs
New file
@@ -0,0 +1,63 @@
//------------------------------------------------------------------------------
// <auto-generated>
//     此代码由工具生成。
//     运行时版本:4.0.30319.42000
//
//     对此文件的更改可能会导致不正确的行为,并且如果
//     重新生成代码,这些更改将会丢失。
// </auto-generated>
//------------------------------------------------------------------------------
namespace HMI.MenuManage.Properties {
    using System;
    /// <summary>
    ///   一个强类型的资源类,用于查找本地化的字符串等。
    /// </summary>
    // 此类是由 StronglyTypedResourceBuilder
    // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
    // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
    // (以 /str 作为命令选项),或重新生成 VS 项目。
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
    internal class Resources {
        private static global::System.Resources.ResourceManager resourceMan;
        private static global::System.Globalization.CultureInfo resourceCulture;
        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        internal Resources() {
        }
        /// <summary>
        ///   返回此类使用的缓存的 ResourceManager 实例。
        /// </summary>
        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
        internal static global::System.Resources.ResourceManager ResourceManager {
            get {
                if (object.ReferenceEquals(resourceMan, null)) {
                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("HMI.MenuManage.Properties.Resources", typeof(Resources).Assembly);
                    resourceMan = temp;
                }
                return resourceMan;
            }
        }
        /// <summary>
        ///   重写当前线程的 CurrentUICulture 属性,对
        ///   使用此强类型资源类的所有资源查找执行重写。
        /// </summary>
        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
        internal static global::System.Globalization.CultureInfo Culture {
            get {
                return resourceCulture;
            }
            set {
                resourceCulture = value;
            }
        }
    }
}
HMI.MenuManage/Properties/Resources.resx
New file
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
  <!--
    Microsoft ResX Schema
    Version 2.0
    The primary goals of this format is to allow a simple XML format
    that is mostly human readable. The generation and parsing of the
    various data types are done through the TypeConverter classes
    associated with the data types.
    Example:
    ... ado.net/XML headers & schema ...
    <resheader name="resmimetype">text/microsoft-resx</resheader>
    <resheader name="version">2.0</resheader>
    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
        <value>[base64 mime encoded serialized .NET Framework object]</value>
    </data>
    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
        <comment>This is a comment</comment>
    </data>
    There are any number of "resheader" rows that contain simple
    name/value pairs.
    Each data row contains a name, and value. The row also contains a
    type or mimetype. Type corresponds to a .NET class that support
    text/value conversion through the TypeConverter architecture.
    Classes that don't support this are serialized and stored with the
    mimetype set.
    The mimetype is used for serialized objects, and tells the
    ResXResourceReader how to depersist the object. This is currently not
    extensible. For a given mimetype the value must be set accordingly:
    Note - application/x-microsoft.net.object.binary.base64 is the format
    that the ResXResourceWriter will generate, however the reader can
    read any of the formats listed below.
    mimetype: application/x-microsoft.net.object.binary.base64
    value   : The object must be serialized with
            : System.Serialization.Formatters.Binary.BinaryFormatter
            : and then encoded with base64 encoding.
    mimetype: application/x-microsoft.net.object.soap.base64
    value   : The object must be serialized with
            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
            : and then encoded with base64 encoding.
    mimetype: application/x-microsoft.net.object.bytearray.base64
    value   : The object must be serialized into a byte array
            : using a System.ComponentModel.TypeConverter
            : and then encoded with base64 encoding.
    -->
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" />
              <xsd:attribute name="type" type="xsd:string" />
              <xsd:attribute name="mimetype" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string" />
              <xsd:attribute name="name" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" />
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
</root>
HMI.MenuManage/Properties/Settings.Designer.cs
New file
@@ -0,0 +1,26 @@
//------------------------------------------------------------------------------
// <auto-generated>
//     此代码由工具生成。
//     运行时版本:4.0.30319.42000
//
//     对此文件的更改可能会导致不正确的行为,并且如果
//     重新生成代码,这些更改将会丢失。
// </auto-generated>
//------------------------------------------------------------------------------
namespace HMI.MenuManage.Properties {
    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.12.0.0")]
    internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
        private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
        public static Settings Default {
            get {
                return defaultInstance;
            }
        }
    }
}
HMI.MenuManage/Properties/Settings.settings
New file
@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
  <Profiles>
    <Profile Name="(Default)" />
  </Profiles>
  <Settings />
</SettingsFile>
HMI.MenuManage/RoleManage.xaml
New file
@@ -0,0 +1,114 @@
<hb:PluginControl x:Class="HMI.MenuManage.RoleManage"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base"
             xmlns:local="clr-namespace:HMI.MenuManage"
             xmlns:dp="clr-namespace:HMI.UI;assembly=HMI.UI"
             xmlns:uic="clr-namespace:HMI.UI.Converter;assembly=HMI.UI"
             xmlns:pw="https://opensource.panuon.com/wpf"
             xmlns:pu="https://opensource.panuon.com/wpf-ui"
             mc:Ignorable="d"
             Padding="10"
             Background="#f0f2f5"
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
    </UserControl.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="6*"/>
            <ColumnDefinition Width="4*"/>
        </Grid.ColumnDefinitions>
        <Border Style="{StaticResource CardBoarder}" Margin="0,0,2,0">
            <StackPanel>
                <DockPanel Margin="0,0,0,10">
                    <Button DockPanel.Dock="Right" Style="{StaticResource DelBtn}"
                            x:Name="deleteBatchBtn" Click="Button_Click"/>
                    <Button DockPanel.Dock="Right" Style="{StaticResource RfhBtn}"
                            Margin="0,0,10,0" x:Name="refreshBtn" Click="Button_Click"/>
                    <Button DockPanel.Dock="Right" Style="{StaticResource AddBtn}"
                            Margin="0,0,10,0" x:Name="addBtn" Click="Button_Click"/>
                    <TextBlock DockPanel.Dock="Left" FontSize="{StaticResource TitleFontSize}"
                           VerticalAlignment="Center" HorizontalAlignment="Left" Text="{DynamicResource HMI.Role.Title}"/>
                </DockPanel>
                <DataGrid x:Name="table" Style="{StaticResource DefaultDataGrid}"
                      SelectedValue="{Binding SelectedValue}"
    ItemsSource="{Binding DataSource}">
                    <DataGrid.Columns>
                        <DataGridTemplateColumn Header="" Width="Auto" CanUserResize="False">
                            <DataGridTemplateColumn.CellTemplate>
                                <DataTemplate>
                                    <CheckBox VerticalAlignment="Center"
                                              HorizontalAlignment="Center" IsChecked="{Binding Selected,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
                                </DataTemplate>
                            </DataGridTemplateColumn.CellTemplate>
                        </DataGridTemplateColumn>
                        <DataGridTextColumn Header="#"
                            Binding="{Binding Converter={StaticResource rowNumber},
                                    RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DataGridRow},AncestorLevel=1}}"
                                            CanUserResize="False"/>
                        <DataGridTextColumn Width="*" Binding="{Binding Code}">
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource HMI.Role.Code}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                        <DataGridTextColumn Width="*" Binding="{Binding Name}">
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource HMI.Role.Name}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                        <DataGridTemplateColumn Width="110" IsReadOnly="True" CanUserResize="False">
                            <DataGridTemplateColumn.Header>
                                <TextBlock Text="{DynamicResource App.Opt}"/>
                            </DataGridTemplateColumn.Header>
                            <DataGridTemplateColumn.CellTemplate>
                                <DataTemplate>
                                    <StackPanel Orientation="Horizontal">
                                        <Button x:Name="editBtn" Style="{StaticResource EditLink}"
                                            Margin="0,0,10,0"
                                            Click="Button_Click"
                                            DataContext="{Binding }"/>
                                        <Button x:Name="delBtn" Style="{StaticResource DeleteLink}"
                                            DataContext="{Binding }"
                                            Click="Button_Click"/>
                                    </StackPanel>
                                </DataTemplate>
                            </DataGridTemplateColumn.CellTemplate>
                        </DataGridTemplateColumn>
                    </DataGrid.Columns>
                </DataGrid>
            </StackPanel>
        </Border>
        <Border Grid.Column="1" Style="{StaticResource CardBoarder}" Margin="2,0,0,0">
            <StackPanel>
                <DockPanel Margin="0,0,0,10" >
                    <Button DockPanel.Dock="Right" Style="{StaticResource SaveBtn}"
                            x:Name="saveMenuBtn" Click="Button_Click"/>
                    <TextBlock DockPanel.Dock="Left" FontSize="{StaticResource TitleFontSize}"
                           VerticalAlignment="Center" HorizontalAlignment="Left" Text="{DynamicResource HMI.Role.Permission}"/>
                </DockPanel>
                <TreeView x:Name="treeViewer" Grid.Column="1"
                          Style="{StaticResource DefaultTreeView}" ItemsSource="{Binding TreeDataSource}">
                    <TreeView.ItemTemplate>
                        <HierarchicalDataTemplate ItemsSource="{Binding Children}">
                            <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
                                <CheckBox VerticalAlignment="Center" IsChecked="{Binding Selected}"
Command="{Binding CheckCommand}"
CommandParameter="{Binding Path=IsChecked,RelativeSource={RelativeSource Mode=Self}}"
/>
                                <!--<Image Width="{StaticResource ContentFontSize}"
                                       Height="{StaticResource ContentFontSize}" Margin="5" Source="{Binding MenuIcon,Converter={StaticResource path2Image},Mode=OneWay}"/>-->
                                <TextBlock VerticalAlignment="Center" Text="{Binding MenuName}"/>
                            </StackPanel>
                        </HierarchicalDataTemplate>
                    </TreeView.ItemTemplate>
                </TreeView>
            </StackPanel>
        </Border>
    </Grid>
</hb:PluginControl>
HMI.MenuManage/RoleManage.xaml.cs
New file
@@ -0,0 +1,184 @@
using HMI.Base;
using HMI.Core.Util;
using HMI.MenuManage.Entity;
using HMI.MenuManage.Model;
using HMI.UI.Util;
using Panuon.WPF.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace HMI.MenuManage
{
    /// <summary>
    /// UserControl1.xaml 的交互逻辑
    /// </summary>
    public partial class RoleManage : PluginControl
    {
        #region 属性
        private RoleManageModel Model;
        #endregion
        public RoleManage()
        {
            Model = new RoleManageModel();
            this.DataContext = Model;
            this.Loaded += RoleManage_Loaded;
            InitializeComponent();
        }
        private void RoleManage_Loaded(object sender, RoutedEventArgs e)
        {
            Model.LoadData();
            this.Loaded -= RoleManage_Loaded;
        }
        public override void DoClose(object sender, EventArgs e)
        {
            try
            {
                Model?.sqliteHelper?.Close();
            }
            catch (Exception ex)
            {
                log.Warn("Do Close Failed.", ex);
            }
        }
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                Button btn = (Button)sender;
                if (btn.Name == "addBtn")
                {
                    try
                    {
                        RoleModal modal = new RoleModal
                        {
                            Title = this.lang("HMI.Role.Add"),
                            Owner = Window.GetWindow(this),
                            sqliteHelper = Model.sqliteHelper
                        };
                        if (modal.Open(new RoleData()) == true)
                        {
                            Model.LoadData();
                        }
                    }
                    catch (Exception ex)
                    {
                        log.Error("Add Role Failed.", ex);
                        ToastHelper.Error(Window.GetWindow(this), $"{LangUtil.Get("App.AddFailed")}: {ex.Message}");
                    }
                }
                else if (btn.Name == "refreshBtn")
                {
                    try
                    {
                        Model.LoadData();
                    }
                    catch (Exception ex)
                    {
                        log.Error("Refresh Role Failed.", ex);
                        ToastHelper.Error(Window.GetWindow(this), $"{LangUtil.Get("App.RefreshFailed")}: {ex.Message}");
                    }
                }
                else if (btn.Name == "menuBtn")
                {
                    try
                    {
                        Model.LoadRelData((RoleData)btn.DataContext);
                    }
                    catch (Exception ex)
                    {
                        log.Error("Load Role-Menu Failed.", ex);
                        ToastHelper.Error(Window.GetWindow(this), $"{LangUtil.Get("App.RefreshFailed")}: {ex.Message}");
                    }
                }
                else if (btn.Name == "editBtn")
                {
                    try
                    {
                        RoleModal modal = new RoleModal
                        {
                            Title = this.lang("HMI.Role.Edit"),
                            Owner = Window.GetWindow(this),
                            sqliteHelper = Model.sqliteHelper
                        };
                        if (modal.Open((RoleData)btn.DataContext) == true)
                        {
                            Model.LoadData();
                        }
                    }
                    catch (Exception ex)
                    {
                        log.Error("Edit Role Failed.", ex);
                        ToastHelper.Error(Window.GetWindow(this), $"{LangUtil.Get("App.EditFailed")}: {ex.Message}");
                    }
                }
                else if (btn.Name == "deleteBatchBtn")
                {
                    try
                    {
                        List<string> codes = Model.DataSource.Where(x => x.Selected).Select(x => x.Code).ToList();
                        if (codes == null || codes.Count <= 0)
                        {
                            ToastHelper.Warning(Window.GetWindow(this), this.lang("HMI.NoDeleteData"));
                            return;
                        }
                        var res = MsgBoxHelper.WarnMessage(Window.GetWindow(this),
                            this.lang("HMI.MakeSureDeletes", codes.Count));
                        if (res == MessageBoxResult.OK)
                        {
                            Model.DeleteRoleByCodes(codes);
                            Model.LoadData();
                        }
                    }
                    catch (Exception ex)
                    {
                        log.Error("Delete Role Batch Failed.", ex);
                        ToastHelper.Error(Window.GetWindow(this), $"{LangUtil.Get("App.DeleteFailed")}: {ex.Message}");
                    }
                }
                else if (btn.Name == "delBtn")
                {
                    try
                    {
                        var data = (RoleData)((Button)sender).DataContext;
                        var res = MsgBoxHelper.WarnMessage(Window.GetWindow(this),
                             this.lang("HMI.MakeSureDeleteSingle", data.Name));
                        if (res == MessageBoxResult.OK)
                        {
                            Model.DelSingleData(data);
                            Model.LoadData();
                        }
                    }
                    catch (Exception ex)
                    {
                        log.Error("Delete Role Failed.", ex);
                        ToastHelper.Error((WindowX)Window.GetWindow(this), LangUtil.Get("App.DeleteFailed"));
                    }
                }
                else if (btn.Name == "saveMenuBtn")
                {
                    try
                    {
                        Model.SaveRelData(Model.SelectedValue);
                        ToastHelper.Success((WindowX)Window.GetWindow(this), LangUtil.Get("App.SaveSuccess"));
                    }
                    catch (Exception ex)
                    {
                        log.Error("Save Role-Menu Failed.", ex);
                        ToastHelper.Error((WindowX)Window.GetWindow(this), LangUtil.Get("App.SaveFailed"));
                    }
                }
            }
            catch (Exception ex)
            {
                log.Error("Something Error...", ex);
            }
        }
    }
}
Diff truncated after the above file
HMI.MenuManage/UserManage.xaml HMI.MenuManage/UserManage.xaml.cs HMI.MenuManage/Util/RoleHelper.cs HMI.MenuManage/Util/RoleMenuHelper.cs HMI.MenuManage/Util/UserRoleRelHelper.cs HMI.MenuManage/packages.config HMI.ParameterManage/HMI.ParameterManage.csproj HMI.ParameterManage/Model/SqlModel.cs HMI.ParameterManage/Model/XmlModel.cs HMI.ParameterManage/ParameterManage.xaml HMI.ParameterManage/ParameterManage.xaml.cs HMI.ParameterManage/ParameterModal.xaml HMI.ParameterManage/ParameterModal.xaml.cs HMI.ParameterManage/ParameterSetting.xaml HMI.ParameterManage/ParameterSetting.xaml.cs HMI.ParameterManage/ProductModal.xaml HMI.ParameterManage/ProductModal.xaml.cs HMI.ParameterManage/Properties/AssemblyInfo.cs HMI.ParameterManage/Properties/Resources.Designer.cs HMI.ParameterManage/Properties/Resources.resx HMI.ParameterManage/Properties/Settings.Designer.cs HMI.ParameterManage/Properties/Settings.settings HMI.ParameterManage/SettingModal.xaml HMI.ParameterManage/SettingModal.xaml.cs HMI.ParameterManage/TestItem.xaml HMI.ParameterManage/TestItem.xaml.cs HMI.ParameterManage/TestItemModal.xaml HMI.ParameterManage/TestItemModal.xaml.cs HMI.ParameterManage/packages.config HMI.SettingControl/HMI.SettingControl.csproj HMI.SettingControl/Properties/AssemblyInfo.cs HMI.SettingControl/Properties/Resources.Designer.cs HMI.SettingControl/Properties/Resources.resx HMI.SettingControl/Properties/Settings.Designer.cs HMI.SettingControl/Properties/Settings.settings HMI.SettingControl/SettingControl.xaml HMI.SettingControl/SettingControl.xaml.cs HMI.SettingControl/SystemConfig.xaml HMI.SettingControl/SystemConfig.xaml.cs HMI.SettingControl/packages.config HMI.StdControl/HMI.StdControl.csproj HMI.StdControl/Properties/AssemblyInfo.cs HMI.StdControl/StdEnableMes.xaml HMI.StdControl/StdEnableMes.xaml.cs HMI.StdControl/StdSettingCheckBox.xaml HMI.StdControl/StdSettingCheckBox.xaml.cs HMI.StdControl/StdUIEventArg.cs HMI.UI/Controls/KeyboradWindow.xaml HMI.UI/Controls/KeyboradWindow.xaml.cs HMI.UI/Controls/LightBall.cs HMI.UI/Controls/MultiCombox.xaml HMI.UI/Controls/MultiCombox.xaml.cs HMI.UI/Controls/NumberInput.cs HMI.UI/Controls/NumberPanel.xaml HMI.UI/Controls/NumberPanel.xaml.cs HMI.UI/Controls/VisualHost.cs HMI.UI/Controls/treegrid/Controls/TreeDataGrid.cs HMI.UI/Controls/treegrid/Controls/TreeGrid.cs HMI.UI/Controls/treegrid/Controls/TreeItemContentControl.cs HMI.UI/Controls/treegrid/Controls/TreeItemData.cs HMI.UI/Controls/treegrid/Controls/TreeListView.cs HMI.UI/Controls/treegrid/Themes/TreeGrid.xaml HMI.UI/Controls/treegrid/Themes/TreeItemContentControl.xaml HMI.UI/Controls/treegrid/Themes/TreeListView.xaml HMI.UI/Controls/treegrid/TreeItemConverter.cs HMI.UI/Controls/treegrid/TreeNodeExtend.cs HMI.UI/Converter/CollectionHasItem.cs HMI.UI/Converter/DataGridRowFlagConverter.cs HMI.UI/Converter/DataGridRowNumberConverter.cs HMI.UI/Converter/DateStrConverter.cs HMI.UI/Converter/IndentConverter.cs HMI.UI/Converter/PathImageConverter.cs HMI.UI/Converter/RadioEnumConverter.cs HMI.UI/Converter/ResultConverter.cs HMI.UI/Converter/ReverseBoolConverter.cs HMI.UI/Converter/VisibilityBoolConverter.cs HMI.UI/DpUtil/CanvasHelper.cs HMI.UI/DpUtil/DataGridHelper.cs HMI.UI/DpUtil/DataGridTextColumnHelper.cs HMI.UI/DpUtil/DpPlugin.cs HMI.UI/DpUtil/FormGroupHelper.cs HMI.UI/DpUtil/GridHelper.cs HMI.UI/DpUtil/Resize.cs HMI.UI/DpUtil/StackPanelHelper.cs HMI.UI/DpUtil/TreeViewHelper.cs HMI.UI/DpUtil/UniformGridHelper.cs HMI.UI/EditBox.cs HMI.UI/Entity/ScannerConfig.cs HMI.UI/HMI.UI.csproj HMI.UI/MDI/BottomBar.xaml HMI.UI/MDI/BottomBar.xaml.cs HMI.UI/MDI/InnerWindow.xaml HMI.UI/MDI/InnerWindow.xaml.cs HMI.UI/MDI/MinInnerWindow.xaml HMI.UI/MDI/MinInnerWindow.xaml.cs HMI.UI/MDI/Workspace.xaml HMI.UI/MDI/Workspace.xaml.cs HMI.UI/MaskControl.xaml HMI.UI/MaskControl.xaml.cs HMI.UI/MaskWindow.xaml HMI.UI/MaskWindow.xaml.cs HMI.UI/Properties/AssemblyInfo.cs HMI.UI/Properties/Resources.Designer.cs HMI.UI/Properties/Resources.resx HMI.UI/Properties/Settings.Designer.cs HMI.UI/Properties/Settings.settings HMI.UI/RequirePasswordByUser.xaml HMI.UI/RequirePasswordByUser.xaml.cs HMI.UI/RequirePasswordWindow.xaml HMI.UI/RequirePasswordWindow.xaml.cs HMI.UI/Resources/Font/Font.xaml HMI.UI/Resources/Font/iconfont.json HMI.UI/Resources/Font/iconfont.ttf HMI.UI/Resources/Icon/dot.png HMI.UI/Resources/Icon/dot.svg HMI.UI/Resources/Icon/dot2.png HMI.UI/Resources/Icon/xinzeng_1.png HMI.UI/ScannerSetting.xaml HMI.UI/ScannerSetting.xaml.cs HMI.UI/ScrollLabel.cs HMI.UI/Style/BaseWindow.xaml HMI.UI/Style/BaseWindow.xaml.cs HMI.UI/Style/Colors.xaml HMI.UI/Style/FormStyle.xaml HMI.UI/Style/MsgBoxStyle.xaml HMI.UI/Style/PanuonDataGrid.xaml HMI.UI/Style/PanuonListView.xaml HMI.UI/Style/PanuonResource.xaml HMI.UI/Style/ScrollBarResource.xaml HMI.UI/Style/WindowResources.xaml HMI.UI/TableComboBox.cs HMI.UI/Themes/Generic.xaml HMI.UI/Util/IValueValidator.cs HMI.UI/Util/MaskHolder.cs HMI.UI/Util/MsgBoxHelper.cs HMI.UI/Util/MsgBoxHelper2.cs HMI.UI/Util/ToastHelper.cs HMI.UI/packages.config HMI.UnitTest/App.config HMI.UnitTest/App.xaml HMI.UnitTest/App.xaml.cs HMI.UnitTest/HMI.UnitTest.csproj HMI.UnitTest/MainWindow.xaml HMI.UnitTest/MainWindow.xaml.cs HMI.UnitTest/Properties/AssemblyInfo.cs HMI.UnitTest/Properties/Resources.Designer.cs HMI.UnitTest/Properties/Resources.resx HMI.UnitTest/Properties/Settings.Designer.cs HMI.UnitTest/Properties/Settings.settings HMI.UnitTest/log4net.config HMI.UnitTest/packages.config HMI.Util/Alarm/AlarmUtil.cs HMI.Util/DateUtil.cs HMI.Util/DbFileHelper.cs HMI.Util/ExcelHelper.cs HMI.Util/HMI.Util.csproj HMI.Util/IniFileHelper.cs HMI.Util/Properties/AssemblyInfo.cs HMI.Util/PropertyHelper.cs HMI.Util/packages.config Libs/25/NationalInstruments.TestStand.Interop.API.dll Libs/25/NationalInstruments.TestStand.Interop.API.xml Libs/25/NationalInstruments.TestStand.Interop.UI.AxControls.dll Libs/25/NationalInstruments.TestStand.Interop.UI.Support.dll Libs/25/NationalInstruments.TestStand.Interop.UI.Support.xml Libs/25/NationalInstruments.TestStand.Interop.UI.dll Libs/25/NationalInstruments.TestStand.Interop.UI.xml Libs/25/NationalInstruments.TestStand.Utility.dll Libs/25/NationalInstruments.TestStand.Utility.xml Libs/DatabaseHelper.dll Libs/HslCommunication.dll Libs/Microsoft.DwayneNeed.Win32.dll Libs/Microsoft.DwayneNeed.dll Libs/NationalInstruments.TestStand.Interop.API.dll Libs/NationalInstruments.TestStand.Interop.API.xml Libs/NationalInstruments.TestStand.Interop.AdapterAPI.dll Libs/NationalInstruments.TestStand.Interop.AdapterAPI.xml Libs/NationalInstruments.TestStand.Interop.SequenceAnalyzer.dll Libs/NationalInstruments.TestStand.Interop.SequenceAnalyzer.xml Libs/NationalInstruments.TestStand.Interop.UI.AxControls.dll Libs/NationalInstruments.TestStand.Interop.UI.Support.dll Libs/NationalInstruments.TestStand.Interop.UI.Support.xml Libs/NationalInstruments.TestStand.Interop.UI.dll Libs/NationalInstruments.TestStand.Interop.UI.xml Libs/NationalInstruments.TestStand.Utility.dll Libs/NationalInstruments.TestStand.Utility.xml Libs/SQLite.Interop.dll Libs/SimensDriver.dll Libs/SupersocketCommon.dll Libs/System.Data.SQLite.EF6.dll Libs/System.Data.SQLite.Linq.dll Libs/System.Data.SQLite.dll Libs/TSSync.Interop.dll Libs/lmes.dll ModBusTcpController/ModBusTcpController.cs ModBusTcpController/ModBusTcpController.csproj ModBusTcpController/Properties/AssemblyInfo.cs OPCClient/Commond.cs OPCClient/OPCClient.cs OPCClient/OPCClient.csproj OPCClient/Properties/AssemblyInfo.cs OPCClient/packages.config PLCHelper/BoolReader.cs PLCHelper/Entity/PLCTagData.cs PLCHelper/IntReader.cs PLCHelper/ModbusHelper.cs PLCHelper/NXI_5010Helper.cs PLCHelper/PLCHelper.csproj PLCHelper/Properties/AssemblyInfo.cs PLCHelper/RestTcpClient.cs PLCHelper/S7Context.cs Plugin.BurnIn/AssemblyInfo.cs Plugin.BurnIn/BurnInUI.xaml Plugin.BurnIn/BurnInUI.xaml.cs Plugin.BurnIn/ChartWindow.xaml Plugin.BurnIn/ChartWindow.xaml.cs Plugin.BurnIn/Constant/CommonConstant.cs Plugin.BurnIn/Cooler.xaml Plugin.BurnIn/Cooler.xaml.cs Plugin.BurnIn/CoolerStatus.xaml Plugin.BurnIn/CoolerStatus.xaml.cs Plugin.BurnIn/DataTable.xaml Plugin.BurnIn/DataTable.xaml.cs Plugin.BurnIn/InfoTable.xaml Plugin.BurnIn/InfoTable.xaml.cs Plugin.BurnIn/InfoWindow.xaml Plugin.BurnIn/InfoWindow.xaml.cs Plugin.BurnIn/ManualDebug/ManualCell.xaml Plugin.BurnIn/ManualDebug/ManualCell.xaml.cs Plugin.BurnIn/ManualDebug/ManualDebug.xaml Plugin.BurnIn/ManualDebug/ManualDebug.xaml.cs Plugin.BurnIn/ManualDebug/ManualDebugStyle.xaml Plugin.BurnIn/ManualDebug/ManualDebugWindow.xaml Plugin.BurnIn/ManualDebug/ManualDebugWindow.xaml.cs Plugin.BurnIn/ManualDebug/StatusBtn.xaml Plugin.BurnIn/ManualDebug/StatusBtn.xaml.cs Plugin.BurnIn/ManualDebug/StatusCell.xaml Plugin.BurnIn/ManualDebug/StatusCell.xaml.cs Plugin.BurnIn/Model/BaseConfig.cs Plugin.BurnIn/Model/BaseModel.cs Plugin.BurnIn/Model/CellInfo.cs Plugin.BurnIn/Model/DataTableModel.cs Plugin.BurnIn/Model/Info.cs Plugin.BurnIn/Model/ScannerConfig.cs Plugin.BurnIn/Model/TcpServer.cs Plugin.BurnIn/Plugin.BurnIn.csproj Plugin.BurnIn/Resource/abort.png Plugin.BurnIn/Resource/alert.png Plugin.BurnIn/Resource/barcode.png Plugin.BurnIn/Resource/checkIn.png Plugin.BurnIn/Resource/checkInFailed2.png Plugin.BurnIn/Resource/ng2.png Plugin.BurnIn/Resource/ok2.png Plugin.BurnIn/Resource/ready.png Plugin.BurnIn/Resource/testing2.png Plugin.BurnIn/Resource/waiting2.png Plugin.BurnIn/Scanner.xaml Plugin.BurnIn/Scanner.xaml.cs Plugin.BurnIn/ScannerSetting.xaml Plugin.BurnIn/ScannerSetting.xaml.cs Plugin.BurnIn/SingleCell.xaml Plugin.BurnIn/SingleCell.xaml.cs Plugin.BurnIn/TestResultView.xaml Plugin.BurnIn/TestResultView.xaml.cs Plugin.BurnIn/UI/LiveChart.xaml Plugin.BurnIn/UI/LiveChart.xaml.cs Plugin.BurnIn/Util/CommonCodeParser.cs Plugin.BurnIn/Util/DpUtil.cs Plugin.BurnIn/Util/FontSizeConverter.cs Plugin.BurnIn/Util/ICodeParser.cs Plugin.BurnIn/Util/JKCodeParser.cs Plugin.BurnIn/Util/ModelCodeParser.cs Plugin.BurnIn/Util/PNUtil.cs Plugin.BurnIn/Util/RunTimeVariablePool.cs Plugin.BurnIn/Util/SNValidator.cs Plugin.BurnIn/Util/ScannerParser.cs Plugin.CurrentSensorControl/AssemblyInfo.cs Plugin.CurrentSensorControl/BurnInCell.xaml Plugin.CurrentSensorControl/BurnInCell.xaml.cs Plugin.CurrentSensorControl/BurnInOven.xaml Plugin.CurrentSensorControl/BurnInOven.xaml.cs Plugin.CurrentSensorControl/BurnInOvenCollection.xaml Plugin.CurrentSensorControl/BurnInOvenCollection.xaml.cs Plugin.CurrentSensorControl/BurnInSettingModal.xaml Plugin.CurrentSensorControl/BurnInSettingModal.xaml.cs Plugin.CurrentSensorControl/ControlPage - 复制.xaml Plugin.CurrentSensorControl/ControlPage - 复制.xaml.cs Plugin.CurrentSensorControl/ControlPage.xaml Plugin.CurrentSensorControl/ControlPage.xaml.cs Plugin.CurrentSensorControl/DataModal.xaml Plugin.CurrentSensorControl/DataModal.xaml.cs Plugin.CurrentSensorControl/Entity/BaseConfig.cs Plugin.CurrentSensorControl/Entity/OPCContext.cs Plugin.CurrentSensorControl/Entity/SensorData.cs Plugin.CurrentSensorControl/Plugin.CurrentSensorControl.csproj Plugin.CurrentSensorControl/SingleSensor.xaml Plugin.CurrentSensorControl/SingleSensor.xaml.cs Plugin.DOControl/ControlList.xaml Plugin.DOControl/ControlList.xaml.cs Plugin.DOControl/DOControl.xaml Plugin.DOControl/DOControl.xaml.cs Plugin.DOControl/DataModal.xaml Plugin.DOControl/DataModal.xaml.cs Plugin.DOControl/Entity/DOControlData.cs Plugin.DOControl/Plugin.DOControl.csproj Plugin.DOControl/Properties/AssemblyInfo.cs Plugin.DOControl/Properties/Resources.Designer.cs Plugin.DOControl/Properties/Resources.resx Plugin.DOControl/Properties/Settings.Designer.cs Plugin.DOControl/Properties/Settings.settings Plugin.DOControl/packages.config Plugin.DODriverControl/Api/Dask.cs Plugin.DODriverControl/Api/DaskHelper.cs Plugin.DODriverControl/Api/IDIOController.cs Plugin.DODriverControl/Api/ModBusTcpController.cs Plugin.DODriverControl/Api/PCIController.cs Plugin.DODriverControl/AssemblyInfo.cs Plugin.DODriverControl/ConfigModal.xaml Plugin.DODriverControl/ConfigModal.xaml.cs Plugin.DODriverControl/DOControl.xaml Plugin.DODriverControl/DOControl.xaml.cs Plugin.DODriverControl/DataModal.xaml Plugin.DODriverControl/DataModal.xaml.cs Plugin.DODriverControl/Entity/ConfigData.cs Plugin.DODriverControl/Entity/DIOData.cs Plugin.DODriverControl/Plugin.DODriverControl.csproj Plugin.DODriverControl/SingleControl.xaml Plugin.DODriverControl/SingleControl.xaml.cs Plugin.DataBoard/DataBoard.xaml Plugin.DataBoard/DataBoard.xaml.cs Plugin.DataBoard/Libs/Static/echarts.min.js Plugin.DataBoard/Libs/Static/test.html Plugin.DataBoard/Libs/echarts.min.js Plugin.DataBoard/OptionData.cs Plugin.DataBoard/PassRateData.cs Plugin.DataBoard/Plugin.DataBoard.csproj Plugin.DataBoard/Properties/AssemblyInfo.cs Plugin.DataBoard/Properties/Resources.Designer.cs Plugin.DataBoard/Properties/Resources.resx Plugin.DataBoard/Properties/Settings.Designer.cs Plugin.DataBoard/Properties/Settings.settings Plugin.DataBoard/SerialData.cs Plugin.DataBoard/SimpleData.cs Plugin.DataBoard/app.config Plugin.DataBoard/packages.config Plugin.DataViewer/AssemblyInfo.cs Plugin.DataViewer/CustomSortColumn.cs Plugin.DataViewer/DataViewerControl.xaml Plugin.DataViewer/DataViewerControl.xaml.cs Plugin.DataViewer/Plugin.DataViewer.csproj Plugin.DeviceConfig/BatchGenerateModal.xaml Plugin.DeviceConfig/BatchGenerateModal.xaml.cs Plugin.DeviceConfig/DeviceConfig.xaml Plugin.DeviceConfig/DeviceConfig.xaml.cs Plugin.DeviceConfig/DeviceEditModal.xaml Plugin.DeviceConfig/DeviceEditModal.xaml.cs Plugin.DeviceConfig/Entity/DeviceChannel.cs Plugin.DeviceConfig/Entity/DeviceConfigVersion.cs Plugin.DeviceConfig/Entity/DeviceDefinition.cs Plugin.DeviceConfig/Entity/DeviceGroupTemplate.cs Plugin.DeviceConfig/Entity/SlotDeviceMapping.cs Plugin.DeviceConfig/Model/DeviceConfigModel.cs Plugin.DeviceConfig/Plugin.DeviceConfig.csproj Plugin.DeviceConfig/VersionManageModal.xaml Plugin.DeviceConfig/VersionManageModal.xaml.cs Plugin.DeviceDebug/DeviceDebug.xaml Plugin.DeviceDebug/DeviceDebug.xaml.cs Plugin.DeviceDebug/Entity/DebugCommand.cs Plugin.DeviceDebug/Entity/DebugOperation.cs Plugin.DeviceDebug/Entity/DebugSession.cs Plugin.DeviceDebug/Entity/DeviceRuntimeStatus.cs Plugin.DeviceDebug/Model/DebugSessionManager.cs Plugin.DeviceDebug/Model/DeviceDebugModel.cs Plugin.DeviceDebug/Panels/CanDebugPanel.xaml Plugin.DeviceDebug/Panels/CanDebugPanel.xaml.cs Plugin.DeviceDebug/Panels/PlcDebugPanel.xaml Plugin.DeviceDebug/Panels/PlcDebugPanel.xaml.cs Plugin.DeviceDebug/Panels/PowerSupplyDebugPanel.xaml Plugin.DeviceDebug/Panels/PowerSupplyDebugPanel.xaml.cs Plugin.DeviceDebug/Plugin.DeviceDebug.csproj Plugin.IntegrationTest/App.xaml Plugin.IntegrationTest/App.xaml.cs Plugin.IntegrationTest/MainWindow.xaml Plugin.IntegrationTest/MainWindow.xaml.cs Plugin.IntegrationTest/Plugin.IntegrationTest.csproj Plugin.JarUI/JarCell.xaml Plugin.JarUI/JarCell.xaml.cs Plugin.JarUI/JarUI.xaml Plugin.JarUI/JarUI.xaml.cs Plugin.JarUI/Plugin.JarUI.csproj Plugin.JarUI/Properties/AssemblyInfo.cs Plugin.JarUI/Properties/Resources.Designer.cs Plugin.JarUI/Properties/Resources.resx Plugin.JarUI/Properties/Settings.Designer.cs Plugin.JarUI/Properties/Settings.settings Plugin.JarUI/packages.config Plugin.ModelManage/AssemblyInfo.cs Plugin.ModelManage/DataModal.xaml Plugin.ModelManage/DataModal.xaml.cs Plugin.ModelManage/Entity/ModelData.cs Plugin.ModelManage/ModelManage.xaml Plugin.ModelManage/ModelManage.xaml.cs Plugin.ModelManage/Plugin.ModelManage.csproj Plugin.QualityReport/AssemblyInfo.cs Plugin.QualityReport/Libs/Static/CMK模板.xls Plugin.QualityReport/Libs/Static/MSA模板.xlsx Plugin.QualityReport/Libs/Static/重复性模板.xls Plugin.QualityReport/Plugin.QualityReport.csproj Plugin.QualityReport/QualityReport.xaml Plugin.QualityReport/QualityReport.xaml.cs Plugin.SeqEditor/AssemblyInfo.cs Plugin.SeqEditor/Constant/DefaultStepName.cs Plugin.SeqEditor/Constant/StepTypeStr.cs Plugin.SeqEditor/Entity/EditablePO/IEPO.cs Plugin.SeqEditor/Entity/EditablePO/NumValTestEPO.cs Plugin.SeqEditor/Entity/EditablePO/StringValTestEPO.cs Plugin.SeqEditor/Entity/EditablePO/ViCallEPO.cs Plugin.SeqEditor/Entity/EditablePO/WaitEPO.cs Plugin.SeqEditor/Entity/SeqUI.cs Plugin.SeqEditor/Entity/StepUI.cs Plugin.SeqEditor/Entity/VarUI.cs Plugin.SeqEditor/Plugin.SeqEditor.csproj Plugin.SeqEditor/RowNumberConverter.cs Plugin.SeqEditor/SeqEditor.xaml Plugin.SeqEditor/SeqEditor.xaml.cs Plugin.SeqEditor/SettingPanel/ActionPanel.xaml Plugin.SeqEditor/SettingPanel/ActionPanel.xaml.cs Plugin.SeqEditor/SettingPanel/CallSeqPanel.xaml Plugin.SeqEditor/SettingPanel/CallSeqPanel.xaml.cs Plugin.SeqEditor/SettingPanel/CommonProperty.xaml Plugin.SeqEditor/SettingPanel/CommonProperty.xaml.cs Plugin.SeqEditor/SettingPanel/LoopPanel.xaml Plugin.SeqEditor/SettingPanel/LoopPanel.xaml.cs Plugin.SeqEditor/SettingPanel/NumValTestPanel.xaml Plugin.SeqEditor/SettingPanel/NumValTestPanel.xaml.cs Plugin.SeqEditor/SettingPanel/StringValTestPanel.xaml Plugin.SeqEditor/SettingPanel/StringValTestPanel.xaml.cs Plugin.SeqEditor/SettingPanel/ViPanel.xaml Plugin.SeqEditor/SettingPanel/ViPanel.xaml.cs Plugin.SeqEditor/SettingPanel/WaitPanel.xaml Plugin.SeqEditor/SettingPanel/WaitPanel.xaml.cs Plugin.SeqEditor/Util/ItemData.cs Plugin.SeqEditor/Util/PoExplain.cs Plugin.SeqEditor/Util/Step2EPO.cs Plugin.SeqEditor/ViewModel/SeqEditorViewModel.cs Plugin.SeqEditor/ViewModel/StepDropHandler.cs Plugin.SeqEditor/ViewModel/TreeListViewHandler.cs Plugin.StandardTest/AssemblyInfo.cs Plugin.StandardTest/Entity/TestStep.cs Plugin.StandardTest/Plugin.StandardTest.csproj Plugin.StandardTest/Scanner.xaml Plugin.StandardTest/Scanner.xaml.cs Plugin.StandardTest/ScannerManager.cs Plugin.StandardTest/ScreenGrid.xaml Plugin.StandardTest/ScreenGrid.xaml.cs Plugin.StandardTest/ScreenTouch.xaml Plugin.StandardTest/ScreenTouch.xaml.cs Plugin.StandardTest/ScreenTouchWin.xaml Plugin.StandardTest/ScreenTouchWin.xaml.cs Plugin.StandardTest/Setting/TcpServer.cs Plugin.StandardTest/Setting/TsSetting.cs Plugin.StandardTest/SingleControl.xaml Plugin.StandardTest/SingleControl.xaml.cs Plugin.StandardTest/StandardTestUI.xaml Plugin.StandardTest/StandardTestUI.xaml.cs Plugin.StandardTest/WorkOrderWin.xaml Plugin.StandardTest/WorkOrderWin.xaml.cs Plugin.TestPlanEditor/AssemblyInfo.cs Plugin.TestPlanEditor/EditorUI.xaml Plugin.TestPlanEditor/EditorUI.xaml.cs Plugin.TestPlanEditor/Plugin.TestPlanEditor.csproj Plugin.TestResult/CSVPrinter.cs Plugin.TestResult/Chart.xaml Plugin.TestResult/Chart.xaml.cs Plugin.TestResult/ChartWindow.xaml Plugin.TestResult/ChartWindow.xaml.cs Plugin.TestResult/Config/log4net.config Plugin.TestResult/Config/log4net.unix.config Plugin.TestResult/Constant/CommonConstant.cs Plugin.TestResult/Cooler.xaml Plugin.TestResult/Cooler.xaml.cs Plugin.TestResult/CoolerStatus.xaml Plugin.TestResult/CoolerStatus.xaml.cs Plugin.TestResult/DataTable.xaml Plugin.TestResult/DataTable.xaml.cs Plugin.TestResult/InfoTable.xaml Plugin.TestResult/InfoTable.xaml.cs Plugin.TestResult/InfoWindow.xaml Plugin.TestResult/InfoWindow.xaml.cs Plugin.TestResult/LiveChart.xaml Plugin.TestResult/LiveChart.xaml.cs Plugin.TestResult/Lixun/SNValidator.cs Plugin.TestResult/ManualDebugWindow.xaml Plugin.TestResult/ManualDebugWindow.xaml.cs Plugin.TestResult/Model/BaseConfig.cs Plugin.TestResult/Model/BaseModel.cs Plugin.TestResult/Model/CellInfo.cs Plugin.TestResult/Model/DataTableModel.cs Plugin.TestResult/Model/Info.cs Plugin.TestResult/Model/ScannerConfig.cs Plugin.TestResult/Model/TcpServer.cs Plugin.TestResult/Plugin.TestResult.csproj Plugin.TestResult/Properties/AssemblyInfo.cs Plugin.TestResult/Properties/Resources.Designer.cs Plugin.TestResult/Properties/Resources.resx Plugin.TestResult/Properties/Settings.Designer.cs Plugin.TestResult/Properties/Settings.settings Plugin.TestResult/Resource/abort.png Plugin.TestResult/Resource/alert.png Plugin.TestResult/Resource/barcode.png Plugin.TestResult/Resource/checkIn.png Plugin.TestResult/Resource/checkInFailed2.png Plugin.TestResult/Resource/ng2.png Plugin.TestResult/Resource/ok2.png Plugin.TestResult/Resource/ready.png Plugin.TestResult/Resource/testing2.png Plugin.TestResult/Resource/waiting2.png Plugin.TestResult/Scanner.xaml Plugin.TestResult/Scanner.xaml.cs Plugin.TestResult/ScannerSetting.xaml Plugin.TestResult/ScannerSetting.xaml.cs Plugin.TestResult/SingleCell.xaml Plugin.TestResult/SingleCell.xaml.cs Plugin.TestResult/TestResultView.xaml Plugin.TestResult/TestResultView.xaml.cs Plugin.TestResult/Util/CommonCodeParser.cs Plugin.TestResult/Util/DpUtil.cs Plugin.TestResult/Util/FontSizeConverter.cs Plugin.TestResult/Util/ICodeParser.cs Plugin.TestResult/Util/JKCodeParser.cs Plugin.TestResult/Util/ModelCodeParser.cs Plugin.TestResult/Util/PNUtil.cs Plugin.TestResult/Util/RunTimeVariablePool.cs Plugin.TestResult/Util/ScannerParser.cs Plugin.TestResult/ViewControl.xaml Plugin.TestResult/ViewControl.xaml.cs Plugin.TestResult/packages.config Plugin.TsExecution/Entity/TestStep.cs Plugin.TsExecution/OpenTK.dll.config Plugin.TsExecution/Plugin.TsExecution.csproj Plugin.TsExecution/Properties/AssemblyInfo.cs Plugin.TsExecution/Properties/Resources.Designer.cs Plugin.TsExecution/Properties/Resources.resx Plugin.TsExecution/Properties/Settings.Designer.cs Plugin.TsExecution/Properties/Settings.settings Plugin.TsExecution/Scanner.xaml Plugin.TsExecution/Scanner.xaml.cs Plugin.TsExecution/ScannerManager.cs Plugin.TsExecution/ScreenGrid.xaml Plugin.TsExecution/ScreenGrid.xaml.cs Plugin.TsExecution/ScreenTouch.xaml Plugin.TsExecution/ScreenTouch.xaml.cs Plugin.TsExecution/ScreenTouchWin.xaml Plugin.TsExecution/ScreenTouchWin.xaml.cs Plugin.TsExecution/Setting/TsSetting.cs Plugin.TsExecution/SingleControl.xaml Plugin.TsExecution/SingleControl.xaml.cs Plugin.TsExecution/TsExecution.xaml Plugin.TsExecution/TsExecution.xaml.cs Plugin.TsExecution/WorkOrderWin.xaml Plugin.TsExecution/WorkOrderWin.xaml.cs Plugin.TsExecution/packages.config Plugin.TsSupport/ITestExecutionWrapper.cs Plugin.TsSupport/Plugin.TsSupport.csproj Plugin.TsSupport/Properties/AssemblyInfo.cs Plugin.TsSupport/TapWrapper.cs Plugin.TsSupport/TestAppManager.cs Plugin.TsSupport/TeststandWrapper.cs Plugin.UICollection/AssemblyInfo.cs Plugin.UICollection/Plugin.UICollection.csproj Plugin.UICollection/TcpDebugger.xaml Plugin.UICollection/TcpDebugger.xaml.cs S7Driver/PLCS7Driver.cs S7Driver/Properties/AssemblyInfo.cs S7Driver/S7Driver.csproj SimpleUtil/DIOController/DIOController.csproj SimpleUtil/DIOController/DIOControllerCreator.cs SimpleUtil/DIOController/Dask.cs SimpleUtil/DIOController/DaskHelper.cs SimpleUtil/DIOController/IDIOController.cs SimpleUtil/DIOController/IniFileHelper.cs SimpleUtil/DIOController/ModBusTcpController.cs SimpleUtil/DIOController/PCI-Dask64.dll SimpleUtil/DIOController/PCIController.cs SimpleUtil/DIOController/Properties/AssemblyInfo.cs SimpleUtil/DIOController/template.ini SimpleUtil/HMI.LogUtil/HMI.LogUtil.csproj SimpleUtil/HMI.LogUtil/LogHelper.cs SimpleUtil/HMI.LogUtil/Properties/AssemblyInfo.cs SimpleUtil/HMI.MQUtil/HMI.MQUtil.csproj SimpleUtil/HMI.MQUtil/MQConnector.cs SimpleUtil/HMI.MQUtil/Properties/AssemblyInfo.cs SimpleUtil/HMI.SerialData.Util/HMI.SerialData.Util.csproj SimpleUtil/HMI.SerialData.Util/Properties/AssemblyInfo.cs SimpleUtil/HMI.SerialData.Util/SerialDataHelper.cs SimpleUtil/HMI.Simple.Util/AlarmUtil.cs SimpleUtil/HMI.Simple.Util/DataSender.cs SimpleUtil/HMI.Simple.Util/HMI.Simple.Util.csproj SimpleUtil/HMI.Simple.Util/MSMQHelper.cs SimpleUtil/HMI.Simple.Util/Parameter.cs SimpleUtil/HMI.Simple.Util/ParameterUtil.cs SimpleUtil/HMI.Simple.Util/ProductModelHelper.cs SimpleUtil/HMI.Simple.Util/ProductUtil.cs SimpleUtil/HMI.Simple.Util/Properties/AssemblyInfo.cs SimpleUtil/HMI.Simple.Util/SerialDataHelper.cs SimpleUtil/HMI.Simple.Util/SqliteHelper.cs SimpleUtil/HMI.Simple.Util/XmlHelper.cs SimpleUtil/HMI.TapApi/HMI.TapApi.csproj SimpleUtil/HMI.TapApi/ProductUtil.cs SimpleUtil/HMI.TapApi/Properties/AssemblyInfo.cs SimpleUtil/HMI.Util.Base/HMI.Util.Base.csproj SimpleUtil/HMI.Util.Base/Properties/AssemblyInfo.cs SimpleUtil/HMI.Util.Base/RegUtil.cs StationGlobalsReader/App.xaml StationGlobalsReader/App.xaml.cs StationGlobalsReader/AssemblyInfo.cs StationGlobalsReader/MainWindow.xaml StationGlobalsReader/MainWindow.xaml.cs StationGlobalsReader/StationGlobalsReader.csproj Util.ManualDebug/AssemblyInfo.cs Util.ManualDebug/ManualCell.xaml Util.ManualDebug/ManualCell.xaml.cs Util.ManualDebug/ManualDebug.xaml Util.ManualDebug/ManualDebug.xaml.cs Util.ManualDebug/ManualDebugStyle.xaml Util.ManualDebug/ManualDebugWindow.xaml Util.ManualDebug/ManualDebugWindow.xaml.cs Util.ManualDebug/Model/CellConfig.cs Util.ManualDebug/Model/TcpServer.cs Util.ManualDebug/OldManualDebug.xaml Util.ManualDebug/OldManualDebug.xaml.cs Util.ManualDebug/StatusBtn.xaml Util.ManualDebug/StatusBtn.xaml.cs Util.ManualDebug/StatusCell.xaml Util.ManualDebug/StatusCell.xaml.cs Util.ManualDebug/Util.ManualDebug.csproj WPFProject/Api/SQLite.Interop.dll WPFProject/App.config WPFProject/App.xaml WPFProject/App.xaml.cs WPFProject/BuildCmd.bat WPFProject/Config/Database/database.db WPFProject/Config/Put Your Config Here.txt WPFProject/Config/log4net.config WPFProject/Controls/DpPlugin.cs WPFProject/Controls/HolderForm.Designer.cs WPFProject/Controls/HolderForm.cs WPFProject/Controls/HolderForm.resx WPFProject/Controls/IconTabItem.cs WPFProject/Controls/MainTabControl.xaml WPFProject/Controls/MainTabControl.xaml.cs WPFProject/Controls/MaskControl.Designer.cs WPFProject/Controls/MaskControl.cs WPFProject/Controls/MenuTabControl.xaml WPFProject/Controls/MenuTabControl.xaml.cs WPFProject/Controls/WinFormHolder.xaml WPFProject/Controls/WinFormHolder.xaml.cs WPFProject/Entity/PluginLoadMessage.cs WPFProject/Icon/JingCeLogo.png WPFProject/Icon/JingCeLogo2.png WPFProject/Icon/LoginLogo.png WPFProject/Icon/b1.jpg WPFProject/Icon/b2.jpg WPFProject/Icon/b3.jpg WPFProject/Icon/b4.jpg WPFProject/Icon/pic1.png WPFProject/Icon/t1.png WPFProject/Icon/t2.png WPFProject/Icon/t3.png WPFProject/Icon/t4.png WPFProject/Icon/xinzeng_1.png WPFProject/JingCeLogo2.ico WPFProject/Lang/en-US/AlarmInfoManage.json WPFProject/Lang/en-US/ChangeModel.json WPFProject/Lang/en-US/DOControl.json WPFProject/Lang/en-US/DataBoard.json WPFProject/Lang/en-US/DataViewer.json WPFProject/Lang/en-US/IOMonitor.json WPFProject/Lang/en-US/LogViewer.json WPFProject/Lang/en-US/ParamManage.json WPFProject/Lang/en-US/SeqEditor.json WPFProject/Lang/en-US/ServoManage.json WPFProject/Lang/en-US/TestExecution.json WPFProject/Lang/en-US/TestResult.json WPFProject/Lang/en-US/app.json WPFProject/Lang/en-US/setting.json WPFProject/Lang/zh-CN/AlarmInfoManage.json WPFProject/Lang/zh-CN/ChangeModel.json WPFProject/Lang/zh-CN/DOControl.json WPFProject/Lang/zh-CN/DataBoard.json WPFProject/Lang/zh-CN/DataViewer.json WPFProject/Lang/zh-CN/IOMonitor.json WPFProject/Lang/zh-CN/LogViewer.json WPFProject/Lang/zh-CN/ParamManage.json WPFProject/Lang/zh-CN/SeqEditor.json WPFProject/Lang/zh-CN/ServoManage.json WPFProject/Lang/zh-CN/TestExecution.json WPFProject/Lang/zh-CN/TestResult.json WPFProject/Lang/zh-CN/app.json WPFProject/Lang/zh-CN/setting.json WPFProject/LoadingWindow.xaml WPFProject/LoadingWindow.xaml.cs WPFProject/Login.xaml WPFProject/Login.xaml.cs WPFProject/MigrationBackup/fde93b81/VisionBuilder.Core/NuGetUpgradeLog.html WPFProject/MigrationBackup/fde93b81/VisionBuilder.Core/VisionBuilder.Core.csproj WPFProject/MigrationBackup/fde93b81/VisionBuilder.Core/packages.config WPFProject/PanuonWindow.xaml WPFProject/PanuonWindow.xaml.cs WPFProject/Properties/AssemblyInfo.cs WPFProject/Properties/Resources.Designer.cs WPFProject/Properties/Resources.resx WPFProject/Properties/Settings.Designer.cs WPFProject/Properties/Settings.settings WPFProject/Properties/app.manifest WPFProject/ResourceListener.cs WPFProject/Resources/Background.png WPFProject/Resources/JingCeLogo.png WPFProject/SQLite.Interop.dll WPFProject/Settings.ini WPFProject/Style/CommonTree.xaml WPFProject/Style/ListViewStyle.xaml WPFProject/Style/MainTab.xaml WPFProject/SuperSocket.SocketEngine.dll WPFProject/TapProject.csproj WPFProject/TapProject.sln WPFProject/UI/ResourceUI.xaml WPFProject/UI/ResourceUI.xaml.cs WPFProject/UI/TestWindow.xaml WPFProject/UI/TestWindow.xaml.cs WPFProject/UI/WorkWindow.xaml WPFProject/UI/WorkWindow.xaml.cs WPFProject/Utils/BinUtil.cs WPFProject/Utils/CommonLogger.cs WPFProject/Utils/Encryptor.cs WPFProject/Utils/LogCleanner.cs WPFProject/Utils/MainToastHandler.cs WPFProject/Utils/Win32Helper.cs WPFProject/packages.config WPFProject/x86/SQLite.Interop.dll WPFProject/变更记录/2025-12-24.txt WPFProject/安装MSMQ.bat check_parent.py debug_check.py docs/TapProject_代码结构.drawio docs/TapProject_代码结构_01.drawio.png docs/TapProject_代码结构_01_预览.png docs/TapProject_代码结构_02.drawio.png docs/TapProject_代码结构_02_预览.png docs/TapProject_代码结构分析.md fix_menu.py