From 010272aa0bca9d9a116624e88aa817bc07b45981 Mon Sep 17 00:00:00 2001 From: Tyrone CT Date: Wed, 26 Nov 2025 16:46:48 +0800 Subject: [PATCH] =?UTF-8?q?DataServices=20=E7=8E=B0=E5=9C=BA=E8=B0=83?= =?UTF-8?q?=E8=AF=95OK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FATrace.App/frmMain.Designer.cs | 62 +++-- FATrace.App/frmMain.cs | 43 ++-- FATrace.Model/HistoryAlarm.cs | 51 ++++ FATrace.Model/RawProUse.cs | 7 + FATrace.WPLApp/App.config | 1 + FATrace.WPLApp/App.xaml.cs | 32 ++- .../CsvModel/RawProUserCsvDtoMap.cs | 45 ++++ FATrace.WPLApp/FATrace.WPLApp.csproj | 9 +- FATrace.WPLApp/MapperProfile/MapperConfig.cs | 35 +++ .../MapperProfile/RawProUseProfile.cs | 20 ++ FATrace.WPLApp/ModelDto/RawProUseDto.cs | 5 + FATrace.WPLApp/Report/UserManual.md | 144 +++++++++++ FATrace.WPLApp/Services/CsvServices.cs | 184 +++++++------- FATrace.WPLApp/Services/DataServices.cs | 139 +++++++---- FATrace.WPLApp/Services/NavigationServices.cs | 5 - FATrace.WPLApp/Utils/PasswordBoxAssistant.cs | 73 ++++++ .../ViewModels/DashboardViewModel.cs | 26 +- .../ViewModels/DialogHelpManualViewModel.cs | 32 +++ .../ViewModels/HistoryAlarmViewModel.cs | 229 ++++++++++++++++++ FATrace.WPLApp/ViewModels/LogViewModel.cs | 178 ++++++++++++++ FATrace.WPLApp/ViewModels/LoginViewModel.cs | 96 ++++++++ FATrace.WPLApp/ViewModels/MainViewModel.cs | 14 +- .../ViewModels/RawProUseViewModel.cs | 90 +++---- FATrace.WPLApp/Views/HelpManualView.xaml | 12 + FATrace.WPLApp/Views/HelpManualView.xaml.cs | 28 +++ FATrace.WPLApp/Views/HistoryAlarmView.xaml | 225 +++++++++++++++++ FATrace.WPLApp/Views/HistoryAlarmView.xaml.cs | 28 +++ FATrace.WPLApp/Views/LogView.xaml | 130 ++++++++++ FATrace.WPLApp/Views/LogView.xaml.cs | 28 +++ FATrace.WPLApp/Views/LoginView.xaml | 134 ++++++++++ FATrace.WPLApp/Views/LoginView.xaml.cs | 28 +++ 31 files changed, 1866 insertions(+), 267 deletions(-) create mode 100644 FATrace.Model/HistoryAlarm.cs create mode 100644 FATrace.WPLApp/CsvModel/RawProUserCsvDtoMap.cs create mode 100644 FATrace.WPLApp/MapperProfile/MapperConfig.cs create mode 100644 FATrace.WPLApp/MapperProfile/RawProUseProfile.cs create mode 100644 FATrace.WPLApp/Report/UserManual.md create mode 100644 FATrace.WPLApp/Utils/PasswordBoxAssistant.cs create mode 100644 FATrace.WPLApp/ViewModels/DialogHelpManualViewModel.cs create mode 100644 FATrace.WPLApp/ViewModels/HistoryAlarmViewModel.cs create mode 100644 FATrace.WPLApp/ViewModels/LogViewModel.cs create mode 100644 FATrace.WPLApp/ViewModels/LoginViewModel.cs create mode 100644 FATrace.WPLApp/Views/HelpManualView.xaml create mode 100644 FATrace.WPLApp/Views/HelpManualView.xaml.cs create mode 100644 FATrace.WPLApp/Views/HistoryAlarmView.xaml create mode 100644 FATrace.WPLApp/Views/HistoryAlarmView.xaml.cs create mode 100644 FATrace.WPLApp/Views/LogView.xaml create mode 100644 FATrace.WPLApp/Views/LogView.xaml.cs create mode 100644 FATrace.WPLApp/Views/LoginView.xaml create mode 100644 FATrace.WPLApp/Views/LoginView.xaml.cs diff --git a/FATrace.App/frmMain.Designer.cs b/FATrace.App/frmMain.Designer.cs index 777ad06..e8f78df 100644 --- a/FATrace.App/frmMain.Designer.cs +++ b/FATrace.App/frmMain.Designer.cs @@ -85,7 +85,7 @@ namespace FATrace.App tabPage3 = new TabPage(); btnLogin = new Button(); txtPassword = new TextBox(); - txtUserName = new TextBox(); + txtCheckUserName = new TextBox(); label17 = new Label(); label16 = new Label(); label15 = new Label(); @@ -103,6 +103,8 @@ namespace FATrace.App dtpSearchStartTime = new DateTimePicker(); dataGridView1 = new DataGridView(); label22 = new Label(); + txtOpName = new TextBox(); + label28 = new Label(); statusStrip1.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit(); splitContainer1.Panel1.SuspendLayout(); @@ -369,11 +371,11 @@ namespace FATrace.App // // btnWeightPrint // - btnWeightPrint.Font = new Font("Microsoft YaHei UI", 15.75F, FontStyle.Bold, GraphicsUnit.Point, 134); + btnWeightPrint.Font = new Font("Microsoft YaHei UI", 20F, FontStyle.Bold); btnWeightPrint.ForeColor = SystemColors.ControlDarkDark; - btnWeightPrint.Location = new Point(741, 119); + btnWeightPrint.Location = new Point(670, 95); btnWeightPrint.Name = "btnWeightPrint"; - btnWeightPrint.Size = new Size(119, 47); + btnWeightPrint.Size = new Size(190, 71); btnWeightPrint.TabIndex = 9; btnWeightPrint.Text = "称重打印"; btnWeightPrint.UseVisualStyleBackColor = true; @@ -735,9 +737,11 @@ namespace FATrace.App // // tabPage3 // + tabPage3.Controls.Add(txtOpName); + tabPage3.Controls.Add(label28); tabPage3.Controls.Add(btnLogin); tabPage3.Controls.Add(txtPassword); - tabPage3.Controls.Add(txtUserName); + tabPage3.Controls.Add(txtCheckUserName); tabPage3.Controls.Add(label17); tabPage3.Controls.Add(label16); tabPage3.Controls.Add(label15); @@ -753,7 +757,7 @@ namespace FATrace.App btnLogin.BackColor = Color.SandyBrown; btnLogin.Font = new Font("微软雅黑", 22F); btnLogin.ForeColor = SystemColors.ButtonHighlight; - btnLogin.Location = new Point(329, 387); + btnLogin.Location = new Point(355, 372); btnLogin.Name = "btnLogin"; btnLogin.Size = new Size(224, 79); btnLogin.TabIndex = 7; @@ -764,24 +768,24 @@ namespace FATrace.App // txtPassword // txtPassword.Font = new Font("微软雅黑", 16F); - txtPassword.Location = new Point(365, 296); + txtPassword.Location = new Point(335, 296); txtPassword.Name = "txtPassword"; - txtPassword.Size = new Size(224, 36); + txtPassword.Size = new Size(289, 36); txtPassword.TabIndex = 6; // - // txtUserName + // txtCheckUserName // - txtUserName.Font = new Font("微软雅黑", 16F); - txtUserName.Location = new Point(365, 218); - txtUserName.Name = "txtUserName"; - txtUserName.Size = new Size(224, 36); - txtUserName.TabIndex = 5; + txtCheckUserName.Font = new Font("微软雅黑", 16F); + txtCheckUserName.Location = new Point(335, 218); + txtCheckUserName.Name = "txtCheckUserName"; + txtCheckUserName.Size = new Size(97, 36); + txtCheckUserName.TabIndex = 5; // // label17 // label17.AutoSize = true; label17.Font = new Font("微软雅黑", 16F); - label17.Location = new Point(277, 296); + label17.Location = new Point(247, 296); label17.Name = "label17"; label17.Size = new Size(62, 30); label17.TabIndex = 4; @@ -791,17 +795,17 @@ namespace FATrace.App // label16.AutoSize = true; label16.Font = new Font("微软雅黑", 16F); - label16.Location = new Point(277, 219); + label16.Location = new Point(247, 219); label16.Name = "label16"; label16.Size = new Size(84, 30); label16.TabIndex = 3; - label16.Text = "用户名:"; + label16.Text = "确认者:"; // // label15 // label15.AutoSize = true; label15.Font = new Font("微软雅黑", 26.75F); - label15.Location = new Point(365, 128); + label15.Location = new Point(361, 128); label15.Name = "label15"; label15.Size = new Size(164, 46); label15.TabIndex = 2; @@ -957,6 +961,24 @@ namespace FATrace.App label22.Text = "历史数据"; label22.TextAlign = ContentAlignment.MiddleCenter; // + // txtOpName + // + txtOpName.Font = new Font("微软雅黑", 16F); + txtOpName.Location = new Point(527, 218); + txtOpName.Name = "txtOpName"; + txtOpName.Size = new Size(97, 36); + txtOpName.TabIndex = 9; + // + // label28 + // + label28.AutoSize = true; + label28.Font = new Font("微软雅黑", 16F); + label28.Location = new Point(439, 219); + label28.Name = "label28"; + label28.Size = new Size(84, 30); + label28.TabIndex = 8; + label28.Text = "操作者:"; + // // frmMain // AutoScaleDimensions = new SizeF(7F, 17F); @@ -1039,7 +1061,7 @@ namespace FATrace.App private Label label14; private Button btnLogin; private TextBox txtPassword; - private TextBox txtUserName; + private TextBox txtCheckUserName; private Label label17; private Label label16; private Label label15; @@ -1077,5 +1099,7 @@ namespace FATrace.App private PictureBox pictureBox2; private PictureBox pictureBox3; private PictureBox pictureBox4; + private TextBox txtOpName; + private Label label28; } } \ No newline at end of file diff --git a/FATrace.App/frmMain.cs b/FATrace.App/frmMain.cs index 883c273..eec39f9 100644 --- a/FATrace.App/frmMain.cs +++ b/FATrace.App/frmMain.cs @@ -182,9 +182,14 @@ namespace FATrace.App } /// - /// 当前用户名 + /// 当前确认者用户名 /// - public string CurrentOperationNo { get; set; } = string.Empty; + public string CurrentCheckUserNo { get; set; } = string.Empty; + + /// + /// 当前操作者用户名 + /// + public string CurrentOpUserNo { get; set; } = string.Empty; public string CurrentOperationNoUserLevel { get; set; } = string.Empty; @@ -696,13 +701,14 @@ namespace FATrace.App Batch = CurSelectedRawProInput.Batch, InBagCode = Code, BoxCode = Code + ",A", - OpUser = CurrentOperationNo, - CheckUser = CurrentOperationNo, + OpUser = CurrentOpUserNo, + CheckUser = CurrentCheckUserNo, RawCode = CurSelectedRawProInput.RawCode, ShelfLife = CurSelectedRawProInput.ShelfLife, RawName = CurSelectedRawProInput.RawName, RemainWeight = NewRemainWeight, Weight = CurWeight, + DeliveryDate= DateTime.Now.ToString("yyyy-MM-dd"), WeightTime = DateTime.Now, StockWeight = CurSelectedRawProInput.Weight, }).ExecuteAffrows(); @@ -714,7 +720,7 @@ namespace FATrace.App txtRemainWeight.Text = CurSelectedRawProInput.RemainWeight.ToString(); //称重检查,如果剩余重量为0,进行提示,当剩余的重量小于称重的重量,进行提示 - if (CurSelectedRawProInput.RemainWeight < CurWeight / 1000) + if (CurSelectedRawProInput.RemainWeight < CurWeight / 1000.0) { //剩余的重量小于称重的重量,进行提示,强制进行清零 Task.Run(() => @@ -928,9 +934,14 @@ namespace FATrace.App { try { - if (string.IsNullOrEmpty(txtUserName.Text)) + if (string.IsNullOrEmpty(txtCheckUserName.Text)) { - MessageBox.Show("请输入用户名称", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); + MessageBox.Show("请输入确认者用户名称", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + if (string.IsNullOrEmpty(txtOpName.Text)) + { + MessageBox.Show("请输入操作者用户名称", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (string.IsNullOrEmpty(txtPassword.Text)) @@ -939,17 +950,17 @@ namespace FATrace.App return; } - var ListUser = FSqlContext.FDb.Select().Where(a => a.UserName == txtUserName.Text.Trim()).ToList(); + var ListUser = FSqlContext.FDb.Select().Where(a => a.UserName == txtCheckUserName.Text.Trim()).ToList(); if (ListUser != null && ListUser.Count() > 0) { if (ListUser.FirstOrDefault().Password == txtPassword.Text.Trim()) { MessageBox.Show("登录成功!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); - Main_PopUserNameEvent(txtPassword.Text.Trim(), ListUser.FirstOrDefault().AccessLevel); + Main_PopUserNameEvent(txtCheckUserName.Text.Trim(), txtOpName.Text.Trim(), ListUser.FirstOrDefault().AccessLevel); this.TabControlMain.SelectedIndex = 0; - txtUserName.Text = ""; + txtCheckUserName.Text = ""; txtPassword.Text = ""; //PopUserNameEvent(txtUserName.Text.Trim()); //this.Close(); @@ -977,15 +988,17 @@ namespace FATrace.App /// 用户登录的事件发布方法 /// /// - private void Main_PopUserNameEvent(string UserName, string UserLevel) + private void Main_PopUserNameEvent(string CheckUserName, string OpUserName, string UserLevel) { try { - CurrentOperationNo = UserName; + CurrentCheckUserNo = CheckUserName; + CurrentOpUserNo = OpUserName; + CurrentOperationNoUserLevel = UserLevel; Invoke(new Action(() => { - tslCurrentUser.Text = "当前用户:" + CurrentOperationNo; + tslCurrentUser.Text = "确认者用户:" + CurrentCheckUserNo; tslCurrentUser.BackColor = Color.Green; tslCurrentUser.ForeColor = Color.White; })); @@ -1006,7 +1019,7 @@ namespace FATrace.App private void btnSysConfig_Click(object sender, EventArgs e) { - if (!string.IsNullOrEmpty(CurrentOperationNo)) + if (!string.IsNullOrEmpty(CurrentCheckUserNo)) { } @@ -1048,7 +1061,7 @@ namespace FATrace.App private void btnMain_Click(object sender, EventArgs e) { - if (!string.IsNullOrEmpty(CurrentOperationNo)) + if (!string.IsNullOrEmpty(CurrentCheckUserNo)) { TabControlMain.SelectedIndex = 0; } diff --git a/FATrace.Model/HistoryAlarm.cs b/FATrace.Model/HistoryAlarm.cs new file mode 100644 index 0000000..a042f54 --- /dev/null +++ b/FATrace.Model/HistoryAlarm.cs @@ -0,0 +1,51 @@ +using FreeSql.DataAnnotations; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FATrace.Model +{ + /// + /// 报警 HistoryAlarm + /// + [Table(Name = "HistoryAlarm")] + public class HistoryAlarm + { + /// + /// 主键 + /// + [Column(IsPrimary = true, IsIdentity = true)] + public long Id { get; set; } + + /// + /// 报警消息分类 + /// + [Column(Name = "Category", IsNullable = false, StringLength = 20)] + public string? Category { get; set; } + + /// + /// 报警消息 + /// + [Column(Name = "Message", IsNullable = false, StringLength = 150)] + public string? Message { get; set; } + + ///// + ///// 报警时长 + ///// 秒 + ///// + //[Column(Name = "Duration")] + //public long Duration { get; set; } + + [Column(Name = "CreateTime")] + public DateTime CreateTime { get; set; } + + /// + /// ///////////////////////////////////////////导航属性/////////////////////////////////////////////////////// + /// + + //public Guid? AlarmAddressId { get; set; } + //public AlarmAddress AlarmAddress { get; set; } + } +} diff --git a/FATrace.Model/RawProUse.cs b/FATrace.Model/RawProUse.cs index f5a6bc7..3a9cc86 100644 --- a/FATrace.Model/RawProUse.cs +++ b/FATrace.Model/RawProUse.cs @@ -49,6 +49,13 @@ namespace FATrace.Model [Column(Name = "Batch", IsNullable = false, StringLength = 50)] public string? Batch { get; set; } + /// + /// 配料日期 当天日期 + /// 年,月,日 + /// + [Column(Name = "DeliveryDate", IsNullable = false, StringLength = 30)] + public string? DeliveryDate { get; set; } + /// /// 保质期 年 /// diff --git a/FATrace.WPLApp/App.config b/FATrace.WPLApp/App.config index 55a0ac9..79bdc4f 100644 --- a/FATrace.WPLApp/App.config +++ b/FATrace.WPLApp/App.config @@ -8,6 +8,7 @@ + diff --git a/FATrace.WPLApp/App.xaml.cs b/FATrace.WPLApp/App.xaml.cs index 2b07f00..e961975 100644 --- a/FATrace.WPLApp/App.xaml.cs +++ b/FATrace.WPLApp/App.xaml.cs @@ -1,20 +1,21 @@ +using AutoMapper; +using DryIoc; +using FATrace.Com; +using FATrace.WPLApp.MapperProfile; +using FATrace.WPLApp.RegionAdapter; using FATrace.WPLApp.Services; +using FATrace.WPLApp.ViewModels; +using FATrace.WPLApp.Views; +using FreeSql; using Prism.DryIoc; using Prism.Ioc; using Prism.Regions; -using System.Windows; -using DryIoc; -using AutoMapper; -using FreeSql; -using FATrace.WPLApp.Views; -using FATrace.WPLApp.ViewModels; -using FATrace.WPLApp.RegionAdapter; using Syncfusion.UI.Xaml.NavigationDrawer; -using System.Windows.Threading; +using System.Diagnostics; using System.Text; using System.Threading.Tasks; -using System.Diagnostics; -using FATrace.Com; +using System.Windows; +using System.Windows.Threading; namespace FATrace.WPLApp { @@ -189,7 +190,7 @@ namespace FATrace.WPLApp // 基础服务注册 containerRegistry.RegisterSingleton(); - containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); containerRegistry.Register(typeof(IMapper), GetMapper); //containerRegistry.RegisterSingleton(); @@ -255,12 +256,17 @@ namespace FATrace.WPLApp //containerRegistry.RegisterForNavigation(); //containerRegistry.RegisterForNavigation(); //containerRegistry.RegisterForNavigation(); - //containerRegistry.RegisterForNavigation(); + containerRegistry.RegisterForNavigation(); // 原料使用查询页 containerRegistry.RegisterForNavigation(); // 原料入库查询页 containerRegistry.RegisterForNavigation(); - + // 用户登录 + containerRegistry.RegisterForNavigation("LoginView"); + containerRegistry.RegisterForNavigation(); + // 日志查看 + containerRegistry.RegisterForNavigation("LogView"); + //containerRegistry.RegisterForNavigation(); //containerRegistry.RegisterForNavigation(); //containerRegistry.RegisterForNavigation(); diff --git a/FATrace.WPLApp/CsvModel/RawProUserCsvDtoMap.cs b/FATrace.WPLApp/CsvModel/RawProUserCsvDtoMap.cs new file mode 100644 index 0000000..b25bff9 --- /dev/null +++ b/FATrace.WPLApp/CsvModel/RawProUserCsvDtoMap.cs @@ -0,0 +1,45 @@ +using CsvHelper.Configuration; +using FATrace.WPLApp.ModelDto; + +namespace FATrace.WPLApp.CsvModel +{ + /// + /// RawProUserCsvDto 的 CSV 列映射,固定输出顺序并设置日期格式。 + /// + public class RawProUserCsvDtoMap : ClassMap + { + public RawProUserCsvDtoMap() + { + //Map(x => x.RawCode).Index(0).Name(nameof(RawProUserCsvDto.RawCode)); + //Map(x => x.RawName).Index(1).Name(nameof(RawProUserCsvDto.RawName)); + //Map(x => x.InBagCode).Index(2).Name(nameof(RawProUserCsvDto.InBagCode)); + //Map(x => x.BoxCode).Index(3).Name(nameof(RawProUserCsvDto.BoxCode)); + //Map(x => x.Batch).Index(4).Name(nameof(RawProUserCsvDto.Batch)); + //Map(x => x.ShelfLife).Index(5).Name(nameof(RawProUserCsvDto.ShelfLife)); + //Map(x => x.Weight).Index(6).Name(nameof(RawProUserCsvDto.Weight)); + //Map(x => x.DeliveryDate).Index(7).Name(nameof(RawProUserCsvDto.DeliveryDate)); + //Map(x => x.RemainWeight).Index(8).Name(nameof(RawProUserCsvDto.RemainWeight)); + //Map(x => x.StockWeight).Index(9).Name(nameof(RawProUserCsvDto.StockWeight)); + //Map(x => x.WeightTime).Index(10).Name(nameof(RawProUserCsvDto.WeightTime)).TypeConverterOption.Format("yyyy-MM-dd HH:mm:ss"); + //Map(x => x.OpUser).Index(11).Name(nameof(RawProUserCsvDto.OpUser)); + //Map(x => x.CheckUser).Index(12).Name(nameof(RawProUserCsvDto.CheckUser)); + //Map(x => x.OutTime).Index(13).Name(nameof(RawProUserCsvDto.OutTime)).TypeConverterOption.Format("yyyy-MM-dd HH:mm:ss"); + + Map(x => x.RawCode).Index(0).Name("原料编号"); + Map(x => x.RawName).Index(1).Name("原料名称"); + Map(x => x.InBagCode).Index(2).Name("内袋二维码"); + Map(x => x.BoxCode).Index(3).Name("外箱二维码"); + Map(x => x.Batch).Index(4).Name("批号"); + Map(x => x.ShelfLife).Index(5).Name("保质期"); + Map(x => x.Weight).Index(6).Name("称重重量(g)"); + Map(x => x.DeliveryDate).Index(7).Name("配料日期"); + Map(x => x.RemainWeight).Index(8).Name("剩余重量(Kg)"); + Map(x => x.StockWeight).Index(9).Name("入库总重量(Kg)"); + Map(x => x.WeightTime).Index(10).Name("称重时间").TypeConverterOption.Format("yyyy-MM-dd HH:mm:ss"); + Map(x => x.OpUser).Index(11).Name("操作者"); + Map(x => x.CheckUser).Index(12).Name("确认者"); + Map(x => x.OutTime).Index(13).Name("外箱扫码时间").TypeConverterOption.Format("yyyy-MM-dd HH:mm:ss"); + + } + } +} diff --git a/FATrace.WPLApp/FATrace.WPLApp.csproj b/FATrace.WPLApp/FATrace.WPLApp.csproj index bc81944..393e1c5 100644 --- a/FATrace.WPLApp/FATrace.WPLApp.csproj +++ b/FATrace.WPLApp/FATrace.WPLApp.csproj @@ -1,4 +1,4 @@ - + Exe @@ -58,7 +58,6 @@ - @@ -68,4 +67,10 @@ + + + PreserveNewest + + + diff --git a/FATrace.WPLApp/MapperProfile/MapperConfig.cs b/FATrace.WPLApp/MapperProfile/MapperConfig.cs new file mode 100644 index 0000000..2bccd3d --- /dev/null +++ b/FATrace.WPLApp/MapperProfile/MapperConfig.cs @@ -0,0 +1,35 @@ +using AutoMapper; +using FATrace.WPLApp.Services; +using Prism.Ioc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FATrace.WPLApp.MapperProfile +{ + public class MapperConfig : IMapperProvider + { + private readonly MapperConfiguration _Configuration; + + public MapperConfig(IContainerProvider container) + { + _Configuration = new MapperConfiguration(configure => + { + //var assemblys = AppDomain.CurrentDomain.GetAssemblies(); + //configure.AddMaps(assemblys); + + configure.ConstructServicesUsing(container.Resolve); + + //扫描profile文件 + configure.AddMaps(AppDomain.CurrentDomain.GetAssemblies()); + }); + } + + public IMapper GetMapper() + { + return _Configuration.CreateMapper(); + } + } +} diff --git a/FATrace.WPLApp/MapperProfile/RawProUseProfile.cs b/FATrace.WPLApp/MapperProfile/RawProUseProfile.cs new file mode 100644 index 0000000..db95254 --- /dev/null +++ b/FATrace.WPLApp/MapperProfile/RawProUseProfile.cs @@ -0,0 +1,20 @@ +using AutoMapper; +using FATrace.Model; +using FATrace.WPLApp.ModelDto; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FATrace.WPLApp.MapperProfile +{ + public class RawProUseProfile : Profile + { + public RawProUseProfile() + { + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + } + } +} diff --git a/FATrace.WPLApp/ModelDto/RawProUseDto.cs b/FATrace.WPLApp/ModelDto/RawProUseDto.cs index a0fdd2d..b3eba06 100644 --- a/FATrace.WPLApp/ModelDto/RawProUseDto.cs +++ b/FATrace.WPLApp/ModelDto/RawProUseDto.cs @@ -39,6 +39,11 @@ namespace FATrace.WPLApp.ModelDto /// public string? Batch { get; set; } + /// + /// 配料日期 + /// + public string? DeliveryDate { get; set; } + /// /// 保质期 年 /// diff --git a/FATrace.WPLApp/Report/UserManual.md b/FATrace.WPLApp/Report/UserManual.md new file mode 100644 index 0000000..7763d7e --- /dev/null +++ b/FATrace.WPLApp/Report/UserManual.md @@ -0,0 +1,144 @@ +# FATrace 用户说明书(WPLApp) + +版本:v1.0 适用模块:FATrace.WPLApp(桌面应用) + +## 1. 简介 + +FATrace 是一套用于食品添加剂生产过程追溯与运行监控的桌面应用。WPLApp 负责生产端的日常使用,包括: + +- 实时运行信息查看与统计总览(Dashboard) +- 历史报警查询(History Alarm) +- 用户登录与状态显示 +- PLC 通信状态指示与当前终端信息(底部状态栏) + +本说明书面向一线操作员、现场工程、以及系统管理员,帮助快速上手并定位常见问题。 + +## 2. 安装与启动 + +- 安装:由管理员完成程序分发与安装(通常包含可执行文件与依赖项)。 +- 运行:双击启动桌面应用,进入主界面。 +- 字体资源:应用内置图标字体,界面会自动加载,无需额外配置。 +- 数据库:应用使用 FreeSql 进行数据访问。数据库连接等参数由管理员预配置。 + +如启动失败、页面空白或频繁报错,请联系系统管理员检查运行环境与数据库连接配置。 + +## 3. 登录与账号 + +- 打开左侧菜单“用户登录”进入登录页。 +- 输入“用户名”和“密码”,点击“登录”或直接按回车键。 +- 登录成功后自动导航到 Dashboard;底部状态栏将显示“当前用户:已登录”。 + +注意:当前版本按用户表中明文密码进行校验,请妥善保管账号信息。若忘记密码,请联系管理员重置。 + +## 4. 主界面与导航 + +应用主界面由顶部、内容区域、底部状态栏组成。常用页面: + +- Dashboard(统计与实时信息) +- 历史报警(History Alarm) +- 原料相关查询(如原料使用、原料入库等,若已在系统中启用) + +通过左侧菜单切换页面。页面内容区支持自动刷新与数据联动,无需手动刷新按钮。 + +## 5. Dashboard(统计与实时信息) + +Dashboard 提供生产运行的概览信息,包括: + +- 统计卡片:按“日、月、年、累计”汇总原料使用重量(RawProUse)。 +- 实时运行信息:系统拦截并呈现关键运行日志与事件(如扫码、喷码、过程提示等)。 +- PLC 连接状态:以指示灯颜色直观显示当前 PLC 通信状态。 +- 预留区域:用于后续扩展显示(如更多曲线、指标或提示)。 + +刷新机制:当产生关键业务事件(例如扫码处理)时,系统自动发布事件触发 Dashboard 刷新,无需手动点击。 + +使用建议: + +- 关注统计卡片的趋势变化,快速判断当日与周期产量情况。 +- 实时信息区域用于排查当班异常,如出现错误或报警文字,可结合“历史报警”页进一步定位。 + +## 6. 历史报警(History Alarm) + +用于查询系统运行过程中记录的历史报警。功能特性: + +- 支持按关键字、类别、时间范围进行过滤查询。 +- 结果列表分页显示,便于快速定位。 +- 报警内容来自系统在关键点输出的报警信息,出现后会自动写入数据库。 + +使用步骤: + +1. 打开“历史报警”页。 +2. 选择查询条件(时间范围、类别、关键字等)。 +3. 点击查询,查看结果列表。 + +排查建议: + +- 若报警持续出现,先检查 PLC 连接与设备状态,再根据报警文本采取现场处理措施。 +- 如怀疑数据库未记录,关注是否出现数据库连接问题或权限异常。 + +## 7. 底部状态栏(FootView) + +底部状态栏持续显示系统运行关键信息: + +- PLC 通信指示灯:绿色表示连接正常;红色表示断开或异常。 +- PLC 终端信息:显示配置的 IP:Port,便于对照设备。 +- 当前用户与登录状态:显示当前登录用户及是否已登录。 + +当 PLC 状态改变时,这里会同步更新,便于第一时间发现通信问题。 + +## 8. 常见操作流程 + +- 开机与登录:启动应用 → 进入“用户登录” → 登录成功 → 自动跳转至 Dashboard。 +- 查看产线运行:在 Dashboard 观察统计卡片与实时信息,注意异常提示。 +- 报警定位与复盘:在“历史报警”中按时间段和关键字查询,结合现场记录进行处理与复盘。 + +## 9. 常见问题与排查 + +- 无法登录: + - 确认用户名、密码输入正确; + - 若提示“用户不存在”,请联系管理员检查用户表; + - 若提示数据库错误,联系管理员检查数据库连接与网络。 + +- Dashboard 统计为空: + - 确认数据库中是否存在有效的原料使用数据; + - 等待系统产生新数据或触发刷新事件; + - 检查时间范围与系统时间是否正确。 + +- 实时信息无更新: + - 确认产线是否有新的扫码或业务事件; + - 若日志区长时间无输出,可能产线空闲或通信异常。 + +- PLC 指示红色: + - 检查 PLC 电源、网线、IP 配置; + - 确认与应用配置一致(IP、端口); + - 联系电气与系统工程师排查网络或驱动问题。 + +- 历史报警无记录: + - 近期是否确有报警触发; + - 检查数据库是否可写; + - 关注应用日志,必要时联系管理员。 + +## 10. 使用技巧 + +- 登录页支持“回车键”快捷登录。 +- 关注底部状态栏的用户与 PLC 状态,能快速判断系统是否处于可用状态。 +- 建议按班次定期查看“历史报警”,形成闭环处理与优化记录。 + +## 11. 安全与权限 + +- 账号仅限本人使用,请勿外借与分享。 +- 退出离岗前可在“用户登录”页切换用户(如需要可联系管理员提供退出功能)。 +- 如需更细粒度权限(基于岗位/等级),请与管理员沟通后续版本规划。 + +## 12. 版本与更新 + +- 本说明书适配当前应用版本的已上线功能。新增功能或 UI 调整将另行更新说明。 +- 如需升级或安装补丁,请联系系统管理员统一安排。 + +## 13. 反馈与支持 + +- 使用中如遇到界面异常、统计不准、报警缺失等问题: + - 记录问题发生时间、操作步骤、界面提示; + - 截图或拍照(含底部状态栏与异常信息); + - 联系系统管理员或运维支持。 + +— 结束 — diff --git a/FATrace.WPLApp/Services/CsvServices.cs b/FATrace.WPLApp/Services/CsvServices.cs index 40f344d..5a81bb6 100644 --- a/FATrace.WPLApp/Services/CsvServices.cs +++ b/FATrace.WPLApp/Services/CsvServices.cs @@ -10,12 +10,15 @@ using CsvHelper; using CsvHelper.Configuration; using FATrace.WPLApp.ModelDto; using Prism.Mvvm; +using FATrace.WPLApp.CsvModel; +using FATrace.Com; namespace FATrace.WPLApp.Services { /// /// CSV服务 ///导出CSV文件 + ///读取CSV 文件 /// public class CsvServices:BindableBase { @@ -26,74 +29,121 @@ namespace FATrace.WPLApp.Services { LogService = logService; FreeSql = freeSql; + + RawProUseCsvPath = GetConfiguredOutputDirectory(); } /// - /// 导出单条记录为 CSV 文件,文件名即记录的 InBagCode(自动追加 .csv 扩展名)。 + /// 导出Csv目录 /// - /// 要导出的数据项,类型为 RawProUserCsvDto - /// 导出目录,不存在将自动创建 - /// 若目标文件已存在,是否允许覆盖 - /// 导出后的完整文件路径 - /// item 或 outputDirectory 为 null - /// InBagCode 为空或仅空白 - /// 文件已存在且不允许覆盖 - public string ExportSingle(RawProUserCsvDto item, string outputDirectory, bool overwrite = false) + private string RawProUseCsvPath { get; set; } + + /// + /// 从 App.config 读取导出目录(key: RawProUseCsvPath)。 + /// + private string GetConfiguredOutputDirectory() { - if (item is null) throw new ArgumentNullException(nameof(item)); - if (outputDirectory is null) throw new ArgumentNullException(nameof(outputDirectory)); + var dir = ConfigHelper.GetValue("RawProUseCsvPath"); + // 支持环境变量(如 %USERPROFILE%) + dir = Environment.ExpandEnvironmentVariables(dir ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(dir)) + throw new InvalidOperationException("配置项 RawProUseCsvPath 为空"); + return dir; + } - var inBagCode = (item.InBagCode ?? string.Empty).Trim(); - if (string.IsNullOrWhiteSpace(inBagCode)) - throw new ArgumentException("InBagCode 不能为空", nameof(item.InBagCode)); - - // 目录确保存在 - Directory.CreateDirectory(outputDirectory); - - var safeName = SanitizeFileName(inBagCode); - var filePath = Path.Combine(outputDirectory, safeName + ".csv"); - - if (File.Exists(filePath) && !overwrite) + /// + /// 导出单条记录为 CSV 文件,文件名为记录的 InBagCode(自动追加 .csv 扩展名)。 + /// 使用 App.config 中的 RawProUseCsvPath 作为导出目录,目录不存在会自动创建。 + /// 失败时记录错误日志并返回空字符串。 + /// + /// 要导出的数据项,类型为 RawProUserCsvDto(InBagCode 不能为空) + /// 若目标文件已存在,是否允许覆盖 + /// 成功:返回导出后的完整文件路径;失败:返回空字符串 + public string ExportSingle(RawProUserCsvDto item, bool overwrite = false) + { + try { - throw new IOException($"文件已存在且不允许覆盖: {filePath}"); + if (item is null) throw new ArgumentNullException(nameof(item)); + // 线程安全:使用局部缓存目录变量,必要时从配置读取一次并回写字段。 + var outputDir = RawProUseCsvPath; + if (string.IsNullOrWhiteSpace(outputDir)) + { + outputDir = GetConfiguredOutputDirectory(); + RawProUseCsvPath = outputDir; + } + + var inBagCode = (item.InBagCode ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(inBagCode)) + throw new ArgumentException("InBagCode 不能为空", nameof(item.InBagCode)); + + // 目录确保存在 + Directory.CreateDirectory(outputDir); + + var safeName = SanitizeFileName(inBagCode); + var filePath = Path.Combine(outputDir, safeName + ".csv"); + + if (File.Exists(filePath) && !overwrite) + { + throw new IOException($"文件已存在且不允许覆盖: {filePath}"); + } + + // 使用 UTF8 BOM,便于 Excel 正确识别中文 + using var writer = new StreamWriter(filePath, false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)); + using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture); + + // 固定列顺序映射,并设置日期格式 + csv.Context.RegisterClassMap(); + + csv.WriteHeader(); + csv.NextRecord(); + csv.WriteRecord(item); + csv.NextRecord(); + + return filePath; + } + catch (Exception ex) + { + var code = item?.InBagCode ?? string.Empty; + var outputDir = RawProUseCsvPath; + LogService.Error($"导出CSV失败,目录={outputDir}, InBagCode={code}", ex); + return string.Empty; } - // 使用 UTF8 BOM,便于 Excel 正确识别中文 - using var writer = new StreamWriter(filePath, false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)); - using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture); - - // 固定列顺序映射,并设置日期格式 - csv.Context.RegisterClassMap(); - - csv.WriteHeader(); - csv.NextRecord(); - csv.WriteRecord(item); - csv.NextRecord(); - - return filePath; } /// - /// 批量导出:把每一条记录分别导出为以各自 InBagCode 命名的 CSV 文件。 + /// 异步导出单条记录(包装线程池执行)。 /// - /// 数据集合 - /// 导出目录,不存在将自动创建 - /// 若目标文件已存在,是否允许覆盖 - /// 成功导出的文件完整路径集合 - public IEnumerable ExportMany(IEnumerable items, string outputDirectory, bool overwrite = false) + public Task ExportSingleAsync(RawProUserCsvDto item, bool overwrite = false) + => Task.Run(() => ExportSingle(item, overwrite)); + + /// + /// 批量导出:将集合中的每一条记录分别导出为以 InBagCode 命名的 CSV 文件。 + /// 成功项返回其路径,失败项返回空字符串(并已记录日志)。 + /// + public IEnumerable ExportMany(IEnumerable items, bool overwrite = false) { if (items is null) throw new ArgumentNullException(nameof(items)); - var results = new List(); - foreach (var item in items) + foreach (var it in items) { - // 逐条导出,沿用相同规则 - var path = ExportSingle(item, outputDirectory, overwrite); - results.Add(path); + results.Add(ExportSingle(it, overwrite)); } return results; } + /// + /// 异步批量导出(并行度受限于线程池;如需更精细控制,可改为并行批处理)。 + /// + public async Task> ExportManyAsync(IEnumerable items, bool overwrite = false) + { + if (items is null) throw new ArgumentNullException(nameof(items)); + var list = items.ToList(); + var tasks = list.Select(it => Task.Run(() => ExportSingle(it, overwrite))); + var paths = await Task.WhenAll(tasks); + return paths.ToList(); + } + /// /// 清理文件名中的非法字符。 /// @@ -108,44 +158,6 @@ namespace FATrace.WPLApp.Services return sb.ToString(); } - /// - /// RawProUserCsvDto 的 CSV 列映射,固定输出顺序并设置日期格式。 - /// - private sealed class RawProUserCsvDtoMap : ClassMap - { - public RawProUserCsvDtoMap() - { - //Map(x => x.RawCode).Index(0).Name(nameof(RawProUserCsvDto.RawCode)); - //Map(x => x.RawName).Index(1).Name(nameof(RawProUserCsvDto.RawName)); - //Map(x => x.InBagCode).Index(2).Name(nameof(RawProUserCsvDto.InBagCode)); - //Map(x => x.BoxCode).Index(3).Name(nameof(RawProUserCsvDto.BoxCode)); - //Map(x => x.Batch).Index(4).Name(nameof(RawProUserCsvDto.Batch)); - //Map(x => x.ShelfLife).Index(5).Name(nameof(RawProUserCsvDto.ShelfLife)); - //Map(x => x.Weight).Index(6).Name(nameof(RawProUserCsvDto.Weight)); - //Map(x => x.DeliveryDate).Index(7).Name(nameof(RawProUserCsvDto.DeliveryDate)); - //Map(x => x.RemainWeight).Index(8).Name(nameof(RawProUserCsvDto.RemainWeight)); - //Map(x => x.StockWeight).Index(9).Name(nameof(RawProUserCsvDto.StockWeight)); - //Map(x => x.WeightTime).Index(10).Name(nameof(RawProUserCsvDto.WeightTime)).TypeConverterOption.Format("yyyy-MM-dd HH:mm:ss"); - //Map(x => x.OpUser).Index(11).Name(nameof(RawProUserCsvDto.OpUser)); - //Map(x => x.CheckUser).Index(12).Name(nameof(RawProUserCsvDto.CheckUser)); - //Map(x => x.OutTime).Index(13).Name(nameof(RawProUserCsvDto.OutTime)).TypeConverterOption.Format("yyyy-MM-dd HH:mm:ss"); - - Map(x => x.RawCode).Index(0).Name("原料编号"); - Map(x => x.RawName).Index(1).Name("原料名称"); - Map(x => x.InBagCode).Index(2).Name("内袋二维码"); - Map(x => x.BoxCode).Index(3).Name("外箱二维码"); - Map(x => x.Batch).Index(4).Name("批号"); - Map(x => x.ShelfLife).Index(5).Name("保质期"); - Map(x => x.Weight).Index(6).Name("称重重量(g)"); - Map(x => x.DeliveryDate).Index(7).Name("配料日期"); - Map(x => x.RemainWeight).Index(8).Name("剩余重量(Kg)"); - Map(x => x.StockWeight).Index(9).Name("入库总重量(Kg)"); - Map(x => x.WeightTime).Index(10).Name("称重时间").TypeConverterOption.Format("yyyy-MM-dd HH:mm:ss"); - Map(x => x.OpUser).Index(11).Name("操作者"); - Map(x => x.CheckUser).Index(12).Name("确认者"); - Map(x => x.OutTime).Index(13).Name("出库时间").TypeConverterOption.Format("yyyy-MM-dd HH:mm:ss"); - - } - } + } } diff --git a/FATrace.WPLApp/Services/DataServices.cs b/FATrace.WPLApp/Services/DataServices.cs index f2fbc3a..c269f05 100644 --- a/FATrace.WPLApp/Services/DataServices.cs +++ b/FATrace.WPLApp/Services/DataServices.cs @@ -1,13 +1,15 @@ +using AutoMapper; using FATrace.Model; +using FATrace.WPLApp.Events; +using FATrace.WPLApp.ModelDto; using FATrace.WPLApp.Models; using HslCommunication; using HslCommunication.Profinet.Keyence; +using Prism.Events; using Prism.Mvvm; using System.Collections.Concurrent; using System.Windows; using ComConfigHelper = FATrace.Com.ConfigHelper; -using Prism.Events; -using FATrace.WPLApp.Events; namespace FATrace.WPLApp.Services { @@ -17,13 +19,14 @@ namespace FATrace.WPLApp.Services /// public class DataServices : BindableBase, IDisposable { - public DataServices(ILogService logService, IFreeSql freeSql, IEventAggregator eventAggregator, SysRunService sysRunService) + public DataServices(ILogService logService, IFreeSql freeSql, IEventAggregator eventAggregator, IMapper mapper, SysRunService sysRunService, CsvServices csvServices) { LogService = logService; FreeSql = freeSql; EventAggregator = eventAggregator; + Mapper = mapper; SysRunService = sysRunService; - + CsvServices = csvServices; LineSglModel = new LineSglModel(); LineSglModel.WeightScanCodeHandle += LineSglModel_WeightScanCodeHandle; LineSglModel.BoxSprayCodeReqHandle += LineSglModel_BoxSprayCodeReqHandle; @@ -32,6 +35,10 @@ namespace FATrace.WPLApp.Services PLCLinkInitial(); StartPlcScan(); //var DD= RevData("DYG05030013,20250923,802,3,01,0001,"); + + //var dd1= RevData("DYG05030013,251111,10193,6,01,3"); + //var dd2 = RevData("DYG05030013,251111,2116,6,01,3"); + } @@ -53,9 +60,13 @@ namespace FATrace.WPLApp.Services if (ReqData.Count > 0) { var CodeItem = ReqData.FirstOrDefault(); - KeyencePlcMcNet.Write("D1150", RevData(CodeItem!.Code + ",A")); - Console.WriteLine($"外箱喷码:{CodeItem.Code}-发送OK"); + var BoxSprayCodeSource = CodeItem!.Code + ",A"; + var BoxSprayCodeRev = RevData(BoxSprayCodeSource); + + KeyencePlcMcNet.Write("D1150", BoxSprayCodeRev); + + Console.WriteLine($"外箱喷码:{BoxSprayCodeSource}-发送OK"); //这里需要删除数据吗?还是等扫码后删除,会不会过来新的箱子 @@ -65,6 +76,7 @@ namespace FATrace.WPLApp.Services //不存在请求的数据,报错 KeyencePlcMcNet.Write("D1102", (Int16)1); Console.WriteLine($"外箱喷码:不存在请求的数据"); + AddAlarm("外箱喷码", "不存在请求的数据"); } @@ -72,10 +84,9 @@ namespace FATrace.WPLApp.Services { await Task.Delay(1000).ConfigureAwait(false); KeyencePlcMcNet!.Write("D1102", (Int16)0); - + //KeyencePlcMcNet.Write("D1150", new Int16[30]); }); - } /// @@ -89,17 +100,20 @@ namespace FATrace.WPLApp.Services //首先复位PLC信号 KeyencePlcMcNet!.Write("D1200", (Int16)0); + string BoxScanCode = string.Empty; + BoxScanCode = this.BoxScanCode; + if (!string.IsNullOrEmpty(BoxScanCode)) { - //var IsExist = FreeSql.Select().Where(a => a.BoxCode == BoxScanCode); - var IsExist = FreeSql.Select().Where(a => a.BoxCode!.Contains(BoxScanCode));// 内包和外包一样的条码测试用 ??????????????? + var IsExist = FreeSql.Select().Where(a => a.BoxCode == BoxScanCode); + //var IsExist = FreeSql.Select().Where(a => a.BoxCode!.Contains(BoxScanCode));// 内包和外包一样的条码测试用 ??????????????? if (IsExist.Count() > 0) { //存在条码数据 OK,返回PLC结果 KeyencePlcMcNet!.Write("D1210", (Int16)1); Console.WriteLine($"外箱扫描码:{BoxScanCode}-存在找到"); - //// 加入队列 + // 加入队列 //EnqueueMessage(WeightScanCode); LogService.Info($"外箱扫描码:{BoxScanCode}"); @@ -109,28 +123,40 @@ namespace FATrace.WPLApp.Services .ExecuteAffrows(); if (DeleteResult > 0) { - Console.WriteLine($"外箱扫描码:{WeightScanCode}-删除临时队列数据成功"); - LogService.Info($"外箱扫描码:{WeightScanCode}-删除临时队列数据成功"); + Console.WriteLine($"外箱扫描码:{BoxScanCode}-删除临时队列数据成功"); + LogService.Info($"外箱扫描码:{BoxScanCode}-删除临时队列数据成功"); } else { - Console.WriteLine($"外箱扫描码:{WeightScanCode}-删除临时队列数据失败"); - LogService.Info($"外箱扫描码:{WeightScanCode}-删除临时队列数据失败"); + Console.WriteLine($"外箱扫描码:{BoxScanCode}-删除临时队列数据失败"); + LogService.Info($"外箱扫描码:{BoxScanCode}-删除临时队列数据失败"); + AddAlarm("外箱扫描", $"{BoxScanCode}-删除临时队列数据失败"); } - var Result = FreeSql.Update() + var UpdatedResult = FreeSql.Update() .Set(p => p.OutTime, DateTime.Now) - .Where(p => p.BoxCode == BoxScanCode + ",A")//外箱二维码就是外箱扫描码 - .ExecuteAffrows(); - if (Result > 0) + .Where(p => p.BoxCode == BoxScanCode)//外箱二维码就是外箱扫描码 + .ExecuteUpdated(); + if (UpdatedResult.Count() > 0) { - Console.WriteLine($"外箱扫描码:{WeightScanCode}更新时间成功"); - LogService.Info($"外箱扫描码:{WeightScanCode}更新时间成功"); + var Data = CsvServices.ExportSingle(Mapper.Map(UpdatedResult.First()), true); + if (Data != null && !string.IsNullOrEmpty(Data)) + { + Console.WriteLine($"外箱扫描码:{BoxScanCode} CSV文件生成"); + } + else + { + Console.WriteLine($"外箱扫描码:{BoxScanCode} CSV文件失败!"); + } + + Console.WriteLine($"外箱扫描码:{BoxScanCode}更新时间成功"); + LogService.Info($"外箱扫描码:{BoxScanCode}更新时间成功"); } else { - Console.WriteLine($"外箱扫描码:{WeightScanCode}更新时间失败"); - LogService.Warn($"外箱扫描码:{WeightScanCode}更新时间失败"); + Console.WriteLine($"外箱扫描码:{BoxScanCode}更新时间失败"); + LogService.Warn($"外箱扫描码:{BoxScanCode}更新时间失败"); + AddAlarm("外箱扫描", $"{BoxScanCode}更新时间失败"); } } else @@ -138,14 +164,16 @@ namespace FATrace.WPLApp.Services //不存在条码数据 NG,返回PLC结果报警 KeyencePlcMcNet!.Write("D1210", (Int16)2); - Console.WriteLine($"外箱扫描码:{WeightScanCode}-未发现外箱扫码数据在数据库中"); - LogService.Warn($"外箱扫描码:{WeightScanCode}-未发现外箱扫码数据在数据库中"); + Console.WriteLine($"外箱扫描码:{BoxScanCode}-未发现外箱扫码数据在数据库中"); + LogService.Warn($"外箱扫描码:{BoxScanCode}-未发现外箱扫码数据在数据库中"); + AddAlarm("外箱扫描", $"{BoxScanCode}-未发现外箱扫码数据在数据库中"); } } else { Console.WriteLine($"外箱扫描码为空"); LogService.Warn("外箱扫描码为空"); + AddAlarm("外箱扫描", "外箱扫描码为空"); } Task.Run(async () => @@ -155,7 +183,10 @@ namespace FATrace.WPLApp.Services KeyencePlcMcNet.Write("D1250", new Int16[30]); }); - try { EventAggregator?.GetEvent()?.Publish(true); } catch { } + + //try { EventAggregator?.GetEvent()?.Publish(true); } catch { } + + } /// @@ -200,6 +231,7 @@ namespace FATrace.WPLApp.Services { Console.WriteLine($"称重扫描码:{WeightScanCode}更新时间失败"); LogService.Warn($"称重扫描码:{WeightScanCode}更新时间失败"); + AddAlarm("称重扫描", $"{WeightScanCode}更新时间失败"); } } else @@ -209,6 +241,7 @@ namespace FATrace.WPLApp.Services Console.WriteLine($"称重扫描码:{WeightScanCode}-未发现内包扫码数据在数据库中"); LogService.Warn($"称重扫描码:{WeightScanCode}-未发现内包扫码数据在数据库中"); + AddAlarm("称重扫描", $"{WeightScanCode}-未发现内包扫码数据在数据库中"); } Task.Run(async () => @@ -225,6 +258,7 @@ namespace FATrace.WPLApp.Services { Console.WriteLine($"称重扫描码为空"); LogService.Warn("称重扫描码为空"); + AddAlarm("称重扫描", "称重扫描码为空"); } } @@ -232,7 +266,9 @@ namespace FATrace.WPLApp.Services public ILogService LogService { get; } public IFreeSql FreeSql { get; } public IEventAggregator EventAggregator { get; } + public IMapper Mapper { get; } public SysRunService SysRunService { get; } + public CsvServices CsvServices { get; } private bool _plcConnected; public bool PlcConnected @@ -281,27 +317,6 @@ namespace FATrace.WPLApp.Services /// private readonly ConcurrentQueue _messageQueue = new(); - /// - /// 入队:将字符串加入 FIFO 队列(先进先出)。允许空字符串,不允许 null。 - /// - /// 要加入队列的字符串 - /// 当 message 为 null 时抛出 - public void EnqueueMessage(string message) - { - if (message is null) throw new ArgumentNullException(nameof(message)); - _messageQueue.Enqueue(message); - } - - /// - /// 出队:尝试从 FIFO 队列取出一个字符串。 - /// - /// 输出参数:若成功则为取出的字符串;若失败(队列为空)则为 default - /// 是否成功取出 - public bool TryDequeueMessage(out string message) - { - return _messageQueue.TryDequeue(out message); - } - /// /// 产线信号模型 /// @@ -327,7 +342,6 @@ namespace FATrace.WPLApp.Services KeyencePlcMcNet.Port = Port; KeyencePlcMcNet.ConnectClose(); - KeyencePlcMcNet.ConnectTimeOut = 3000; // 连接3秒超时 OperateResult connect = KeyencePlcMcNet.ConnectServer(); if (connect.IsSuccess)//初始连接状态的显示判断 @@ -443,7 +457,7 @@ namespace FATrace.WPLApp.Services OperateResultWeightScanCode = KeyencePlcMcNet!.ReadString("D1050", 20); if (OperateResultWeightScanCode.IsSuccess) { - WeightScanCode = RevData(OperateResultWeightScanCode.Content).Replace("\r", "").Replace("\n", ""); + WeightScanCode = RevData(OperateResultWeightScanCode.Content).Replace("\r", "").Replace("\n", "").Trim(); } OperateResultWeightScanSgl = KeyencePlcMcNet!.ReadInt16("D1000"); if (OperateResultWeightScanSgl.IsSuccess) @@ -462,7 +476,8 @@ namespace FATrace.WPLApp.Services OperateResultBoxScanCode = KeyencePlcMcNet!.ReadString("D1250", 20); if (OperateResultBoxScanCode.IsSuccess) { - BoxScanCode = RevData(OperateResultBoxScanCode.Content).Replace("\r", "").Replace("\n", ""); + BoxScanCode = RevData(OperateResultBoxScanCode.Content).Replace("\r", "").Replace("\n", "").Trim(); + //BoxScanCode = OperateResultBoxScanCode.Content.Replace("\r", "").Replace("\n", ""); } OperateResultBoxScanSgl = KeyencePlcMcNet!.ReadInt16("D1200"); if (OperateResultBoxScanSgl.IsSuccess) @@ -558,7 +573,7 @@ namespace FATrace.WPLApp.Services /// /// 把字符串对调 /// - private string RevData(string Code) + public string RevData(string Code) { // 需求:PLC 字符串按“字”(2字节)进行高低字节对调,导致 ABCDEF -> BADCFE // 纠正方式:按两个字符一组交换顺序,奇数长度保留最后一个字符 @@ -578,6 +593,8 @@ namespace FATrace.WPLApp.Services else { // 奇数长度,最后一个字符原样保留 + //sb.Append((char)0x0); + sb.Append('\0'); sb.Append(src[i]); } } @@ -585,5 +602,25 @@ namespace FATrace.WPLApp.Services return sb.ToString(); } + /// + /// 写入历史报警 + /// + private void AddAlarm(string category, string message) + { + try + { + FreeSql.Insert(new HistoryAlarm + { + Category = category, + Message = message, + CreateTime = DateTime.Now + }).ExecuteAffrows(); + } + catch (Exception ex) + { + LogService.Error($"写入历史报警失败: {ex.Message}"); + } + } + } } diff --git a/FATrace.WPLApp/Services/NavigationServices.cs b/FATrace.WPLApp/Services/NavigationServices.cs index 58eafdb..ba35e6c 100644 --- a/FATrace.WPLApp/Services/NavigationServices.cs +++ b/FATrace.WPLApp/Services/NavigationServices.cs @@ -1,10 +1,5 @@ using FATrace.WPLApp.ModelDto; -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace FATrace.WPLApp.Services { diff --git a/FATrace.WPLApp/Utils/PasswordBoxAssistant.cs b/FATrace.WPLApp/Utils/PasswordBoxAssistant.cs new file mode 100644 index 0000000..86e2aec --- /dev/null +++ b/FATrace.WPLApp/Utils/PasswordBoxAssistant.cs @@ -0,0 +1,73 @@ +using System.Windows; +using System.Windows.Controls; + +namespace FATrace.WPLApp.Utils +{ + /// + /// 允许将 PasswordBox 的 Password 与 ViewModel 绑定 + /// 用法: + /// + /// + public static class PasswordBoxAssistant + { + public static readonly DependencyProperty BoundPasswordProperty = + DependencyProperty.RegisterAttached("BoundPassword", typeof(string), typeof(PasswordBoxAssistant), new PropertyMetadata(string.Empty, OnBoundPasswordChanged)); + + public static readonly DependencyProperty BindPasswordProperty = + DependencyProperty.RegisterAttached("BindPassword", typeof(bool), typeof(PasswordBoxAssistant), new PropertyMetadata(false, OnBindPasswordChanged)); + + private static readonly DependencyProperty IsUpdatingProperty = + DependencyProperty.RegisterAttached("IsUpdating", typeof(bool), typeof(PasswordBoxAssistant)); + + public static string GetBoundPassword(DependencyObject dp) => (string)dp.GetValue(BoundPasswordProperty); + public static void SetBoundPassword(DependencyObject dp, string value) => dp.SetValue(BoundPasswordProperty, value); + + public static bool GetBindPassword(DependencyObject dp) => (bool)dp.GetValue(BindPasswordProperty); + public static void SetBindPassword(DependencyObject dp, bool value) => dp.SetValue(BindPasswordProperty, value); + + private static bool GetIsUpdating(DependencyObject dp) => (bool)dp.GetValue(IsUpdatingProperty); + private static void SetIsUpdating(DependencyObject dp, bool value) => dp.SetValue(IsUpdatingProperty, value); + + private static void OnBoundPasswordChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e) + { + if (dp is PasswordBox passwordBox) + { + passwordBox.PasswordChanged -= HandlePasswordChanged; + if (!(bool)GetIsUpdating(passwordBox)) + { + passwordBox.Password = e.NewValue?.ToString() ?? string.Empty; + } + passwordBox.PasswordChanged += HandlePasswordChanged; + } + } + + private static void OnBindPasswordChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e) + { + if (dp is PasswordBox passwordBox) + { + bool wasBound = (bool)(e.OldValue ?? false); + bool needBind = (bool)(e.NewValue ?? false); + + if (wasBound) + { + passwordBox.PasswordChanged -= HandlePasswordChanged; + } + if (needBind) + { + passwordBox.PasswordChanged += HandlePasswordChanged; + } + } + } + + private static void HandlePasswordChanged(object sender, RoutedEventArgs e) + { + if (sender is PasswordBox passwordBox) + { + SetIsUpdating(passwordBox, true); + SetBoundPassword(passwordBox, passwordBox.Password); + SetIsUpdating(passwordBox, false); + } + } + } +} diff --git a/FATrace.WPLApp/ViewModels/DashboardViewModel.cs b/FATrace.WPLApp/ViewModels/DashboardViewModel.cs index e8dc2ff..618cb21 100644 --- a/FATrace.WPLApp/ViewModels/DashboardViewModel.cs +++ b/FATrace.WPLApp/ViewModels/DashboardViewModel.cs @@ -118,31 +118,7 @@ namespace FATrace.WPLApp.ViewModels public DelegateCommand RefreshCommand { get; } public DelegateCommand ClearLogsCommand { get; } #endregion - - private void StartLogTimer() - { - if (_logTimer != null) return; - _logTimer = new DispatcherTimer - { - Interval = TimeSpan.FromMilliseconds(500) - }; - _logTimer.Tick += (s, e) => - { - // 从 DataServices 的队列中取出消息并展示 - int drain = 0; - while (_data.TryDequeueMessage(out var msg)) - { - if (!string.IsNullOrWhiteSpace(msg)) - { - AppendLiveMessage(msg); - drain++; - if (drain >= 50) break; // 防止一次处理过多 - } - } - }; - _logTimer.Start(); - } - + private void AppendLiveMessage(string message) { if (string.IsNullOrWhiteSpace(message)) return; diff --git a/FATrace.WPLApp/ViewModels/DialogHelpManualViewModel.cs b/FATrace.WPLApp/ViewModels/DialogHelpManualViewModel.cs new file mode 100644 index 0000000..2a748a7 --- /dev/null +++ b/FATrace.WPLApp/ViewModels/DialogHelpManualViewModel.cs @@ -0,0 +1,32 @@ +using AutoMapper; +using FATrace.WPLApp.Core; +using Prism.Services.Dialogs; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FATrace.WPLApp.ViewModels +{ + public class DialogHelpManualViewModel : DialogViewModel + { + + /// + /// 窗口打开时的传递的参数 + /// + /// + public override void OnDialogOpened(IDialogParameters parameters) + { + //CurMeterName = parameters.GetValue("Name"); + ////RefreshChartSelectedData(); + + //var Data = FreeSql.Select().Where(a => a.MeterName == CurMeterName).ToList(); + + //ConfigLimitDtoItems = new ObservableCollection(Mapper.Map>(Data)); + + } + + } +} diff --git a/FATrace.WPLApp/ViewModels/HistoryAlarmViewModel.cs b/FATrace.WPLApp/ViewModels/HistoryAlarmViewModel.cs new file mode 100644 index 0000000..fc947c1 --- /dev/null +++ b/FATrace.WPLApp/ViewModels/HistoryAlarmViewModel.cs @@ -0,0 +1,229 @@ +using FATrace.WPLApp.Core; +using Prism.Commands; +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using FreeSql; +using FATrace.Model; +using FATrace.WPLApp.Services; +using System.Windows; +using System.Linq; + +namespace FATrace.WPLApp.ViewModels +{ + /// + /// 历史报警查询 VM + /// 支持按时间范围/类别/消息模糊查询,分页展示。 + /// + public class HistoryAlarmViewModel : NavigationViewModel + { + private readonly IFreeSql _fsql; + private readonly ILogService _log; + + public HistoryAlarmViewModel(IFreeSql fsql, ILogService log) + { + _fsql = fsql; + _log = log; + + Items = new ObservableCollection(); + CategoryList = new ObservableCollection(); + + // 默认时间范围为今日 + StartTime = DateTime.Today; + EndTime = DateTime.Today.AddDays(1).AddSeconds(-1); + + SearchCommand = new DelegateCommand(async () => await SearchAsync(), () => !IsBusy) + .ObservesProperty(() => IsBusy); + ClearCommand = new DelegateCommand(ClearFilters, () => !IsBusy) + .ObservesProperty(() => IsBusy); + + FirstPageCommand = new DelegateCommand(async () => { if (PageIndex == 1) return; PageIndex = 1; await SearchAsync(); }, () => !IsBusy && PageIndex > 1) + .ObservesProperty(() => IsBusy).ObservesProperty(() => PageIndex); + PrevPageCommand = new DelegateCommand(async () => { if (PageIndex <= 1) return; PageIndex -= 1; await SearchAsync(); }, () => !IsBusy && PageIndex > 1) + .ObservesProperty(() => IsBusy).ObservesProperty(() => PageIndex); + NextPageCommand = new DelegateCommand(async () => { if (PageIndex >= TotalPages) return; PageIndex += 1; await SearchAsync(); }, () => !IsBusy && PageIndex < TotalPages) + .ObservesProperty(() => IsBusy).ObservesProperty(() => PageIndex).ObservesProperty(() => TotalPages); + LastPageCommand = new DelegateCommand(async () => { if (TotalPages <= 0 || PageIndex == TotalPages) return; PageIndex = TotalPages; await SearchAsync(); }, () => !IsBusy && PageIndex < TotalPages) + .ObservesProperty(() => IsBusy).ObservesProperty(() => PageIndex).ObservesProperty(() => TotalPages); + } + + #region 查询条件 + public string? Category { get => _category; set { _category = value; RaisePropertyChanged(); } } + private string? _category; + + public ObservableCollection CategoryList { get; } + + public string? Keyword { get => _keyword; set { _keyword = value; RaisePropertyChanged(); } } + private string? _keyword; + + public DateTime? StartTime { get => _startTime; set { _startTime = value; RaisePropertyChanged(); } } + private DateTime? _startTime; + + public DateTime? EndTime { get => _endTime; set { _endTime = value; RaisePropertyChanged(); } } + private DateTime? _endTime; + #endregion + + #region 列表/状态/分页 + public ObservableCollection Items { get; } + + private bool _isBusy; + public bool IsBusy { get => _isBusy; set { _isBusy = value; RaisePropertyChanged(); } } + + private int _totalCount; + public int TotalCount { get => _totalCount; set { _totalCount = value; RaisePropertyChanged(); } } + + private int _pageIndex = 1; + public int PageIndex { get => _pageIndex; set { _pageIndex = value < 1 ? 1 : value; RaisePropertyChanged(); } } + + private int _pageSize = 20; + public int PageSize + { + get => _pageSize; + set + { + var newSize = value <= 0 ? 20 : value; + if (_pageSize != newSize) + { + _pageSize = newSize; + RaisePropertyChanged(); + PageIndex = 1; + if (!IsBusy) _ = SearchAsync(); + } + } + } + + private int _totalPages; + public int TotalPages { get => _totalPages; set { _totalPages = value; RaisePropertyChanged(); } } + #endregion + + #region 命令 + public DelegateCommand SearchCommand { get; } + public DelegateCommand ClearCommand { get; } + public DelegateCommand FirstPageCommand { get; } + public DelegateCommand PrevPageCommand { get; } + public DelegateCommand NextPageCommand { get; } + public DelegateCommand LastPageCommand { get; } + #endregion + + private void ClearFilters() + { + Category = "全部"; + Keyword = string.Empty; + StartTime = DateTime.Today; + EndTime = DateTime.Today.AddDays(1).AddSeconds(-1); + } + + private async Task SearchAsync() + { + if (IsBusy) return; + try + { + if (StartTime.HasValue && EndTime.HasValue && StartTime > EndTime) + { + MessageBox.Show("开始时间不能大于结束时间", "提示", MessageBoxButton.OK, MessageBoxImage.Warning); + return; + } + + IsBusy = true; + _log.Info("HistoryAlarm 查询开始"); + + var data = await Task.Run(() => + { + var q = _fsql.Select(); + + DateTime? start = StartTime; + DateTime? end = EndTime; + if (start.HasValue) q = q.Where(a => a.CreateTime >= start.Value); + if (end.HasValue) + { + var endInclusive = end.Value; + if (endInclusive.TimeOfDay == TimeSpan.Zero) endInclusive = endInclusive.Date.AddDays(1).AddTicks(-1); + q = q.Where(a => a.CreateTime <= endInclusive); + } + + if (!string.IsNullOrWhiteSpace(Category) && Category != "全部") q = q.Where(a => a.Category == Category); + if (!string.IsNullOrWhiteSpace(Keyword)) q = q.Where(a => a.Message!.Contains(Keyword)); + + q = q.OrderByDescending(a => a.CreateTime); + + var size = PageSize <= 0 ? 20 : PageSize; + var page = PageIndex < 1 ? 1 : PageIndex; + var list = q.Count(out var total) + .Page(page, size) + .ToList(); + + var pages = total <= 0 || size <= 0 ? 0 : (int)Math.Ceiling(total * 1.0 / size); + if (pages > 0 && page > pages) + { + page = pages; + list = q.Page(page, size).ToList(); + } + + return (items: list, total: (int)total, normalizedPage: page, totalPages: pages); + }); + + Application.Current.Dispatcher.Invoke(() => + { + Items.Clear(); + foreach (var it in data.items) Items.Add(it); + TotalCount = data.total; + TotalPages = data.totalPages; + PageIndex = data.normalizedPage == 0 ? 1 : data.normalizedPage; + }); + + _log.Info($"HistoryAlarm 查询完成,记录数: {TotalCount}"); + } + catch (Exception ex) + { + _log.Error($"HistoryAlarm 查询失败: {ex}"); + MessageBox.Show($"查询失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + IsBusy = false; + } + } + + public override async void OnNavigatedTo(Prism.Regions.NavigationContext navigationContext) + { + await LoadCategoriesAsync(); + await SearchAsync(); + } + + private async Task LoadCategoriesAsync() + { + try + { + var list = await Task.Run(() => + { + // 直接在数据库端去重,避免将大量重复数据拉回内存 + var cateList = _fsql.Select() + .Where(a => a.Category != null && a.Category != "") + .Distinct() + .ToList(a => a.Category); + return (cateList ?? new System.Collections.Generic.List()) + .Where(s => !string.IsNullOrWhiteSpace(s)) + .OrderBy(s => s) + .ToList(); + }); + + Application.Current.Dispatcher.Invoke(() => + { + CategoryList.Clear(); + CategoryList.Add("全部"); + foreach (var s in list) CategoryList.Add(s); + if (string.IsNullOrWhiteSpace(Category)) Category = "全部"; + }); + } + catch (Exception ex) + { + _log.Warn($"加载报警类别失败: {ex.Message}"); + Application.Current.Dispatcher.Invoke(() => + { + if (CategoryList.Count == 0) CategoryList.Add("全部"); + if (string.IsNullOrWhiteSpace(Category)) Category = "全部"; + }); + } + } + } +} diff --git a/FATrace.WPLApp/ViewModels/LogViewModel.cs b/FATrace.WPLApp/ViewModels/LogViewModel.cs new file mode 100644 index 0000000..709917f --- /dev/null +++ b/FATrace.WPLApp/ViewModels/LogViewModel.cs @@ -0,0 +1,178 @@ +using FATrace.WPLApp.Core; +using Prism.Commands; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Threading; + +namespace FATrace.WPLApp.ViewModels +{ + public class LogViewModel : NavigationViewModel + { + private readonly DispatcherTimer _timer; + + public LogViewModel() + { + Levels = new ObservableCollection(new[] { "ALL", "DEBUG", "INFO", "WARN", "ERROR", "FATAL" }); + _selectedDate = DateTime.Today; + _levelFilter = Levels.First(); + _maxLines = 2000; + + RefreshCommand = new DelegateCommand(async () => await LoadAsync(), () => !IsBusy).ObservesProperty(() => IsBusy); + OpenFolderCommand = new DelegateCommand(OpenFolder); + + _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) }; + _timer.Tick += async (_, __) => + { + if (AutoRefresh && !IsBusy) + await LoadAsync(); + }; + _timer.Start(); + + // 初始加载当日日志 + _ = LoadAsync(); + } + + public ObservableCollection Levels { get; } + + private ObservableCollection _items = new(); + public ObservableCollection Items { get => _items; set { _items = value; RaisePropertyChanged(); } } + + private DateTime? _selectedDate; + public DateTime? SelectedDate { get => _selectedDate; set { _selectedDate = value; RaisePropertyChanged(); _ = LoadAsync(); } } + + private string _levelFilter; + public string LevelFilter { get => _levelFilter; set { _levelFilter = value; RaisePropertyChanged(); _ = LoadAsync(); } } + + private string _keyword; + public string Keyword { get => _keyword; set { _keyword = value; RaisePropertyChanged(); _ = LoadAsync(); } } + + private bool _autoRefresh; + public bool AutoRefresh { get => _autoRefresh; set { _autoRefresh = value; RaisePropertyChanged(); } } + + private int _maxLines; + public int MaxLines { get => _maxLines; set { _maxLines = value; RaisePropertyChanged(); } } + + private bool _isBusy; + public bool IsBusy { get => _isBusy; set { _isBusy = value; RaisePropertyChanged(); } } + + public DelegateCommand RefreshCommand { get; } + public DelegateCommand OpenFolderCommand { get; } + + private string GetLogDirectory() + { + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs"); + } + + private string GetLogFilePath(DateTime? date) + { + var dir = GetLogDirectory(); + var dt = date ?? DateTime.Today; + var file = dt.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log"; + return Path.Combine(dir, file); + } + + private static bool PassLevel(string levelFilter, string level) + { + if (string.Equals(levelFilter, "ALL", StringComparison.OrdinalIgnoreCase)) return true; + return string.Equals(level, levelFilter, StringComparison.OrdinalIgnoreCase); + } + + private async Task LoadAsync() + { + if (IsBusy) return; + IsBusy = true; + try + { + var path = GetLogFilePath(SelectedDate); + var list = await Task.Run(() => ReadLogFile(path, MaxLines)); + + var filtered = list.Where(x => PassLevel(LevelFilter, x.Level) + && (string.IsNullOrWhiteSpace(Keyword) || (x.Message?.IndexOf(Keyword, StringComparison.OrdinalIgnoreCase) >= 0) + || (x.Logger?.IndexOf(Keyword, StringComparison.OrdinalIgnoreCase) >= 0))); + + Items = new ObservableCollection(filtered); + } + catch + { + Items = new ObservableCollection(); + } + finally + { + IsBusy = false; + } + } + + private static IReadOnlyList ReadLogFile(string path, int maxLines) + { + var result = new LinkedList(); + if (!File.Exists(path)) return result.ToList(); + + using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var sr = new StreamReader(fs, Encoding.UTF8); + + string? line; + while ((line = sr.ReadLine()) != null) + { + var entry = ParseLine(line); + result.AddLast(entry); + if (result.Count > maxLines) result.RemoveFirst(); + } + return result.ToList(); + } + + private static LogEntry ParseLine(string line) + { + // layout: longdate|LEVEL|logger|threadid|message[|exception] + var parts = line.Split('|'); + var entry = new LogEntry(); + if (parts.Length >= 4) + { + entry.Time = parts[0]; + entry.Level = parts[1]; + entry.Logger = parts[2]; + entry.ThreadId = parts[3]; + if (parts.Length >= 5) + { + entry.Message = string.Join("|", parts.Skip(4)); + } + } + else + { + entry.Message = line; + } + return entry; + } + + private void OpenFolder() + { + var dir = GetLogDirectory(); + Directory.CreateDirectory(dir); + try + { + Process.Start(new ProcessStartInfo + { + FileName = dir, + UseShellExecute = true + }); + } + catch { } + } + } + + public class LogEntry + { + public string Time { get; set; } + public string Level { get; set; } + public string Logger { get; set; } + public string ThreadId { get; set; } + public string Message { get; set; } + } +} diff --git a/FATrace.WPLApp/ViewModels/LoginViewModel.cs b/FATrace.WPLApp/ViewModels/LoginViewModel.cs new file mode 100644 index 0000000..43489d3 --- /dev/null +++ b/FATrace.WPLApp/ViewModels/LoginViewModel.cs @@ -0,0 +1,96 @@ +using FATrace.Model; +using FATrace.WPLApp.Core; +using FATrace.WPLApp.Services; +using FreeSql; +using Prism.Commands; +using Prism.Regions; +using System; +using System.Security; +using System.Threading.Tasks; +using System.Windows; + +namespace FATrace.WPLApp.ViewModels +{ + public class LoginViewModel : NavigationViewModel + { + private readonly IFreeSql _fsql; + private readonly ILogService _log; + private readonly SysRunService _sys; + private readonly IRegionManager _regionManager; + + public LoginViewModel(IFreeSql fsql, ILogService log, SysRunService sysRunService, IRegionManager regionManager) + { + _fsql = fsql; + _log = log; + _sys = sysRunService; + _regionManager = regionManager; + + LoginCommand = new DelegateCommand(async () => await DoLoginAsync(), () => !IsBusy) + .ObservesProperty(() => IsBusy); + } + + private string _userName; + public string UserName { get => _userName; set { _userName = value; RaisePropertyChanged(); } } + + private string _password; + public string Password { get => _password; set { _password = value; RaisePropertyChanged(); } } + + private bool _isBusy; + public bool IsBusy { get => _isBusy; set { _isBusy = value; RaisePropertyChanged(); } } + + public DelegateCommand LoginCommand { get; } + + private async Task DoLoginAsync() + { + if (IsBusy) return; + try + { + if (string.IsNullOrWhiteSpace(UserName) || string.IsNullOrWhiteSpace(Password)) + { + MessageBox.Show("请输入用户名和密码", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + return; + } + + IsBusy = true; + var user = await Task.Run(() => + { + return _fsql.Select() + .Where(a => a.UserName == UserName) + .First(); + }); + + if (user == null) + { + MessageBox.Show("用户不存在", "登录失败", MessageBoxButton.OK, MessageBoxImage.Warning); + return; + } + + // 明文密码对比(与现有模型字段一致)。如后续改为哈希,可在此替换校验逻辑。 + if (!string.Equals(user.Password, Password)) + { + MessageBox.Show("密码错误", "登录失败", MessageBoxButton.OK, MessageBoxImage.Warning); + return; + } + + _sys.CurUser = user.UserName; + _log.Info($"用户登录成功: {user.UserName}"); + UserName=""; + Password=""; + try + { + _regionManager.Regions["ContentRegion"].RequestNavigate("DashBoardView"); + } + catch { } + } + catch (Exception ex) + { + _log.Error($"登录异常: {ex}"); + MessageBox.Show($"登录异常: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + IsBusy = false; + } + } + } +} diff --git a/FATrace.WPLApp/ViewModels/MainViewModel.cs b/FATrace.WPLApp/ViewModels/MainViewModel.cs index b419dd2..a8a9057 100644 --- a/FATrace.WPLApp/ViewModels/MainViewModel.cs +++ b/FATrace.WPLApp/ViewModels/MainViewModel.cs @@ -90,12 +90,12 @@ namespace FATrace.WPLApp.ViewModels }); break; - case "报表数据": - this.regionManager.Regions["ContentRegion"].RequestNavigate("ReportView"); + //case "报表数据": + // this.regionManager.Regions["ContentRegion"].RequestNavigate("ReportView"); //this.regionManager.Regions["ContentRegion"].Activate(viewB); - break; + //break; case "原料使用查询": this.regionManager.Regions["ContentRegion"].RequestNavigate("RawProUseView"); break; @@ -105,15 +105,17 @@ namespace FATrace.WPLApp.ViewModels case "历史报警": this.regionManager.Regions["ContentRegion"].RequestNavigate("HistoryAlarmView"); - //this.regionManager.Regions["ContentRegion"].Activate(viewB); break; + case "日志信息": + this.regionManager.Regions["ContentRegion"].RequestNavigate("LogView"); + break; case "用户登录": - this.regionManager.Regions["ContentRegion"].RequestNavigate("UserView"); + this.regionManager.Regions["ContentRegion"].RequestNavigate("LoginView"); //this.regionManager.Regions["ContentRegion"].Activate(Shift); break; case "使用手册": - this.regionManager.Regions["ContentRegion"].RequestNavigate("HelpManualView"); + //this.regionManager.Regions["ContentRegion"].RequestNavigate("HelpManualView"); //弹窗 break; diff --git a/FATrace.WPLApp/ViewModels/RawProUseViewModel.cs b/FATrace.WPLApp/ViewModels/RawProUseViewModel.cs index 24efc44..c61fde2 100644 --- a/FATrace.WPLApp/ViewModels/RawProUseViewModel.cs +++ b/FATrace.WPLApp/ViewModels/RawProUseViewModel.cs @@ -365,56 +365,56 @@ namespace FATrace.WPLApp.ViewModels /// private void ExportToCsv() { - if (IsBusy) return; - if (Items.Count == 0) - { - System.Windows.MessageBox.Show("无可导出的数据", "提示", MessageBoxButton.OK, MessageBoxImage.Information); - return; - } + //if (IsBusy) return; + //if (Items.Count == 0) + //{ + // System.Windows.MessageBox.Show("无可导出的数据", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + // return; + //} - try - { - IsBusy = true; + //try + //{ + // IsBusy = true; - var dr = "D:\\迅雷下载"; - // 转换为 RawProUserCsvDto - var all = Items.Select(it => new RawProUserCsvDto - { - RawCode = it.RawCode, - RawName = it.RawName, - InBagCode = it.InBagCode, - BoxCode = it.BoxCode, - Batch = it.Batch, - ShelfLife = it.ShelfLife, - Weight = it.Weight, - DeliveryDate = it.WeightTime.ToString("yyyyMMdd"), - RemainWeight = it.RemainWeight, - StockWeight = it.StockWeight, - WeightTime = it.WeightTime, - OpUser = it.OpUser, - CheckUser = it.CheckUser, - OutTime = it.OutTime - }).ToList(); + // var dr = "D:\\迅雷下载"; + // // 转换为 RawProUserCsvDto + // var all = Items.Select(it => new RawProUserCsvDto + // { + // RawCode = it.RawCode, + // RawName = it.RawName, + // InBagCode = it.InBagCode, + // BoxCode = it.BoxCode, + // Batch = it.Batch, + // ShelfLife = it.ShelfLife, + // Weight = it.Weight, + // DeliveryDate = it.WeightTime.ToString("yyyyMMdd"), + // RemainWeight = it.RemainWeight, + // StockWeight = it.StockWeight, + // WeightTime = it.WeightTime, + // OpUser = it.OpUser, + // CheckUser = it.CheckUser, + // OutTime = it.OutTime + // }).ToList(); - // 过滤 InBagCode 为空的记录,避免导出失败 - var valid = all.Where(x => !string.IsNullOrWhiteSpace(x.InBagCode)).ToList(); - int skipped = all.Count - valid.Count; + // // 过滤 InBagCode 为空的记录,避免导出失败 + // var valid = all.Where(x => !string.IsNullOrWhiteSpace(x.InBagCode)).ToList(); + // int skipped = all.Count - valid.Count; - var svc = csvServices; - var paths = svc.ExportMany(valid, dr, overwrite: true); + // var svc = csvServices; + // var paths = svc.ExportMany(valid, dr, overwrite: true); - _log.Info($"RawProUse CSV 导出完成: 目录={dr}, 成功={valid.Count}, 跳过={skipped}"); - System.Windows.MessageBox.Show($"导出完成:成功 {valid.Count} 条,跳过 {skipped} 条。\n目录:{dr}", "提示", MessageBoxButton.OK, MessageBoxImage.Information); - } - catch (Exception ex) - { - _log.Error($"RawProUse CSV 导出失败: {ex}"); - System.Windows.MessageBox.Show($"导出失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); - } - finally - { - IsBusy = false; - } + // _log.Info($"RawProUse CSV 导出完成: 目录={dr}, 成功={valid.Count}, 跳过={skipped}"); + // System.Windows.MessageBox.Show($"导出完成:成功 {valid.Count} 条,跳过 {skipped} 条。\n目录:{dr}", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + //} + //catch (Exception ex) + //{ + // _log.Error($"RawProUse CSV 导出失败: {ex}"); + // System.Windows.MessageBox.Show($"导出失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + //} + //finally + //{ + // IsBusy = false; + //} } /// diff --git a/FATrace.WPLApp/Views/HelpManualView.xaml b/FATrace.WPLApp/Views/HelpManualView.xaml new file mode 100644 index 0000000..f5be18d --- /dev/null +++ b/FATrace.WPLApp/Views/HelpManualView.xaml @@ -0,0 +1,12 @@ + + + + + diff --git a/FATrace.WPLApp/Views/HelpManualView.xaml.cs b/FATrace.WPLApp/Views/HelpManualView.xaml.cs new file mode 100644 index 0000000..fceb016 --- /dev/null +++ b/FATrace.WPLApp/Views/HelpManualView.xaml.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace FATrace.WPLApp.Views +{ + /// + /// HelpManualView.xaml 的交互逻辑 + /// + public partial class HelpManualView : UserControl + { + public HelpManualView() + { + InitializeComponent(); + } + } +} diff --git a/FATrace.WPLApp/Views/HistoryAlarmView.xaml b/FATrace.WPLApp/Views/HistoryAlarmView.xaml new file mode 100644 index 0000000..54158d5 --- /dev/null +++ b/FATrace.WPLApp/Views/HistoryAlarmView.xaml @@ -0,0 +1,225 @@ + + + + Segoe MDL2 Assets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +