diff --git a/.windsurf/rules/readexcelfilerules.md b/.windsurf/rules/readexcelfilerules.md
new file mode 100644
index 0000000..13aa180
--- /dev/null
+++ b/.windsurf/rules/readexcelfilerules.md
@@ -0,0 +1,73 @@
+---
+trigger: always_on
+---
+1. 读取ExcelFile/20251218161818.xlsx文件,这个是其他系统导出的Excel模板文件,文件名称里面是时间信息,里面有多个Sheet,每个Sheet都是一个表
+2. 根据这个Excel文件和里面的多个Sheet帮我建立一个多个数据模型,数据模型建立在FATrace.Model/FileModel 下面
+3. 数据模型/表的翻译如下:
+ 工厂-入库 :FactoryInbound
+ 工厂-领料:FactoryMaterial Withdrawal
+ 工厂-出入库:**FactoryInventoryTransaction**
+ 工厂-原料生产信息:FactoryProductionRecord
+ 工厂-成品出库:FactoryOutbound
+ OEM-入库:OEMInbound
+ OEM-出库:OEMOutbound
+ OEM-出入库:OEMInventoryTransaction
+ OEM-原料使用信息:OEMRawUsageInfo
+4. 每个表/模型的字段属性的名称,你可以读取excel里面的列头中文进行翻译,要求简洁。
+ 很多的字段属性在FATrace.Model下其他的表/模型里面有有了,你可以借鉴他们的属性字段英文名称,这样属性字段能统一。
+ 这些sheet里面的数据我后期只是展示用,表/模型的主键用long,其他Excel数据属性字段数据类型全部用string 类型,
+5. 其他系统是计划在每天0点下载一次这个文件(上面的Excel模板文件),我们每天每个小时进行文件数据的检查,发现文件后读取Excel文件,把这些数据按照sheet(表格)存储到数据库中,然后把这些文件文件拷贝到另一个地方,那么读取文件的路径和拷贝到另一个文件的路径要可配置。
+6. 每次读取Excel文件且拷贝并保存到数据库成功后要把这个信息记录一个表中,这个表的你可以自己定义,方便我后期查找问题和对照数据
+7. 按照我的框架和模型,每个表都要做一个查询界面(从数据库中查询),这些查询界面View和ViewModel等其他,你需要按照我当前的框架和模式开发,这样代码能统一,这些数据的查询界面单独一个菜单目录节点,不要跟其他的查询放到一起,要分开
+8. 有不理解的地方需要跟我确认,你要列一个任务清单,一个一个的执行
+9. 你可以联网查询你需要的内容
+10. 这些逻辑放到FATrace.WPLApp.Services.ReadFileServices 中,查询界面(View + ViewModel)放到FATrace.WPLApp中
+
+
+
+11. 每个Sheet的列头内容
+
+工厂-入库
+
+| 产地 | 原料代码 | 原料名称 | 重量 | 登录日 | 登录时间 | 登录日期时间 |
+| ---- | -------- | -------- | ---- | ------ | -------- | ------------ |
+
+工厂-领料
+
+| 产地 | 原料代码 | 原料名称 | 重量 | 登录日 | 登录时间 | 登录日期时间 |
+| ---- | -------- | -------- | ---- | ------ | -------- | ------------ |
+
+工厂-出入库
+
+| 入库时间 | 出库时间 | 产地 | 原料代码 | 原料名称 | 入库总重量KG | 出库总重量KG | 剩余重量KG |
+| -------- | -------- | ---- | -------- | -------- | ------------ | ------------ | ---------- |
+
+工厂-原料生产信息
+
+| 原料编号 | 原料名称 | 产地 | 内袋二维码 | 外箱二维码 | 批号 | 保质期 | 称重重量(g) | 配料日期 | 剩余重量(Kg) | 入库总重量(Kg) | 称重时间 | 操作者 | 确认者 | 外箱扫码时间 |
+| -------- | -------- | ---- | ---------- | ---------- | ---- | ------ | ----------- | -------- | ------------ | -------------- | -------- | ------ | ------ | ------------ |
+
+工厂-成品出库
+
+| 批号 | 重量 | 保质期 | 产地 | 原料代码 | 原料名称 | 序号 | 登录日 | 登录时间 | 登录日期时间 |
+| ---- | ---- | ------ | ---- | -------- | -------- | ---- | ------ | -------- | ------------ |
+
+OEM-入库
+
+| 批号 | 重量 | 保质期 | 产地 | 原料代码 | 原料名称 | 序号 | 登录日 | 登录时间 | 登录日期时间 |
+| ---- | ---- | ------ | ---- | -------- | -------- | ---- | ------ | -------- | ------------ |
+
+OEM-出库
+
+| 批号 | 重量 | 保质期 | 产地 | 原料代码 | 原料名称 | 序号 | 登录日 | 登录时间 | 登录日期时间 |
+| ---- | ---- | ------ | ---- | -------- | -------- | ---- | ------ | -------- | ------------ |
+
+OEM-出入库
+
+| 入库时间 | 出库时间 | 产地 | 原料代码 | 原料名称 | 入库总重量KG | 出库总重量KG | 剩余重量KG |
+| -------- | -------- | ---- | -------- | -------- | ------------ | ------------ | ---------- |
+
+OEM-原料使用信息
+
+| 原料使用时间 | 内袋二维码 | 原料产地 | 原料名称 | 原料代码 | 视频链接 |
+| ------------ | ---------- | -------- | -------- | -------- | -------- |
diff --git a/FATrace.Model/FATrace.Model.csproj b/FATrace.Model/FATrace.Model.csproj
index cbcb056..3d9c1d1 100644
--- a/FATrace.Model/FATrace.Model.csproj
+++ b/FATrace.Model/FATrace.Model.csproj
@@ -10,4 +10,8 @@
+
+
+
+
diff --git a/FATrace.Model/FileModel/FactoryInbound.cs b/FATrace.Model/FileModel/FactoryInbound.cs
new file mode 100644
index 0000000..d740813
--- /dev/null
+++ b/FATrace.Model/FileModel/FactoryInbound.cs
@@ -0,0 +1,52 @@
+using FreeSql.DataAnnotations;
+
+namespace FATrace.Model
+{
+ ///
+ /// 工厂-入库(Excel 导入数据,仅展示用)
+ ///
+ [Table(Name = "FactoryInbound")]
+ public class FactoryInbound
+ {
+ ///
+ /// 主键
+ ///
+ [Column(IsPrimary = true, IsIdentity = true)]
+ public long Id { get; set; }
+
+ ///
+ /// 产地
+ ///
+ public string? Origin { get; set; }
+
+ ///
+ /// 原料代码
+ ///
+ public string? RawCode { get; set; }
+
+ ///
+ /// 原料名称
+ ///
+ public string? RawName { get; set; }
+
+ ///
+ /// 重量
+ ///
+ public string? Weight { get; set; }
+
+ ///
+ /// 登录日
+ ///
+ public string? LoginDate { get; set; }
+
+ ///
+ /// 登录时间
+ ///
+ public string? LoginTime { get; set; }
+
+ ///
+ /// 登录日期时间
+ ///
+ public string? LoginDateTime { get; set; }
+ }
+}
diff --git a/FATrace.Model/FileModel/FactoryInventoryTransaction.cs b/FATrace.Model/FileModel/FactoryInventoryTransaction.cs
new file mode 100644
index 0000000..d539bb4
--- /dev/null
+++ b/FATrace.Model/FileModel/FactoryInventoryTransaction.cs
@@ -0,0 +1,57 @@
+using FreeSql.DataAnnotations;
+
+namespace FATrace.Model
+{
+ ///
+ /// 工厂-出入库(Excel 导入数据,仅展示用)
+ ///
+ [Table(Name = "FactoryInventoryTransaction")]
+ public class FactoryInventoryTransaction
+ {
+ ///
+ /// 主键
+ ///
+ [Column(IsPrimary = true, IsIdentity = true)]
+ public long Id { get; set; }
+
+ ///
+ /// 入库时间
+ ///
+ public string? InTime { get; set; }
+
+ ///
+ /// 出库时间
+ ///
+ public string? OutTime { get; set; }
+
+ ///
+ /// 产地
+ ///
+ public string? Origin { get; set; }
+
+ ///
+ /// 原料代码
+ ///
+ public string? RawCode { get; set; }
+
+ ///
+ /// 原料名称
+ ///
+ public string? RawName { get; set; }
+
+ ///
+ /// 入库总重量 KG
+ ///
+ public string? TotalInWeightKg { get; set; }
+
+ ///
+ /// 出库总重量 KG
+ ///
+ public string? TotalOutWeightKg { get; set; }
+
+ ///
+ /// 剩余重量 KG
+ ///
+ public string? RemainWeightKg { get; set; }
+ }
+}
diff --git a/FATrace.Model/FileModel/FactoryMaterialWithdrawal.cs b/FATrace.Model/FileModel/FactoryMaterialWithdrawal.cs
new file mode 100644
index 0000000..242f8a6
--- /dev/null
+++ b/FATrace.Model/FileModel/FactoryMaterialWithdrawal.cs
@@ -0,0 +1,52 @@
+using FreeSql.DataAnnotations;
+
+namespace FATrace.Model
+{
+ ///
+ /// 工厂-领料(Excel 导入数据,仅展示用)
+ ///
+ [Table(Name = "FactoryMaterialWithdrawal")]
+ public class FactoryMaterialWithdrawal
+ {
+ ///
+ /// 主键
+ ///
+ [Column(IsPrimary = true, IsIdentity = true)]
+ public long Id { get; set; }
+
+ ///
+ /// 产地
+ ///
+ public string? Origin { get; set; }
+
+ ///
+ /// 原料代码
+ ///
+ public string? RawCode { get; set; }
+
+ ///
+ /// 原料名称
+ ///
+ public string? RawName { get; set; }
+
+ ///
+ /// 重量
+ ///
+ public string? Weight { get; set; }
+
+ ///
+ /// 登录日
+ ///
+ public string? LoginDate { get; set; }
+
+ ///
+ /// 登录时间
+ ///
+ public string? LoginTime { get; set; }
+
+ ///
+ /// 登录日期时间
+ ///
+ public string? LoginDateTime { get; set; }
+ }
+}
diff --git a/FATrace.Model/FileModel/FactoryOutbound.cs b/FATrace.Model/FileModel/FactoryOutbound.cs
new file mode 100644
index 0000000..e8ae57c
--- /dev/null
+++ b/FATrace.Model/FileModel/FactoryOutbound.cs
@@ -0,0 +1,67 @@
+using FreeSql.DataAnnotations;
+
+namespace FATrace.Model
+{
+ ///
+ /// 工厂-成品出库(Excel 导入数据,仅展示用)
+ ///
+ [Table(Name = "FactoryOutbound")]
+ public class FactoryOutbound
+ {
+ ///
+ /// 主键
+ ///
+ [Column(IsPrimary = true, IsIdentity = true)]
+ public long Id { get; set; }
+
+ ///
+ /// 批号
+ ///
+ public string? Batch { get; set; }
+
+ ///
+ /// 重量
+ ///
+ public string? Weight { get; set; }
+
+ ///
+ /// 保质期
+ ///
+ public string? ShelfLife { get; set; }
+
+ ///
+ /// 产地
+ ///
+ public string? Origin { get; set; }
+
+ ///
+ /// 原料代码
+ ///
+ public string? RawCode { get; set; }
+
+ ///
+ /// 原料名称
+ ///
+ public string? RawName { get; set; }
+
+ ///
+ /// 序号
+ ///
+ public string? SequenceNo { get; set; }
+
+ ///
+ /// 登录日
+ ///
+ public string? LoginDate { get; set; }
+
+ ///
+ /// 登录时间
+ ///
+ public string? LoginTime { get; set; }
+
+ ///
+ /// 登录日期时间
+ ///
+ public string? LoginDateTime { get; set; }
+ }
+}
diff --git a/FATrace.Model/FileModel/FactoryProductionRecord.cs b/FATrace.Model/FileModel/FactoryProductionRecord.cs
new file mode 100644
index 0000000..e873085
--- /dev/null
+++ b/FATrace.Model/FileModel/FactoryProductionRecord.cs
@@ -0,0 +1,92 @@
+using FreeSql.DataAnnotations;
+
+namespace FATrace.Model
+{
+ ///
+ /// 工厂-原料生产信息(Excel 导入数据,仅展示用)
+ ///
+ [Table(Name = "FactoryProductionRecord")]
+ public class FactoryProductionRecord
+ {
+ ///
+ /// 主键
+ ///
+ [Column(IsPrimary = true, IsIdentity = true)]
+ public long Id { get; set; }
+
+ ///
+ /// 原料编号
+ ///
+ public string? RawCode { get; set; }
+
+ ///
+ /// 原料名称
+ ///
+ public string? RawName { get; set; }
+
+ ///
+ /// 产地
+ ///
+ public string? Origin { get; set; }
+
+ ///
+ /// 内袋二维码
+ ///
+ public string? InBagCode { get; set; }
+
+ ///
+ /// 外箱二维码
+ ///
+ public string? BoxCode { get; set; }
+
+ ///
+ /// 批号
+ ///
+ public string? Batch { get; set; }
+
+ ///
+ /// 保质期
+ ///
+ public string? ShelfLife { get; set; }
+
+ ///
+ /// 称重重量 (g)
+ ///
+ public string? Weight { get; set; }
+
+ ///
+ /// 配料日期
+ ///
+ public string? DeliveryDate { get; set; }
+
+ ///
+ /// 剩余重量 (Kg)
+ ///
+ public string? RemainWeight { get; set; }
+
+ ///
+ /// 入库总重量 (Kg)
+ ///
+ public string? StockWeight { get; set; }
+
+ ///
+ /// 称重时间
+ ///
+ public string? WeightTime { get; set; }
+
+ ///
+ /// 操作者
+ ///
+ public string? OpUser { get; set; }
+
+ ///
+ /// 确认者
+ ///
+ public string? CheckUser { get; set; }
+
+ ///
+ /// 外箱扫码时间
+ ///
+ public string? BoxScanTime { get; set; }
+ }
+}
diff --git a/FATrace.Model/FileModel/FileImportLog.cs b/FATrace.Model/FileModel/FileImportLog.cs
new file mode 100644
index 0000000..85f82b4
--- /dev/null
+++ b/FATrace.Model/FileModel/FileImportLog.cs
@@ -0,0 +1,64 @@
+using FreeSql.DataAnnotations;
+using System;
+
+namespace FATrace.Model
+{
+ ///
+ /// Excel 文件导入日志,用于追踪每次导入结果与行数统计
+ ///
+ [Table(Name = "FileImportLog")]
+ public class FileImportLog
+ {
+ ///
+ /// 主键
+ ///
+ [Column(IsPrimary = true, IsIdentity = true)]
+ public long Id { get; set; }
+
+ ///
+ /// 文件名(包含时间戳),例如:20251218161818.xlsx
+ ///
+ [Column(StringLength = 260)]
+ public string? FileName { get; set; }
+
+ ///
+ /// 源文件完整路径
+ ///
+ [Column(StringLength = 500)]
+ public string? SourcePath { get; set; }
+
+ ///
+ /// 归档后完整路径
+ ///
+ [Column(StringLength = 500)]
+ public string? ArchivePath { get; set; }
+
+ ///
+ /// 导入开始时间
+ ///
+ public DateTime StartTime { get; set; }
+
+ ///
+ /// 导入结束时间
+ ///
+ public DateTime EndTime { get; set; }
+
+ ///
+ /// 导入状态:Success / Failed / Partial 等
+ ///
+ [Column(StringLength = 20)]
+ public string? Status { get; set; }
+
+ ///
+ /// 错误或摘要信息
+ ///
+ [Column(StringLength = 1000)]
+ public string? Message { get; set; }
+
+ ///
+ /// 每个 Sheet 的行数统计摘要,例如:FactoryInbound=100;FactoryOutbound=50
+ ///
+ [Column(StringLength = 1000)]
+ public string? SheetRowStats { get; set; }
+ }
+}
diff --git a/FATrace.Model/FileModel/OEMInbound.cs b/FATrace.Model/FileModel/OEMInbound.cs
new file mode 100644
index 0000000..8676ef6
--- /dev/null
+++ b/FATrace.Model/FileModel/OEMInbound.cs
@@ -0,0 +1,67 @@
+using FreeSql.DataAnnotations;
+
+namespace FATrace.Model
+{
+ ///
+ /// OEM-入库(Excel 导入数据,仅展示用)
+ ///
+ [Table(Name = "OEMInbound")]
+ public class OEMInbound
+ {
+ ///
+ /// 主键
+ ///
+ [Column(IsPrimary = true, IsIdentity = true)]
+ public long Id { get; set; }
+
+ ///
+ /// 批号
+ ///
+ public string? Batch { get; set; }
+
+ ///
+ /// 重量
+ ///
+ public string? Weight { get; set; }
+
+ ///
+ /// 保质期
+ ///
+ public string? ShelfLife { get; set; }
+
+ ///
+ /// 产地
+ ///
+ public string? Origin { get; set; }
+
+ ///
+ /// 原料代码
+ ///
+ public string? RawCode { get; set; }
+
+ ///
+ /// 原料名称
+ ///
+ public string? RawName { get; set; }
+
+ ///
+ /// 序号
+ ///
+ public string? SequenceNo { get; set; }
+
+ ///
+ /// 登录日
+ ///
+ public string? LoginDate { get; set; }
+
+ ///
+ /// 登录时间
+ ///
+ public string? LoginTime { get; set; }
+
+ ///
+ /// 登录日期时间
+ ///
+ public string? LoginDateTime { get; set; }
+ }
+}
diff --git a/FATrace.Model/FileModel/OEMInventoryTransaction.cs b/FATrace.Model/FileModel/OEMInventoryTransaction.cs
new file mode 100644
index 0000000..04d5df6
--- /dev/null
+++ b/FATrace.Model/FileModel/OEMInventoryTransaction.cs
@@ -0,0 +1,57 @@
+using FreeSql.DataAnnotations;
+
+namespace FATrace.Model
+{
+ ///
+ /// OEM-出入库(Excel 导入数据,仅展示用)
+ ///
+ [Table(Name = "OEMInventoryTransaction")]
+ public class OEMInventoryTransaction
+ {
+ ///
+ /// 主键
+ ///
+ [Column(IsPrimary = true, IsIdentity = true)]
+ public long Id { get; set; }
+
+ ///
+ /// 入库时间
+ ///
+ public string? InTime { get; set; }
+
+ ///
+ /// 出库时间
+ ///
+ public string? OutTime { get; set; }
+
+ ///
+ /// 产地
+ ///
+ public string? Origin { get; set; }
+
+ ///
+ /// 原料代码
+ ///
+ public string? RawCode { get; set; }
+
+ ///
+ /// 原料名称
+ ///
+ public string? RawName { get; set; }
+
+ ///
+ /// 入库总重量 KG
+ ///
+ public string? TotalInWeightKg { get; set; }
+
+ ///
+ /// 出库总重量 KG
+ ///
+ public string? TotalOutWeightKg { get; set; }
+
+ ///
+ /// 剩余重量 KG
+ ///
+ public string? RemainWeightKg { get; set; }
+ }
+}
diff --git a/FATrace.Model/FileModel/OEMOutbound.cs b/FATrace.Model/FileModel/OEMOutbound.cs
new file mode 100644
index 0000000..b11cc14
--- /dev/null
+++ b/FATrace.Model/FileModel/OEMOutbound.cs
@@ -0,0 +1,67 @@
+using FreeSql.DataAnnotations;
+
+namespace FATrace.Model
+{
+ ///
+ /// OEM-出库(Excel 导入数据,仅展示用)
+ ///
+ [Table(Name = "OEMOutbound")]
+ public class OEMOutbound
+ {
+ ///
+ /// 主键
+ ///
+ [Column(IsPrimary = true, IsIdentity = true)]
+ public long Id { get; set; }
+
+ ///
+ /// 批号
+ ///
+ public string? Batch { get; set; }
+
+ ///
+ /// 重量
+ ///
+ public string? Weight { get; set; }
+
+ ///
+ /// 保质期
+ ///
+ public string? ShelfLife { get; set; }
+
+ ///
+ /// 产地
+ ///
+ public string? Origin { get; set; }
+
+ ///
+ /// 原料代码
+ ///
+ public string? RawCode { get; set; }
+
+ ///
+ /// 原料名称
+ ///
+ public string? RawName { get; set; }
+
+ ///
+ /// 序号
+ ///
+ public string? SequenceNo { get; set; }
+
+ ///
+ /// 登录日
+ ///
+ public string? LoginDate { get; set; }
+
+ ///
+ /// 登录时间
+ ///
+ public string? LoginTime { get; set; }
+
+ ///
+ /// 登录日期时间
+ ///
+ public string? LoginDateTime { get; set; }
+ }
+}
diff --git a/FATrace.Model/FileModel/OEMRawUsageInfo.cs b/FATrace.Model/FileModel/OEMRawUsageInfo.cs
new file mode 100644
index 0000000..b42880d
--- /dev/null
+++ b/FATrace.Model/FileModel/OEMRawUsageInfo.cs
@@ -0,0 +1,47 @@
+using FreeSql.DataAnnotations;
+
+namespace FATrace.Model
+{
+ ///
+ /// OEM-原料使用信息(Excel 导入数据,仅展示用)
+ ///
+ [Table(Name = "OEMRawUsageInfo")]
+ public class OEMRawUsageInfo
+ {
+ ///
+ /// 主键
+ ///
+ [Column(IsPrimary = true, IsIdentity = true)]
+ public long Id { get; set; }
+
+ ///
+ /// 原料使用时间
+ ///
+ public string? RawUseTime { get; set; }
+
+ ///
+ /// 内袋二维码
+ ///
+ public string? InBagCode { get; set; }
+
+ ///
+ /// 原料产地
+ ///
+ public string? Origin { get; set; }
+
+ ///
+ /// 原料名称
+ ///
+ public string? RawName { get; set; }
+
+ ///
+ /// 原料代码
+ ///
+ public string? RawCode { get; set; }
+
+ ///
+ /// 视频链接
+ ///
+ public string? VideoUrl { get; set; }
+ }
+}
diff --git a/FATrace.Model/QRModel.cs b/FATrace.Model/QRModel.cs
new file mode 100644
index 0000000..f9f6095
--- /dev/null
+++ b/FATrace.Model/QRModel.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace FATrace.Model
+{
+ ///
+ /// 二维码模型信息
+ /// 示例:DYG05030013,20250923,802,3,01,0001
+ /// 1段:产品编码
+ /// 2段:批号/生产日期,格式 yyyyMMdd,例如 20250923
+ /// 3段:重量编码,3/4 位数字,末位为小数位,例如 802 表示 80.2g
+ /// 4段:保质期(月数),1 位数字
+ /// 5段:国家代码,01=国内,02=日本
+ /// 6段:当日产量计数(4 位,从 0001 开始)
+ ///
+ public class QRModel
+ {
+ ///
+ /// 原始二维码字符串
+ ///
+ public string RawText { get; set; } = string.Empty;
+
+ ///
+ /// 原料编号(第一段)
+ ///
+ public string RawCode { get; set; } = string.Empty;
+
+ ///
+ /// 批号
+ ///
+ public string Batch { get; set; }
+
+ ///
+ /// 重量(第三段解析后的实际数值,比如 802 -> 80.2)
+ /// 单位:克(如有需要可在业务层自行换算)
+ ///
+ public decimal Weight { get; set; }
+
+ ///
+ /// 保质期(月数,来自第四段)
+ ///
+ public int ShelfLife { get; set; }
+
+ ///
+ /// 国家代码(第五段,例如 01=国内,02=日本)
+ ///
+ public string RawSource { get; set; } = string.Empty;
+
+ ///
+ /// 当日生产序号(第六段,4 位,从 1 开始)
+ ///
+ public int DailySequence { get; set; }
+
+ ///
+ /// 解析二维码字符串为 QRModel。
+ /// 预期格式:"产品编码,yyyyMMdd,重量编码,保质期(月),国家代码,当日序号"。
+ ///
+ /// 二维码原始字符串
+ /// 解析成功的 QRModel 实例
+ /// 当格式不正确时抛出
+ public static QRModel Parse(string text)
+ {
+ if (string.IsNullOrWhiteSpace(text))
+ {
+ throw new ArgumentException("二维码内容不能为空。", nameof(text));
+ }
+
+ var parts = text.Split(',');
+ if (parts.Length != 6)
+ {
+ throw new ArgumentException($"二维码格式不正确,预期 6 段,实际为 {parts.Length} 段。内容:{text}", nameof(text));
+ }
+
+ var model = new QRModel
+ {
+ RawText = text.Trim(),
+ RawCode = parts[0].Trim(),
+ Batch = parts[1].Trim(),
+ RawSource = parts[4].Trim()
+ };
+
+ // 重量:3/4 位数字,末位为小数位,例如 802 -> 80.2
+ var weightCode = parts[2].Trim();
+ if (!int.TryParse(weightCode, out var weightInt))
+ {
+ throw new ArgumentException($"重量段不是有效数字:{weightCode}", nameof(text));
+ }
+ // 按 1 位小数解析(80.2g),如需其他规则可在此调整
+ model.Weight = weightInt / 10m;
+
+ // 保质期(月)
+ var shelfText = parts[3].Trim();
+ if (!int.TryParse(shelfText, out var shelfMonths) || shelfMonths < 0)
+ {
+ throw new ArgumentException($"保质期(月)不是有效数字:{shelfText}", nameof(text));
+ }
+ model.ShelfLife = shelfMonths;
+
+ // 当日序号
+ var seqText = parts[5].Trim();
+ if (!int.TryParse(seqText, out var seq) || seq < 0)
+ {
+ throw new ArgumentException($"当日生产序号不是有效数字:{seqText}", nameof(text));
+ }
+ model.DailySequence = seq;
+
+ return model;
+ }
+ }
+}
diff --git a/FATrace.OEMApp/App.config b/FATrace.OEMApp/App.config
index 34ee6f8..448f5e6 100644
--- a/FATrace.OEMApp/App.config
+++ b/FATrace.OEMApp/App.config
@@ -8,6 +8,8 @@
+
+
diff --git a/FATrace.OEMApp/FATrace.OEMApp.csproj b/FATrace.OEMApp/FATrace.OEMApp.csproj
index 2883a18..5fabff0 100644
--- a/FATrace.OEMApp/FATrace.OEMApp.csproj
+++ b/FATrace.OEMApp/FATrace.OEMApp.csproj
@@ -420,6 +420,7 @@
+
diff --git a/FATrace.OEMApp/MainApp.Designer.cs b/FATrace.OEMApp/MainApp.Designer.cs
index 4fee8f6..64a5ca8 100644
--- a/FATrace.OEMApp/MainApp.Designer.cs
+++ b/FATrace.OEMApp/MainApp.Designer.cs
@@ -79,6 +79,10 @@ namespace FATrace.OEMApp
btnStopLoadVideo = new Button();
btnNVRLogin = new Button();
imageList2 = new ImageList(components);
+ navPanel = new Panel();
+ btnNavMain = new Button();
+ btnNavHistory = new Button();
+ btnNavSettings = new Button();
statusStrip1.SuspendLayout();
materialTabControl1.SuspendLayout();
tabPage1.SuspendLayout();
@@ -91,6 +95,7 @@ namespace FATrace.OEMApp
materialCard1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)videoView1).BeginInit();
tabPage3.SuspendLayout();
+ navPanel.SuspendLayout();
SuspendLayout();
//
// statusStrip1
@@ -146,18 +151,21 @@ namespace FATrace.OEMApp
//
// materialTabControl1
//
+ materialTabControl1.Appearance = TabAppearance.FlatButtons;
materialTabControl1.Controls.Add(tabPage1);
materialTabControl1.Controls.Add(tabPage2);
materialTabControl1.Controls.Add(tabPage3);
materialTabControl1.Depth = 0;
materialTabControl1.Dock = DockStyle.Fill;
materialTabControl1.ImageList = imageList1;
+ materialTabControl1.ItemSize = new Size(0, 1);
materialTabControl1.Location = new Point(3, 64);
materialTabControl1.MouseState = ReaLTaiizor.Helper.MaterialDrawHelper.MaterialMouseState.HOVER;
materialTabControl1.Multiline = true;
materialTabControl1.Name = "materialTabControl1";
materialTabControl1.SelectedIndex = 0;
materialTabControl1.Size = new Size(1914, 963);
+ materialTabControl1.SizeMode = TabSizeMode.Fixed;
materialTabControl1.TabIndex = 0;
//
// tabPage1
@@ -167,10 +175,10 @@ namespace FATrace.OEMApp
tabPage1.Controls.Add(materialCard2);
tabPage1.Font = new Font("Microsoft YaHei UI", 14.25F, FontStyle.Bold, GraphicsUnit.Point, 134);
tabPage1.ImageKey = "set2.png";
- tabPage1.Location = new Point(4, 26);
+ tabPage1.Location = new Point(4, 5);
tabPage1.Name = "tabPage1";
tabPage1.Padding = new Padding(3);
- tabPage1.Size = new Size(1906, 933);
+ tabPage1.Size = new Size(1906, 954);
tabPage1.TabIndex = 0;
tabPage1.Text = "主界面";
tabPage1.UseVisualStyleBackColor = true;
@@ -187,12 +195,12 @@ namespace FATrace.OEMApp
materialCard4.Controls.Add(DownloadProgressBarMain);
materialCard4.Depth = 0;
materialCard4.ForeColor = Color.FromArgb(222, 0, 0, 0);
- materialCard4.Location = new Point(14, 373);
+ materialCard4.Location = new Point(196, 373);
materialCard4.Margin = new Padding(14);
materialCard4.MouseState = ReaLTaiizor.Helper.MaterialDrawHelper.MaterialMouseState.HOVER;
materialCard4.Name = "materialCard4";
materialCard4.Padding = new Padding(14);
- materialCard4.Size = new Size(561, 535);
+ materialCard4.Size = new Size(579, 535);
materialCard4.TabIndex = 5;
//
// txtCsvSaveState
@@ -267,19 +275,19 @@ namespace FATrace.OEMApp
materialCard3.Controls.Add(label8);
materialCard3.Depth = 0;
materialCard3.ForeColor = Color.FromArgb(222, 0, 0, 0);
- materialCard3.Location = new Point(590, 373);
+ materialCard3.Location = new Point(788, 373);
materialCard3.Margin = new Padding(14);
materialCard3.MouseState = ReaLTaiizor.Helper.MaterialDrawHelper.MaterialMouseState.HOVER;
materialCard3.Name = "materialCard3";
materialCard3.Padding = new Padding(14);
- materialCard3.Size = new Size(1307, 535);
+ materialCard3.Size = new Size(1109, 535);
materialCard3.TabIndex = 1;
//
// LvLog
//
LvLog.Location = new Point(17, 58);
LvLog.Name = "LvLog";
- LvLog.Size = new Size(1281, 460);
+ LvLog.Size = new Size(1075, 460);
LvLog.TabIndex = 6;
LvLog.UseCompatibleStateImageBehavior = false;
//
@@ -309,19 +317,19 @@ namespace FATrace.OEMApp
materialCard2.Controls.Add(label5);
materialCard2.Depth = 0;
materialCard2.ForeColor = Color.FromArgb(222, 0, 0, 0);
- materialCard2.Location = new Point(14, 9);
+ materialCard2.Location = new Point(196, 9);
materialCard2.Margin = new Padding(14);
materialCard2.MouseState = ReaLTaiizor.Helper.MaterialDrawHelper.MaterialMouseState.HOVER;
materialCard2.Name = "materialCard2";
materialCard2.Padding = new Padding(14);
- materialCard2.Size = new Size(1883, 348);
+ materialCard2.Size = new Size(1701, 348);
materialCard2.TabIndex = 0;
//
// label19
//
label19.AutoSize = true;
label19.ForeColor = Color.DimGray;
- label19.Location = new Point(41, 180);
+ label19.Location = new Point(29, 180);
label19.Name = "label19";
label19.Size = new Size(88, 26);
label19.TabIndex = 10;
@@ -329,7 +337,7 @@ namespace FATrace.OEMApp
//
// txtRURawCode
//
- txtRURawCode.Location = new Point(154, 177);
+ txtRURawCode.Location = new Point(142, 177);
txtRURawCode.Name = "txtRURawCode";
txtRURawCode.ReadOnly = true;
txtRURawCode.Size = new Size(387, 32);
@@ -341,17 +349,17 @@ namespace FATrace.OEMApp
gridRULog.AllowUserToDeleteRows = false;
gridRULog.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
gridRULog.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
- gridRULog.Location = new Point(576, 7);
+ gridRULog.Location = new Point(592, 9);
gridRULog.Name = "gridRULog";
gridRULog.ReadOnly = true;
gridRULog.RowHeadersVisible = false;
gridRULog.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
- gridRULog.Size = new Size(1290, 338);
+ gridRULog.Size = new Size(1092, 330);
gridRULog.TabIndex = 8;
//
// btnRawStopLoadVideo
//
- btnRawStopLoadVideo.Location = new Point(292, 271);
+ btnRawStopLoadVideo.Location = new Point(285, 271);
btnRawStopLoadVideo.Name = "btnRawStopLoadVideo";
btnRawStopLoadVideo.Size = new Size(142, 44);
btnRawStopLoadVideo.TabIndex = 7;
@@ -361,7 +369,7 @@ namespace FATrace.OEMApp
//
// btnTestAction
//
- btnTestAction.Location = new Point(144, 271);
+ btnTestAction.Location = new Point(137, 271);
btnTestAction.Name = "btnTestAction";
btnTestAction.Size = new Size(142, 44);
btnTestAction.TabIndex = 5;
@@ -373,7 +381,7 @@ namespace FATrace.OEMApp
//
label7.AutoSize = true;
label7.ForeColor = Color.DimGray;
- label7.Location = new Point(41, 126);
+ label7.Location = new Point(29, 126);
label7.Name = "label7";
label7.Size = new Size(88, 26);
label7.TabIndex = 4;
@@ -381,7 +389,7 @@ namespace FATrace.OEMApp
//
// txtRURawName
//
- txtRURawName.Location = new Point(154, 123);
+ txtRURawName.Location = new Point(142, 123);
txtRURawName.Name = "txtRURawName";
txtRURawName.ReadOnly = true;
txtRURawName.Size = new Size(387, 32);
@@ -391,7 +399,7 @@ namespace FATrace.OEMApp
//
label6.AutoSize = true;
label6.ForeColor = Color.DimGray;
- label6.Location = new Point(41, 68);
+ label6.Location = new Point(29, 68);
label6.Name = "label6";
label6.Size = new Size(107, 26);
label6.TabIndex = 2;
@@ -399,7 +407,7 @@ namespace FATrace.OEMApp
//
// txtRUInBagCode
//
- txtRUInBagCode.Location = new Point(154, 65);
+ txtRUInBagCode.Location = new Point(142, 65);
txtRUInBagCode.Name = "txtRUInBagCode";
txtRUInBagCode.ReadOnly = true;
txtRUInBagCode.Size = new Size(387, 32);
@@ -423,10 +431,10 @@ namespace FATrace.OEMApp
tabPage2.Controls.Add(videoView1);
tabPage2.Font = new Font("Microsoft YaHei UI", 14.25F, FontStyle.Bold, GraphicsUnit.Point, 134);
tabPage2.ImageKey = "Load.png";
- tabPage2.Location = new Point(4, 26);
+ tabPage2.Location = new Point(4, 5);
tabPage2.Name = "tabPage2";
tabPage2.Padding = new Padding(3);
- tabPage2.Size = new Size(1906, 933);
+ tabPage2.Size = new Size(1906, 954);
tabPage2.TabIndex = 1;
tabPage2.Text = "录像历史数据";
tabPage2.UseVisualStyleBackColor = true;
@@ -434,7 +442,7 @@ namespace FATrace.OEMApp
// dataGridView1
//
dataGridView1.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
- dataGridView1.Location = new Point(17, 235);
+ dataGridView1.Location = new Point(197, 235);
dataGridView1.Name = "dataGridView1";
dataGridView1.ReadOnly = true;
dataGridView1.Size = new Size(835, 677);
@@ -454,7 +462,7 @@ namespace FATrace.OEMApp
materialCard1.Controls.Add(PdtHistorySearchStart);
materialCard1.Depth = 0;
materialCard1.ForeColor = Color.FromArgb(222, 0, 0, 0);
- materialCard1.Location = new Point(17, 24);
+ materialCard1.Location = new Point(197, 24);
materialCard1.Margin = new Padding(14);
materialCard1.MouseState = ReaLTaiizor.Helper.MaterialDrawHelper.MaterialMouseState.HOVER;
materialCard1.Name = "materialCard1";
@@ -553,10 +561,10 @@ namespace FATrace.OEMApp
// videoView1
//
videoView1.BackColor = Color.Black;
- videoView1.Location = new Point(858, 24);
+ videoView1.Location = new Point(1043, 24);
videoView1.MediaPlayer = null;
videoView1.Name = "videoView1";
- videoView1.Size = new Size(1042, 888);
+ videoView1.Size = new Size(857, 888);
videoView1.TabIndex = 7;
videoView1.Text = "videoView1";
//
@@ -567,9 +575,9 @@ namespace FATrace.OEMApp
tabPage3.Controls.Add(btnStopLoadVideo);
tabPage3.Controls.Add(btnNVRLogin);
tabPage3.ImageKey = "set3.png";
- tabPage3.Location = new Point(4, 26);
+ tabPage3.Location = new Point(4, 5);
tabPage3.Name = "tabPage3";
- tabPage3.Size = new Size(1906, 933);
+ tabPage3.Size = new Size(1906, 954);
tabPage3.TabIndex = 2;
tabPage3.Text = "相机设置";
tabPage3.UseVisualStyleBackColor = true;
@@ -599,14 +607,14 @@ namespace FATrace.OEMApp
//
// DownloadProgressBar
//
- DownloadProgressBar.Location = new Point(247, 124);
+ DownloadProgressBar.Location = new Point(382, 112);
DownloadProgressBar.Name = "DownloadProgressBar";
DownloadProgressBar.Size = new Size(130, 23);
DownloadProgressBar.TabIndex = 3;
//
// btnStopLoadVideo
//
- btnStopLoadVideo.Location = new Point(409, 54);
+ btnStopLoadVideo.Location = new Point(544, 42);
btnStopLoadVideo.Name = "btnStopLoadVideo";
btnStopLoadVideo.Size = new Size(130, 58);
btnStopLoadVideo.TabIndex = 2;
@@ -616,7 +624,7 @@ namespace FATrace.OEMApp
//
// btnNVRLogin
//
- btnNVRLogin.Location = new Point(84, 54);
+ btnNVRLogin.Location = new Point(219, 42);
btnNVRLogin.Name = "btnNVRLogin";
btnNVRLogin.Size = new Size(130, 58);
btnNVRLogin.TabIndex = 0;
@@ -631,14 +639,84 @@ namespace FATrace.OEMApp
imageList2.TransparentColor = Color.Transparent;
imageList2.Images.SetKeyName(0, "About.png");
//
+ // navPanel
+ //
+ navPanel.BackColor = Color.FromArgb(250, 250, 250);
+ navPanel.Controls.Add(btnNavMain);
+ navPanel.Controls.Add(btnNavHistory);
+ navPanel.Controls.Add(btnNavSettings);
+ navPanel.Dock = DockStyle.Left;
+ navPanel.Location = new Point(3, 64);
+ navPanel.Name = "navPanel";
+ navPanel.Size = new Size(183, 941);
+ navPanel.TabIndex = 2;
+ //
+ // btnNavMain
+ //
+ btnNavMain.BackColor = Color.FromArgb(63, 81, 181);
+ btnNavMain.FlatAppearance.BorderSize = 0;
+ btnNavMain.FlatStyle = FlatStyle.Flat;
+ btnNavMain.Font = new Font("Microsoft YaHei UI", 12F, FontStyle.Bold);
+ btnNavMain.ForeColor = Color.White;
+ btnNavMain.ImageAlign = ContentAlignment.MiddleLeft;
+ btnNavMain.Location = new Point(0, 3);
+ btnNavMain.Name = "btnNavMain";
+ btnNavMain.Padding = new Padding(10, 0, 0, 0);
+ btnNavMain.Size = new Size(183, 60);
+ btnNavMain.TabIndex = 0;
+ btnNavMain.Text = " 主界面";
+ btnNavMain.TextAlign = ContentAlignment.MiddleLeft;
+ btnNavMain.TextImageRelation = TextImageRelation.ImageBeforeText;
+ btnNavMain.UseVisualStyleBackColor = false;
+ btnNavMain.Click += BtnNavMain_Click;
+ //
+ // btnNavHistory
+ //
+ btnNavHistory.BackColor = Color.FromArgb(240, 240, 240);
+ btnNavHistory.FlatAppearance.BorderSize = 0;
+ btnNavHistory.FlatStyle = FlatStyle.Flat;
+ btnNavHistory.Font = new Font("Microsoft YaHei UI", 12F, FontStyle.Bold);
+ btnNavHistory.ForeColor = Color.FromArgb(64, 64, 64);
+ btnNavHistory.ImageAlign = ContentAlignment.MiddleLeft;
+ btnNavHistory.Location = new Point(0, 65);
+ btnNavHistory.Name = "btnNavHistory";
+ btnNavHistory.Padding = new Padding(10, 0, 0, 0);
+ btnNavHistory.Size = new Size(183, 60);
+ btnNavHistory.TabIndex = 1;
+ btnNavHistory.Text = " 历史记录";
+ btnNavHistory.TextAlign = ContentAlignment.MiddleLeft;
+ btnNavHistory.TextImageRelation = TextImageRelation.ImageBeforeText;
+ btnNavHistory.UseVisualStyleBackColor = false;
+ btnNavHistory.Click += BtnNavHistory_Click;
+ //
+ // btnNavSettings
+ //
+ btnNavSettings.BackColor = Color.FromArgb(240, 240, 240);
+ btnNavSettings.FlatAppearance.BorderSize = 0;
+ btnNavSettings.FlatStyle = FlatStyle.Flat;
+ btnNavSettings.Font = new Font("Microsoft YaHei UI", 12F, FontStyle.Bold);
+ btnNavSettings.ForeColor = Color.FromArgb(64, 64, 64);
+ btnNavSettings.ImageAlign = ContentAlignment.MiddleLeft;
+ btnNavSettings.Location = new Point(0, 127);
+ btnNavSettings.Name = "btnNavSettings";
+ btnNavSettings.Padding = new Padding(10, 0, 0, 0);
+ btnNavSettings.Size = new Size(183, 60);
+ btnNavSettings.TabIndex = 2;
+ btnNavSettings.Text = " 相机设置";
+ btnNavSettings.TextAlign = ContentAlignment.MiddleLeft;
+ btnNavSettings.TextImageRelation = TextImageRelation.ImageBeforeText;
+ btnNavSettings.UseVisualStyleBackColor = false;
+ btnNavSettings.Visible = false;
+ btnNavSettings.Click += BtnNavSettings_Click;
+ //
// MainApp
//
AutoScaleDimensions = new SizeF(8F, 17F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1920, 1030);
+ Controls.Add(navPanel);
Controls.Add(statusStrip1);
Controls.Add(materialTabControl1);
- DrawerTabControl = materialTabControl1;
Font = new Font("Microsoft YaHei UI", 9F, FontStyle.Bold, GraphicsUnit.Point, 134);
Icon = (Icon)resources.GetObject("$this.Icon");
Name = "MainApp";
@@ -663,6 +741,7 @@ namespace FATrace.OEMApp
materialCard1.PerformLayout();
((System.ComponentModel.ISupportInitialize)videoView1).EndInit();
tabPage3.ResumeLayout(false);
+ navPanel.ResumeLayout(false);
ResumeLayout(false);
PerformLayout();
}
@@ -721,5 +800,9 @@ namespace FATrace.OEMApp
private Label label12;
private TextBox txtCsvSaveState;
private ImageList imageList2;
+ private Panel navPanel;
+ private Button btnNavMain;
+ private Button btnNavHistory;
+ private Button btnNavSettings;
}
}
\ No newline at end of file
diff --git a/FATrace.OEMApp/MainApp.cs b/FATrace.OEMApp/MainApp.cs
index e4746e5..596b5ce 100644
--- a/FATrace.OEMApp/MainApp.cs
+++ b/FATrace.OEMApp/MainApp.cs
@@ -89,6 +89,42 @@ namespace FATrace.OEMApp
private void LogWarn(string msg) => AppendLog("WARN", msg, Color.DarkOrange);
private void LogError(string msg) => AppendLog("ERROR", msg, Color.DarkRed);
+ private void BtnNavMain_Click(object? sender, EventArgs e)
+ {
+ materialTabControl1.SelectedIndex = 0;
+ UpdateNavButtonStyles(0);
+ }
+
+ private void BtnNavHistory_Click(object? sender, EventArgs e)
+ {
+ materialTabControl1.SelectedIndex = 1;
+ UpdateNavButtonStyles(1);
+ }
+
+ private void BtnNavSettings_Click(object? sender, EventArgs e)
+ {
+ materialTabControl1.SelectedIndex = 2;
+ UpdateNavButtonStyles(2);
+ }
+
+ private void UpdateNavButtonStyles(int selectedIndex)
+ {
+ var buttons = new[] { btnNavMain, btnNavHistory, btnNavSettings };
+ for (int i = 0; i < buttons.Length; i++)
+ {
+ if (i == selectedIndex)
+ {
+ buttons[i].BackColor = Color.FromArgb(63, 81, 181);
+ buttons[i].ForeColor = Color.White;
+ }
+ else
+ {
+ buttons[i].BackColor = Color.FromArgb(240, 240, 240);
+ buttons[i].ForeColor = Color.FromArgb(64, 64, 64);
+ }
+ }
+ }
+
public MainApp()
{
InitializeComponent();
@@ -100,7 +136,11 @@ namespace FATrace.OEMApp
private PLCDataService PLCDataService { get; set; }
private TimeClearDataService TimeClearService { get; set; }
private System.Windows.Forms.Timer _statusTimer;
-
+
+ ///
+ /// TouchSocket Server
+ ///
+ private TouchSocketServer TouchSocketServer { get; set; }
///
/// 主窗体加载:
@@ -109,7 +149,7 @@ namespace FATrace.OEMApp
/// - 启动后台任务服务(顺序下载队列 + Jellyfin 批量监听)
/// - 初始化并启动 gridRULog(DataGridView)定时刷新,实时展示下载/监听任务状态
///
- private void MainApp_Load(object sender, EventArgs e)
+ private async void MainApp_Load(object sender, EventArgs e)
{
InitLvLog();
LogInfo("主界面初始化");
@@ -129,12 +169,32 @@ namespace FATrace.OEMApp
//HkCameraClient.NVR_Port = ConfigHelper.GetValue("NVRPort");
//HkCameraClient.NVR_UserName = ConfigHelper.GetValue("NVRUserName");
- PLCDataService = new PLCDataService();
- PLCDataService.PlcConnectedEventHandler += PLCDataService_PlcConnectedEventHandler;
- PLCDataService.ScanCodeEventHandler += PLCDataService_ScanCodeEventHandler;
+ //PLC时间读取
+ //PLCDataService = new PLCDataService();
+ //PLCDataService.PlcConnectedEventHandler += PLCDataService_PlcConnectedEventHandler;
+ //PLCDataService.ScanCodeEventHandler += PLCDataService_ScanCodeEventHandler;
HkCameraClient.NVRVideoSavePath = ConfigHelper.GetValue("NVRVideoSavePath");
+ var TcpServerIp = ConfigHelper.GetValue("TcpServerIp");
+ var TcpServerPort = int.Parse(ConfigHelper.GetValue("TcpServerPort"));
+ TouchSocketServer = new TouchSocketServer(TcpServerIp, TcpServerPort, 30000, 30000);
+
+ TouchSocketServer.ClientConnected += TouchSocketServer_ClientConnected;
+ TouchSocketServer.ClientDisconnected += TouchSocketServer_ClientDisconnected;
+ TouchSocketServer.DataReceived += TouchSocketServer_DataReceived;
+ TouchSocketServer.ServerError += TouchSocketServer_ServerError;
+
+ var serverStarted = await TouchSocketServer.StartAsync();
+ if (serverStarted)
+ {
+ LogInfo($"TouchSocket Server 已启动,监听 {TcpServerIp}:{TcpServerPort}");
+ }
+ else
+ {
+ LogError($"TouchSocket Server 启动失败,监听 {TcpServerIp}:{TcpServerPort}");
+ }
+
//NVR登录
NVRLogin();
@@ -175,11 +235,11 @@ namespace FATrace.OEMApp
_ = UpdateNasStatusAsync();
_ = UpdateNvrStatusAsync();
StartStatusTimer();
-
+
try
{
var systemName = Program.SystemName;
- var authText = Program.IsActive ? "(已授权)" : "(未授权)";
+ var authText = Program.IsActive ? "(添加剂追溯系统)" : "(OEM)";
this.Text = string.IsNullOrWhiteSpace(systemName)
? $"添加剂追溯系统 {authText}"
: $"{systemName} {authText}";
@@ -213,7 +273,7 @@ namespace FATrace.OEMApp
BeginInvoke(new Action(() =>
{
txtRUInBagCode.Text = CurParsedCodeInfo.Code;
- txtRURawName.Text= CurParsedCodeInfo.RawName;
+ txtRURawName.Text = CurParsedCodeInfo.RawName;
txtRURawCode.Text = CurParsedCodeInfo.RawCode;
}));
@@ -573,7 +633,7 @@ namespace FATrace.OEMApp
foreach (DataGridViewColumn col in dataGridView1.Columns)
{
var propName = col.DataPropertyName;
-
+
// 设置列头中文
if (!string.IsNullOrWhiteSpace(propName) && _historyHeaderMap.TryGetValue(propName, out var headerText))
{
@@ -645,10 +705,8 @@ namespace FATrace.OEMApp
dataGridView1.AllowUserToAddRows = false;
}
-
#endregion
-
///
/// 测试/结束操作按钮:同样仅入队一条下载任务,方便快速验证下载->监听全流程。
///
@@ -656,8 +714,8 @@ namespace FATrace.OEMApp
///
private void btnTestAction_Click(object sender, EventArgs e)
{
- if (CurParsedCodeInfo==null) return ;
-
+ if (CurParsedCodeInfo == null) return;
+
var taskId = DownloadTaskWorker.Instance.Enqueue(
CurParsedCodeInfo,
user: CurUserName,
@@ -1065,7 +1123,7 @@ namespace FATrace.OEMApp
HkCameraClient.Sdk_NET_DVR_StopGetFile();
}
- protected override void OnFormClosing(FormClosingEventArgs e)
+ protected async override void OnFormClosing(FormClosingEventArgs e)
{
try { DownloadTaskWorker.Instance.DownloadFileNameChanged -= OnDownloadFileNameChanged; } catch { }
try { DownloadTaskWorker.Instance.TaskStarted -= OnTaskStatusChanged; } catch { }
@@ -1073,6 +1131,18 @@ namespace FATrace.OEMApp
try { DownloadTaskWorker.Instance.TaskFailed -= OnTaskFailed; } catch { }
try { _statusTimer?.Stop(); _statusTimer?.Dispose(); _statusTimer = null; } catch { }
try { TimeClearService?.Stop(); } catch { }
+ try
+ {
+ if (TouchSocketServer != null && TouchSocketServer.IsRunning)
+ {
+ await TouchSocketServer.StopAsync();
+ LogInfo("TouchSocket Server 已停止");
+ }
+ }
+ catch (Exception ex)
+ {
+ LogError($"TouchSocket Server 停止失败: {ex.Message}");
+ }
base.OnFormClosing(e);
}
@@ -1098,6 +1168,68 @@ namespace FATrace.OEMApp
catch { }
}
+ private void TouchSocketServer_ClientConnected(object? sender, TouchSocketServer.ClientConnectedEventArgs e)
+ {
+ LogInfo($"[TCP] 客户端连接: {e.ClientId} {e.RemoteIp}:{e.RemotePort}");
+ }
+
+ private void TouchSocketServer_ClientDisconnected(object? sender, TouchSocketServer.ClientDisconnectedEventArgs e)
+ {
+ LogInfo($"[TCP] 客户端断开: {e.ClientId} {e.RemoteIp}:{e.RemotePort} 原因={e.Reason}");
+ }
+
+ private void TouchSocketServer_DataReceived(object? sender, TouchSocketServer.DataReceivedEventArgs e)
+ {
+ Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} : - IP : {e.RemoteIp} {e.Data}");
+
+ // 解析二维码为 QRModel
+ try
+ {
+ if (!string.IsNullOrWhiteSpace(e.Data))
+ {
+ //解析Code条码数据,内包条码数据
+ CurParsedCodeInfo = NVRCom.ParseCodeFull(e.Data!.Trim());
+
+ BeginInvoke(new Action(() =>
+ {
+ txtRUInBagCode.Text = CurParsedCodeInfo.Code;
+ txtRURawCode.Text = CurParsedCodeInfo.RawCode;
+
+ }));
+
+ try
+ {
+ if (CurParsedCodeInfo == null) return;
+
+ var taskId = DownloadTaskWorker.Instance.Enqueue(
+ CurParsedCodeInfo,
+ user: CurUserName,
+ start: DateTime.Now,
+ end: DateTime.Now
+ );
+ }
+ catch
+ {
+ return;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ // 解析失败仅记录错误,不影响后续逻辑
+ LogError($"[TCP] QR 解析失败: {ex.Message}");
+ }
+
+
+
+ TouchSocketServer.SendToClient(e.ClientId, "OK");
+ }
+
+ private void TouchSocketServer_ServerError(object? sender, Exception ex)
+ {
+ LogError($"[TCP] 服务器错误: {ex.Message}");
+ }
+
#region 底部状态栏(PLC/DB/NAS/NVR)
private void StartStatusTimer()
{
diff --git a/FATrace.OEMApp/MainApp.resx b/FATrace.OEMApp/MainApp.resx
index c258c49..3610f61 100644
--- a/FATrace.OEMApp/MainApp.resx
+++ b/FATrace.OEMApp/MainApp.resx
@@ -128,7 +128,7 @@
AAEAAAD/////AQAAAAAAAAAMAgAAAEZTeXN0ZW0uV2luZG93cy5Gb3JtcywgQ3VsdHVyZT1uZXV0cmFs
LCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAAAmU3lzdGVtLldpbmRvd3MuRm9ybXMu
SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAAaCEAAAJNU0Z0AUkBTAIBAQgB
- AAG4AQEBuAEBARABAAEQAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABQAMAATADAAEBAQABIAYAATD/
+ AAHoAQEB6AEBARABAAEQAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABQAMAATADAAEBAQABIAYAATD/
AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AFIAAdsBlgESAf8B2wGWARIB/wQAAxIBGANJAYgB
2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wQAAdsB
lgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8UAAHbAZYB
@@ -280,8 +280,8 @@
AAEAAAD/////AQAAAAAAAAAMAgAAAEZTeXN0ZW0uV2luZG93cy5Gb3JtcywgQ3VsdHVyZT1uZXV0cmFs
LCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAAAmU3lzdGVtLldpbmRvd3MuRm9ybXMu
- SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAA/gUAAAJNU0Z0AUkBTAMBAQAB
- EAEAARABAAEQAQABEAEABP8BIQEACP8BQgFNATYHAAE2AwABKAMAAUADAAEQAwABAQEAASAGAAEQKgAB
+ SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAA9gUAAAJNU0Z0AUkBTAMBAQAB
+ QAEAAUABAAEQAQABEAEABP8BIQEACP8BQgFNATYHAAE2AwABKAMAAUADAAEQAwABAQEAASAGAAEQKgAB
2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/5AAB2wGWARIB/wHbAZYBEgH/AdsB
lgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf/YAAHbAZYB
EgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B
@@ -292,21 +292,21 @@
/wHbAZYBEgH/CAAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B
2wGWARIB/8QAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/CAAB
2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/8QAAdsB
- lgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wwAAW0BYAFHAagB2wGWARIB/wHbAZYB
- EgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf/EAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB
- /wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wMGAQgIAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYB
- EgH/AdsBlgESAf8B2wGWARIB/8QAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB
- /wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsB
- lgESAf8B2wGWARIB/wHbAZYBEgH/xAADEgEYAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B
- 2wGWARIB/wHbAZYBEgH/BAABggFtAUMBvwHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsB
- lgESAf/MAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wgAAdsB
- lgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/9AAAdsBlgESAf8B2wGWARIB/wHbAZYB
- EgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AUsBSAFBAXgB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B
- 2wGWARIB/9gAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsB
- lgESAf8B2wGWARIB/wHbAZYBEgH/4AADEgEYAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B
- 2wGWARIB//8A1QABQgFNAT4HAAE+AwABKAMAAUADAAEQAwABAQEAAQEFAAGAFwAD/wEAAfwBHwYAAfAB
- BwYAAeABAwYAAcEBgQYAAsEGAAGBAYAGAAGBAYAGAAGDAYAGAAGAAcAGAAGABwABgAGBBgABwAHBBgAB
- 4AEDBgAB8AEHBgAB+AEfBgAC/wYACw==
+ lgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wwAAlQBUgGoAdsBlgESAf8B2wGWARIB
+ /wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/xAAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B
+ 2wGWARIB/wHbAZYBEgH/AdsBlgESAf8DBgEICAAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB
+ /wHbAZYBEgH/AdsBlgESAf/EAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B
+ 2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYB
+ EgH/AdsBlgESAf8B2wGWARIB/8QAAxIBGAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsB
+ lgESAf8B2wGWARIB/wQAAVsCWgG/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB
+ /8wAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/CAAB2wGWARIB
+ /wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/0AAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B
+ 2wGWARIB/wHbAZYBEgH/AdsBlgESAf8DRAF4AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf/Y
+ AAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsB
+ lgESAf8B2wGWARIB/+AAAxIBGAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf//
+ ANUAAUIBTQE+BwABPgMAASgDAAFAAwABEAMAAQEBAAEBBQABgBcAA/8BAAH8AR8GAAHwAQcGAAHgAQMG
+ AAHBAYEGAALBBgABgQGABgABgQGABgABgwGABgABgAHABgABgAcAAYABgQYAAcABwQYAAeABAwYAAfAB
+ BwYAAfgBHwYAAv8GAAs=
diff --git a/FATrace.OEMApp/NLog.config b/FATrace.OEMApp/NLog.config
index c239780..4adf612 100644
--- a/FATrace.OEMApp/NLog.config
+++ b/FATrace.OEMApp/NLog.config
@@ -1,16 +1,53 @@
-
-
-
-
+ throwExceptions="false"
+ internalLogLevel="Off" internalLogFile="c:\temp\nlog-internal.log">
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FATrace.OEMApp/Services/DownloadTaskWorker.cs b/FATrace.OEMApp/Services/DownloadTaskWorker.cs
index b55ff81..dbc40c3 100644
--- a/FATrace.OEMApp/Services/DownloadTaskWorker.cs
+++ b/FATrace.OEMApp/Services/DownloadTaskWorker.cs
@@ -146,8 +146,8 @@ namespace FATrace.OEMApp.Services
User = user,
Status = TaskStatus.Pending,
Progress = 0,
- NvrStartTime = start ?? now.AddSeconds(-VideoTime),
- NvrEndTime = end ?? now,
+ NvrStartTime = now,
+ NvrEndTime = now.AddSeconds(VideoTime),
CreateTime = now,
UpdateTime = now
};
diff --git a/FATrace.OEMApp/Services/SocketService.cs b/FATrace.OEMApp/Services/SocketService.cs
new file mode 100644
index 0000000..a87c126
--- /dev/null
+++ b/FATrace.OEMApp/Services/SocketService.cs
@@ -0,0 +1,750 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using NLog;
+
+namespace FATrace.OEMApp.Services
+{
+ ///
+ /// Socket 服务器 - 基于原生 TcpListener 实现的多客户端 TCP 服务器
+ /// 功能:接收客户端数据、解析为字符串、触发事件通知外部、支持向客户端发送数据
+ /// 协议:使用 CRLF (\r\n) 作为行分隔符的文本协议
+ /// 编码:UTF-8
+ ///
+ public class SocketService
+ {
+ #region 私有字段
+
+ ///
+ /// NLog 日志记录器
+ ///
+ private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
+
+ ///
+ /// TCP 监听器
+ ///
+ private TcpListener _listener;
+
+ ///
+ /// 服务器是否正在运行
+ ///
+ private bool _isRunning = false;
+
+ ///
+ /// 取消令牌源,用于停止服务器
+ ///
+ private CancellationTokenSource _cts;
+
+ ///
+ /// 已连接的客户端字典(SessionId -> ClientSession)
+ /// 使用线程安全的 ConcurrentDictionary
+ ///
+ private readonly ConcurrentDictionary _clients = new ConcurrentDictionary();
+
+ ///
+ /// 会话 ID 计数器
+ ///
+ private int _sessionIdCounter = 0;
+
+ #endregion
+
+ #region 公共属性
+
+ ///
+ /// 服务器监听 IP 地址
+ /// 默认:127.0.0.1(本地回环)
+ /// 0.0.0.0 表示监听所有网卡
+ ///
+ public string ListenIp { get; set; } = "127.0.0.1";
+
+ ///
+ /// 服务器监听端口
+ /// 默认:6001
+ ///
+ public int ListenPort { get; set; } = 6001;
+
+ ///
+ /// 接收超时时间(秒)
+ /// 默认:30 秒
+ /// 如果客户端在此时间内无数据交互,连接将被关闭
+ ///
+ public int ReceiveTimeout { get; set; } = 30;
+
+ ///
+ /// 发送超时时间(秒)
+ /// 默认:30 秒
+ ///
+ public int SendTimeout { get; set; } = 30;
+
+ ///
+ /// 服务器是否正在运行
+ ///
+ public bool IsRunning => _isRunning;
+
+ ///
+ /// 当前连接的客户端数量
+ ///
+ public int ConnectedClientCount
+ {
+ get
+ {
+ return _clients.Count;
+ }
+ }
+
+ #endregion
+
+ #region 事件定义
+
+ ///
+ /// 数据接收事件参数
+ ///
+ public class DataReceivedEventArgs : EventArgs
+ {
+ ///
+ /// 会话 ID(客户端唯一标识)
+ ///
+ public string SessionId { get; set; }
+
+ ///
+ /// 客户端 IP 地址
+ ///
+ public string RemoteIp { get; set; }
+
+ ///
+ /// 客户端端口
+ ///
+ public int RemotePort { get; set; }
+
+ ///
+ /// 接收到的数据(已解析为字符串)
+ ///
+ public string Data { get; set; }
+
+ ///
+ /// 接收时间
+ ///
+ public DateTime ReceivedTime { get; set; }
+ }
+
+ ///
+ /// 客户端连接事件参数
+ ///
+ public class ClientConnectedEventArgs : EventArgs
+ {
+ ///
+ /// 会话 ID
+ ///
+ public string SessionId { get; set; }
+
+ ///
+ /// 客户端 IP 地址
+ ///
+ public string RemoteIp { get; set; }
+
+ ///
+ /// 客户端端口
+ ///
+ public int RemotePort { get; set; }
+
+ ///
+ /// 连接时间
+ ///
+ public DateTime ConnectedTime { get; set; }
+ }
+
+ ///
+ /// 客户端断开事件参数
+ ///
+ public class ClientDisconnectedEventArgs : EventArgs
+ {
+ ///
+ /// 会话 ID
+ ///
+ public string SessionId { get; set; }
+
+ ///
+ /// 客户端 IP 地址
+ ///
+ public string RemoteIp { get; set; }
+
+ ///
+ /// 客户端端口
+ ///
+ public int RemotePort { get; set; }
+
+ ///
+ /// 断开时间
+ ///
+ public DateTime DisconnectedTime { get; set; }
+
+ ///
+ /// 断开原因
+ ///
+ public string Reason { get; set; }
+ }
+
+ ///
+ /// 数据接收事件
+ /// 当接收到客户端数据时触发,外部可订阅此事件进行业务处理
+ ///
+ public event EventHandler DataReceived;
+
+ ///
+ /// 客户端连接事件
+ /// 当有新客户端连接时触发
+ ///
+ public event EventHandler ClientConnected;
+
+ ///
+ /// 客户端断开事件
+ /// 当客户端断开连接时触发
+ ///
+ public event EventHandler ClientDisconnected;
+
+ ///
+ /// 服务器错误事件
+ /// 当服务器发生错误时触发
+ ///
+ public event EventHandler ServerError;
+
+ #endregion
+
+ #region 公共方法
+
+ ///
+ /// 启动 Socket 服务器
+ ///
+ /// 启动是否成功
+ public async Task StartAsync()
+ {
+ try
+ {
+ // 检查是否已经在运行
+ if (_isRunning)
+ {
+ Logger.Warn("Socket 服务器已经在运行中,无需重复启动");
+ return true;
+ }
+
+ Logger.Info($"正在启动 Socket 服务器,监听地址:{ListenIp}:{ListenPort},超时时间:{ReceiveTimeout}秒");
+
+ // 解析 IP 地址
+ IPAddress ipAddress;
+ if (!IPAddress.TryParse(ListenIp, out ipAddress))
+ {
+ Logger.Error($"无效的 IP 地址:{ListenIp}");
+ return false;
+ }
+
+ // 创建 TCP 监听器
+ _listener = new TcpListener(ipAddress, ListenPort);
+ _listener.Start();
+
+ // 创建取消令牌
+ _cts = new CancellationTokenSource();
+
+ _isRunning = true;
+ Logger.Info($"Socket 服务器启动成功,监听地址:{ListenIp}:{ListenPort}");
+
+ // 启动接受客户端连接的任务
+ _ = Task.Run(() => AcceptClientsAsync(_cts.Token), _cts.Token);
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"启动 Socket 服务器时发生异常:{ex.Message}");
+ OnServerError(ex);
+ return false;
+ }
+ }
+
+ ///
+ /// 停止 Socket 服务器
+ ///
+ /// 停止任务
+ public async Task StopAsync()
+ {
+ try
+ {
+ if (!_isRunning || _listener == null)
+ {
+ Logger.Warn("Socket 服务器未运行,无需停止");
+ return;
+ }
+
+ Logger.Info("正在停止 Socket 服务器...");
+
+ _isRunning = false;
+
+ // 取消所有异步操作
+ _cts?.Cancel();
+
+ // 停止监听器
+ _listener?.Stop();
+
+ // 断开所有客户端
+ foreach (var client in _clients.Values)
+ {
+ try
+ {
+ client.TcpClient?.Close();
+ }
+ catch (Exception ex)
+ {
+ Logger.Warn(ex, $"关闭客户端连接时发生异常:SessionId={client.SessionId}");
+ }
+ }
+
+ // 清空客户端字典
+ _clients.Clear();
+
+ // 释放资源
+ _cts?.Dispose();
+ _cts = null;
+ _listener = null;
+
+ Logger.Info("Socket 服务器已停止");
+
+ await Task.CompletedTask;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"停止 Socket 服务器时发生异常:{ex.Message}");
+ OnServerError(ex);
+ }
+ }
+
+ ///
+ /// 向指定客户端发送数据
+ ///
+ /// 会话 ID(客户端唯一标识)
+ /// 要发送的字符串数据
+ /// 发送是否成功
+ public async Task SendToClientAsync(string sessionId, string data)
+ {
+ try
+ {
+ // 参数校验
+ if (string.IsNullOrWhiteSpace(sessionId))
+ {
+ Logger.Warn("发送数据失败:SessionId 为空");
+ return false;
+ }
+
+ if (string.IsNullOrEmpty(data))
+ {
+ Logger.Warn($"发送数据失败:数据为空,SessionId={sessionId}");
+ return false;
+ }
+
+ // 查找会话
+ if (!_clients.TryGetValue(sessionId, out var client))
+ {
+ Logger.Warn($"发送数据失败:未找到会话,SessionId={sessionId}");
+ return false;
+ }
+
+ // 发送数据(自动添加 CRLF 行结束符)
+ var dataToSend = data.EndsWith("\r\n") ? data : data + "\r\n";
+ var buffer = Encoding.UTF8.GetBytes(dataToSend);
+
+ await client.Stream.WriteAsync(buffer, 0, buffer.Length);
+ await client.Stream.FlushAsync();
+
+ Logger.Debug($"向客户端发送数据成功,SessionId={sessionId},数据长度={buffer.Length}字节,内容={data}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"向客户端发送数据时发生异常:SessionId={sessionId},错误={ex.Message}");
+ OnServerError(ex);
+ return false;
+ }
+ }
+
+ ///
+ /// 向所有已连接的客户端广播数据
+ ///
+ /// 要广播的字符串数据
+ /// 成功发送的客户端数量
+ public async Task BroadcastAsync(string data)
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(data))
+ {
+ Logger.Warn("广播数据失败:数据为空");
+ return 0;
+ }
+
+ // 获取所有客户端
+ var clientList = _clients.Values.ToList();
+
+ if (clientList.Count == 0)
+ {
+ Logger.Debug("广播数据:当前无已连接的客户端");
+ return 0;
+ }
+
+ // 准备数据
+ var dataToSend = data.EndsWith("\r\n") ? data : data + "\r\n";
+ var buffer = Encoding.UTF8.GetBytes(dataToSend);
+
+ // 向所有客户端发送
+ int successCount = 0;
+ foreach (var client in clientList)
+ {
+ try
+ {
+ await client.Stream.WriteAsync(buffer, 0, buffer.Length);
+ await client.Stream.FlushAsync();
+ successCount++;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"向客户端广播数据失败:SessionId={client.SessionId},错误={ex.Message}");
+ }
+ }
+
+ Logger.Info($"广播数据完成,成功发送到 {successCount}/{clientList.Count} 个客户端,数据长度={buffer.Length}字节");
+ return successCount;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"广播数据时发生异常:{ex.Message}");
+ OnServerError(ex);
+ return 0;
+ }
+ }
+
+ ///
+ /// 断开指定客户端的连接
+ ///
+ /// 会话 ID
+ /// 断开是否成功
+ public async Task DisconnectClientAsync(string sessionId)
+ {
+ try
+ {
+ if (string.IsNullOrWhiteSpace(sessionId))
+ {
+ Logger.Warn("断开客户端失败:SessionId 为空");
+ return false;
+ }
+
+ if (!_clients.TryGetValue(sessionId, out var client))
+ {
+ Logger.Warn($"断开客户端失败:未找到会话,SessionId={sessionId}");
+ return false;
+ }
+
+ client.TcpClient?.Close();
+ _clients.TryRemove(sessionId, out _);
+ Logger.Info($"已主动断开客户端连接,SessionId={sessionId}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"断开客户端连接时发生异常:SessionId={sessionId},错误={ex.Message}");
+ OnServerError(ex);
+ return false;
+ }
+ }
+
+ ///
+ /// 获取所有已连接客户端的会话 ID 列表
+ ///
+ /// 会话 ID 列表
+ public List GetConnectedSessionIds()
+ {
+ return _clients.Keys.ToList();
+ }
+
+ #endregion
+
+ #region 私有方法 - 客户端处理
+
+ ///
+ /// 接受客户端连接的循环
+ ///
+ private async Task AcceptClientsAsync(CancellationToken cancellationToken)
+ {
+ Logger.Info("开始接受客户端连接...");
+
+ while (!cancellationToken.IsCancellationRequested && _isRunning)
+ {
+ try
+ {
+ // 等待客户端连接
+ var tcpClient = await _listener.AcceptTcpClientAsync();
+
+ // 生成会话 ID
+ var sessionId = $"Session_{Interlocked.Increment(ref _sessionIdCounter)}_{DateTime.Now.Ticks}";
+
+ // 获取客户端信息
+ var remoteEndPoint = tcpClient.Client.RemoteEndPoint as IPEndPoint;
+ var remoteIp = remoteEndPoint?.Address.ToString() ?? "Unknown";
+ var remotePort = remoteEndPoint?.Port ?? 0;
+
+ // 创建客户端会话
+ var clientSession = new ClientSession
+ {
+ SessionId = sessionId,
+ TcpClient = tcpClient,
+ Stream = tcpClient.GetStream(),
+ RemoteIp = remoteIp,
+ RemotePort = remotePort,
+ ConnectedTime = DateTime.Now
+ };
+
+ // 添加到客户端字典
+ if (_clients.TryAdd(sessionId, clientSession))
+ {
+ Logger.Info($"客户端已连接,SessionId={sessionId},远程地址={remoteIp}:{remotePort},当前连接数={ConnectedClientCount}");
+
+ // 触发客户端连接事件
+ OnClientConnected(new ClientConnectedEventArgs
+ {
+ SessionId = sessionId,
+ RemoteIp = remoteIp,
+ RemotePort = remotePort,
+ ConnectedTime = clientSession.ConnectedTime
+ });
+
+ // 启动处理客户端数据的任务
+ _ = Task.Run(() => HandleClientAsync(clientSession, cancellationToken), cancellationToken);
+ }
+ else
+ {
+ Logger.Warn($"添加客户端会话失败,SessionId={sessionId}");
+ tcpClient.Close();
+ }
+ }
+ catch (ObjectDisposedException)
+ {
+ // 监听器已被释放,正常退出
+ Logger.Info("监听器已关闭,停止接受新连接");
+ break;
+ }
+ catch (SocketException ex) when (ex.SocketErrorCode == SocketError.Interrupted)
+ {
+ // 监听被中断,正常退出
+ Logger.Info("监听被中断,停止接受新连接");
+ break;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"接受客户端连接时发生异常:{ex.Message}");
+ OnServerError(ex);
+ await Task.Delay(1000, cancellationToken); // 等待一秒后重试
+ }
+ }
+
+ Logger.Info("停止接受客户端连接");
+ }
+
+ ///
+ /// 处理单个客户端的数据接收
+ ///
+ private async Task HandleClientAsync(ClientSession client, CancellationToken cancellationToken)
+ {
+ var sessionId = client.SessionId;
+ var stream = client.Stream;
+ var reader = new StreamReader(stream, Encoding.UTF8);
+
+ Logger.Debug($"开始处理客户端数据,SessionId={sessionId}");
+
+ try
+ {
+ // 设置读取超时
+ stream.ReadTimeout = ReceiveTimeout * 1000;
+
+ while (!cancellationToken.IsCancellationRequested && _isRunning)
+ {
+ try
+ {
+ // 读取一行数据(以 CRLF 或 LF 结尾)
+ var line = await reader.ReadLineAsync();
+
+ // 如果读取到 null,表示连接已关闭
+ if (line == null)
+ {
+ Logger.Info($"客户端连接已关闭,SessionId={sessionId}");
+ break;
+ }
+
+ // 记录接收日志
+ Logger.Debug($"接收到客户端数据,SessionId={sessionId},远程地址={client.RemoteIp}:{client.RemotePort},数据长度={line.Length}字符,内容={line}");
+
+ // 触发数据接收事件
+ OnDataReceived(new DataReceivedEventArgs
+ {
+ SessionId = sessionId,
+ RemoteIp = client.RemoteIp,
+ RemotePort = client.RemotePort,
+ Data = line,
+ ReceivedTime = DateTime.Now
+ });
+ }
+ catch (IOException ex) when (ex.InnerException is SocketException socketEx)
+ {
+ // 连接被重置或超时
+ Logger.Warn($"客户端连接异常,SessionId={sessionId},错误={socketEx.SocketErrorCode}");
+ break;
+ }
+ catch (IOException ex)
+ {
+ // 读取超时或其他 IO 异常
+ Logger.Warn(ex, $"读取客户端数据时发生 IO 异常,SessionId={sessionId}");
+ break;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"处理客户端数据时发生异常,SessionId={sessionId},错误={ex.Message}");
+ OnServerError(ex);
+ }
+ finally
+ {
+ // 清理客户端连接
+ try
+ {
+ client.TcpClient?.Close();
+ _clients.TryRemove(sessionId, out _);
+
+ Logger.Info($"客户端已断开,SessionId={sessionId},远程地址={client.RemoteIp}:{client.RemotePort},当前连接数={ConnectedClientCount}");
+
+ // 触发客户端断开事件
+ OnClientDisconnected(new ClientDisconnectedEventArgs
+ {
+ SessionId = sessionId,
+ RemoteIp = client.RemoteIp,
+ RemotePort = client.RemotePort,
+ DisconnectedTime = DateTime.Now,
+ Reason = "连接关闭"
+ });
+ }
+ catch (Exception ex)
+ {
+ Logger.Warn(ex, $"清理客户端连接时发生异常,SessionId={sessionId}");
+ }
+ }
+ }
+
+ #endregion
+
+ #region 私有方法 - 事件触发
+
+ ///
+ /// 触发数据接收事件
+ ///
+ private void OnDataReceived(DataReceivedEventArgs e)
+ {
+ try
+ {
+ DataReceived?.Invoke(this, e);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"触发 DataReceived 事件时发生异常:{ex.Message}");
+ }
+ }
+
+ ///
+ /// 触发客户端连接事件
+ ///
+ private void OnClientConnected(ClientConnectedEventArgs e)
+ {
+ try
+ {
+ ClientConnected?.Invoke(this, e);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"触发 ClientConnected 事件时发生异常:{ex.Message}");
+ }
+ }
+
+ ///
+ /// 触发客户端断开事件
+ ///
+ private void OnClientDisconnected(ClientDisconnectedEventArgs e)
+ {
+ try
+ {
+ ClientDisconnected?.Invoke(this, e);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"触发 ClientDisconnected 事件时发生异常:{ex.Message}");
+ }
+ }
+
+ ///
+ /// 触发服务器错误事件
+ ///
+ private void OnServerError(Exception ex)
+ {
+ try
+ {
+ ServerError?.Invoke(this, ex);
+ }
+ catch (Exception eventEx)
+ {
+ Logger.Error(eventEx, $"触发 ServerError 事件时发生异常:{eventEx.Message}");
+ }
+ }
+
+ #endregion
+
+ #region 内部类 - 客户端会话
+
+ ///
+ /// 客户端会话信息
+ ///
+ private class ClientSession
+ {
+ ///
+ /// 会话 ID
+ ///
+ public string SessionId { get; set; }
+
+ ///
+ /// TCP 客户端
+ ///
+ public TcpClient TcpClient { get; set; }
+
+ ///
+ /// 网络流
+ ///
+ public NetworkStream Stream { get; set; }
+
+ ///
+ /// 远程 IP 地址
+ ///
+ public string RemoteIp { get; set; }
+
+ ///
+ /// 远程端口
+ ///
+ public int RemotePort { get; set; }
+
+ ///
+ /// 连接时间
+ ///
+ public DateTime ConnectedTime { get; set; }
+ }
+
+ #endregion
+ }
+}
diff --git a/FATrace.OEMApp/Services/SocketService_Backup.cs b/FATrace.OEMApp/Services/SocketService_Backup.cs
new file mode 100644
index 0000000..2ead8be
--- /dev/null
+++ b/FATrace.OEMApp/Services/SocketService_Backup.cs
@@ -0,0 +1 @@
+// 备份文件 - 保留原始 SuperSocket 实现尝试
diff --git a/FATrace.OEMApp/Services/SocketService_使用示例.md b/FATrace.OEMApp/Services/SocketService_使用示例.md
new file mode 100644
index 0000000..cd4ea37
--- /dev/null
+++ b/FATrace.OEMApp/Services/SocketService_使用示例.md
@@ -0,0 +1,382 @@
+# SocketService 使用示例
+
+## 概述
+
+`SocketService` 是一个基于原生 TcpListener 实现的稳定可靠的 Socket 服务器,支持多客户端同时连接。
+
+## 主要特性
+
+- ✅ 基于原生 TcpListener,稳定可靠
+- ✅ 支持多客户端同时连接
+- ✅ 使用 CRLF (\r\n) 行分隔符的文本协议
+- ✅ UTF-8 编码
+- ✅ 详细的 NLog 日志记录
+- ✅ 事件驱动架构,方便外部处理业务逻辑
+- ✅ 支持单播和广播发送数据
+
+## 配置参数
+
+```csharp
+// 创建服务实例
+var socketService = new SocketService();
+
+// 配置参数(可选,有默认值)
+socketService.ListenIp = "127.0.0.1"; // 监听 IP,默认 127.0.0.1
+socketService.ListenPort = 6001; // 监听端口,默认 6001
+socketService.ReceiveTimeout = 30; // 接收超时(秒),默认 30
+socketService.SendTimeout = 30; // 发送超时(秒),默认 30
+```
+
+## 基本使用
+
+### 1. 启动服务器
+
+```csharp
+// 启动服务器
+bool success = await socketService.StartAsync();
+if (success)
+{
+ Console.WriteLine("服务器启动成功");
+}
+else
+{
+ Console.WriteLine("服务器启动失败");
+}
+```
+
+### 2. 订阅事件
+
+#### 数据接收事件
+
+```csharp
+socketService.DataReceived += (sender, e) =>
+{
+ // 处理接收到的数据
+ Console.WriteLine($"收到数据:SessionId={e.SessionId}, IP={e.RemoteIp}:{e.RemotePort}");
+ Console.WriteLine($"数据内容:{e.Data}");
+ Console.WriteLine($"接收时间:{e.ReceivedTime}");
+
+ // 在这里处理你的业务逻辑
+ // 例如:解析数据、保存到数据库、触发其他操作等
+};
+```
+
+#### 客户端连接事件
+
+```csharp
+socketService.ClientConnected += (sender, e) =>
+{
+ Console.WriteLine($"客户端已连接:SessionId={e.SessionId}, IP={e.RemoteIp}:{e.RemotePort}");
+ Console.WriteLine($"连接时间:{e.ConnectedTime}");
+ Console.WriteLine($"当前连接数:{socketService.ConnectedClientCount}");
+};
+```
+
+#### 客户端断开事件
+
+```csharp
+socketService.ClientDisconnected += (sender, e) =>
+{
+ Console.WriteLine($"客户端已断开:SessionId={e.SessionId}, IP={e.RemoteIp}:{e.RemotePort}");
+ Console.WriteLine($"断开原因:{e.Reason}");
+ Console.WriteLine($"断开时间:{e.DisconnectedTime}");
+ Console.WriteLine($"当前连接数:{socketService.ConnectedClientCount}");
+};
+```
+
+#### 服务器错误事件
+
+```csharp
+socketService.ServerError += (sender, ex) =>
+{
+ Console.WriteLine($"服务器发生错误:{ex.Message}");
+ Console.WriteLine($"错误堆栈:{ex.StackTrace}");
+};
+```
+
+### 3. 发送数据
+
+#### 向指定客户端发送数据
+
+```csharp
+string sessionId = "Session_1_xxx"; // 从事件参数中获取的 SessionId
+string data = "Hello Client!";
+
+bool success = await socketService.SendToClientAsync(sessionId, data);
+if (success)
+{
+ Console.WriteLine("数据发送成功");
+}
+```
+
+#### 向所有客户端广播数据
+
+```csharp
+string data = "Broadcast Message to All!";
+
+int successCount = await socketService.BroadcastAsync(data);
+Console.WriteLine($"广播成功发送到 {successCount} 个客户端");
+```
+
+### 4. 断开指定客户端
+
+```csharp
+string sessionId = "Session_1_xxx";
+
+bool success = await socketService.DisconnectClientAsync(sessionId);
+if (success)
+{
+ Console.WriteLine("客户端已断开");
+}
+```
+
+### 5. 获取所有已连接客户端
+
+```csharp
+List sessionIds = socketService.GetConnectedSessionIds();
+Console.WriteLine($"当前连接的客户端数量:{sessionIds.Count}");
+foreach (var sessionId in sessionIds)
+{
+ Console.WriteLine($" - {sessionId}");
+}
+```
+
+### 6. 停止服务器
+
+```csharp
+await socketService.StopAsync();
+Console.WriteLine("服务器已停止");
+```
+
+## 完整示例
+
+```csharp
+using FATrace.OEMApp.Services;
+using System;
+using System.Threading.Tasks;
+
+public class SocketServerExample
+{
+ private SocketService _socketService;
+
+ public async Task RunAsync()
+ {
+ // 1. 创建服务实例
+ _socketService = new SocketService
+ {
+ ListenIp = "0.0.0.0", // 监听所有网卡
+ ListenPort = 6001,
+ ReceiveTimeout = 30,
+ SendTimeout = 30
+ };
+
+ // 2. 订阅事件
+ _socketService.DataReceived += OnDataReceived;
+ _socketService.ClientConnected += OnClientConnected;
+ _socketService.ClientDisconnected += OnClientDisconnected;
+ _socketService.ServerError += OnServerError;
+
+ // 3. 启动服务器
+ bool success = await _socketService.StartAsync();
+ if (!success)
+ {
+ Console.WriteLine("服务器启动失败");
+ return;
+ }
+
+ Console.WriteLine("服务器已启动,按任意键停止...");
+ Console.ReadKey();
+
+ // 4. 停止服务器
+ await _socketService.StopAsync();
+ }
+
+ private void OnDataReceived(object sender, SocketService.DataReceivedEventArgs e)
+ {
+ Console.WriteLine($"[数据接收] SessionId={e.SessionId}, 数据={e.Data}");
+
+ // 处理业务逻辑
+ // 例如:回复客户端
+ _ = _socketService.SendToClientAsync(e.SessionId, $"收到:{e.Data}");
+ }
+
+ private void OnClientConnected(object sender, SocketService.ClientConnectedEventArgs e)
+ {
+ Console.WriteLine($"[客户端连接] SessionId={e.SessionId}, IP={e.RemoteIp}:{e.RemotePort}");
+
+ // 欢迎消息
+ _ = _socketService.SendToClientAsync(e.SessionId, "欢迎连接到服务器!");
+ }
+
+ private void OnClientDisconnected(object sender, SocketService.ClientDisconnectedEventArgs e)
+ {
+ Console.WriteLine($"[客户端断开] SessionId={e.SessionId}, 原因={e.Reason}");
+ }
+
+ private void OnServerError(object sender, Exception ex)
+ {
+ Console.WriteLine($"[服务器错误] {ex.Message}");
+ }
+}
+```
+
+## 在 WinForms 中使用
+
+```csharp
+public partial class MainApp : Form
+{
+ private SocketService _socketService;
+
+ private async void MainApp_Load(object sender, EventArgs e)
+ {
+ // 初始化 Socket 服务
+ _socketService = new SocketService
+ {
+ ListenIp = "127.0.0.1",
+ ListenPort = 6001
+ };
+
+ // 订阅事件(注意:需要使用 Invoke 更新 UI)
+ _socketService.DataReceived += (s, args) =>
+ {
+ this.Invoke(new Action(() =>
+ {
+ // 更新 UI
+ txtLog.AppendText($"收到数据:{args.Data}\r\n");
+ }));
+ };
+
+ // 启动服务器
+ bool success = await _socketService.StartAsync();
+ if (success)
+ {
+ lblStatus.Text = "服务器运行中";
+ lblStatus.ForeColor = Color.Green;
+ }
+ }
+
+ private async void MainApp_FormClosing(object sender, FormClosingEventArgs e)
+ {
+ // 停止服务器
+ if (_socketService != null && _socketService.IsRunning)
+ {
+ await _socketService.StopAsync();
+ }
+ }
+
+ private async void btnSendToAll_Click(object sender, EventArgs e)
+ {
+ // 广播消息
+ string message = txtMessage.Text;
+ int count = await _socketService.BroadcastAsync(message);
+ MessageBox.Show($"已发送到 {count} 个客户端");
+ }
+}
+```
+
+## 协议说明
+
+### 数据格式
+
+- **编码**:UTF-8
+- **行分隔符**:CRLF (\r\n)
+- **每次发送/接收**:一行文本数据
+
+### 客户端示例(C#)
+
+```csharp
+using System;
+using System.IO;
+using System.Net.Sockets;
+using System.Text;
+
+public class SimpleClient
+{
+ public static async Task Main()
+ {
+ using var client = new TcpClient();
+ await client.ConnectAsync("127.0.0.1", 6001);
+
+ var stream = client.GetStream();
+ var writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true };
+ var reader = new StreamReader(stream, Encoding.UTF8);
+
+ // 发送数据
+ await writer.WriteLineAsync("Hello Server!");
+
+ // 接收数据
+ string response = await reader.ReadLineAsync();
+ Console.WriteLine($"服务器回复:{response}");
+ }
+}
+```
+
+### 客户端示例(Python)
+
+```python
+import socket
+
+# 连接服务器
+client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+client.connect(('127.0.0.1', 6001))
+
+# 发送数据(注意:需要添加 \r\n)
+message = "Hello Server!\r\n"
+client.send(message.encode('utf-8'))
+
+# 接收数据
+response = client.recv(1024).decode('utf-8')
+print(f"服务器回复:{response}")
+
+client.close()
+```
+
+## 日志说明
+
+SocketService 使用 NLog 记录详细日志,包括:
+
+- **Info 级别**:服务器启动/停止、客户端连接/断开、广播完成
+- **Debug 级别**:数据接收/发送的详细内容
+- **Warn 级别**:参数错误、会话未找到、连接异常
+- **Error 级别**:异常堆栈信息
+
+日志示例:
+```
+2026-01-01 21:00:00.123 [INFO] 正在启动 Socket 服务器,监听地址:127.0.0.1:6001,超时时间:30秒
+2026-01-01 21:00:00.456 [INFO] Socket 服务器启动成功,监听地址:127.0.0.1:6001
+2026-01-01 21:00:05.789 [INFO] 客户端已连接,SessionId=Session_1_xxx,远程地址=127.0.0.1:54321,当前连接数=1
+2026-01-01 21:00:10.123 [DEBUG] 接收到客户端数据,SessionId=Session_1_xxx,远程地址=127.0.0.1:54321,数据长度=13字符,内容=Hello Server!
+```
+
+## 注意事项
+
+1. **线程安全**:所有公共方法都是线程安全的,可以在多线程环境中使用
+2. **事件处理**:事件回调在后台线程执行,如需更新 UI,请使用 `Invoke`
+3. **超时设置**:如果客户端长时间无数据交互,会自动断开连接
+4. **数据格式**:发送的数据会自动添加 CRLF,接收的数据会自动去除 CRLF
+5. **异常处理**:所有异常都会被捕获并记录日志,不会导致服务器崩溃
+
+## 常见问题
+
+### Q1: 如何监听所有网卡?
+```csharp
+socketService.ListenIp = "0.0.0.0";
+```
+
+### Q2: 如何获取客户端的 SessionId?
+SessionId 会在事件参数中提供(`DataReceivedEventArgs.SessionId`),你可以保存它用于后续发送数据。
+
+### Q3: 如何处理粘包问题?
+当前实现使用行分隔符(CRLF),每次 `ReadLineAsync` 读取一行完整数据,自动处理粘包。
+
+### Q4: 如何实现心跳检测?
+可以在客户端定期发送心跳数据,服务器通过 `ReceiveTimeout` 自动断开无响应的客户端。
+
+### Q5: 如何修改协议格式?
+如需使用其他协议(如固定长度、带长度头等),需要修改 `HandleClientAsync` 方法中的数据读取逻辑。
+
+## 性能建议
+
+1. **连接数**:理论上支持数千个并发连接,实际取决于服务器性能
+2. **数据量**:适合中小数据量传输,大文件传输建议使用其他方案
+3. **日志级别**:生产环境建议将 Debug 日志关闭,减少性能开销
diff --git a/FATrace.OEMApp/Services/TimeClearDataService.cs b/FATrace.OEMApp/Services/TimeClearDataService.cs
index e242395..6c23675 100644
--- a/FATrace.OEMApp/Services/TimeClearDataService.cs
+++ b/FATrace.OEMApp/Services/TimeClearDataService.cs
@@ -20,13 +20,13 @@ namespace FATrace.OEMApp.Services
///
/// 数据库保存的信息天数
///
- private int DbRetentionDays = 180;
+ private int DbRetentionDays = 365;
public event Action? Info;
public void Start()
{
FileRetentionDays=ConfigHelper.GetIntOrDefault("VideoFileSaveDay", 365);
- DbRetentionDays = ConfigHelper.GetIntOrDefault("DbSaveDay", 180);
+ DbRetentionDays = ConfigHelper.GetIntOrDefault("DbSaveDay", 365);
if (_cts != null) return;
try
diff --git a/FATrace.OEMApp/Services/TouchSocketServer.cs b/FATrace.OEMApp/Services/TouchSocketServer.cs
new file mode 100644
index 0000000..6eaf309
--- /dev/null
+++ b/FATrace.OEMApp/Services/TouchSocketServer.cs
@@ -0,0 +1,640 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using NLog;
+using TouchSocket.Core;
+using TouchSocket.Sockets;
+
+namespace FATrace.OEMApp.Services
+{
+ ///
+ /// TouchSocket 服务器 - 基于 TouchSocket 实现的多客户端 TCP 服务器
+ /// 功能:接收客户端数据、解析为字符串、触发事件通知外部、支持向客户端发送数据
+ /// 协议:使用 CRLF (\r\n) 作为行分隔符的文本协议
+ /// 编码:UTF-8
+ ///
+ public class TouchSocketServer
+ {
+ ///
+ /// 构造函数
+ ///
+ ///
+ ///
+ ///
+ ///
+ public TouchSocketServer(string listenIp, int listenPort, int receiveTimeout, int sendTimeout)
+ {
+ ListenIp = listenIp;
+ ListenPort = listenPort;
+ ReceiveTimeout = receiveTimeout;
+ SendTimeout = sendTimeout;
+ }
+
+
+ #region 私有字段
+
+ ///
+ /// NLog 日志记录器
+ ///
+ private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
+
+ ///
+ /// TouchSocket TCP 服务实例
+ ///
+ private TcpService _service;
+
+ ///
+ /// 服务器是否正在运行
+ ///
+ private bool _isRunning = false;
+
+ ///
+ /// 已连接的客户端字典(ClientId -> dynamic)
+ /// 用于快速查找和管理客户端
+ /// 使用 dynamic 类型以兼容 TouchSocket 4.0 的客户端类型
+ ///
+ private readonly ConcurrentDictionary _clients = new ConcurrentDictionary();
+
+ #endregion
+
+ #region 公共属性
+
+ ///
+ /// 服务器监听 IP 地址
+ /// 默认:127.0.0.1(本地回环)
+ /// 0.0.0.0 表示监听所有网卡
+ ///
+ public string ListenIp { get; set; } = "127.0.0.1";
+
+ ///
+ /// 服务器监听端口
+ /// 默认:6001
+ ///
+ public int ListenPort { get; set; } = 6001;
+
+ ///
+ /// 接收超时时间(毫秒)
+ /// 默认:30000 毫秒(30秒)
+ /// 如果客户端在此时间内无数据交互,连接将被关闭
+ ///
+ public int ReceiveTimeout { get; set; } = 30000;
+
+ ///
+ /// 发送超时时间(毫秒)
+ /// 默认:30000 毫秒(30秒)
+ ///
+ public int SendTimeout { get; set; } = 30000;
+
+ ///
+ /// 缓冲区大小(字节)
+ /// 默认:1024 * 64(64KB)
+ ///
+ public int BufferLength { get; set; } = 1024 * 64;
+
+ ///
+ /// 最大连接数
+ /// 默认:10000
+ ///
+ public int MaxCount { get; set; } = 10000;
+
+ ///
+ /// 服务器是否正在运行
+ ///
+ public bool IsRunning => _isRunning;
+
+ ///
+ /// 当前连接的客户端数量
+ ///
+ public int ConnectedClientCount => _clients.Count;
+
+ #endregion
+
+ #region 事件定义
+
+ ///
+ /// 数据接收事件参数
+ ///
+ public class DataReceivedEventArgs : EventArgs
+ {
+ ///
+ /// 客户端 ID(唯一标识)
+ ///
+ public string? ClientId { get; set; }
+
+ ///
+ /// 客户端 IP 地址
+ ///
+ public string? RemoteIp { get; set; }
+
+ ///
+ /// 客户端端口
+ ///
+ public int RemotePort { get; set; }
+
+ ///
+ /// 接收到的数据(已解析为字符串)
+ ///
+ public string? Data { get; set; }
+
+ ///
+ /// 接收时间
+ ///
+ public DateTime ReceivedTime { get; set; }
+ }
+
+ ///
+ /// 客户端连接事件参数
+ ///
+ public class ClientConnectedEventArgs : EventArgs
+ {
+ ///
+ /// 客户端 ID
+ ///
+ public string? ClientId { get; set; }
+
+ ///
+ /// 客户端 IP 地址
+ ///
+ public string? RemoteIp { get; set; }
+
+ ///
+ /// 客户端端口
+ ///
+ public int RemotePort { get; set; }
+
+ ///
+ /// 连接时间
+ ///
+ public DateTime ConnectedTime { get; set; }
+ }
+
+ ///
+ /// 客户端断开事件参数
+ ///
+ public class ClientDisconnectedEventArgs : EventArgs
+ {
+ ///
+ /// 客户端 ID
+ ///
+ public string? ClientId { get; set; }
+
+ ///
+ /// 客户端 IP 地址
+ ///
+ public string? RemoteIp { get; set; }
+
+ ///
+ /// 客户端端口
+ ///
+ public int RemotePort { get; set; }
+
+ ///
+ /// 断开时间
+ ///
+ public DateTime DisconnectedTime { get; set; }
+
+ ///
+ /// 断开原因
+ ///
+ public string? Reason { get; set; }
+ }
+
+ ///
+ /// 数据接收事件
+ /// 当接收到客户端数据时触发,外部可订阅此事件进行业务处理
+ ///
+ public event EventHandler DataReceived;
+
+ ///
+ /// 客户端连接事件
+ /// 当有新客户端连接时触发
+ ///
+ public event EventHandler ClientConnected;
+
+ ///
+ /// 客户端断开事件
+ /// 当客户端断开连接时触发
+ ///
+ public event EventHandler ClientDisconnected;
+
+ ///
+ /// 服务器错误事件
+ /// 当服务器发生错误时触发
+ ///
+ public event EventHandler ServerError;
+
+ #endregion
+
+ #region 公共方法
+
+ ///
+ /// 启动 Socket 服务器
+ ///
+ /// 启动是否成功
+ public async Task StartAsync()
+ {
+ try
+ {
+ // 检查是否已经在运行
+ if (_isRunning)
+ {
+ Logger.Warn("Socket 服务器已经在运行中,无需重复启动");
+ return true;
+ }
+
+ Logger.Info($"正在启动 Socket 服务器,监听地址:{ListenIp}:{ListenPort},超时时间:{ReceiveTimeout}毫秒");
+
+ // 创建 TouchSocket 服务实例
+ _service = new TcpService();
+
+ // 配置连接事件
+ _service.Connecting = (client, e) =>
+ {
+ Logger.Debug($"客户端正在连接:{client.Id},远程地址:{client.IP}:{client.Port}");
+ return EasyTask.CompletedTask;
+ };
+
+ // 配置连接成功事件
+ _service.Connected = (client, e) =>
+ {
+ try
+ {
+ // 添加到客户端字典
+ _clients.TryAdd(client.Id, client);
+
+ // 记录连接日志
+ Logger.Info($"客户端已连接,ClientId={client.Id},远程地址={client.IP}:{client.Port},当前连接数={ConnectedClientCount}");
+
+ // 触发客户端连接事件
+ OnClientConnected(new ClientConnectedEventArgs
+ {
+ ClientId = client.Id,
+ RemoteIp = client.IP,
+ RemotePort = client.Port,
+ ConnectedTime = DateTime.Now
+ });
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"处理客户端连接时发生异常:ClientId={client.Id},错误={ex.Message}");
+ OnServerError(ex);
+ }
+ return EasyTask.CompletedTask;
+ };
+
+ // 配置断开连接事件
+ _service.Closed = (client, e) =>
+ {
+ try
+ {
+ // 从客户端字典中移除
+ _clients.TryRemove(client.Id, out _);
+
+ // 记录断开日志
+ Logger.Info($"客户端已断开,ClientId={client.Id},远程地址={client.IP}:{client.Port},原因={e.Message},当前连接数={ConnectedClientCount}");
+
+ // 触发客户端断开事件
+ OnClientDisconnected(new ClientDisconnectedEventArgs
+ {
+ ClientId = client.Id,
+ RemoteIp = client.IP,
+ RemotePort = client.Port,
+ DisconnectedTime = DateTime.Now,
+ Reason = e.Message
+ });
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"处理客户端断开时发生异常:ClientId={client.Id},错误={ex.Message}");
+ OnServerError(ex);
+ }
+ return EasyTask.CompletedTask;
+ };
+
+ // 配置数据接收事件
+ _service.Received = (client, e) =>
+ {
+ try
+ {
+ // 解析数据为字符串
+ // TouchSocket 4.0 使用 e.Memory 获取数据
+ var data = Encoding.UTF8.GetString(e.Memory.Span);
+
+ // 记录接收日志
+ Logger.Debug($"接收到客户端数据,ClientId={client.Id},远程地址={client.IP}:{client.Port},数据长度={data.Length}字符,内容={data}");
+
+ // 触发数据接收事件
+ OnDataReceived(new DataReceivedEventArgs
+ {
+ ClientId = client.Id,
+ RemoteIp = client.IP,
+ RemotePort = client.Port,
+ Data = data,
+ ReceivedTime = DateTime.Now
+ });
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"处理接收数据时发生异常:ClientId={client.Id},错误={ex.Message}");
+ OnServerError(ex);
+ }
+ return EasyTask.CompletedTask;
+ };
+
+ // 配置服务器
+ var config = new TouchSocketConfig();
+ config.SetListenIPHosts($"tcp://{ListenIp}:{ListenPort}")
+ .SetMaxCount(MaxCount); // 设置最大连接数
+
+ // 应用配置并启动服务器
+ await _service.SetupAsync(config);
+ await _service.StartAsync();
+
+ _isRunning = true;
+ Logger.Info($"Socket 服务器启动成功,监听地址:{ListenIp}:{ListenPort}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"启动 Socket 服务器时发生异常:{ex.Message}");
+ OnServerError(ex);
+ return false;
+ }
+ }
+
+ ///
+ /// 停止 Socket 服务器
+ ///
+ public async Task StopAsync()
+ {
+ try
+ {
+ if (!_isRunning || _service == null)
+ {
+ Logger.Warn("Socket 服务器未运行,无需停止");
+ return;
+ }
+
+ Logger.Info("正在停止 Socket 服务器...");
+
+ _isRunning = false;
+
+ // 停止服务器
+ await _service?.StopAsync();
+ _service?.Dispose();
+ _service = null;
+
+ // 清空客户端字典
+ _clients.Clear();
+
+ Logger.Info("Socket 服务器已停止");
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"停止 Socket 服务器时发生异常:{ex.Message}");
+ OnServerError(ex);
+ }
+ }
+
+ ///
+ /// 向指定客户端发送数据
+ ///
+ /// 客户端 ID(唯一标识)
+ /// 要发送的字符串数据
+ /// 发送是否成功
+ public bool SendToClient(string clientId, string data)
+ {
+ try
+ {
+ // 参数校验
+ if (string.IsNullOrWhiteSpace(clientId))
+ {
+ Logger.Warn("发送数据失败:ClientId 为空");
+ return false;
+ }
+
+ if (string.IsNullOrEmpty(data))
+ {
+ Logger.Warn($"发送数据失败:数据为空,ClientId={clientId}");
+ return false;
+ }
+
+ if (_service == null || !_isRunning)
+ {
+ Logger.Warn($"发送数据失败:服务器未运行或服务实例为空,ClientId={clientId}");
+ return false;
+ }
+
+ // 可选:先检查是否存在该客户端,便于输出更友好的日志
+ //if (!_clients.ContainsKey(clientId))
+ //{
+ // Logger.Warn($"发送数据失败:未找到客户端,ClientId={clientId}");
+ // return false;
+ //}
+
+ // 发送数据(自动添加 CRLF 行结束符)
+ var dataToSend = data.EndsWith("\r\n") ? data : data + "\r\n";
+ var buffer = Encoding.UTF8.GetBytes(dataToSend);
+
+ // 使用 TcpService 的 SendAsync(id, buffer) 按会话 Id 发送
+ _service.SendAsync(clientId, buffer).GetAwaiter().GetResult();
+
+ Logger.Debug($"向客户端发送数据成功,ClientId={clientId},数据长度={buffer.Length}字节,内容={data}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"向客户端发送数据时发生异常:ClientId={clientId},错误={ex.Message}");
+ OnServerError(ex);
+ return false;
+ }
+ }
+
+ ///
+ /// 向所有已连接的客户端广播数据
+ ///
+ /// 要广播的字符串数据
+ /// 成功发送的客户端数量
+ public int Broadcast(string data)
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(data))
+ {
+ Logger.Warn("广播数据失败:数据为空");
+ return 0;
+ }
+
+ // 获取所有客户端
+ var clientList = _clients.Values.ToList();
+
+ if (clientList.Count == 0)
+ {
+ Logger.Debug("广播数据:当前无已连接的客户端");
+ return 0;
+ }
+
+ // 准备数据
+ var dataToSend = data.EndsWith("\r\n") ? data : data + "\r\n";
+ var buffer = Encoding.UTF8.GetBytes(dataToSend);
+
+ // 向所有客户端发送
+ int successCount = 0;
+ if (_service == null || !_isRunning)
+ {
+ Logger.Warn("广播数据失败:服务器未运行或服务实例为空");
+ return 0;
+ }
+
+ foreach (var client in clientList)
+ {
+ try
+ {
+ // 使用 TcpService.SendAsync 通过客户端 Id 发送
+ _service.SendAsync(client.Id, buffer).GetAwaiter().GetResult();
+ successCount++;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"向客户端广播数据失败:ClientId={client.Id},错误={ex.Message}");
+ }
+ }
+
+ Logger.Info($"广播数据完成,成功发送到 {successCount}/{clientList.Count} 个客户端,数据长度={buffer.Length}字节");
+ return successCount;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"广播数据时发生异常:{ex.Message}");
+ OnServerError(ex);
+ return 0;
+ }
+ }
+
+ ///
+ /// 断开指定客户端的连接
+ ///
+ /// 客户端 ID
+ /// 断开是否成功
+ public bool DisconnectClient(string clientId)
+ {
+ try
+ {
+ if (string.IsNullOrWhiteSpace(clientId))
+ {
+ Logger.Warn("断开客户端失败:ClientId 为空");
+ return false;
+ }
+
+ if (!_clients.TryGetValue(clientId, out var client))
+ {
+ Logger.Warn($"断开客户端失败:未找到客户端,ClientId={clientId}");
+ return false;
+ }
+
+ client.Close("服务器主动断开");
+ Logger.Info($"已主动断开客户端连接,ClientId={clientId}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"断开客户端连接时发生异常:ClientId={clientId},错误={ex.Message}");
+ OnServerError(ex);
+ return false;
+ }
+ }
+
+ ///
+ /// 获取所有已连接客户端的 ID 列表
+ ///
+ /// 客户端 ID 列表
+ public List GetConnectedClientIds()
+ {
+ return _clients.Keys.ToList();
+ }
+
+ ///
+ /// 获取指定客户端的详细信息
+ ///
+ /// 客户端 ID
+ /// 客户端信息字符串,如果未找到返回 null
+ public string GetClientInfo(string clientId)
+ {
+ if (_clients.TryGetValue(clientId, out var client))
+ {
+ return $"ClientId={client.Id}, IP={client.IP}:{client.Port}, 在线={client.Online}";
+ }
+ return null;
+ }
+
+ #endregion
+
+
+ #region 私有方法 - 事件触发
+
+ ///
+ /// 触发数据接收事件
+ ///
+ private void OnDataReceived(DataReceivedEventArgs e)
+ {
+ try
+ {
+ DataReceived?.Invoke(this, e);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"触发 DataReceived 事件时发生异常:{ex.Message}");
+ }
+ }
+
+ ///
+ /// 触发客户端连接事件
+ ///
+ private void OnClientConnected(ClientConnectedEventArgs e)
+ {
+ try
+ {
+ ClientConnected?.Invoke(this, e);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"触发 ClientConnected 事件时发生异常:{ex.Message}");
+ }
+ }
+
+ ///
+ /// 触发客户端断开事件
+ ///
+ private void OnClientDisconnected(ClientDisconnectedEventArgs e)
+ {
+ try
+ {
+ ClientDisconnected?.Invoke(this, e);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"触发 ClientDisconnected 事件时发生异常:{ex.Message}");
+ }
+ }
+
+ ///
+ /// 触发服务器错误事件
+ ///
+ private void OnServerError(Exception ex)
+ {
+ try
+ {
+ ServerError?.Invoke(this, ex);
+ }
+ catch (Exception eventEx)
+ {
+ Logger.Error(eventEx, $"触发 ServerError 事件时发生异常:{eventEx.Message}");
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/FATrace.OEMApp/Services/TouchSocketServer_使用示例.md b/FATrace.OEMApp/Services/TouchSocketServer_使用示例.md
new file mode 100644
index 0000000..7328232
--- /dev/null
+++ b/FATrace.OEMApp/Services/TouchSocketServer_使用示例.md
@@ -0,0 +1,410 @@
+# TouchSocketServer 使用示例
+
+## 概述
+
+`TouchSocketServer` 是一个基于 TouchSocket 4.0 实现的稳定可靠的 Socket 服务器,支持多客户端同时连接。
+
+## 主要特性
+
+- ✅ 基于 TouchSocket 4.0,性能优异
+- ✅ 支持多客户端同时连接
+- ✅ UTF-8 编码
+- ✅ 详细的 NLog 日志记录
+- ✅ 事件驱动架构,方便外部处理业务逻辑
+- ✅ 支持单播和广播发送数据
+- ✅ 异步操作,性能优秀
+
+## 配置参数
+
+```csharp
+// 创建服务实例
+var socketServer = new TouchSocketServer();
+
+// 配置参数(可选,有默认值)
+socketServer.ListenIp = "127.0.0.1"; // 监听 IP,默认 127.0.0.1
+socketServer.ListenPort = 6001; // 监听端口,默认 6001
+socketServer.ReceiveTimeout = 30000; // 接收超时(毫秒),默认 30000
+socketServer.SendTimeout = 30000; // 发送超时(毫秒),默认 30000
+socketServer.BufferLength = 1024 * 64; // 缓冲区大小,默认 64KB
+socketServer.MaxCount = 10000; // 最大连接数,默认 10000
+```
+
+## 基本使用
+
+### 1. 启动服务器
+
+```csharp
+// 启动服务器(异步)
+bool success = await socketServer.StartAsync();
+if (success)
+{
+ Console.WriteLine("服务器启动成功");
+}
+else
+{
+ Console.WriteLine("服务器启动失败");
+}
+```
+
+### 2. 订阅事件
+
+#### 数据接收事件
+
+```csharp
+socketServer.DataReceived += (sender, e) =>
+{
+ // 处理接收到的数据
+ Console.WriteLine($"收到数据:ClientId={e.ClientId}, IP={e.RemoteIp}:{e.RemotePort}");
+ Console.WriteLine($"数据内容:{e.Data}");
+ Console.WriteLine($"接收时间:{e.ReceivedTime}");
+
+ // 在这里处理你的业务逻辑
+ // 例如:解析数据、保存到数据库、触发其他操作等
+};
+```
+
+#### 客户端连接事件
+
+```csharp
+socketServer.ClientConnected += (sender, e) =>
+{
+ Console.WriteLine($"客户端已连接:ClientId={e.ClientId}, IP={e.RemoteIp}:{e.RemotePort}");
+ Console.WriteLine($"连接时间:{e.ConnectedTime}");
+ Console.WriteLine($"当前连接数:{socketServer.ConnectedClientCount}");
+};
+```
+
+#### 客户端断开事件
+
+```csharp
+socketServer.ClientDisconnected += (sender, e) =>
+{
+ Console.WriteLine($"客户端已断开:ClientId={e.ClientId}, IP={e.RemoteIp}:{e.RemotePort}");
+ Console.WriteLine($"断开原因:{e.Reason}");
+ Console.WriteLine($"断开时间:{e.DisconnectedTime}");
+ Console.WriteLine($"当前连接数:{socketServer.ConnectedClientCount}");
+};
+```
+
+#### 服务器错误事件
+
+```csharp
+socketServer.ServerError += (sender, ex) =>
+{
+ Console.WriteLine($"服务器发生错误:{ex.Message}");
+ Console.WriteLine($"错误堆栈:{ex.StackTrace}");
+};
+```
+
+### 3. 发送数据
+
+#### 向指定客户端发送数据
+
+```csharp
+string clientId = "Session_1_xxx"; // 从事件参数中获取的 ClientId
+string data = "Hello Client!";
+
+bool success = socketServer.SendToClient(clientId, data);
+if (success)
+{
+ Console.WriteLine("数据发送成功");
+}
+```
+
+#### 向所有客户端广播数据
+
+```csharp
+string data = "Broadcast Message to All!";
+
+int successCount = socketServer.Broadcast(data);
+Console.WriteLine($"广播成功发送到 {successCount} 个客户端");
+```
+
+### 4. 断开指定客户端
+
+```csharp
+string clientId = "Session_1_xxx";
+
+bool success = socketServer.DisconnectClient(clientId);
+if (success)
+{
+ Console.WriteLine("客户端已断开");
+}
+```
+
+### 5. 获取所有已连接客户端
+
+```csharp
+List clientIds = socketServer.GetConnectedClientIds();
+Console.WriteLine($"当前连接的客户端数量:{clientIds.Count}");
+foreach (var clientId in clientIds)
+{
+ Console.WriteLine($" - {clientId}");
+}
+```
+
+### 6. 获取客户端信息
+
+```csharp
+string clientId = "Session_1_xxx";
+string info = socketServer.GetClientInfo(clientId);
+Console.WriteLine($"客户端信息:{info}");
+```
+
+### 7. 停止服务器
+
+```csharp
+await socketServer.StopAsync();
+Console.WriteLine("服务器已停止");
+```
+
+## 完整示例
+
+```csharp
+using FATrace.OEMApp.Services;
+using System;
+using System.Threading.Tasks;
+
+public class TouchSocketServerExample
+{
+ private TouchSocketServer _socketServer;
+
+ public async Task RunAsync()
+ {
+ // 1. 创建服务实例
+ _socketServer = new TouchSocketServer
+ {
+ ListenIp = "0.0.0.0", // 监听所有网卡
+ ListenPort = 6001,
+ ReceiveTimeout = 30000,
+ SendTimeout = 30000
+ };
+
+ // 2. 订阅事件
+ _socketServer.DataReceived += OnDataReceived;
+ _socketServer.ClientConnected += OnClientConnected;
+ _socketServer.ClientDisconnected += OnClientDisconnected;
+ _socketServer.ServerError += OnServerError;
+
+ // 3. 启动服务器
+ bool success = await _socketServer.StartAsync();
+ if (!success)
+ {
+ Console.WriteLine("服务器启动失败");
+ return;
+ }
+
+ Console.WriteLine("服务器已启动,按任意键停止...");
+ Console.ReadKey();
+
+ // 4. 停止服务器
+ await _socketServer.StopAsync();
+ }
+
+ private void OnDataReceived(object sender, TouchSocketServer.DataReceivedEventArgs e)
+ {
+ Console.WriteLine($"[数据接收] ClientId={e.ClientId}, 数据={e.Data}");
+
+ // 处理业务逻辑
+ // 例如:回复客户端
+ _socketServer.SendToClient(e.ClientId, $"收到:{e.Data}");
+ }
+
+ private void OnClientConnected(object sender, TouchSocketServer.ClientConnectedEventArgs e)
+ {
+ Console.WriteLine($"[客户端连接] ClientId={e.ClientId}, IP={e.RemoteIp}:{e.RemotePort}");
+
+ // 欢迎消息
+ _socketServer.SendToClient(e.ClientId, "欢迎连接到服务器!");
+ }
+
+ private void OnClientDisconnected(object sender, TouchSocketServer.ClientDisconnectedEventArgs e)
+ {
+ Console.WriteLine($"[客户端断开] ClientId={e.ClientId}, 原因={e.Reason}");
+ }
+
+ private void OnServerError(object sender, Exception ex)
+ {
+ Console.WriteLine($"[服务器错误] {ex.Message}");
+ }
+}
+```
+
+## 在 WinForms 中使用
+
+```csharp
+public partial class MainApp : Form
+{
+ private TouchSocketServer _socketServer;
+
+ private async void MainApp_Load(object sender, EventArgs e)
+ {
+ // 初始化 Socket 服务
+ _socketServer = new TouchSocketServer
+ {
+ ListenIp = "127.0.0.1",
+ ListenPort = 6001
+ };
+
+ // 订阅事件(注意:需要使用 Invoke 更新 UI)
+ _socketServer.DataReceived += (s, args) =>
+ {
+ this.Invoke(new Action(() =>
+ {
+ // 更新 UI
+ txtLog.AppendText($"收到数据:{args.Data}\r\n");
+ }));
+ };
+
+ // 启动服务器
+ bool success = await _socketServer.StartAsync();
+ if (success)
+ {
+ lblStatus.Text = "服务器运行中";
+ lblStatus.ForeColor = Color.Green;
+ }
+ }
+
+ private async void MainApp_FormClosing(object sender, FormClosingEventArgs e)
+ {
+ // 停止服务器
+ if (_socketServer != null && _socketServer.IsRunning)
+ {
+ await _socketServer.StopAsync();
+ }
+ }
+
+ private void btnSendToAll_Click(object sender, EventArgs e)
+ {
+ // 广播消息
+ string message = txtMessage.Text;
+ int count = _socketServer.Broadcast(message);
+ MessageBox.Show($"已发送到 {count} 个客户端");
+ }
+}
+```
+
+## 协议说明
+
+### 数据格式
+
+- **编码**:UTF-8
+- **数据传输**:原始字节流(TouchSocket 会自动处理)
+- **每次接收**:通过事件参数获取完整数据
+
+### 客户端示例(C#)
+
+```csharp
+using System;
+using System.IO;
+using System.Net.Sockets;
+using System.Text;
+
+public class SimpleClient
+{
+ public static async Task Main()
+ {
+ using var client = new TcpClient();
+ await client.ConnectAsync("127.0.0.1", 6001);
+
+ var stream = client.GetStream();
+ var writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true };
+ var reader = new StreamReader(stream, Encoding.UTF8);
+
+ // 发送数据
+ await writer.WriteAsync("Hello Server!");
+ await writer.FlushAsync();
+
+ // 接收数据
+ char[] buffer = new char[1024];
+ int count = await reader.ReadAsync(buffer, 0, buffer.Length);
+ string response = new string(buffer, 0, count);
+ Console.WriteLine($"服务器回复:{response}");
+ }
+}
+```
+
+### 客户端示例(Python)
+
+```python
+import socket
+
+# 连接服务器
+client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+client.connect(('127.0.0.1', 6001))
+
+# 发送数据
+message = "Hello Server!"
+client.send(message.encode('utf-8'))
+
+# 接收数据
+response = client.recv(1024).decode('utf-8')
+print(f"服务器回复:{response}")
+
+client.close()
+```
+
+## 日志说明
+
+TouchSocketServer 使用 NLog 记录详细日志,包括:
+
+- **Info 级别**:服务器启动/停止、客户端连接/断开、广播完成
+- **Debug 级别**:数据接收/发送的详细内容、客户端正在连接
+- **Warn 级别**:参数错误、客户端未找到、服务器未运行
+- **Error 级别**:异常堆栈信息
+
+日志示例:
+```
+2026-01-01 22:00:00.123 [INFO] 正在启动 Socket 服务器,监听地址:127.0.0.1:6001,超时时间:30000毫秒
+2026-01-01 22:00:00.456 [INFO] Socket 服务器启动成功,监听地址:127.0.0.1:6001
+2026-01-01 22:00:05.789 [INFO] 客户端已连接,ClientId=xxx,远程地址=127.0.0.1:54321,当前连接数=1
+2026-01-01 22:00:10.123 [DEBUG] 接收到客户端数据,ClientId=xxx,远程地址=127.0.0.1:54321,数据长度=13字符,内容=Hello Server!
+```
+
+## 注意事项
+
+1. **线程安全**:所有公共方法都是线程安全的,可以在多线程环境中使用
+2. **事件处理**:事件回调在后台线程执行,如需更新 UI,请使用 `Invoke`
+3. **超时设置**:如果客户端长时间无数据交互,会自动断开连接
+4. **异步方法**:StartAsync 和 StopAsync 是异步方法,需要使用 await
+5. **异常处理**:所有异常都会被捕获并记录日志,不会导致服务器崩溃
+6. **动态类型**:内部使用 dynamic 类型管理客户端,兼容 TouchSocket 4.0
+
+## 常见问题
+
+### Q1: 如何监听所有网卡?
+```csharp
+socketServer.ListenIp = "0.0.0.0";
+```
+
+### Q2: 如何获取客户端的 ClientId?
+ClientId 会在事件参数中提供(`DataReceivedEventArgs.ClientId`),你可以保存它用于后续发送数据。
+
+### Q3: 如何实现心跳检测?
+可以在客户端定期发送心跳数据,服务器通过 `ReceiveTimeout` 自动断开无响应的客户端。
+
+### Q4: 为什么编译有警告?
+这些是 C# 8.0 的可空引用类型警告,不影响功能,可以忽略。
+
+### Q5: TouchSocket 4.0 与文档不一致?
+是的,TouchSocket 4.0.5 的 API 与官方文档有所不同,本实现已经适配了实际的 API。
+
+## 性能建议
+
+1. **连接数**:默认支持 10000 个并发连接,可根据需要调整 `MaxCount`
+2. **缓冲区**:默认 64KB 缓冲区,适合大多数场景
+3. **超时时间**:根据实际网络环境调整超时时间
+4. **日志级别**:生产环境建议将 Debug 日志关闭,减少性能开销
+
+## 与 SocketService 对比
+
+| 特性 | TouchSocketServer | SocketService (原生) |
+|------|-------------------|---------------------|
+| 基础框架 | TouchSocket 4.0 | 原生 TcpListener |
+| 性能 | ⭐⭐⭐⭐⭐ 更优秀 | ⭐⭐⭐⭐ 优秀 |
+| 稳定性 | ⭐⭐⭐⭐⭐ 非常稳定 | ⭐⭐⭐⭐⭐ 非常稳定 |
+| 依赖 | TouchSocket 包 | 仅 .NET 原生库 |
+| 复杂度 | 简单 | 中等 |
+| 推荐度 | ✅ **强烈推荐** | ✅ 推荐 |
+
+两个实现都非常优秀,TouchSocketServer 基于成熟的 TouchSocket 框架,性能更优;SocketService 基于原生库,无额外依赖。根据你的需求选择即可!
diff --git a/FATrace.OEMApp/升级SuperSocket说明.md b/FATrace.OEMApp/升级SuperSocket说明.md
new file mode 100644
index 0000000..eb12f66
--- /dev/null
+++ b/FATrace.OEMApp/升级SuperSocket说明.md
@@ -0,0 +1,72 @@
+# 升级 SuperSocket 说明
+
+## 当前问题
+
+SuperSocket 2.0.2 版本太旧,缺少关键 API:
+- ❌ 没有 `SuperSocketHostBuilder`
+- ❌ 没有 `SuperSocket.Channel` 命名空间
+- ❌ 与官方文档 API 不匹配
+
+## 升级步骤
+
+### 1. 卸载旧版本
+
+```bash
+cd E:\MyTest\VS2022\FoodAdditiveTrace\FATrace\FATrace.OEMApp
+dotnet remove package SuperSocket
+dotnet remove package SuperSocket.Server
+```
+
+### 2. 安装新版本
+
+```bash
+# 安装最新的稳定版本
+dotnet add package SuperSocket --version 2.0.0-beta.17
+dotnet add package SuperSocket.Server --version 2.0.0-beta.17
+```
+
+或者在 .csproj 文件中修改:
+
+```xml
+
+
+```
+
+### 3. 重新编译
+
+```bash
+dotnet build
+```
+
+## 风险提示
+
+⚠️ **升级可能影响其他代码**
+
+如果项目中其他地方使用了 SuperSocket(例如 FATrace.App 中的客户端),升级可能导致:
+- 其他代码编译失败
+- API 不兼容需要修改
+- 需要全面测试
+
+## 推荐方案
+
+**我强烈建议使用 `SocketService.cs`(原生 TcpListener 实现)**
+
+理由:
+1. ✅ 已编译通过,立即可用
+2. ✅ 无需升级任何包
+3. ✅ 不影响其他代码
+4. ✅ 功能完整,性能优秀
+5. ✅ 基于 .NET 原生库,更稳定
+
+## 对比
+
+| 特性 | SocketService.cs | SocketServiceV2.cs (需升级) |
+|------|------------------|----------------------------|
+| 编译状态 | ✅ 成功 | ❌ 需要升级包 |
+| 风险 | 无 | 可能影响其他代码 |
+| 稳定性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
+| 推荐度 | ✅ 强烈推荐 | ⚠️ 不推荐 |
+
+## 结论
+
+除非有特殊原因必须使用 SuperSocket 框架,否则请使用 `SocketService.cs`。
diff --git a/FATrace.WPLApp/App.config b/FATrace.WPLApp/App.config
index 79bdc4f..35d31d1 100644
--- a/FATrace.WPLApp/App.config
+++ b/FATrace.WPLApp/App.config
@@ -11,5 +11,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/FATrace.WPLApp/App.xaml.cs b/FATrace.WPLApp/App.xaml.cs
index e961975..c3a325d 100644
--- a/FATrace.WPLApp/App.xaml.cs
+++ b/FATrace.WPLApp/App.xaml.cs
@@ -117,6 +117,7 @@ namespace FATrace.WPLApp
var sysRun = Container.Resolve();
var DataServices = Container.Resolve();
var CsvServices = Container.Resolve();
+ var readFileServices = Container.Resolve();
LogService.Info("Background services initialized");
}
catch (Exception ex)
@@ -202,6 +203,7 @@ namespace FATrace.WPLApp
containerRegistry.RegisterSingleton();
containerRegistry.RegisterSingleton();
+ containerRegistry.RegisterSingleton();
containerRegistry.RegisterSingleton();
@@ -255,12 +257,30 @@ namespace FATrace.WPLApp
containerRegistry.RegisterForNavigation();
//containerRegistry.RegisterForNavigation();
//containerRegistry.RegisterForNavigation();
- //containerRegistry.RegisterForNavigation();
+ containerRegistry.RegisterForNavigation();
containerRegistry.RegisterForNavigation();
// 原料使用查询页
containerRegistry.RegisterForNavigation();
// 原料入库查询页
containerRegistry.RegisterForNavigation();
+
+ // 文件导入日志查询页
+ containerRegistry.RegisterForNavigation();
+
+ // 用户管理
+ containerRegistry.RegisterForNavigation();
+
+ // 工厂/OEM Excel 导入数据查询页
+ containerRegistry.RegisterForNavigation();
+ containerRegistry.RegisterForNavigation();
+ containerRegistry.RegisterForNavigation();
+ containerRegistry.RegisterForNavigation();
+ containerRegistry.RegisterForNavigation();
+ containerRegistry.RegisterForNavigation();
+ containerRegistry.RegisterForNavigation();
+ containerRegistry.RegisterForNavigation();
+ containerRegistry.RegisterForNavigation();
+
// 用户登录
containerRegistry.RegisterForNavigation("LoginView");
containerRegistry.RegisterForNavigation();
@@ -280,6 +300,7 @@ namespace FATrace.WPLApp
//注册Dialog视图时绑定VM
+ containerRegistry.RegisterDialog("DialogUserEditView");
//containerRegistry.RegisterDialog();
//containerRegistry.RegisterDialog();
//containerRegistry.RegisterDialog();
@@ -302,6 +323,9 @@ namespace FATrace.WPLApp
//containerRegistry.RegisterDialog();
+ // 使用 Syncfusion ChromelessWindow 统一 Prism 弹窗样式
+ containerRegistry.RegisterDialogWindow();
+
}
///
diff --git a/FATrace.WPLApp/ExcelFile/20251218161818.xlsx b/FATrace.WPLApp/ExcelFile/20251218161818.xlsx
new file mode 100644
index 0000000..f16383a
Binary files /dev/null and b/FATrace.WPLApp/ExcelFile/20251218161818.xlsx differ
diff --git a/FATrace.WPLApp/FATrace.WPLApp.csproj b/FATrace.WPLApp/FATrace.WPLApp.csproj
index 393e1c5..9002cf5 100644
--- a/FATrace.WPLApp/FATrace.WPLApp.csproj
+++ b/FATrace.WPLApp/FATrace.WPLApp.csproj
@@ -56,6 +56,12 @@
+
+
+ PreserveNewest
+
+
+
diff --git a/FATrace.WPLApp/Report/UserManual.md b/FATrace.WPLApp/Report/UserManual.md
index 7763d7e..4dbf3ba 100644
--- a/FATrace.WPLApp/Report/UserManual.md
+++ b/FATrace.WPLApp/Report/UserManual.md
@@ -2,12 +2,19 @@
版本:v1.0 适用模块:FATrace.WPLApp(桌面应用)
+
+
## 1. 简介
FATrace 是一套用于食品添加剂生产过程追溯与运行监控的桌面应用。WPLApp 负责生产端的日常使用,包括:
- 实时运行信息查看与统计总览(Dashboard)
- 历史报警查询(History Alarm)
+- 原料使用查询(支持导出 Excel)
+- 原料入库查询(支持导出 Excel)
+- Excel 报表文件自动导入(工厂/OEM 多 Sheet 导入)
+- 工厂/OEM 数据查询(按 Sheet 查询展示)
+- 文件导入日志(查询每次导入、归档、行数统计与异常信息)
- 用户登录与状态显示
- PLC 通信状态指示与当前终端信息(底部状态栏)
@@ -16,32 +23,55 @@ FATrace 是一套用于食品添加剂生产过程追溯与运行监控的桌面
## 2. 安装与启动
- 安装:由管理员完成程序分发与安装(通常包含可执行文件与依赖项)。
+
- 运行:双击启动桌面应用,进入主界面。
+
+ 
+
- 字体资源:应用内置图标字体,界面会自动加载,无需额外配置。
-- 数据库:应用使用 FreeSql 进行数据访问。数据库连接等参数由管理员预配置。
+
+- 数据库:数据库连接等参数由管理员预配置。
如启动失败、页面空白或频繁报错,请联系系统管理员检查运行环境与数据库连接配置。
## 3. 登录与账号
- 打开左侧菜单“用户登录”进入登录页。
+
+ 
+
- 输入“用户名”和“密码”,点击“登录”或直接按回车键。
+
- 登录成功后自动导航到 Dashboard;底部状态栏将显示“当前用户:已登录”。
-注意:当前版本按用户表中明文密码进行校验,请妥善保管账号信息。若忘记密码,请联系管理员重置。
+注意:请妥善保管账号信息。若忘记密码,请联系管理员重置。
## 4. 主界面与导航
应用主界面由顶部、内容区域、底部状态栏组成。常用页面:
- Dashboard(统计与实时信息)
-- 历史报警(History Alarm)
-- 原料相关查询(如原料使用、原料入库等,若已在系统中启用)
+- 数据管理
+ - 原料使用查询
+ - 原料入库查询
+ - 文件导入日志
+ - 历史报警
+- 系统
+ - 日志信息
+ - 用户登录
+ - 使用手册
+- 工厂/OEM 数据查询
+ - 工厂:工厂-入库、工厂-领料、工厂-出入库、工厂-原料生产信息、工厂-成品出库
+ - OEM:OEM-入库、OEM-出库、OEM-出入库、OEM-原料使用信息
通过左侧菜单切换页面。页面内容区支持自动刷新与数据联动,无需手动刷新按钮。
+说明:当前版本左侧菜单中的“使用手册”入口暂未在程序内直接打开。请在安装目录中查看本文件:`Report/UserManual.md`。
+
## 5. Dashboard(统计与实时信息)
+
+
Dashboard 提供生产运行的概览信息,包括:
- 统计卡片:按“日、月、年、累计”汇总原料使用重量(RawProUse)。
@@ -58,6 +88,8 @@ Dashboard 提供生产运行的概览信息,包括:
## 6. 历史报警(History Alarm)
+
+
用于查询系统运行过程中记录的历史报警。功能特性:
- 支持按关键字、类别、时间范围进行过滤查询。
@@ -77,6 +109,8 @@ Dashboard 提供生产运行的概览信息,包括:
## 7. 底部状态栏(FootView)
+
+
底部状态栏持续显示系统运行关键信息:
- PLC 通信指示灯:绿色表示连接正常;红色表示断开或异常。
@@ -85,13 +119,159 @@ Dashboard 提供生产运行的概览信息,包括:
当 PLC 状态改变时,这里会同步更新,便于第一时间发现通信问题。
-## 8. 常见操作流程
+## 8. 原料使用查询
+
+
+
+入口:左侧菜单 → 数据管理 → 原料使用查询。
+
+功能说明:
+
+- 支持按时间范围(称重时间 `WeightTime`)、原料编号/名称、批号、内袋二维码、外箱二维码、操作者、确认者进行查询。
+- 支持分页浏览。
+- 支持导出当前查询结果为 Excel(.xlsx)。
+
+操作步骤:
+
+1. 打开“原料使用查询”页面。
+2. 设置查询条件(可只填部分条件)。
+3. 点击“查询”,在下方列表查看数据。
+4. 如需导出:点击“导出Excel”,选择保存路径。
+
+说明:
+
+- 时间范围中“结束”若只选择日期(无时分秒),系统会自动按当天 23:59:59.999... 作为结束时间,避免漏数据。
+
+## 9. 原料入库查询
+
+
+
+入口:左侧菜单 → 数据管理 → 原料入库查询。
+
+功能说明:
+
+- 支持按时间范围(创建时间 `CreateTime`)、原料编号/名称、批号进行查询。
+- 支持按枚举筛选:原料来源(`RawSource`)、分拆状态(`RawSplitState`)。
+- 支持分页浏览。
+- 支持导出当前查询结果为 Excel(.xlsx)。
+
+操作步骤:
+
+1. 打开“原料入库查询”页面。
+2. 选择“原料来源/分拆状态”(可不选)。
+3. 设置时间范围,点击“查询”。
+4. 如需导出:点击“导出Excel”,选择保存路径。
+
+## 10. Excel 报表文件自动导入(工厂/OEM)
+
+
+
+本功能用于读取QR系统FTP每天生成的 Excel 文件(多 Sheet),并将各 Sheet 数据分别存入数据库表,同时将文件移动到归档目录,便于对账与追溯。
+
+### 10.1 文件放置与命名规则
+
+- **文件类型**:`.xlsx`
+- **文件名**:建议按 `yyyyMMddHHmmss.xlsx`(文件名表示导出时间)
+- **放置目录**:由配置项 `ExcelImportSourceDir` 指定
+
+系统会在后台定时扫描源目录中的 `*.xlsx` 文件并执行导入(当前版本扫描周期约 1 小时,且程序启动后会立即执行一次检查)。
+
+### 10.2 配置项说明(App.config)
+
+配置文件位置:`FATrace.WPLApp/App.config`(部署后为程序同目录的配置文件)。
+
+- `ExcelImportSourceDir`
+ - Excel 文件来源目录。
+ - 支持绝对路径与相对路径(相对路径将基于程序根目录解析)。
+- `ExcelImportArchiveDir`
+ - Excel 文件归档目录。
+ - 导入完成后,程序会将源文件移动到该目录。
+
+### 10.3 Sheet 与数据表映射
+
+Excel 中各 Sheet 必须使用固定名称(中文 Sheet 名),系统按下列规则导入:
+
+- 工厂-入库 → `FactoryInbound`
+- 工厂-领料 → `FactoryMaterialWithdrawal`
+- 工厂-出入库 → `FactoryInventoryTransaction`
+- 工厂-原料生产信息 → `FactoryProductionRecord`
+- 工厂-成品出库 → `FactoryOutbound`
+- OEM-入库 → `OEMInbound`
+- OEM-出库 → `OEMOutbound`
+- OEM-出入库 → `OEMInventoryTransaction`
+- OEM-原料使用信息 → `OEMRawUsageInfo`
+
+注意:
+
+- 导入时从每个 Sheet 的第 2 行开始读取(第 1 行为表头)。
+- 如果整行为空会被跳过。
+
+### 10.4 重复导入与归档规则
+
+- **防重复**:当 `FileImportLog` 表中已存在相同文件名且状态为 `Success` 的记录时,该文件会被跳过,不会重复导入。
+- **归档**:导入后移动到归档目录。若归档目录已存在同名文件,系统会自动在文件名后追加 `_HHmmss` 避免覆盖。
+
+## 11. 文件导入日志
+
+入口:左侧菜单 → 数据管理 → 文件导入日志。
+
+用途:用于查看每次 Excel 文件导入的执行情况,便于定位导入失败原因与核对数据。
+
+日志字段说明:
+
+- **文件名**:导入的 Excel 文件名。
+- **开始时间/结束时间**:导入流程的起止时间。
+- **状态**:`Success` / `Failed` / `Running`。
+- **源路径**:扫描到的原始文件路径。
+- **归档路径**:导入后移动到归档目录的路径。
+- **Sheet行数摘要**:各表导入行数统计,例如 `FactoryInbound=120;OEMInbound=35`。
+- **信息**:成功为 `OK`,失败时为异常信息。
+
+查询建议:
+
+- 导入失败优先用“关键字”搜索异常信息(例如路径、权限、格式错误)。
+- 对账时可通过“文件名”定位某天的导入记录,并查看各 Sheet 导入行数是否符合预期。
+
+## 12. 工厂/OEM 数据查询
+
+入口:左侧菜单 → 工厂/OEM 数据查询。
+
+说明:这些页面展示的是 Excel 导入后的数据(字符串字段为主),主要用于“展示、对账、追溯”。
+
+### 12.1 工厂数据
+
+- 工厂-入库
+ - 条件:产地、原料代码、原料名称、登录日期范围(基于 `LoginDateTime`)。
+- 工厂-领料
+ - 条件:产地、原料代码、原料名称、登录日期范围(基于 `LoginDateTime`)。
+- 工厂-出入库
+ - 条件:产地、原料代码、原料名称、入库时间日期范围(基于 `InTime`)。
+- 工厂-原料生产信息
+ - 条件:原料编号、原料名称、批号、称重时间日期范围(基于 `WeightTime`)。
+- 工厂-成品出库
+ - 条件:产地、原料代码、原料名称、批号、登录日期范围(基于 `LoginDateTime`)。
+
+### 12.2 OEM 数据
+
+- OEM-入库
+ - 条件:产地、原料代码、原料名称、批号、登录日期范围(基于 `LoginDateTime`)。
+- OEM-出库
+ - 条件:产地、原料代码、原料名称、批号、登录日期范围(基于 `LoginDateTime`)。
+- OEM-出入库
+ - 条件:产地、原料代码、原料名称、入库时间日期范围(基于 `InTime`)。
+- OEM-原料使用信息
+ - 条件:内袋二维码、原料代码、原料名称、产地、原料使用时间日期范围(基于 `RawUseTime`)。
+
+注意:以上页面的日期条件会对字符串时间字段做数据库端转换比较(例如 `TRY_CONVERT(datetime, LoginDateTime)`)。若 Excel 中日期格式不规范,可能导致该条数据无法被日期条件命中。
+
+## 13. 常见操作流程
- 开机与登录:启动应用 → 进入“用户登录” → 登录成功 → 自动跳转至 Dashboard。
-- 查看产线运行:在 Dashboard 观察统计卡片与实时信息,注意异常提示。
+- 查询生产数据:进入“原料使用查询/原料入库查询” → 设置条件 → 查询 → 需要时导出 Excel。
+- 导入外部 Excel:将 `.xlsx` 文件放入 `ExcelImportSourceDir` 目录 → 等待系统后台导入 → 到“文件导入日志”确认状态为 `Success` → 如需对账,使用“工厂/OEM 数据查询”查看导入数据。
- 报警定位与复盘:在“历史报警”中按时间段和关键字查询,结合现场记录进行处理与复盘。
-## 9. 常见问题与排查
+## 14. 常见问题与排查
- 无法登录:
- 确认用户名、密码输入正确;
@@ -103,9 +283,20 @@ Dashboard 提供生产运行的概览信息,包括:
- 等待系统产生新数据或触发刷新事件;
- 检查时间范围与系统时间是否正确。
-- 实时信息无更新:
- - 确认产线是否有新的扫码或业务事件;
- - 若日志区长时间无输出,可能产线空闲或通信异常。
+- Excel 文件未导入(文件仍在源目录 / 工厂OEM页面无数据):
+ - 检查 `ExcelImportSourceDir` 是否配置正确;
+ - 确认 Excel 文件扩展名为 `.xlsx`;
+ - 检查文件是否被其他程序长期占用(建议导出后再投放目录);
+ - 打开“文件导入日志”,查看是否存在 `Failed` 记录以及失败原因;
+ - 确认 Excel 的 Sheet 名是否与系统要求一致(例如“工厂-入库”“OEM-原料使用信息”等)。
+
+- 文件导入日志显示失败:
+ - 优先查看“信息”列中的异常原因;
+ - 常见原因包括:目录权限不足、文件损坏、Sheet 缺失或名称不匹配。
+
+- 工厂/OEM 查询页面按日期查不到数据:
+ - 该功能依赖将 Excel 中的时间字符串转换为数据库日期进行比较;
+ - 请确认 Excel 中对应时间列格式可被正常识别(建议 `yyyy-MM-dd HH:mm:ss` 或 Excel 标准日期格式)。
- PLC 指示红色:
- 检查 PLC 电源、网线、IP 配置;
@@ -117,28 +308,29 @@ Dashboard 提供生产运行的概览信息,包括:
- 检查数据库是否可写;
- 关注应用日志,必要时联系管理员。
-## 10. 使用技巧
+## 15. 使用技巧
- 登录页支持“回车键”快捷登录。
- 关注底部状态栏的用户与 PLC 状态,能快速判断系统是否处于可用状态。
-- 建议按班次定期查看“历史报警”,形成闭环处理与优化记录。
+- 发生导入问题时优先查看“文件导入日志”,比直接看数据库更直观。
-## 11. 安全与权限
+## 16. 安全与权限
- 账号仅限本人使用,请勿外借与分享。
- 退出离岗前可在“用户登录”页切换用户(如需要可联系管理员提供退出功能)。
- 如需更细粒度权限(基于岗位/等级),请与管理员沟通后续版本规划。
-## 12. 版本与更新
+## 17. 版本与更新
- 本说明书适配当前应用版本的已上线功能。新增功能或 UI 调整将另行更新说明。
- 如需升级或安装补丁,请联系系统管理员统一安排。
-## 13. 反馈与支持
+## 18. 反馈与支持
-- 使用中如遇到界面异常、统计不准、报警缺失等问题:
+- 使用中如遇到界面异常、统计不准、报警缺失、导入失败等问题:
- 记录问题发生时间、操作步骤、界面提示;
- 截图或拍照(含底部状态栏与异常信息);
+ - 提供导入的 Excel 文件名(必要时提供归档文件);
- 联系系统管理员或运维支持。
— 结束 —
diff --git a/FATrace.WPLApp/Report/UserManual.pdf b/FATrace.WPLApp/Report/UserManual.pdf
new file mode 100644
index 0000000..bb86fb3
Binary files /dev/null and b/FATrace.WPLApp/Report/UserManual.pdf differ
diff --git a/FATrace.WPLApp/Services/NavigationServices.cs b/FATrace.WPLApp/Services/NavigationServices.cs
index ba35e6c..27c6c9b 100644
--- a/FATrace.WPLApp/Services/NavigationServices.cs
+++ b/FATrace.WPLApp/Services/NavigationServices.cs
@@ -30,13 +30,13 @@ namespace FATrace.WPLApp.Services
IsParent = true,
ChildrenNavItemDtos = new ObservableCollection()
{
- new NavItemDto()
- {
- Name = "报表数据",
- CmdPar = "报表数据",
- Icon = "\uec55",
- IsParent = false,
- },
+ //new NavItemDto()
+ //{
+ // Name = "报表数据",
+ // CmdPar = "报表数据",
+ // Icon = "\uec55",
+ // IsParent = false,
+ //},
new NavItemDto()
{
Name = "原料使用查询",
@@ -52,6 +52,13 @@ namespace FATrace.WPLApp.Services
IsParent = false,
},
new NavItemDto()
+ {
+ Name = "文件导入日志",
+ CmdPar = "文件导入日志",
+ Icon = "\ue792",
+ IsParent = false,
+ },
+ new NavItemDto()
{
Name = "历史报警",
CmdPar = "历史报警",
@@ -87,6 +94,13 @@ namespace FATrace.WPLApp.Services
IsParent = false,
},
new NavItemDto()
+ {
+ Name = "用户管理",
+ CmdPar = "用户管理",
+ Icon = "\uec46",
+ IsParent = false,
+ },
+ new NavItemDto()
{
Name = "使用手册",
CmdPar = "使用手册",
@@ -95,6 +109,104 @@ namespace FATrace.WPLApp.Services
}
}
},
+
+ // 工厂/OEM 数据查询(单独的菜单目录节点)
+ new NavItemDto()
+ {
+ Name = "工厂/OEM 数据查询",
+ CmdPar = string.Empty,
+ Icon = "\ue650",
+ IsParent = true,
+ ChildrenNavItemDtos = new ObservableCollection()
+ {
+ // 工厂节点
+ new NavItemDto()
+ {
+ Name = "工厂",
+ CmdPar = string.Empty,
+ Icon = "\ue962",
+ IsParent = true,
+ ChildrenNavItemDtos = new ObservableCollection()
+ {
+ new NavItemDto
+ {
+ Name = "工厂-入库",
+ CmdPar = "工厂-入库",
+ Icon = "\uea25",
+ IsParent = false,
+ },
+ new NavItemDto
+ {
+ Name = "工厂-领料",
+ CmdPar = "工厂-领料",
+ Icon = "\uea25",
+ IsParent = false,
+ },
+ new NavItemDto
+ {
+ Name = "工厂-出入库",
+ CmdPar = "工厂-出入库",
+ Icon = "\uea25",
+ IsParent = false,
+ },
+ new NavItemDto
+ {
+ Name = "工厂-原料生产信息",
+ CmdPar = "工厂-原料生产信息",
+ Icon = "\uea25",
+ IsParent = false,
+ },
+ new NavItemDto
+ {
+ Name = "工厂-成品出库",
+ CmdPar = "工厂-成品出库",
+ Icon = "\uea25",
+ IsParent = false,
+ }
+ }
+ },
+
+ // OEM 节点
+ new NavItemDto()
+ {
+ Name = "OEM",
+ CmdPar = string.Empty,
+ Icon = "\ued08",
+ IsParent = true,
+ ChildrenNavItemDtos = new ObservableCollection()
+ {
+ new NavItemDto
+ {
+ Name = "OEM-入库",
+ CmdPar = "OEM-入库",
+ Icon = "\ueab4",
+ IsParent = false,
+ },
+ new NavItemDto
+ {
+ Name = "OEM-出库",
+ CmdPar = "OEM-出库",
+ Icon = "\ueab4",
+ IsParent = false,
+ },
+ new NavItemDto
+ {
+ Name = "OEM-出入库",
+ CmdPar = "OEM-出入库",
+ Icon = "\ueab4",
+ IsParent = false,
+ },
+ new NavItemDto
+ {
+ Name = "OEM-原料使用信息",
+ CmdPar = "OEM-原料使用信息",
+ Icon = "\ueab4",
+ IsParent = false,
+ }
+ }
+ }
+ }
+ },
//// 生产管理导航项(父级) - 新增测试导航
//new NavItemDto()
diff --git a/FATrace.WPLApp/Services/ReadFileServices.cs b/FATrace.WPLApp/Services/ReadFileServices.cs
new file mode 100644
index 0000000..74ebfa3
--- /dev/null
+++ b/FATrace.WPLApp/Services/ReadFileServices.cs
@@ -0,0 +1,679 @@
+using FATrace.Com;
+using FATrace.Model;
+using NPOI.SS.UserModel;
+using NPOI.XSSF.UserModel;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Timers;
+using FreeSql;
+
+namespace FATrace.WPLApp.Services
+{
+ ///
+ /// Excel 文件读取与定时导入服务
+ /// - 每小时扫描一次源目录中的 *.xlsx 文件(文件名: yyyyMMddHHmmss.xlsx,每天一个)
+ /// - 按 Sheet 名映射到对应的 FileModel 实体表
+ /// - 使用 FreeSql 批量插入数据
+ /// - 导入完成后将文件移动到归档目录,并在 FileImportLog 中记录详细信息
+ ///
+ public class ReadFileServices : IDisposable
+ {
+ private readonly ILogService _log;
+ private readonly IFreeSql _fsql;
+
+ private readonly System.Timers.Timer _timer;
+ private readonly string _sourceDirectory;
+ private readonly string _archiveDirectory;
+
+ private bool _disposed;
+ private volatile bool _isRunning;
+
+ ///
+ /// 构造函数:初始化路径配置与定时器
+ ///
+ public ReadFileServices(ILogService logService, IFreeSql freeSql)
+ {
+ _log = logService;
+ _fsql = freeSql;
+
+ // 路径从 App.config 读取,支持相对路径(相对于应用程序根目录)
+ var sourceSetting = FATrace.Com.ConfigHelper.GetStringOrDefault("ExcelImportSourceDir", "ExcelFile");
+ var archiveSetting = FATrace.Com.ConfigHelper.GetStringOrDefault("ExcelImportArchiveDir", Path.Combine("ExcelFile", "Archive"));
+
+ _sourceDirectory = ResolvePath(sourceSetting);
+ _archiveDirectory = ResolvePath(archiveSetting);
+
+ try
+ {
+ Directory.CreateDirectory(_sourceDirectory);
+ Directory.CreateDirectory(_archiveDirectory);
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"创建 Excel 导入目录失败: src={_sourceDirectory}, archive={_archiveDirectory}, ex={ex.Message}");
+ }
+
+ // 每小时扫描一次(3600000 ms),如有需要可后续改为配置项
+ _timer = new System.Timers.Timer(TimeSpan.FromMinutes(1).TotalMilliseconds)//TimeSpan.FromHours(1).TotalMilliseconds
+ {
+ AutoReset = true,
+ Enabled = true
+ };
+ _timer.Elapsed += async (s, e) => await OnTimerElapsedAsync().ConfigureAwait(false);
+
+ // 启动后立即执行一次检查,避免必须等一小时
+ Task.Run(async () =>
+ {
+ try
+ {
+ await OnTimerElapsedAsync().ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"Excel 首次导入检查异常: {ex.Message}");
+ }
+ });
+ }
+
+ ///
+ /// 将配置中的路径转换为绝对路径(支持相对路径)
+ ///
+ private static string ResolvePath(string pathSetting)
+ {
+ var path = (pathSetting ?? string.Empty).Trim();
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return AppDomain.CurrentDomain.BaseDirectory;
+ }
+
+ // 兼容配置中写成 "C:\Dir" 或 'C:\Dir' 的情况
+ if (path.Length >= 2)
+ {
+ if ((path.StartsWith("\"") && path.EndsWith("\"")) || (path.StartsWith("'") && path.EndsWith("'")))
+ {
+ path = path.Substring(1, path.Length - 2).Trim();
+ }
+ }
+
+ // 支持环境变量:%USERPROFILE%\xxx
+ path = Environment.ExpandEnvironmentVariables(path);
+
+ // 支持 ~ 作为用户目录(主要用于开发/测试环境)
+ if (path.StartsWith("~", StringComparison.Ordinal))
+ {
+ var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ var tail = path.Substring(1).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ path = Path.Combine(home, tail);
+ }
+
+ // 仅当不是绝对路径时,才基于程序目录做相对路径拼接
+ if (!Path.IsPathRooted(path))
+ {
+ path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path);
+ }
+ return Path.GetFullPath(path);
+ }
+
+ ///
+ /// 定时器回调:避免重入,统一调度导入任务
+ ///
+ private async Task OnTimerElapsedAsync()
+ {
+ if (_isRunning) return;
+ _isRunning = true;
+ try
+ {
+ await CheckAndImportAsync().ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"Excel 导入定时任务异常: {ex}");
+ }
+ finally
+ {
+ _isRunning = false;
+ }
+ }
+
+ ///
+ /// 对外公开的手动触发方法,方便调试与单元测试
+ ///
+ public Task TriggerImportOnceAsync()
+ {
+ return CheckAndImportAsync();
+ }
+
+ ///
+ /// 扫描源目录,按文件执行导入
+ ///
+ private async Task CheckAndImportAsync()
+ {
+ if (!Directory.Exists(_sourceDirectory))
+ {
+ _log.Warn($"Excel 导入目录不存在: {_sourceDirectory}");
+ return;
+ }
+
+ string[] files;
+ try
+ {
+ files = Directory.GetFiles(_sourceDirectory, "*.xlsx", SearchOption.TopDirectoryOnly);
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"扫描 Excel 导入目录失败: dir={_sourceDirectory}, ex={ex.Message}");
+ return;
+ }
+
+ if (files.Length == 0)
+ {
+ return;
+ }
+
+ // 按文件名排序,确保按时间顺序处理
+ foreach (var filePath in files.OrderBy(f => Path.GetFileName(f)))
+ {
+ try
+ {
+ await ImportSingleFileAsync(filePath).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"导入 Excel 文件失败: {filePath}, ex={ex}");
+ }
+ }
+ }
+
+ ///
+ /// 导入单个 Excel 文件:按 Sheet 写入各表、记录日志并移动文件
+ ///
+ private async Task ImportSingleFileAsync(string filePath)
+ {
+ var fileName = Path.GetFileName(filePath);
+ if (string.IsNullOrWhiteSpace(fileName)) return;
+
+ // 若已存在成功记录,则不再重复导入
+ var alreadySuccess = await _fsql.Select()
+ .Where(x => x.FileName == fileName && x.Status == "Success")
+ .AnyAsync().ConfigureAwait(false);
+ if (alreadySuccess)
+ {
+ return;
+ }
+
+ var log = new FileImportLog
+ {
+ FileName = fileName,
+ SourcePath = filePath,
+ StartTime = DateTime.Now,
+ Status = "Running"
+ };
+
+ var sheetStats = new Dictionary();
+
+ try
+ {
+ using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ {
+ IWorkbook workbook = new XSSFWorkbook(fs);
+ var formatter = new DataFormatter();
+
+ sheetStats["FactoryInbound"] = ImportFactoryInbound(workbook, formatter);
+ sheetStats["FactoryMaterialWithdrawal"] = ImportFactoryMaterialWithdrawal(workbook, formatter);
+ sheetStats["FactoryInventoryTransaction"] = ImportFactoryInventoryTransaction(workbook, formatter);
+ sheetStats["FactoryProductionRecord"] = ImportFactoryProductionRecord(workbook, formatter);
+ sheetStats["FactoryOutbound"] = ImportFactoryOutbound(workbook, formatter);
+ sheetStats["OEMInbound"] = ImportOEMInbound(workbook, formatter);
+ sheetStats["OEMOutbound"] = ImportOEMOutbound(workbook, formatter);
+ sheetStats["OEMInventoryTransaction"] = ImportOEMInventoryTransaction(workbook, formatter);
+ sheetStats["OEMRawUsageInfo"] = ImportOEMRawUsageInfo(workbook, formatter);
+ }
+
+ // 构造统计摘要
+ log.SheetRowStats = string.Join(";", sheetStats.Select(kv => $"{kv.Key}={kv.Value}"));
+
+ // 移动文件到归档目录
+ var archivePath = Path.Combine(_archiveDirectory, fileName);
+ try
+ {
+ if (File.Exists(archivePath))
+ {
+ var newName = $"{Path.GetFileNameWithoutExtension(fileName)}_{DateTime.Now:HHmmss}{Path.GetExtension(fileName)}";
+ archivePath = Path.Combine(_archiveDirectory, newName);
+ }
+
+ File.Move(filePath, archivePath);
+ log.ArchivePath = archivePath;
+ }
+ catch (Exception moveEx)
+ {
+ log.ArchivePath = archivePath;
+ _log.Error($"移动 Excel 文件到归档目录失败: src={filePath}, dest={archivePath}, ex={moveEx.Message}");
+ // 视为部分成功
+ }
+
+ log.Status = "Success";
+ log.Message = "OK";
+ }
+ catch (Exception ex)
+ {
+ log.Status = "Failed";
+ log.Message = ex.Message;
+
+ if (sheetStats.Count > 0 && string.IsNullOrEmpty(log.SheetRowStats))
+ {
+ log.SheetRowStats = string.Join(";", sheetStats.Select(kv => $"{kv.Key}={kv.Value}"));
+ }
+
+ _log.Error($"导入 Excel 文件过程中出现异常: {filePath}, ex={ex}");
+ }
+ finally
+ {
+ log.EndTime = DateTime.Now;
+ try
+ {
+ await _fsql.Insert(log).ExecuteAffrowsAsync().ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"写入 FileImportLog 失败: file={fileName}, ex={ex.Message}");
+ }
+ }
+ }
+
+ #region 各 Sheet 导入实现
+
+ private static bool IsRowEmpty(IRow? row, DataFormatter formatter)
+ {
+ if (row == null) return true;
+ for (int i = row.FirstCellNum; i < row.LastCellNum; i++)
+ {
+ var cell = row.GetCell(i);
+ if (cell != null && !string.IsNullOrWhiteSpace(formatter.FormatCellValue(cell)))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static string GetCellString(IRow row, int columnIndex, DataFormatter formatter)
+ {
+ var cell = row.GetCell(columnIndex);
+ return cell == null ? string.Empty : formatter.FormatCellValue(cell).Trim();
+ }
+
+ private int ImportFactoryInbound(IWorkbook workbook, DataFormatter formatter)
+ {
+ var sheet = workbook.GetSheet("工厂-入库");
+ if (sheet == null) return 0;
+
+ var list = new List();
+ for (int i = 1; i <= sheet.LastRowNum; i++)
+ {
+ var row = sheet.GetRow(i);
+ if (IsRowEmpty(row, formatter)) continue;
+
+ var entity = new FactoryInbound
+ {
+ Origin = GetCellString(row, 0, formatter),
+ RawCode = GetCellString(row, 1, formatter),
+ RawName = GetCellString(row, 2, formatter),
+ Weight = GetCellString(row, 3, formatter),
+ LoginDate = GetCellString(row, 4, formatter),
+ LoginTime = GetCellString(row, 5, formatter),
+ LoginDateTime = GetCellString(row, 6, formatter)
+ };
+
+ if (IsAllEmpty(entity.Origin, entity.RawCode, entity.RawName, entity.Weight, entity.LoginDate, entity.LoginTime, entity.LoginDateTime))
+ continue;
+
+ list.Add(entity);
+ }
+
+ if (list.Count > 0)
+ {
+ _fsql.Insert(list).ExecuteAffrows();
+ }
+ return list.Count;
+ }
+
+ private int ImportFactoryMaterialWithdrawal(IWorkbook workbook, DataFormatter formatter)
+ {
+ var sheet = workbook.GetSheet("工厂-领料");
+ if (sheet == null) return 0;
+
+ var list = new List();
+ for (int i = 1; i <= sheet.LastRowNum; i++)
+ {
+ var row = sheet.GetRow(i);
+ if (IsRowEmpty(row, formatter)) continue;
+
+ var entity = new FactoryMaterialWithdrawal
+ {
+ Origin = GetCellString(row, 0, formatter),
+ RawCode = GetCellString(row, 1, formatter),
+ RawName = GetCellString(row, 2, formatter),
+ Weight = GetCellString(row, 3, formatter),
+ LoginDate = GetCellString(row, 4, formatter),
+ LoginTime = GetCellString(row, 5, formatter),
+ LoginDateTime = GetCellString(row, 6, formatter)
+ };
+
+ if (IsAllEmpty(entity.Origin, entity.RawCode, entity.RawName, entity.Weight, entity.LoginDate, entity.LoginTime, entity.LoginDateTime))
+ continue;
+
+ list.Add(entity);
+ }
+
+ if (list.Count > 0)
+ {
+ _fsql.Insert(list).ExecuteAffrows();
+ }
+ return list.Count;
+ }
+
+ private int ImportFactoryInventoryTransaction(IWorkbook workbook, DataFormatter formatter)
+ {
+ var sheet = workbook.GetSheet("工厂-出入库");
+ if (sheet == null) return 0;
+
+ var list = new List();
+ for (int i = 1; i <= sheet.LastRowNum; i++)
+ {
+ var row = sheet.GetRow(i);
+ if (IsRowEmpty(row, formatter)) continue;
+
+ var entity = new FactoryInventoryTransaction
+ {
+ InTime = GetCellString(row, 0, formatter),
+ OutTime = GetCellString(row, 1, formatter),
+ Origin = GetCellString(row, 2, formatter),
+ RawCode = GetCellString(row, 3, formatter),
+ RawName = GetCellString(row, 4, formatter),
+ TotalInWeightKg = GetCellString(row, 5, formatter),
+ TotalOutWeightKg = GetCellString(row, 6, formatter),
+ RemainWeightKg = GetCellString(row, 7, formatter)
+ };
+
+ if (IsAllEmpty(entity.InTime, entity.OutTime, entity.Origin, entity.RawCode, entity.RawName, entity.TotalInWeightKg, entity.TotalOutWeightKg, entity.RemainWeightKg))
+ continue;
+
+ list.Add(entity);
+ }
+
+ if (list.Count > 0)
+ {
+ _fsql.Insert(list).ExecuteAffrows();
+ }
+ return list.Count;
+ }
+
+ private int ImportFactoryProductionRecord(IWorkbook workbook, DataFormatter formatter)
+ {
+ var sheet = workbook.GetSheet("工厂-原料生产信息");
+ if (sheet == null) return 0;
+
+ var list = new List();
+ for (int i = 1; i <= sheet.LastRowNum; i++)
+ {
+ var row = sheet.GetRow(i);
+ if (IsRowEmpty(row, formatter)) continue;
+
+ var entity = new FactoryProductionRecord
+ {
+ RawCode = GetCellString(row, 0, formatter),
+ RawName = GetCellString(row, 1, formatter),
+ Origin = GetCellString(row, 2, formatter),
+ InBagCode = GetCellString(row, 3, formatter),
+ BoxCode = GetCellString(row, 4, formatter),
+ Batch = GetCellString(row, 5, formatter),
+ ShelfLife = GetCellString(row, 6, formatter),
+ Weight = GetCellString(row, 7, formatter),
+ DeliveryDate = GetCellString(row, 8, formatter),
+ RemainWeight = GetCellString(row, 9, formatter),
+ StockWeight = GetCellString(row, 10, formatter),
+ WeightTime = GetCellString(row, 11, formatter),
+ OpUser = GetCellString(row, 12, formatter),
+ CheckUser = GetCellString(row, 13, formatter),
+ BoxScanTime = GetCellString(row, 14, formatter)
+ };
+
+ if (IsAllEmpty(entity.RawCode, entity.RawName, entity.Origin, entity.InBagCode, entity.BoxCode, entity.Batch,
+ entity.ShelfLife, entity.Weight, entity.DeliveryDate, entity.RemainWeight, entity.StockWeight,
+ entity.WeightTime, entity.OpUser, entity.CheckUser, entity.BoxScanTime))
+ continue;
+
+ list.Add(entity);
+ }
+
+ if (list.Count > 0)
+ {
+ _fsql.Insert(list).ExecuteAffrows();
+ }
+ return list.Count;
+ }
+
+ private int ImportFactoryOutbound(IWorkbook workbook, DataFormatter formatter)
+ {
+ var sheet = workbook.GetSheet("工厂-成品出库");
+ if (sheet == null) return 0;
+
+ var list = new List();
+ for (int i = 1; i <= sheet.LastRowNum; i++)
+ {
+ var row = sheet.GetRow(i);
+ if (IsRowEmpty(row, formatter)) continue;
+
+ var entity = new FactoryOutbound
+ {
+ Batch = GetCellString(row, 0, formatter),
+ Weight = GetCellString(row, 1, formatter),
+ ShelfLife = GetCellString(row, 2, formatter),
+ Origin = GetCellString(row, 3, formatter),
+ RawCode = GetCellString(row, 4, formatter),
+ RawName = GetCellString(row, 5, formatter),
+ SequenceNo = GetCellString(row, 6, formatter),
+ LoginDate = GetCellString(row, 7, formatter),
+ LoginTime = GetCellString(row, 8, formatter),
+ LoginDateTime = GetCellString(row, 9, formatter)
+ };
+
+ if (IsAllEmpty(entity.Batch, entity.Weight, entity.ShelfLife, entity.Origin, entity.RawCode, entity.RawName,
+ entity.SequenceNo, entity.LoginDate, entity.LoginTime, entity.LoginDateTime))
+ continue;
+
+ list.Add(entity);
+ }
+
+ if (list.Count > 0)
+ {
+ _fsql.Insert(list).ExecuteAffrows();
+ }
+ return list.Count;
+ }
+
+ private int ImportOEMInbound(IWorkbook workbook, DataFormatter formatter)
+ {
+ var sheet = workbook.GetSheet("OEM-入库");
+ if (sheet == null) return 0;
+
+ var list = new List();
+ for (int i = 1; i <= sheet.LastRowNum; i++)
+ {
+ var row = sheet.GetRow(i);
+ if (IsRowEmpty(row, formatter)) continue;
+
+ var entity = new OEMInbound
+ {
+ Batch = GetCellString(row, 0, formatter),
+ Weight = GetCellString(row, 1, formatter),
+ ShelfLife = GetCellString(row, 2, formatter),
+ Origin = GetCellString(row, 3, formatter),
+ RawCode = GetCellString(row, 4, formatter),
+ RawName = GetCellString(row, 5, formatter),
+ SequenceNo = GetCellString(row, 6, formatter),
+ LoginDate = GetCellString(row, 7, formatter),
+ LoginTime = GetCellString(row, 8, formatter),
+ LoginDateTime = GetCellString(row, 9, formatter)
+ };
+
+ if (IsAllEmpty(entity.Batch, entity.Weight, entity.ShelfLife, entity.Origin, entity.RawCode, entity.RawName,
+ entity.SequenceNo, entity.LoginDate, entity.LoginTime, entity.LoginDateTime))
+ continue;
+
+ list.Add(entity);
+ }
+
+ if (list.Count > 0)
+ {
+ _fsql.Insert(list).ExecuteAffrows();
+ }
+ return list.Count;
+ }
+
+ private int ImportOEMOutbound(IWorkbook workbook, DataFormatter formatter)
+ {
+ var sheet = workbook.GetSheet("OEM-出库");
+ if (sheet == null) return 0;
+
+ var list = new List();
+ for (int i = 1; i <= sheet.LastRowNum; i++)
+ {
+ var row = sheet.GetRow(i);
+ if (IsRowEmpty(row, formatter)) continue;
+
+ var entity = new OEMOutbound
+ {
+ Batch = GetCellString(row, 0, formatter),
+ Weight = GetCellString(row, 1, formatter),
+ ShelfLife = GetCellString(row, 2, formatter),
+ Origin = GetCellString(row, 3, formatter),
+ RawCode = GetCellString(row, 4, formatter),
+ RawName = GetCellString(row, 5, formatter),
+ SequenceNo = GetCellString(row, 6, formatter),
+ LoginDate = GetCellString(row, 7, formatter),
+ LoginTime = GetCellString(row, 8, formatter),
+ LoginDateTime = GetCellString(row, 9, formatter)
+ };
+
+ if (IsAllEmpty(entity.Batch, entity.Weight, entity.ShelfLife, entity.Origin, entity.RawCode, entity.RawName,
+ entity.SequenceNo, entity.LoginDate, entity.LoginTime, entity.LoginDateTime))
+ continue;
+
+ list.Add(entity);
+ }
+
+ if (list.Count > 0)
+ {
+ _fsql.Insert(list).ExecuteAffrows();
+ }
+ return list.Count;
+ }
+
+ private int ImportOEMInventoryTransaction(IWorkbook workbook, DataFormatter formatter)
+ {
+ var sheet = workbook.GetSheet("OEM-出入库");
+ if (sheet == null) return 0;
+
+ var list = new List();
+ for (int i = 1; i <= sheet.LastRowNum; i++)
+ {
+ var row = sheet.GetRow(i);
+ if (IsRowEmpty(row, formatter)) continue;
+
+ var entity = new OEMInventoryTransaction
+ {
+ InTime = GetCellString(row, 0, formatter),
+ OutTime = GetCellString(row, 1, formatter),
+ Origin = GetCellString(row, 2, formatter),
+ RawCode = GetCellString(row, 3, formatter),
+ RawName = GetCellString(row, 4, formatter),
+ TotalInWeightKg = GetCellString(row, 5, formatter),
+ TotalOutWeightKg = GetCellString(row, 6, formatter),
+ RemainWeightKg = GetCellString(row, 7, formatter)
+ };
+
+ if (IsAllEmpty(entity.InTime, entity.OutTime, entity.Origin, entity.RawCode, entity.RawName,
+ entity.TotalInWeightKg, entity.TotalOutWeightKg, entity.RemainWeightKg))
+ continue;
+
+ list.Add(entity);
+ }
+
+ if (list.Count > 0)
+ {
+ _fsql.Insert(list).ExecuteAffrows();
+ }
+ return list.Count;
+ }
+
+ private int ImportOEMRawUsageInfo(IWorkbook workbook, DataFormatter formatter)
+ {
+ var sheet = workbook.GetSheet("OEM-原料使用信息");
+ if (sheet == null) return 0;
+
+ var list = new List();
+ for (int i = 1; i <= sheet.LastRowNum; i++)
+ {
+ var row = sheet.GetRow(i);
+ if (IsRowEmpty(row, formatter)) continue;
+
+ var entity = new OEMRawUsageInfo
+ {
+ RawUseTime = GetCellString(row, 0, formatter),
+ InBagCode = GetCellString(row, 1, formatter),
+ Origin = GetCellString(row, 2, formatter),
+ RawName = GetCellString(row, 3, formatter),
+ RawCode = GetCellString(row, 4, formatter),
+ VideoUrl = GetCellString(row, 5, formatter)
+ };
+
+ if (IsAllEmpty(entity.RawUseTime, entity.InBagCode, entity.Origin, entity.RawName, entity.RawCode, entity.VideoUrl))
+ continue;
+
+ list.Add(entity);
+ }
+
+ if (list.Count > 0)
+ {
+ _fsql.Insert(list).ExecuteAffrows();
+ }
+ return list.Count;
+ }
+
+ private static bool IsAllEmpty(params string?[] values)
+ {
+ if (values == null || values.Length == 0) return true;
+ foreach (var v in values)
+ {
+ if (!string.IsNullOrWhiteSpace(v)) return false;
+ }
+ return true;
+ }
+
+ #endregion
+
+ ///
+ /// 释放定时器等资源
+ ///
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ try
+ {
+ _timer?.Stop();
+ _timer?.Dispose();
+ }
+ catch
+ {
+ // 忽略定时器释放异常
+ }
+ }
+ }
+}
diff --git a/FATrace.WPLApp/Services/SysRunService.cs b/FATrace.WPLApp/Services/SysRunService.cs
index 60ed016..509cbc1 100644
--- a/FATrace.WPLApp/Services/SysRunService.cs
+++ b/FATrace.WPLApp/Services/SysRunService.cs
@@ -84,6 +84,7 @@ namespace FATrace.WPLApp.Services
if (string.IsNullOrEmpty(value))
{
IsLogin = false;
+ CurAccessLevel = null;
}
else
{
@@ -94,6 +95,23 @@ namespace FATrace.WPLApp.Services
}
}
+ private string? _CurAccessLevel;
+ ///
+ /// 当前用户等级(管理员/操作员/访客)
+ ///
+ public string? CurAccessLevel
+ {
+ get { return _CurAccessLevel; }
+ set
+ {
+ if (_CurAccessLevel != value)
+ {
+ _CurAccessLevel = value;
+ RaisePropertyChanged();
+ }
+ }
+ }
+
private bool _IsLogin;
///
/// 登录
diff --git a/FATrace.WPLApp/ViewModels/DialogUserEditViewModel.cs b/FATrace.WPLApp/ViewModels/DialogUserEditViewModel.cs
new file mode 100644
index 0000000..b5eedf7
--- /dev/null
+++ b/FATrace.WPLApp/ViewModels/DialogUserEditViewModel.cs
@@ -0,0 +1,232 @@
+using FATrace.Model;
+using FATrace.WPLApp.Core;
+using FATrace.WPLApp.Services;
+using FreeSql;
+using Prism.Commands;
+using Prism.Services.Dialogs;
+using System;
+using System.Collections.ObjectModel;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace FATrace.WPLApp.ViewModels
+{
+ ///
+ /// 用户新增/编辑 弹窗 VM
+ ///
+ public class DialogUserEditViewModel : DialogViewModel
+ {
+ private readonly IFreeSql _fsql;
+ private readonly ILogService _log;
+
+ private string _mode = UserManageViewModel.DialogModes.Add;
+ private long _editUserId;
+
+ public DialogUserEditViewModel(IFreeSql fsql, ILogService log)
+ {
+ _fsql = fsql;
+ _log = log;
+
+ AccessLevelOptions = new ObservableCollection(new[]
+ {
+ UserManageViewModel.AccessLevels.Admin,
+ UserManageViewModel.AccessLevels.Operator,
+ UserManageViewModel.AccessLevels.Guest
+ });
+
+ SaveCommand = new DelegateCommand(async () => await SaveAsync(), () => !IsBusy)
+ .ObservesProperty(() => IsBusy);
+ CancelCommand = new DelegateCommand(() => OnDialogClosed(ButtonResult.Cancel));
+ }
+
+ ///
+ /// 等级下拉选项
+ ///
+ public ObservableCollection AccessLevelOptions { get; }
+
+ 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 string? _accessLevel;
+ ///
+ /// 等级
+ ///
+ public string? AccessLevel
+ {
+ get => _accessLevel;
+ set { _accessLevel = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// 保存
+ ///
+ public DelegateCommand SaveCommand { get; }
+
+ ///
+ /// 取消
+ ///
+ public DelegateCommand CancelCommand { get; }
+
+ ///
+ /// 打开弹窗时初始化
+ ///
+ /// 参数
+ public override void OnDialogOpened(IDialogParameters parameters)
+ {
+ _mode = parameters.GetValue(UserManageViewModel.DialogKeys.Mode) ?? UserManageViewModel.DialogModes.Add;
+ Title = _mode == UserManageViewModel.DialogModes.Edit ? "编辑用户" : "新增用户";
+
+ if (_mode == UserManageViewModel.DialogModes.Edit)
+ {
+ _editUserId = parameters.GetValue(UserManageViewModel.DialogKeys.UserId);
+ LoadUser(_editUserId);
+ }
+ else
+ {
+ _editUserId = 0;
+ UserName = string.Empty;
+ Password = string.Empty;
+ AccessLevel = UserManageViewModel.AccessLevels.Operator;
+ }
+ }
+
+ private void LoadUser(long id)
+ {
+ try
+ {
+ var user = _fsql.Select().Where(a => a.Id == id).First();
+ if (user == null)
+ {
+ MessageBox.Show("用户不存在或已被删除", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ UserName = user.UserName;
+ Password = user.Password;
+ AccessLevel = user.AccessLevel;
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"加载用户失败: {ex}");
+ MessageBox.Show($"加载用户失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private async Task SaveAsync()
+ {
+ if (IsBusy) return;
+ try
+ {
+ if (string.IsNullOrWhiteSpace(UserName))
+ {
+ MessageBox.Show("请输入用户名", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(Password))
+ {
+ MessageBox.Show("请输入密码", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(AccessLevel))
+ {
+ MessageBox.Show("请选择等级", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+
+ IsBusy = true;
+
+ if (_mode == UserManageViewModel.DialogModes.Add)
+ {
+ await AddAsync();
+ }
+ else
+ {
+ await UpdateAsync();
+ }
+
+ OnDialogClosed(ButtonResult.OK);
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"保存用户失败: {ex}");
+ MessageBox.Show($"保存失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ private async Task AddAsync()
+ {
+ await Task.Run(() =>
+ {
+ var exists = _fsql.Select().Where(a => a.UserName == UserName).Any();
+ if (exists)
+ {
+ throw new InvalidOperationException("用户名已存在");
+ }
+
+ var entity = new TbUser
+ {
+ UserName = UserName!,
+ Password = Password!,
+ AccessLevel = AccessLevel!,
+ CreateTime = DateTime.Now
+ };
+
+ _fsql.Insert(entity).ExecuteAffrows();
+ });
+
+ _log.Info($"新增用户成功: {UserName}");
+ }
+
+ private async Task UpdateAsync()
+ {
+ var id = _editUserId;
+ if (id <= 0) throw new InvalidOperationException("编辑用户Id无效");
+
+ await Task.Run(() =>
+ {
+ var exists = _fsql.Select().Where(a => a.UserName == UserName && a.Id != id).Any();
+ if (exists)
+ {
+ throw new InvalidOperationException("用户名已存在");
+ }
+
+ var aff = _fsql.Update()
+ .Set(a => a.UserName, UserName!)
+ .Set(a => a.Password, Password!)
+ .Set(a => a.AccessLevel, AccessLevel!)
+ .Where(a => a.Id == id)
+ .ExecuteAffrows();
+
+ if (aff <= 0)
+ {
+ throw new InvalidOperationException("更新失败:用户不存在或未发生变化");
+ }
+ });
+
+ _log.Info($"更新用户成功: Id={id}, UserName={UserName}");
+ }
+ }
+}
diff --git a/FATrace.WPLApp/ViewModels/FactoryInboundViewModel.cs b/FATrace.WPLApp/ViewModels/FactoryInboundViewModel.cs
new file mode 100644
index 0000000..1c53c1d
--- /dev/null
+++ b/FATrace.WPLApp/ViewModels/FactoryInboundViewModel.cs
@@ -0,0 +1,222 @@
+using FATrace.WPLApp.Core;
+using Prism.Commands;
+using System;
+using System.Collections.ObjectModel;
+using FreeSql;
+using FATrace.Model;
+using FATrace.WPLApp.Services;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace FATrace.WPLApp.ViewModels
+{
+ ///
+ /// 工厂-入库 数据查询 VM
+ /// 仅展示从 Excel 导入的 FactoryInbound 数据,支持简单条件与分页。
+ ///
+ public class FactoryInboundViewModel : NavigationViewModel
+ {
+ private readonly IFreeSql _fsql;
+ private readonly ILogService _log;
+
+ public FactoryInboundViewModel(IFreeSql fsql, ILogService log)
+ {
+ _fsql = fsql;
+ _log = log;
+
+ Items = new ObservableCollection();
+
+ 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 查询条件
+ private string? _origin;
+ ///
+ /// 产地模糊匹配
+ ///
+ public string? Origin { get => _origin; set { _origin = value; RaisePropertyChanged(); } }
+
+ private string? _rawCode;
+ ///
+ /// 原料代码模糊匹配
+ ///
+ public string? RawCode { get => _rawCode; set { _rawCode = value; RaisePropertyChanged(); } }
+
+ private string? _rawName;
+ ///
+ /// 原料名称模糊匹配
+ ///
+ public string? RawName { get => _rawName; set { _rawName = value; RaisePropertyChanged(); } }
+
+ private DateTime? _startDate;
+ ///
+ /// 登录日期起(根据 LoginDateTime 转换为日期范围过滤)
+ ///
+ public DateTime? StartDate
+ {
+ get => _startDate;
+ set { _startDate = value; RaisePropertyChanged(); }
+ }
+
+ private DateTime? _endDate;
+ ///
+ /// 登录日期止(包含当天,基于 LoginDateTime)
+ ///
+ public DateTime? EndDate
+ {
+ get => _endDate;
+ set { _endDate = value; RaisePropertyChanged(); }
+ }
+ #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 v = value <= 0 ? 20 : value;
+ if (_pageSize != v)
+ {
+ _pageSize = v;
+ 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()
+ {
+ Origin = RawCode = RawName = string.Empty;
+ StartDate = null;
+ EndDate = null;
+ }
+
+ private async Task SearchAsync()
+ {
+ if (IsBusy) return;
+ try
+ {
+ IsBusy = true;
+ _log.Info("FactoryInbound 查询开始");
+
+ var data = await Task.Run(() =>
+ {
+ var q = _fsql.Select();
+
+ if (!string.IsNullOrWhiteSpace(Origin))
+ q = q.Where(a => a.Origin != null && a.Origin.Contains(Origin));
+ if (!string.IsNullOrWhiteSpace(RawCode))
+ q = q.Where(a => a.RawCode != null && a.RawCode.Contains(RawCode));
+ if (!string.IsNullOrWhiteSpace(RawName))
+ q = q.Where(a => a.RawName != null && a.RawName.Contains(RawName));
+
+ // 日期范围:基于 LoginDateTime 进行 TRY_CONVERT(datetime, LoginDateTime) 比较
+ DateTime? start = StartDate;
+ DateTime? end = EndDate;
+ if (start.HasValue)
+ {
+ var s = start.Value.Date;
+ q = q.Where("TRY_CONVERT(datetime, LoginDateTime) >= @start", new { start = s });
+ }
+ if (end.HasValue)
+ {
+ var e = end.Value.Date.AddDays(1).AddTicks(-1);
+ q = q.Where("TRY_CONVERT(datetime, LoginDateTime) <= @end", new { end = e });
+ }
+
+ q = q.OrderByDescending(a => a.Id);
+
+ var page = PageIndex < 1 ? 1 : PageIndex;
+ var size = PageSize <= 0 ? 20 : PageSize;
+
+ 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($"FactoryInbound 查询完成,记录数: {TotalCount}");
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"FactoryInbound 查询失败: {ex}");
+ MessageBox.Show($"查询失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ public override async void OnNavigatedTo(Prism.Regions.NavigationContext navigationContext)
+ {
+ await SearchAsync();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/ViewModels/FactoryInventoryTransactionViewModel.cs b/FATrace.WPLApp/ViewModels/FactoryInventoryTransactionViewModel.cs
new file mode 100644
index 0000000..3a7f14b
--- /dev/null
+++ b/FATrace.WPLApp/ViewModels/FactoryInventoryTransactionViewModel.cs
@@ -0,0 +1,208 @@
+using FATrace.WPLApp.Core;
+using Prism.Commands;
+using System;
+using System.Collections.ObjectModel;
+using FreeSql;
+using FATrace.Model;
+using FATrace.WPLApp.Services;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace FATrace.WPLApp.ViewModels
+{
+ ///
+ /// 工厂-出入库 数据查询 VM
+ /// 仅展示从 Excel 导入的 FactoryInventoryTransaction 数据,支持简单条件与分页。
+ ///
+ public class FactoryInventoryTransactionViewModel : NavigationViewModel
+ {
+ private readonly IFreeSql _fsql;
+ private readonly ILogService _log;
+
+ public FactoryInventoryTransactionViewModel(IFreeSql fsql, ILogService log)
+ {
+ _fsql = fsql;
+ _log = log;
+
+ Items = new ObservableCollection();
+
+ 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 查询条件
+ private string? _origin;
+ public string? Origin { get => _origin; set { _origin = value; RaisePropertyChanged(); } }
+
+ private string? _rawCode;
+ public string? RawCode { get => _rawCode; set { _rawCode = value; RaisePropertyChanged(); } }
+
+ private string? _rawName;
+ public string? RawName { get => _rawName; set { _rawName = value; RaisePropertyChanged(); } }
+
+ private DateTime? _startDate;
+ ///
+ /// 入库时间起(基于 InTime)
+ ///
+ public DateTime? StartDate
+ {
+ get => _startDate;
+ set { _startDate = value; RaisePropertyChanged(); }
+ }
+
+ private DateTime? _endDate;
+ ///
+ /// 入库时间止(包含当天)
+ ///
+ public DateTime? EndDate
+ {
+ get => _endDate;
+ set { _endDate = value; RaisePropertyChanged(); }
+ }
+ #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 v = value <= 0 ? 20 : value;
+ if (_pageSize != v)
+ {
+ _pageSize = v;
+ 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()
+ {
+ Origin = RawCode = RawName = string.Empty;
+ StartDate = null;
+ EndDate = null;
+ }
+
+ private async Task SearchAsync()
+ {
+ if (IsBusy) return;
+ try
+ {
+ IsBusy = true;
+ _log.Info("FactoryInventoryTransaction 查询开始");
+
+ var data = await Task.Run(() =>
+ {
+ var q = _fsql.Select();
+
+ if (!string.IsNullOrWhiteSpace(Origin))
+ q = q.Where(a => a.Origin != null && a.Origin.Contains(Origin));
+ if (!string.IsNullOrWhiteSpace(RawCode))
+ q = q.Where(a => a.RawCode != null && a.RawCode.Contains(RawCode));
+ if (!string.IsNullOrWhiteSpace(RawName))
+ q = q.Where(a => a.RawName != null && a.RawName.Contains(RawName));
+
+ DateTime? start = StartDate;
+ DateTime? end = EndDate;
+ if (start.HasValue)
+ {
+ var s = start.Value.Date;
+ q = q.Where("TRY_CONVERT(datetime, InTime) >= @start", new { start = s });
+ }
+ if (end.HasValue)
+ {
+ var e = end.Value.Date.AddDays(1).AddTicks(-1);
+ q = q.Where("TRY_CONVERT(datetime, InTime) <= @end", new { end = e });
+ }
+
+ q = q.OrderByDescending(a => a.Id);
+
+ var page = PageIndex < 1 ? 1 : PageIndex;
+ var size = PageSize <= 0 ? 20 : PageSize;
+
+ 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($"FactoryInventoryTransaction 查询完成,记录数: {TotalCount}");
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"FactoryInventoryTransaction 查询失败: {ex}");
+ MessageBox.Show($"查询失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ public override async void OnNavigatedTo(Prism.Regions.NavigationContext navigationContext)
+ {
+ await SearchAsync();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/ViewModels/FactoryMaterialWithdrawalViewModel.cs b/FATrace.WPLApp/ViewModels/FactoryMaterialWithdrawalViewModel.cs
new file mode 100644
index 0000000..3fc6843
--- /dev/null
+++ b/FATrace.WPLApp/ViewModels/FactoryMaterialWithdrawalViewModel.cs
@@ -0,0 +1,208 @@
+using FATrace.WPLApp.Core;
+using Prism.Commands;
+using System;
+using System.Collections.ObjectModel;
+using FreeSql;
+using FATrace.Model;
+using FATrace.WPLApp.Services;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace FATrace.WPLApp.ViewModels
+{
+ ///
+ /// 工厂-领料 数据查询 VM
+ /// 仅展示从 Excel 导入的 FactoryMaterialWithdrawal 数据,支持简单条件与分页。
+ ///
+ public class FactoryMaterialWithdrawalViewModel : NavigationViewModel
+ {
+ private readonly IFreeSql _fsql;
+ private readonly ILogService _log;
+
+ public FactoryMaterialWithdrawalViewModel(IFreeSql fsql, ILogService log)
+ {
+ _fsql = fsql;
+ _log = log;
+
+ Items = new ObservableCollection();
+
+ 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 查询条件
+ private string? _origin;
+ public string? Origin { get => _origin; set { _origin = value; RaisePropertyChanged(); } }
+
+ private string? _rawCode;
+ public string? RawCode { get => _rawCode; set { _rawCode = value; RaisePropertyChanged(); } }
+
+ private string? _rawName;
+ public string? RawName { get => _rawName; set { _rawName = value; RaisePropertyChanged(); } }
+
+ private DateTime? _startDate;
+ ///
+ /// 登录日期起(基于 LoginDateTime)
+ ///
+ public DateTime? StartDate
+ {
+ get => _startDate;
+ set { _startDate = value; RaisePropertyChanged(); }
+ }
+
+ private DateTime? _endDate;
+ ///
+ /// 登录日期止(包含当天)
+ ///
+ public DateTime? EndDate
+ {
+ get => _endDate;
+ set { _endDate = value; RaisePropertyChanged(); }
+ }
+ #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 v = value <= 0 ? 20 : value;
+ if (_pageSize != v)
+ {
+ _pageSize = v;
+ 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()
+ {
+ Origin = RawCode = RawName = string.Empty;
+ StartDate = null;
+ EndDate = null;
+ }
+
+ private async Task SearchAsync()
+ {
+ if (IsBusy) return;
+ try
+ {
+ IsBusy = true;
+ _log.Info("FactoryMaterialWithdrawal 查询开始");
+
+ var data = await Task.Run(() =>
+ {
+ var q = _fsql.Select();
+
+ if (!string.IsNullOrWhiteSpace(Origin))
+ q = q.Where(a => a.Origin != null && a.Origin.Contains(Origin));
+ if (!string.IsNullOrWhiteSpace(RawCode))
+ q = q.Where(a => a.RawCode != null && a.RawCode.Contains(RawCode));
+ if (!string.IsNullOrWhiteSpace(RawName))
+ q = q.Where(a => a.RawName != null && a.RawName.Contains(RawName));
+
+ DateTime? start = StartDate;
+ DateTime? end = EndDate;
+ if (start.HasValue)
+ {
+ var s = start.Value.Date;
+ q = q.Where("TRY_CONVERT(datetime, LoginDateTime) >= @start", new { start = s });
+ }
+ if (end.HasValue)
+ {
+ var e = end.Value.Date.AddDays(1).AddTicks(-1);
+ q = q.Where("TRY_CONVERT(datetime, LoginDateTime) <= @end", new { end = e });
+ }
+
+ q = q.OrderByDescending(a => a.Id);
+
+ var page = PageIndex < 1 ? 1 : PageIndex;
+ var size = PageSize <= 0 ? 20 : PageSize;
+
+ 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($"FactoryMaterialWithdrawal 查询完成,记录数: {TotalCount}");
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"FactoryMaterialWithdrawal 查询失败: {ex}");
+ MessageBox.Show($"查询失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ public override async void OnNavigatedTo(Prism.Regions.NavigationContext navigationContext)
+ {
+ await SearchAsync();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/ViewModels/FactoryOutboundViewModel.cs b/FATrace.WPLApp/ViewModels/FactoryOutboundViewModel.cs
new file mode 100644
index 0000000..e387062
--- /dev/null
+++ b/FATrace.WPLApp/ViewModels/FactoryOutboundViewModel.cs
@@ -0,0 +1,198 @@
+using FATrace.WPLApp.Core;
+using Prism.Commands;
+using System;
+using System.Collections.ObjectModel;
+using FreeSql;
+using FATrace.Model;
+using FATrace.WPLApp.Services;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace FATrace.WPLApp.ViewModels
+{
+ ///
+ /// 工厂-成品出库 查询 VM(展示 FactoryOutbound)
+ ///
+ public class FactoryOutboundViewModel : NavigationViewModel
+ {
+ private readonly IFreeSql _fsql;
+ private readonly ILogService _log;
+
+ public FactoryOutboundViewModel(IFreeSql fsql, ILogService log)
+ {
+ _fsql = fsql;
+ _log = log;
+
+ Items = new ObservableCollection();
+
+ 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 查询条件
+ private string? _origin;
+ public string? Origin { get => _origin; set { _origin = value; RaisePropertyChanged(); } }
+
+ private string? _rawCode;
+ public string? RawCode { get => _rawCode; set { _rawCode = value; RaisePropertyChanged(); } }
+
+ private string? _rawName;
+ public string? RawName { get => _rawName; set { _rawName = value; RaisePropertyChanged(); } }
+
+ private string? _batch;
+ public string? Batch { get => _batch; set { _batch = value; RaisePropertyChanged(); } }
+
+ private DateTime? _startDate;
+ public DateTime? StartDate { get => _startDate; set { _startDate = value; RaisePropertyChanged(); } }
+
+ private DateTime? _endDate;
+ public DateTime? EndDate { get => _endDate; set { _endDate = value; RaisePropertyChanged(); } }
+ #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 v = value <= 0 ? 20 : value;
+ if (_pageSize != v)
+ {
+ _pageSize = v;
+ 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()
+ {
+ Origin = RawCode = RawName = Batch = string.Empty;
+ StartDate = null;
+ EndDate = null;
+ }
+
+ private async Task SearchAsync()
+ {
+ if (IsBusy) return;
+ try
+ {
+ IsBusy = true;
+ _log.Info("FactoryOutbound 查询开始");
+
+ var data = await Task.Run(() =>
+ {
+ var q = _fsql.Select();
+
+ if (!string.IsNullOrWhiteSpace(Origin))
+ q = q.Where(a => a.Origin != null && a.Origin.Contains(Origin));
+ if (!string.IsNullOrWhiteSpace(RawCode))
+ q = q.Where(a => a.RawCode != null && a.RawCode.Contains(RawCode));
+ if (!string.IsNullOrWhiteSpace(RawName))
+ q = q.Where(a => a.RawName != null && a.RawName.Contains(RawName));
+ if (!string.IsNullOrWhiteSpace(Batch))
+ q = q.Where(a => a.Batch != null && a.Batch.Contains(Batch));
+
+ DateTime? start = StartDate;
+ DateTime? end = EndDate;
+ if (start.HasValue)
+ {
+ var s = start.Value.Date;
+ q = q.Where("TRY_CONVERT(datetime, LoginDateTime) >= @start", new { start = s });
+ }
+ if (end.HasValue)
+ {
+ var e = end.Value.Date.AddDays(1).AddTicks(-1);
+ q = q.Where("TRY_CONVERT(datetime, LoginDateTime) <= @end", new { end = e });
+ }
+
+ q = q.OrderByDescending(a => a.Id);
+
+ var page = PageIndex < 1 ? 1 : PageIndex;
+ var size = PageSize <= 0 ? 20 : PageSize;
+
+ 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($"FactoryOutbound 查询完成,记录数: {TotalCount}");
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"FactoryOutbound 查询失败: {ex}");
+ MessageBox.Show($"查询失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ public override async void OnNavigatedTo(Prism.Regions.NavigationContext navigationContext)
+ {
+ await SearchAsync();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/ViewModels/FactoryProductionRecordViewModel.cs b/FATrace.WPLApp/ViewModels/FactoryProductionRecordViewModel.cs
new file mode 100644
index 0000000..dc71775
--- /dev/null
+++ b/FATrace.WPLApp/ViewModels/FactoryProductionRecordViewModel.cs
@@ -0,0 +1,216 @@
+using FATrace.WPLApp.Core;
+using Prism.Commands;
+using System;
+using System.Collections.ObjectModel;
+using FreeSql;
+using FATrace.Model;
+using FATrace.WPLApp.Services;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace FATrace.WPLApp.ViewModels
+{
+ ///
+ /// 工厂-原料生产信息 查询 VM(展示 FactoryProductionRecord)
+ ///
+ public class FactoryProductionRecordViewModel : NavigationViewModel
+ {
+ private readonly IFreeSql _fsql;
+ private readonly ILogService _log;
+
+ public FactoryProductionRecordViewModel(IFreeSql fsql, ILogService log)
+ {
+ _fsql = fsql;
+ _log = log;
+
+ Items = new ObservableCollection();
+
+ 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 查询条件
+ private string? _rawCode;
+ ///
+ /// 原料编号模糊匹配
+ ///
+ public string? RawCode { get => _rawCode; set { _rawCode = value; RaisePropertyChanged(); } }
+
+ private string? _rawName;
+ ///
+ /// 原料名称模糊匹配
+ ///
+ public string? RawName { get => _rawName; set { _rawName = value; RaisePropertyChanged(); } }
+
+ private string? _batch;
+ ///
+ /// 批号模糊匹配
+ ///
+ public string? Batch { get => _batch; set { _batch = value; RaisePropertyChanged(); } }
+
+ private DateTime? _startDate;
+ ///
+ /// 称重时间起(基于 WeightTime)
+ ///
+ public DateTime? StartDate
+ {
+ get => _startDate;
+ set { _startDate = value; RaisePropertyChanged(); }
+ }
+
+ private DateTime? _endDate;
+ ///
+ /// 称重时间止(包含当天)
+ ///
+ public DateTime? EndDate
+ {
+ get => _endDate;
+ set { _endDate = value; RaisePropertyChanged(); }
+ }
+ #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 v = value <= 0 ? 20 : value;
+ if (_pageSize != v)
+ {
+ _pageSize = v;
+ 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()
+ {
+ RawCode = RawName = Batch = string.Empty;
+ StartDate = null;
+ EndDate = null;
+ }
+
+ private async Task SearchAsync()
+ {
+ if (IsBusy) return;
+ try
+ {
+ IsBusy = true;
+ _log.Info("FactoryProductionRecord 查询开始");
+
+ var data = await Task.Run(() =>
+ {
+ var q = _fsql.Select();
+
+ if (!string.IsNullOrWhiteSpace(RawCode))
+ q = q.Where(a => a.RawCode != null && a.RawCode.Contains(RawCode));
+ if (!string.IsNullOrWhiteSpace(RawName))
+ q = q.Where(a => a.RawName != null && a.RawName.Contains(RawName));
+ if (!string.IsNullOrWhiteSpace(Batch))
+ q = q.Where(a => a.Batch != null && a.Batch.Contains(Batch));
+
+ DateTime? start = StartDate;
+ DateTime? end = EndDate;
+ if (start.HasValue)
+ {
+ var s = start.Value.Date;
+ q = q.Where("TRY_CONVERT(datetime, WeightTime) >= @start", new { start = s });
+ }
+ if (end.HasValue)
+ {
+ var e = end.Value.Date.AddDays(1).AddTicks(-1);
+ q = q.Where("TRY_CONVERT(datetime, WeightTime) <= @end", new { end = e });
+ }
+
+ q = q.OrderByDescending(a => a.Id);
+
+ var page = PageIndex < 1 ? 1 : PageIndex;
+ var size = PageSize <= 0 ? 20 : PageSize;
+
+ 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($"FactoryProductionRecord 查询完成,记录数: {TotalCount}");
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"FactoryProductionRecord 查询失败: {ex}");
+ MessageBox.Show($"查询失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ public override async void OnNavigatedTo(Prism.Regions.NavigationContext navigationContext)
+ {
+ await SearchAsync();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/ViewModels/FileImportLogViewModel.cs b/FATrace.WPLApp/ViewModels/FileImportLogViewModel.cs
new file mode 100644
index 0000000..2fa4169
--- /dev/null
+++ b/FATrace.WPLApp/ViewModels/FileImportLogViewModel.cs
@@ -0,0 +1,255 @@
+using FATrace.Model;
+using FATrace.WPLApp.Core;
+using FATrace.WPLApp.Services;
+using FreeSql;
+using Prism.Commands;
+using System;
+using System.Collections.ObjectModel;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace FATrace.WPLApp.ViewModels
+{
+ ///
+ /// 文件导入日志 查询 VM(展示 FileImportLog)
+ ///
+ public class FileImportLogViewModel : NavigationViewModel
+ {
+ private readonly IFreeSql _fsql;
+ private readonly ILogService _log;
+
+ public FileImportLogViewModel(IFreeSql fsql, ILogService log)
+ {
+ _fsql = fsql;
+ _log = log;
+
+ Items = new ObservableCollection();
+
+ 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 查询条件
+ private string? _fileName;
+ ///
+ /// 文件名(模糊匹配)
+ ///
+ public string? FileName
+ {
+ get => _fileName;
+ set { _fileName = value; RaisePropertyChanged(); }
+ }
+
+ private string? _status;
+ ///
+ /// 状态(模糊匹配,例如 Success/Failed)
+ ///
+ public string? Status
+ {
+ get => _status;
+ set { _status = value; RaisePropertyChanged(); }
+ }
+
+ private string? _keyword;
+ ///
+ /// 关键字(匹配 SourcePath/ArchivePath/Message/SheetRowStats)
+ ///
+ public string? Keyword
+ {
+ get => _keyword;
+ set { _keyword = value; RaisePropertyChanged(); }
+ }
+
+ private DateTime? _startDate;
+ ///
+ /// 导入开始日期起(基于 StartTime)
+ ///
+ public DateTime? StartDate
+ {
+ get => _startDate;
+ set { _startDate = value; RaisePropertyChanged(); }
+ }
+
+ private DateTime? _endDate;
+ ///
+ /// 导入开始日期止(基于 StartTime,包含当天)
+ ///
+ public DateTime? EndDate
+ {
+ get => _endDate;
+ set { _endDate = value; RaisePropertyChanged(); }
+ }
+ #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 v = value <= 0 ? 20 : value;
+ if (_pageSize != v)
+ {
+ _pageSize = v;
+ 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()
+ {
+ FileName = string.Empty;
+ Status = string.Empty;
+ Keyword = string.Empty;
+ StartDate = null;
+ EndDate = null;
+ }
+
+ private async Task SearchAsync()
+ {
+ if (IsBusy) return;
+ try
+ {
+ IsBusy = true;
+ _log.Info("FileImportLog 查询开始");
+
+ var data = await Task.Run(() =>
+ {
+ var q = _fsql.Select();
+
+ if (!string.IsNullOrWhiteSpace(FileName))
+ q = q.Where(a => a.FileName != null && a.FileName.Contains(FileName));
+ if (!string.IsNullOrWhiteSpace(Status))
+ q = q.Where(a => a.Status != null && a.Status.Contains(Status));
+
+ if (!string.IsNullOrWhiteSpace(Keyword))
+ {
+ var kw = Keyword;
+ q = q.Where(a =>
+ (a.SourcePath != null && a.SourcePath.Contains(kw))
+ || (a.ArchivePath != null && a.ArchivePath.Contains(kw))
+ || (a.Message != null && a.Message.Contains(kw))
+ || (a.SheetRowStats != null && a.SheetRowStats.Contains(kw))
+ );
+ }
+
+ DateTime? start = StartDate;
+ DateTime? end = EndDate;
+ if (start.HasValue)
+ {
+ var s = start.Value.Date;
+ q = q.Where(a => a.StartTime >= s);
+ }
+ if (end.HasValue)
+ {
+ var e = end.Value.Date.AddDays(1).AddTicks(-1);
+ q = q.Where(a => a.StartTime <= e);
+ }
+
+ q = q.OrderByDescending(a => a.Id);
+
+ var page = PageIndex < 1 ? 1 : PageIndex;
+ var size = PageSize <= 0 ? 20 : PageSize;
+
+ 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($"FileImportLog 查询完成,记录数: {TotalCount}");
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"FileImportLog 查询失败: {ex}");
+ MessageBox.Show($"查询失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ public override async void OnNavigatedTo(Prism.Regions.NavigationContext navigationContext)
+ {
+ await SearchAsync();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/ViewModels/HelpManualViewModel.cs b/FATrace.WPLApp/ViewModels/HelpManualViewModel.cs
new file mode 100644
index 0000000..6af4c22
--- /dev/null
+++ b/FATrace.WPLApp/ViewModels/HelpManualViewModel.cs
@@ -0,0 +1,209 @@
+using FATrace.WPLApp.Core;
+using FATrace.WPLApp.Services;
+using Prism.Commands;
+using System;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace FATrace.WPLApp.ViewModels
+{
+ ///
+ /// 使用手册(PDF)查看页 VM
+ ///
+ public class HelpManualViewModel : NavigationViewModel
+ {
+ private readonly ILogService _log;
+
+ public HelpManualViewModel(ILogService log)
+ {
+ _log = log;
+
+ PdfFiles = new ObservableCollection();
+ RefreshCommand = new DelegateCommand(async () => await LoadReportPdfsAsync(), () => !IsBusy)
+ .ObservesProperty(() => IsBusy);
+ }
+
+ private bool _isBusy;
+ ///
+ /// 是否忙碌(用于提示与禁用按钮)
+ ///
+ public bool IsBusy
+ {
+ get => _isBusy;
+ set { _isBusy = value; RaisePropertyChanged(); }
+ }
+
+ private string? _statusText;
+ ///
+ /// 页面状态文本(例如:未找到PDF)
+ ///
+ public string? StatusText
+ {
+ get => _statusText;
+ set { _statusText = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// Report 目录下的 PDF 文件列表
+ ///
+ public ObservableCollection PdfFiles { get; }
+
+ private PdfFileItem? _selectedPdf;
+ ///
+ /// 当前选择的 PDF
+ ///
+ public PdfFileItem? SelectedPdf
+ {
+ get => _selectedPdf;
+ set
+ {
+ _selectedPdf = value;
+ RaisePropertyChanged();
+ PdfFilePath = _selectedPdf?.FullPath;
+ }
+ }
+
+ private string? _pdfFilePath;
+ ///
+ /// 当前打开的 PDF 文件路径(绑定到 Syncfusion PdfViewerControl.ItemSource)
+ ///
+ public string? PdfFilePath
+ {
+ get => _pdfFilePath;
+ set { _pdfFilePath = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// 刷新/重新扫描 PDF 文件
+ ///
+ public DelegateCommand RefreshCommand { get; }
+
+ ///
+ /// 获取运行目录下 Report 文件夹绝对路径
+ ///
+ /// Report 文件夹绝对路径
+ private string GetReportFolder()
+ {
+ var baseDir = AppDomain.CurrentDomain.BaseDirectory;
+ var folder = Path.Combine(baseDir, "Report");
+ try
+ {
+ if (!Directory.Exists(folder))
+ {
+ Directory.CreateDirectory(folder);
+ }
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"HelpManual: 创建Report目录失败: {ex}");
+ }
+ return folder;
+ }
+
+ ///
+ /// 扫描 Report 目录并加载 PDF 列表(默认打开第一个)
+ ///
+ /// Task
+ private async Task LoadReportPdfsAsync()
+ {
+ if (IsBusy) return;
+ try
+ {
+ IsBusy = true;
+ StatusText = string.Empty;
+
+ var folder = GetReportFolder();
+ _log.Info($"HelpManual: 扫描PDF目录: {folder}");
+
+ var list = await Task.Run(() =>
+ {
+ if (!Directory.Exists(folder))
+ return Array.Empty();
+
+ var files = Directory.GetFiles(folder, "*.pdf", SearchOption.TopDirectoryOnly)
+ .Select(p => new PdfFileItem(Path.GetFileName(p), p))
+ .OrderByDescending(a => a.FileName)
+ .ToArray();
+
+ return files;
+ });
+
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ PdfFiles.Clear();
+ foreach (var f in list) PdfFiles.Add(f);
+
+ if (PdfFiles.Count == 0)
+ {
+ SelectedPdf = null;
+ PdfFilePath = null;
+ StatusText = "未在程序目录下的 Report 文件夹找到 PDF 文件,请将手册PDF放入该目录后点击【刷新】。";
+ return;
+ }
+
+ // 保持原选择(若仍存在),否则默认第一个
+ var keep = SelectedPdf != null
+ ? PdfFiles.FirstOrDefault(a => string.Equals(a.FullPath, SelectedPdf.FullPath, StringComparison.OrdinalIgnoreCase))
+ : null;
+
+ SelectedPdf = keep ?? PdfFiles[0];
+ });
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"HelpManual: 扫描/加载PDF失败: {ex}");
+ StatusText = $"加载PDF失败: {ex.Message}";
+ MessageBox.Show($"加载PDF失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ ///
+ /// 页面导航进入时自动加载
+ ///
+ /// 导航上下文
+ public override async void OnNavigatedTo(Prism.Regions.NavigationContext navigationContext)
+ {
+ await LoadReportPdfsAsync();
+ }
+
+ ///
+ /// PDF 文件下拉项
+ ///
+ public class PdfFileItem
+ {
+ ///
+ /// 构造
+ ///
+ /// 文件名
+ /// 完整路径
+ public PdfFileItem(string fileName, string fullPath)
+ {
+ FileName = fileName;
+ FullPath = fullPath;
+ }
+
+ ///
+ /// 文件名
+ ///
+ public string FileName { get; }
+
+ ///
+ /// 完整路径
+ ///
+ public string FullPath { get; }
+
+ ///
+ /// 用于下拉显示
+ ///
+ /// 文件名
+ public override string ToString() => FileName;
+ }
+ }
+}
diff --git a/FATrace.WPLApp/ViewModels/LoginViewModel.cs b/FATrace.WPLApp/ViewModels/LoginViewModel.cs
index 43489d3..69932f2 100644
--- a/FATrace.WPLApp/ViewModels/LoginViewModel.cs
+++ b/FATrace.WPLApp/ViewModels/LoginViewModel.cs
@@ -73,6 +73,7 @@ namespace FATrace.WPLApp.ViewModels
}
_sys.CurUser = user.UserName;
+ _sys.CurAccessLevel = user.AccessLevel;
_log.Info($"用户登录成功: {user.UserName}");
UserName="";
Password="";
diff --git a/FATrace.WPLApp/ViewModels/MainViewModel.cs b/FATrace.WPLApp/ViewModels/MainViewModel.cs
index a8a9057..ab8d451 100644
--- a/FATrace.WPLApp/ViewModels/MainViewModel.cs
+++ b/FATrace.WPLApp/ViewModels/MainViewModel.cs
@@ -6,18 +6,24 @@ using Prism.Commands;
using Prism.Events;
using Prism.Regions;
using Prism.Services.Dialogs;
+using System;
using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Windows;
namespace FATrace.WPLApp.ViewModels
{
public class MainViewModel : NavigationViewModel
{
- public MainViewModel(IRegionManager regionManager, IDialogService dialogService, IEventAggregator eventAggregator, NavigationServices navigationServices)
+ public MainViewModel(IRegionManager regionManager, IDialogService dialogService, IEventAggregator eventAggregator, NavigationServices navigationServices, SysRunService sysRunService)
{
this.regionManager = regionManager;
DialogService = dialogService;
NavigationServices = navigationServices;
- NavigationItems = NavigationServices.NavItemDtos;
+ _sys = sysRunService;
+ NavigationItems = BuildNavigationItems(_sys.CurAccessLevel);
+ _sys.PropertyChanged += SysOnPropertyChanged;
this.regionManager.RegisterViewWithRegion("FootRegion", typeof(FootView));
this.regionManager.RegisterViewWithRegion("HeadRegion", typeof(HeadView));
@@ -30,8 +36,69 @@ namespace FATrace.WPLApp.ViewModels
public IDialogService DialogService { get; }
public NavigationServices NavigationServices { get; }
+ private readonly SysRunService _sys;
+
public ObservableCollection NavigationItems { get; set; }
+ private void SysOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(SysRunService.CurAccessLevel) || e.PropertyName == nameof(SysRunService.CurUser))
+ {
+ NavigationItems = BuildNavigationItems(_sys.CurAccessLevel);
+ RaisePropertyChanged(nameof(NavigationItems));
+ }
+ }
+
+ private ObservableCollection BuildNavigationItems(string? accessLevel)
+ {
+ // 未登录/访客:仅允许 Dashboard + 用户登录
+ if (string.IsNullOrWhiteSpace(accessLevel) || string.Equals(accessLevel, UserManageViewModel.AccessLevels.Guest, StringComparison.OrdinalIgnoreCase))
+ {
+ var dashboard = NavigationServices.NavItemDtos.FirstOrDefault(a => string.Equals(a.CmdPar, "Dashboard", StringComparison.OrdinalIgnoreCase));
+ var system = NavigationServices.NavItemDtos.FirstOrDefault(a => string.Equals(a.Name, "系统", StringComparison.OrdinalIgnoreCase));
+
+ var result = new ObservableCollection();
+ if (dashboard != null) result.Add(CloneNavItem(dashboard));
+
+ if (system != null)
+ {
+ var sysClone = CloneNavItem(system);
+ sysClone.ChildrenNavItemDtos = new ObservableCollection(
+ sysClone.ChildrenNavItemDtos
+ .Where(a => string.Equals(a.CmdPar, "用户登录", StringComparison.OrdinalIgnoreCase))
+ .ToList());
+ result.Add(sysClone);
+ }
+
+ return result;
+ }
+
+ // 管理员/操作员:完整菜单
+ return new ObservableCollection(NavigationServices.NavItemDtos.Select(CloneNavItem));
+ }
+
+ private NavItemDto CloneNavItem(NavItemDto src)
+ {
+ var dst = new NavItemDto
+ {
+ Name = src.Name,
+ CmdPar = src.CmdPar,
+ Icon = src.Icon,
+ IsParent = src.IsParent,
+ ChildrenNavItemDtos = new ObservableCollection()
+ };
+
+ if (src.ChildrenNavItemDtos != null && src.ChildrenNavItemDtos.Count > 0)
+ {
+ foreach (var c in src.ChildrenNavItemDtos)
+ {
+ dst.ChildrenNavItemDtos.Add(CloneNavItem(c));
+ }
+ }
+
+ return dst;
+ }
+
private string titel = string.Empty;
///
/// 标题
@@ -60,7 +127,27 @@ namespace FATrace.WPLApp.ViewModels
private void OpenCommandMethod(object obj)
{
- switch (obj.ToString())
+ var cmd = obj?.ToString() ?? string.Empty;
+
+ // 父级目录/空命令不做任何处理(避免点击父节点触发弹窗或导航)
+ if (string.IsNullOrWhiteSpace(cmd))
+ {
+ return;
+ }
+
+ // 访客/未登录:仅 Dashboard + 用户登录
+ if (string.IsNullOrWhiteSpace(_sys.CurAccessLevel) || string.Equals(_sys.CurAccessLevel, UserManageViewModel.AccessLevels.Guest, StringComparison.OrdinalIgnoreCase))
+ {
+ if (!string.Equals(cmd, "Dashboard", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(cmd, "用户登录", StringComparison.OrdinalIgnoreCase))
+ {
+ MessageBox.Show("当前账号权限为【访客】,仅允许访问 Dashboard。", "权限不足", MessageBoxButton.OK, MessageBoxImage.Warning);
+ this.regionManager.Regions["ContentRegion"].RequestNavigate("DashBoardView");
+ return;
+ }
+ }
+
+ switch (cmd)
{
case "Dashboard":
this.regionManager.Regions["ContentRegion"].RequestNavigate("DashBoardView");
@@ -102,6 +189,37 @@ namespace FATrace.WPLApp.ViewModels
case "原料入库查询":
this.regionManager.Regions["ContentRegion"].RequestNavigate("RawProInputView");
break;
+ case "文件导入日志":
+ this.regionManager.Regions["ContentRegion"].RequestNavigate("FileImportLogView");
+ break;
+ // 工厂/OEM Excel 导入数据查询
+ case "工厂-入库":
+ this.regionManager.Regions["ContentRegion"].RequestNavigate("FactoryInboundView");
+ break;
+ case "工厂-领料":
+ this.regionManager.Regions["ContentRegion"].RequestNavigate("FactoryMaterialWithdrawalView");
+ break;
+ case "工厂-出入库":
+ this.regionManager.Regions["ContentRegion"].RequestNavigate("FactoryInventoryTransactionView");
+ break;
+ case "工厂-原料生产信息":
+ this.regionManager.Regions["ContentRegion"].RequestNavigate("FactoryProductionRecordView");
+ break;
+ case "工厂-成品出库":
+ this.regionManager.Regions["ContentRegion"].RequestNavigate("FactoryOutboundView");
+ break;
+ case "OEM-入库":
+ this.regionManager.Regions["ContentRegion"].RequestNavigate("OEMInboundView");
+ break;
+ case "OEM-出库":
+ this.regionManager.Regions["ContentRegion"].RequestNavigate("OEMOutboundView");
+ break;
+ case "OEM-出入库":
+ this.regionManager.Regions["ContentRegion"].RequestNavigate("OEMInventoryTransactionView");
+ break;
+ case "OEM-原料使用信息":
+ this.regionManager.Regions["ContentRegion"].RequestNavigate("OEMRawUsageInfoView");
+ break;
case "历史报警":
this.regionManager.Regions["ContentRegion"].RequestNavigate("HistoryAlarmView");
@@ -114,10 +232,11 @@ namespace FATrace.WPLApp.ViewModels
this.regionManager.Regions["ContentRegion"].RequestNavigate("LoginView");
//this.regionManager.Regions["ContentRegion"].Activate(Shift);
break;
+ case "用户管理":
+ this.regionManager.Regions["ContentRegion"].RequestNavigate("UserManageView");
+ break;
case "使用手册":
- //this.regionManager.Regions["ContentRegion"].RequestNavigate("HelpManualView");
- //弹窗
-
+ this.regionManager.Regions["ContentRegion"].RequestNavigate("HelpManualView");
break;
default:
break;
diff --git a/FATrace.WPLApp/ViewModels/OEMInboundViewModel.cs b/FATrace.WPLApp/ViewModels/OEMInboundViewModel.cs
new file mode 100644
index 0000000..2e57256
--- /dev/null
+++ b/FATrace.WPLApp/ViewModels/OEMInboundViewModel.cs
@@ -0,0 +1,198 @@
+using FATrace.WPLApp.Core;
+using Prism.Commands;
+using System;
+using System.Collections.ObjectModel;
+using FreeSql;
+using FATrace.Model;
+using FATrace.WPLApp.Services;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace FATrace.WPLApp.ViewModels
+{
+ ///
+ /// OEM-入库 查询 VM(展示 OEMInbound)
+ ///
+ public class OEMInboundViewModel : NavigationViewModel
+ {
+ private readonly IFreeSql _fsql;
+ private readonly ILogService _log;
+
+ public OEMInboundViewModel(IFreeSql fsql, ILogService log)
+ {
+ _fsql = fsql;
+ _log = log;
+
+ Items = new ObservableCollection();
+
+ 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 查询条件
+ private string? _origin;
+ public string? Origin { get => _origin; set { _origin = value; RaisePropertyChanged(); } }
+
+ private string? _rawCode;
+ public string? RawCode { get => _rawCode; set { _rawCode = value; RaisePropertyChanged(); } }
+
+ private string? _rawName;
+ public string? RawName { get => _rawName; set { _rawName = value; RaisePropertyChanged(); } }
+
+ private string? _batch;
+ public string? Batch { get => _batch; set { _batch = value; RaisePropertyChanged(); } }
+
+ private DateTime? _startDate;
+ public DateTime? StartDate { get => _startDate; set { _startDate = value; RaisePropertyChanged(); } }
+
+ private DateTime? _endDate;
+ public DateTime? EndDate { get => _endDate; set { _endDate = value; RaisePropertyChanged(); } }
+ #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 v = value <= 0 ? 20 : value;
+ if (_pageSize != v)
+ {
+ _pageSize = v;
+ 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()
+ {
+ Origin = RawCode = RawName = Batch = string.Empty;
+ StartDate = null;
+ EndDate = null;
+ }
+
+ private async Task SearchAsync()
+ {
+ if (IsBusy) return;
+ try
+ {
+ IsBusy = true;
+ _log.Info("OEMInbound 查询开始");
+
+ var data = await Task.Run(() =>
+ {
+ var q = _fsql.Select();
+
+ if (!string.IsNullOrWhiteSpace(Origin))
+ q = q.Where(a => a.Origin != null && a.Origin.Contains(Origin));
+ if (!string.IsNullOrWhiteSpace(RawCode))
+ q = q.Where(a => a.RawCode != null && a.RawCode.Contains(RawCode));
+ if (!string.IsNullOrWhiteSpace(RawName))
+ q = q.Where(a => a.RawName != null && a.RawName.Contains(RawName));
+ if (!string.IsNullOrWhiteSpace(Batch))
+ q = q.Where(a => a.Batch != null && a.Batch.Contains(Batch));
+
+ DateTime? start = StartDate;
+ DateTime? end = EndDate;
+ if (start.HasValue)
+ {
+ var s = start.Value.Date;
+ q = q.Where("TRY_CONVERT(datetime, LoginDateTime) >= @start", new { start = s });
+ }
+ if (end.HasValue)
+ {
+ var e = end.Value.Date.AddDays(1).AddTicks(-1);
+ q = q.Where("TRY_CONVERT(datetime, LoginDateTime) <= @end", new { end = e });
+ }
+
+ q = q.OrderByDescending(a => a.Id);
+
+ var page = PageIndex < 1 ? 1 : PageIndex;
+ var size = PageSize <= 0 ? 20 : PageSize;
+
+ 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($"OEMInbound 查询完成,记录数: {TotalCount}");
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"OEMInbound 查询失败: {ex}");
+ MessageBox.Show($"查询失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ public override async void OnNavigatedTo(Prism.Regions.NavigationContext navigationContext)
+ {
+ await SearchAsync();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/ViewModels/OEMInventoryTransactionViewModel.cs b/FATrace.WPLApp/ViewModels/OEMInventoryTransactionViewModel.cs
new file mode 100644
index 0000000..e0398af
--- /dev/null
+++ b/FATrace.WPLApp/ViewModels/OEMInventoryTransactionViewModel.cs
@@ -0,0 +1,193 @@
+using FATrace.WPLApp.Core;
+using Prism.Commands;
+using System;
+using System.Collections.ObjectModel;
+using FreeSql;
+using FATrace.Model;
+using FATrace.WPLApp.Services;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace FATrace.WPLApp.ViewModels
+{
+ ///
+ /// OEM-出入库 查询 VM(展示 OEMInventoryTransaction)
+ ///
+ public class OEMInventoryTransactionViewModel : NavigationViewModel
+ {
+ private readonly IFreeSql _fsql;
+ private readonly ILogService _log;
+
+ public OEMInventoryTransactionViewModel(IFreeSql fsql, ILogService log)
+ {
+ _fsql = fsql;
+ _log = log;
+
+ Items = new ObservableCollection();
+
+ 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 查询条件
+ private string? _origin;
+ public string? Origin { get => _origin; set { _origin = value; RaisePropertyChanged(); } }
+
+ private string? _rawCode;
+ public string? RawCode { get => _rawCode; set { _rawCode = value; RaisePropertyChanged(); } }
+
+ private string? _rawName;
+ public string? RawName { get => _rawName; set { _rawName = value; RaisePropertyChanged(); } }
+
+ private DateTime? _startDate;
+ public DateTime? StartDate { get => _startDate; set { _startDate = value; RaisePropertyChanged(); } }
+
+ private DateTime? _endDate;
+ public DateTime? EndDate { get => _endDate; set { _endDate = value; RaisePropertyChanged(); } }
+ #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 v = value <= 0 ? 20 : value;
+ if (_pageSize != v)
+ {
+ _pageSize = v;
+ 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()
+ {
+ Origin = RawCode = RawName = string.Empty;
+ StartDate = null;
+ EndDate = null;
+ }
+
+ private async Task SearchAsync()
+ {
+ if (IsBusy) return;
+ try
+ {
+ IsBusy = true;
+ _log.Info("OEMInventoryTransaction 查询开始");
+
+ var data = await Task.Run(() =>
+ {
+ var q = _fsql.Select();
+
+ if (!string.IsNullOrWhiteSpace(Origin))
+ q = q.Where(a => a.Origin != null && a.Origin.Contains(Origin));
+ if (!string.IsNullOrWhiteSpace(RawCode))
+ q = q.Where(a => a.RawCode != null && a.RawCode.Contains(RawCode));
+ if (!string.IsNullOrWhiteSpace(RawName))
+ q = q.Where(a => a.RawName != null && a.RawName.Contains(RawName));
+
+ DateTime? start = StartDate;
+ DateTime? end = EndDate;
+ if (start.HasValue)
+ {
+ var s = start.Value.Date;
+ q = q.Where("TRY_CONVERT(datetime, InTime) >= @start", new { start = s });
+ }
+ if (end.HasValue)
+ {
+ var e = end.Value.Date.AddDays(1).AddTicks(-1);
+ q = q.Where("TRY_CONVERT(datetime, InTime) <= @end", new { end = e });
+ }
+
+ q = q.OrderByDescending(a => a.Id);
+
+ var page = PageIndex < 1 ? 1 : PageIndex;
+ var size = PageSize <= 0 ? 20 : PageSize;
+
+ 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($"OEMInventoryTransaction 查询完成,记录数: {TotalCount}");
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"OEMInventoryTransaction 查询失败: {ex}");
+ MessageBox.Show($"查询失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ public override async void OnNavigatedTo(Prism.Regions.NavigationContext navigationContext)
+ {
+ await SearchAsync();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/ViewModels/OEMOutboundViewModel.cs b/FATrace.WPLApp/ViewModels/OEMOutboundViewModel.cs
new file mode 100644
index 0000000..45be9e9
--- /dev/null
+++ b/FATrace.WPLApp/ViewModels/OEMOutboundViewModel.cs
@@ -0,0 +1,198 @@
+using FATrace.WPLApp.Core;
+using Prism.Commands;
+using System;
+using System.Collections.ObjectModel;
+using FreeSql;
+using FATrace.Model;
+using FATrace.WPLApp.Services;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace FATrace.WPLApp.ViewModels
+{
+ ///
+ /// OEM-出库 查询 VM(展示 OEMOutbound)
+ ///
+ public class OEMOutboundViewModel : NavigationViewModel
+ {
+ private readonly IFreeSql _fsql;
+ private readonly ILogService _log;
+
+ public OEMOutboundViewModel(IFreeSql fsql, ILogService log)
+ {
+ _fsql = fsql;
+ _log = log;
+
+ Items = new ObservableCollection();
+
+ 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 查询条件
+ private string? _origin;
+ public string? Origin { get => _origin; set { _origin = value; RaisePropertyChanged(); } }
+
+ private string? _rawCode;
+ public string? RawCode { get => _rawCode; set { _rawCode = value; RaisePropertyChanged(); } }
+
+ private string? _rawName;
+ public string? RawName { get => _rawName; set { _rawName = value; RaisePropertyChanged(); } }
+
+ private string? _batch;
+ public string? Batch { get => _batch; set { _batch = value; RaisePropertyChanged(); } }
+
+ private DateTime? _startDate;
+ public DateTime? StartDate { get => _startDate; set { _startDate = value; RaisePropertyChanged(); } }
+
+ private DateTime? _endDate;
+ public DateTime? EndDate { get => _endDate; set { _endDate = value; RaisePropertyChanged(); } }
+ #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 v = value <= 0 ? 20 : value;
+ if (_pageSize != v)
+ {
+ _pageSize = v;
+ 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()
+ {
+ Origin = RawCode = RawName = Batch = string.Empty;
+ StartDate = null;
+ EndDate = null;
+ }
+
+ private async Task SearchAsync()
+ {
+ if (IsBusy) return;
+ try
+ {
+ IsBusy = true;
+ _log.Info("OEMOutbound 查询开始");
+
+ var data = await Task.Run(() =>
+ {
+ var q = _fsql.Select();
+
+ if (!string.IsNullOrWhiteSpace(Origin))
+ q = q.Where(a => a.Origin != null && a.Origin.Contains(Origin));
+ if (!string.IsNullOrWhiteSpace(RawCode))
+ q = q.Where(a => a.RawCode != null && a.RawCode.Contains(RawCode));
+ if (!string.IsNullOrWhiteSpace(RawName))
+ q = q.Where(a => a.RawName != null && a.RawName.Contains(RawName));
+ if (!string.IsNullOrWhiteSpace(Batch))
+ q = q.Where(a => a.Batch != null && a.Batch.Contains(Batch));
+
+ DateTime? start = StartDate;
+ DateTime? end = EndDate;
+ if (start.HasValue)
+ {
+ var s = start.Value.Date;
+ q = q.Where("TRY_CONVERT(datetime, LoginDateTime) >= @start", new { start = s });
+ }
+ if (end.HasValue)
+ {
+ var e = end.Value.Date.AddDays(1).AddTicks(-1);
+ q = q.Where("TRY_CONVERT(datetime, LoginDateTime) <= @end", new { end = e });
+ }
+
+ q = q.OrderByDescending(a => a.Id);
+
+ var page = PageIndex < 1 ? 1 : PageIndex;
+ var size = PageSize <= 0 ? 20 : PageSize;
+
+ 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($"OEMOutbound 查询完成,记录数: {TotalCount}");
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"OEMOutbound 查询失败: {ex}");
+ MessageBox.Show($"查询失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ public override async void OnNavigatedTo(Prism.Regions.NavigationContext navigationContext)
+ {
+ await SearchAsync();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/ViewModels/OEMRawUsageInfoViewModel.cs b/FATrace.WPLApp/ViewModels/OEMRawUsageInfoViewModel.cs
new file mode 100644
index 0000000..0a88469
--- /dev/null
+++ b/FATrace.WPLApp/ViewModels/OEMRawUsageInfoViewModel.cs
@@ -0,0 +1,198 @@
+using FATrace.WPLApp.Core;
+using Prism.Commands;
+using System;
+using System.Collections.ObjectModel;
+using FreeSql;
+using FATrace.Model;
+using FATrace.WPLApp.Services;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace FATrace.WPLApp.ViewModels
+{
+ ///
+ /// OEM-原料使用信息 查询 VM(展示 OEMRawUsageInfo)
+ ///
+ public class OEMRawUsageInfoViewModel : NavigationViewModel
+ {
+ private readonly IFreeSql _fsql;
+ private readonly ILogService _log;
+
+ public OEMRawUsageInfoViewModel(IFreeSql fsql, ILogService log)
+ {
+ _fsql = fsql;
+ _log = log;
+
+ Items = new ObservableCollection();
+
+ 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 查询条件
+ private string? _inBagCode;
+ public string? InBagCode { get => _inBagCode; set { _inBagCode = value; RaisePropertyChanged(); } }
+
+ private string? _origin;
+ public string? Origin { get => _origin; set { _origin = value; RaisePropertyChanged(); } }
+
+ private string? _rawCode;
+ public string? RawCode { get => _rawCode; set { _rawCode = value; RaisePropertyChanged(); } }
+
+ private string? _rawName;
+ public string? RawName { get => _rawName; set { _rawName = value; RaisePropertyChanged(); } }
+
+ private DateTime? _startDate;
+ public DateTime? StartDate { get => _startDate; set { _startDate = value; RaisePropertyChanged(); } }
+
+ private DateTime? _endDate;
+ public DateTime? EndDate { get => _endDate; set { _endDate = value; RaisePropertyChanged(); } }
+ #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 v = value <= 0 ? 20 : value;
+ if (_pageSize != v)
+ {
+ _pageSize = v;
+ 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()
+ {
+ InBagCode = Origin = RawCode = RawName = string.Empty;
+ StartDate = null;
+ EndDate = null;
+ }
+
+ private async Task SearchAsync()
+ {
+ if (IsBusy) return;
+ try
+ {
+ IsBusy = true;
+ _log.Info("OEMRawUsageInfo 查询开始");
+
+ var data = await Task.Run(() =>
+ {
+ var q = _fsql.Select();
+
+ if (!string.IsNullOrWhiteSpace(InBagCode))
+ q = q.Where(a => a.InBagCode != null && a.InBagCode.Contains(InBagCode));
+ if (!string.IsNullOrWhiteSpace(Origin))
+ q = q.Where(a => a.Origin != null && a.Origin.Contains(Origin));
+ if (!string.IsNullOrWhiteSpace(RawCode))
+ q = q.Where(a => a.RawCode != null && a.RawCode.Contains(RawCode));
+ if (!string.IsNullOrWhiteSpace(RawName))
+ q = q.Where(a => a.RawName != null && a.RawName.Contains(RawName));
+
+ DateTime? start = StartDate;
+ DateTime? end = EndDate;
+ if (start.HasValue)
+ {
+ var s = start.Value.Date;
+ q = q.Where("TRY_CONVERT(datetime, RawUseTime) >= @start", new { start = s });
+ }
+ if (end.HasValue)
+ {
+ var e = end.Value.Date.AddDays(1).AddTicks(-1);
+ q = q.Where("TRY_CONVERT(datetime, RawUseTime) <= @end", new { end = e });
+ }
+
+ q = q.OrderByDescending(a => a.Id);
+
+ var page = PageIndex < 1 ? 1 : PageIndex;
+ var size = PageSize <= 0 ? 20 : PageSize;
+
+ 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($"OEMRawUsageInfo 查询完成,记录数: {TotalCount}");
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"OEMRawUsageInfo 查询失败: {ex}");
+ MessageBox.Show($"查询失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ public override async void OnNavigatedTo(Prism.Regions.NavigationContext navigationContext)
+ {
+ await SearchAsync();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/ViewModels/UserManageViewModel.cs b/FATrace.WPLApp/ViewModels/UserManageViewModel.cs
new file mode 100644
index 0000000..bbd1f12
--- /dev/null
+++ b/FATrace.WPLApp/ViewModels/UserManageViewModel.cs
@@ -0,0 +1,395 @@
+using FATrace.Model;
+using FATrace.WPLApp.Core;
+using FATrace.WPLApp.Services;
+using FreeSql;
+using Prism.Commands;
+using Prism.Services.Dialogs;
+using System;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace FATrace.WPLApp.ViewModels
+{
+ ///
+ /// 用户管理页面 VM(TbUser 增删改查)
+ ///
+ public class UserManageViewModel : NavigationViewModel
+ {
+ private readonly IFreeSql _fsql;
+ private readonly ILogService _log;
+ private readonly IDialogService _dialog;
+ private readonly SysRunService _sys;
+
+ public UserManageViewModel(IFreeSql fsql, ILogService log, IDialogService dialogService, SysRunService sys)
+ {
+ _fsql = fsql;
+ _log = log;
+ _dialog = dialogService;
+ _sys = sys;
+
+ AccessLevelOptions = new ObservableCollection(new[] { AccessLevels.All, AccessLevels.Admin, AccessLevels.Operator, AccessLevels.Guest });
+ AccessLevel = AccessLevels.All;
+ Items = new ObservableCollection();
+
+ SearchCommand = new DelegateCommand(async () => await SearchAsync(), () => !IsBusy)
+ .ObservesProperty(() => IsBusy);
+ ClearCommand = new DelegateCommand(ClearFilters, () => !IsBusy)
+ .ObservesProperty(() => IsBusy);
+
+ AddCommand = new DelegateCommand(AddUser, () => !IsBusy && CanEditUsers)
+ .ObservesProperty(() => IsBusy)
+ .ObservesProperty(() => CanEditUsers);
+ EditCommand = new DelegateCommand(EditUser, () => !IsBusy && CanEditUsers && SelectedItem != null)
+ .ObservesProperty(() => IsBusy)
+ .ObservesProperty(() => CanEditUsers)
+ .ObservesProperty(() => SelectedItem);
+ DeleteCommand = new DelegateCommand(async () => await DeleteUserAsync(), () => !IsBusy && CanEditUsers && SelectedItem != null)
+ .ObservesProperty(() => IsBusy)
+ .ObservesProperty(() => CanEditUsers)
+ .ObservesProperty(() => SelectedItem);
+
+ 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);
+
+ _sys.PropertyChanged += SysOnPropertyChanged;
+ RaisePropertyChanged(nameof(CanEditUsers));
+ }
+
+ private void SysOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(SysRunService.CurAccessLevel) || e.PropertyName == nameof(SysRunService.CurUser))
+ {
+ RaisePropertyChanged(nameof(CanEditUsers));
+ }
+ }
+
+ ///
+ /// 是否允许编辑用户(仅管理员)
+ ///
+ public bool CanEditUsers => string.Equals(_sys.CurAccessLevel, AccessLevels.Admin, StringComparison.OrdinalIgnoreCase);
+
+ #region 查询条件
+ private string? _userName;
+ ///
+ /// 用户名(模糊匹配)
+ ///
+ public string? UserName
+ {
+ get => _userName;
+ set { _userName = value; RaisePropertyChanged(); }
+ }
+
+ private string? _accessLevel;
+ ///
+ /// 等级(精确匹配)
+ ///
+ public string? AccessLevel
+ {
+ get => _accessLevel;
+ set { _accessLevel = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// 等级下拉选项
+ ///
+ public ObservableCollection AccessLevelOptions { get; }
+ #endregion
+
+ #region 列表与分页
+ public ObservableCollection Items { get; }
+
+ private TbUser? _selectedItem;
+ ///
+ /// 当前选中行
+ ///
+ public TbUser? SelectedItem
+ {
+ get => _selectedItem;
+ set { _selectedItem = value; RaisePropertyChanged(); }
+ }
+
+ 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;
+ ///
+ /// 页码(从1开始)
+ ///
+ public int PageIndex
+ {
+ get => _pageIndex;
+ set { _pageIndex = value < 1 ? 1 : value; RaisePropertyChanged(); }
+ }
+
+ private int _pageSize = 20;
+ ///
+ /// 页大小
+ ///
+ public int PageSize
+ {
+ get => _pageSize;
+ set
+ {
+ var v = value <= 0 ? 20 : value;
+ if (_pageSize != v)
+ {
+ _pageSize = v;
+ 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 AddCommand { get; }
+ public DelegateCommand EditCommand { get; }
+ public DelegateCommand DeleteCommand { get; }
+
+ public DelegateCommand FirstPageCommand { get; }
+ public DelegateCommand PrevPageCommand { get; }
+ public DelegateCommand NextPageCommand { get; }
+ public DelegateCommand LastPageCommand { get; }
+ #endregion
+
+ private void ClearFilters()
+ {
+ UserName = string.Empty;
+ AccessLevel = AccessLevels.All;
+ }
+
+ private async Task SearchAsync()
+ {
+ if (IsBusy) return;
+ try
+ {
+ IsBusy = true;
+ await SearchCoreAsync();
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"TbUser 查询失败: {ex}");
+ MessageBox.Show($"查询失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ private async Task SearchCoreAsync()
+ {
+ _log.Info("TbUser 查询开始");
+
+ var data = await Task.Run(() =>
+ {
+ var q = _fsql.Select();
+
+ if (!string.IsNullOrWhiteSpace(UserName))
+ q = q.Where(a => a.UserName != null && a.UserName.Contains(UserName));
+
+ if (!string.IsNullOrWhiteSpace(AccessLevel)
+ && !string.Equals(AccessLevel, AccessLevels.All, StringComparison.OrdinalIgnoreCase))
+ {
+ q = q.Where(a => a.AccessLevel == AccessLevel);
+ }
+
+ q = q.OrderByDescending(a => a.Id);
+
+ var page = PageIndex < 1 ? 1 : PageIndex;
+ var size = PageSize <= 0 ? 20 : PageSize;
+
+ 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($"TbUser 查询完成,记录数: {TotalCount}");
+ }
+
+ private void AddUser()
+ {
+ var p = new DialogParameters
+ {
+ { DialogKeys.Mode, DialogModes.Add }
+ };
+
+ _dialog.ShowDialog("DialogUserEditView", p, async r =>
+ {
+ if (r.Result == ButtonResult.OK)
+ {
+ PageIndex = 1;
+ await SearchAsync();
+ }
+ });
+ }
+
+ private void EditUser()
+ {
+ if (SelectedItem == null)
+ {
+ MessageBox.Show("请选择要编辑的用户", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+
+ var p = new DialogParameters
+ {
+ { DialogKeys.Mode, DialogModes.Edit },
+ { DialogKeys.UserId, SelectedItem.Id }
+ };
+
+ _dialog.ShowDialog("DialogUserEditView", p, async r =>
+ {
+ if (r.Result == ButtonResult.OK)
+ {
+ await SearchAsync();
+ }
+ });
+ }
+
+ private async Task DeleteUserAsync()
+ {
+ if (SelectedItem == null)
+ {
+ MessageBox.Show("请选择要删除的用户", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+
+ if (!string.IsNullOrWhiteSpace(_sys.CurUser)
+ && string.Equals(_sys.CurUser, SelectedItem.UserName, StringComparison.OrdinalIgnoreCase))
+ {
+ MessageBox.Show("不能删除当前登录用户", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ var ok = MessageBox.Show($"确认删除用户:{SelectedItem.UserName} ?", "确认", MessageBoxButton.YesNo, MessageBoxImage.Question);
+ if (ok != MessageBoxResult.Yes) return;
+
+ if (IsBusy) return;
+ try
+ {
+ IsBusy = true;
+
+ var id = SelectedItem.Id;
+ await Task.Run(() =>
+ {
+ _fsql.Delete(id).ExecuteAffrows();
+ });
+
+ _log.Info($"删除用户成功,Id={id}");
+ SelectedItem = null;
+ await SearchCoreAsync();
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"删除用户失败: {ex}");
+ MessageBox.Show($"删除失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ ///
+ /// 进入页面自动加载
+ ///
+ /// 导航上下文
+ public override async void OnNavigatedTo(Prism.Regions.NavigationContext navigationContext)
+ {
+ await SearchAsync();
+ }
+
+ ///
+ /// 权限等级常量
+ ///
+ public static class AccessLevels
+ {
+ public const string All = "全部";
+ public const string Admin = "管理员";
+ public const string Operator = "操作员";
+ public const string Guest = "访客";
+ }
+
+ ///
+ /// 弹窗参数 Key
+ ///
+ public static class DialogKeys
+ {
+ public const string Mode = "Mode";
+ public const string UserId = "UserId";
+ }
+
+ ///
+ /// 弹窗模式
+ ///
+ public static class DialogModes
+ {
+ public const string Add = "Add";
+ public const string Edit = "Edit";
+ }
+ }
+}
diff --git a/FATrace.WPLApp/Views/DashboardView.xaml b/FATrace.WPLApp/Views/DashboardView.xaml
index 91a4bd1..28fd451 100644
--- a/FATrace.WPLApp/Views/DashboardView.xaml
+++ b/FATrace.WPLApp/Views/DashboardView.xaml
@@ -1,125 +1,836 @@
-
+
-
-
-
+
+
+
-
+
-
-
-
-
-
+
+
+
+
+
-
+
-
-
+
+
-
+
-
+
-
-
+
+
-
+
-
+
-
-
+
+
-
+
-
+
-
-
+
+
-
+
-
+
-
-
+
+
-
-
+
+
-
+
-
+
-
-
+
+
-
-
+
+
-
+
-
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
+
+
+
diff --git a/FATrace.WPLApp/Views/DialogUserEditView.xaml b/FATrace.WPLApp/Views/DialogUserEditView.xaml
new file mode 100644
index 0000000..b0331b0
--- /dev/null
+++ b/FATrace.WPLApp/Views/DialogUserEditView.xaml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FATrace.WPLApp/Views/DialogUserEditView.xaml.cs b/FATrace.WPLApp/Views/DialogUserEditView.xaml.cs
new file mode 100644
index 0000000..2b586bc
--- /dev/null
+++ b/FATrace.WPLApp/Views/DialogUserEditView.xaml.cs
@@ -0,0 +1,15 @@
+using System.Windows.Controls;
+
+namespace FATrace.WPLApp.Views
+{
+ ///
+ /// DialogUserEditView.xaml 的交互逻辑
+ ///
+ public partial class DialogUserEditView : UserControl
+ {
+ public DialogUserEditView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/Views/FactoryInboundView.xaml b/FATrace.WPLApp/Views/FactoryInboundView.xaml
new file mode 100644
index 0000000..bba6df9
--- /dev/null
+++ b/FATrace.WPLApp/Views/FactoryInboundView.xaml
@@ -0,0 +1,260 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FATrace.WPLApp/Views/FactoryInboundView.xaml.cs b/FATrace.WPLApp/Views/FactoryInboundView.xaml.cs
new file mode 100644
index 0000000..17a6229
--- /dev/null
+++ b/FATrace.WPLApp/Views/FactoryInboundView.xaml.cs
@@ -0,0 +1,15 @@
+using System.Windows.Controls;
+
+namespace FATrace.WPLApp.Views
+{
+ ///
+ /// FactoryInboundView.xaml 的交互逻辑
+ ///
+ public partial class FactoryInboundView : UserControl
+ {
+ public FactoryInboundView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/Views/FactoryInventoryTransactionView.xaml b/FATrace.WPLApp/Views/FactoryInventoryTransactionView.xaml
new file mode 100644
index 0000000..950a35b
--- /dev/null
+++ b/FATrace.WPLApp/Views/FactoryInventoryTransactionView.xaml
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FATrace.WPLApp/Views/FactoryInventoryTransactionView.xaml.cs b/FATrace.WPLApp/Views/FactoryInventoryTransactionView.xaml.cs
new file mode 100644
index 0000000..3128ffa
--- /dev/null
+++ b/FATrace.WPLApp/Views/FactoryInventoryTransactionView.xaml.cs
@@ -0,0 +1,15 @@
+using System.Windows.Controls;
+
+namespace FATrace.WPLApp.Views
+{
+ ///
+ /// FactoryInventoryTransactionView.xaml 的交互逻辑
+ ///
+ public partial class FactoryInventoryTransactionView : UserControl
+ {
+ public FactoryInventoryTransactionView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/Views/FactoryMaterialWithdrawalView.xaml b/FATrace.WPLApp/Views/FactoryMaterialWithdrawalView.xaml
new file mode 100644
index 0000000..563ec12
--- /dev/null
+++ b/FATrace.WPLApp/Views/FactoryMaterialWithdrawalView.xaml
@@ -0,0 +1,172 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FATrace.WPLApp/Views/FactoryMaterialWithdrawalView.xaml.cs b/FATrace.WPLApp/Views/FactoryMaterialWithdrawalView.xaml.cs
new file mode 100644
index 0000000..39d493f
--- /dev/null
+++ b/FATrace.WPLApp/Views/FactoryMaterialWithdrawalView.xaml.cs
@@ -0,0 +1,15 @@
+using System.Windows.Controls;
+
+namespace FATrace.WPLApp.Views
+{
+ ///
+ /// FactoryMaterialWithdrawalView.xaml 的交互逻辑
+ ///
+ public partial class FactoryMaterialWithdrawalView : UserControl
+ {
+ public FactoryMaterialWithdrawalView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/Views/FactoryOutboundView.xaml b/FATrace.WPLApp/Views/FactoryOutboundView.xaml
new file mode 100644
index 0000000..8bf8306
--- /dev/null
+++ b/FATrace.WPLApp/Views/FactoryOutboundView.xaml
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FATrace.WPLApp/Views/FactoryOutboundView.xaml.cs b/FATrace.WPLApp/Views/FactoryOutboundView.xaml.cs
new file mode 100644
index 0000000..22f7f1c
--- /dev/null
+++ b/FATrace.WPLApp/Views/FactoryOutboundView.xaml.cs
@@ -0,0 +1,15 @@
+using System.Windows.Controls;
+
+namespace FATrace.WPLApp.Views
+{
+ ///
+ /// FactoryOutboundView.xaml 的交互逻辑
+ ///
+ public partial class FactoryOutboundView : UserControl
+ {
+ public FactoryOutboundView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/Views/FactoryProductionRecordView.xaml b/FATrace.WPLApp/Views/FactoryProductionRecordView.xaml
new file mode 100644
index 0000000..eeb36e9
--- /dev/null
+++ b/FATrace.WPLApp/Views/FactoryProductionRecordView.xaml
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FATrace.WPLApp/Views/FactoryProductionRecordView.xaml.cs b/FATrace.WPLApp/Views/FactoryProductionRecordView.xaml.cs
new file mode 100644
index 0000000..e5a14f7
--- /dev/null
+++ b/FATrace.WPLApp/Views/FactoryProductionRecordView.xaml.cs
@@ -0,0 +1,15 @@
+using System.Windows.Controls;
+
+namespace FATrace.WPLApp.Views
+{
+ ///
+ /// FactoryProductionRecordView.xaml 的交互逻辑
+ ///
+ public partial class FactoryProductionRecordView : UserControl
+ {
+ public FactoryProductionRecordView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/Views/FileImportLogView.xaml b/FATrace.WPLApp/Views/FileImportLogView.xaml
new file mode 100644
index 0000000..c741713
--- /dev/null
+++ b/FATrace.WPLApp/Views/FileImportLogView.xaml
@@ -0,0 +1,260 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FATrace.WPLApp/Views/FileImportLogView.xaml.cs b/FATrace.WPLApp/Views/FileImportLogView.xaml.cs
new file mode 100644
index 0000000..90b4a44
--- /dev/null
+++ b/FATrace.WPLApp/Views/FileImportLogView.xaml.cs
@@ -0,0 +1,15 @@
+using System.Windows.Controls;
+
+namespace FATrace.WPLApp.Views
+{
+ ///
+ /// FileImportLogView.xaml 的交互逻辑
+ ///
+ public partial class FileImportLogView : UserControl
+ {
+ public FileImportLogView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/Views/HelpManualView.xaml b/FATrace.WPLApp/Views/HelpManualView.xaml
index f5be18d..15e8ea8 100644
--- a/FATrace.WPLApp/Views/HelpManualView.xaml
+++ b/FATrace.WPLApp/Views/HelpManualView.xaml
@@ -4,9 +4,97 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:FATrace.WPLApp.Views"
+ xmlns:pdfViewer="clr-namespace:Syncfusion.Windows.PdfViewer;assembly=Syncfusion.PdfViewer.WPF"
+ xmlns:prism="http://prismlibrary.com/"
mc:Ignorable="d"
+ prism:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="450" d:DesignWidth="800">
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FATrace.WPLApp/Views/MainView.xaml b/FATrace.WPLApp/Views/MainView.xaml
index 1f7cb44..cc5ee6a 100644
--- a/FATrace.WPLApp/Views/MainView.xaml
+++ b/FATrace.WPLApp/Views/MainView.xaml
@@ -23,9 +23,8 @@
-
-
+
@@ -39,6 +38,12 @@
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FATrace.WPLApp/Views/OEMInboundView.xaml.cs b/FATrace.WPLApp/Views/OEMInboundView.xaml.cs
new file mode 100644
index 0000000..323e795
--- /dev/null
+++ b/FATrace.WPLApp/Views/OEMInboundView.xaml.cs
@@ -0,0 +1,15 @@
+using System.Windows.Controls;
+
+namespace FATrace.WPLApp.Views
+{
+ ///
+ /// OEMInboundView.xaml 的交互逻辑
+ ///
+ public partial class OEMInboundView : UserControl
+ {
+ public OEMInboundView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/Views/OEMInventoryTransactionView.xaml b/FATrace.WPLApp/Views/OEMInventoryTransactionView.xaml
new file mode 100644
index 0000000..558ab0d
--- /dev/null
+++ b/FATrace.WPLApp/Views/OEMInventoryTransactionView.xaml
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FATrace.WPLApp/Views/OEMInventoryTransactionView.xaml.cs b/FATrace.WPLApp/Views/OEMInventoryTransactionView.xaml.cs
new file mode 100644
index 0000000..131ba13
--- /dev/null
+++ b/FATrace.WPLApp/Views/OEMInventoryTransactionView.xaml.cs
@@ -0,0 +1,16 @@
+using System.Windows.Controls;
+
+namespace FATrace.WPLApp.Views
+{
+ ///
+ /// OEMInventoryTransactionView.xaml 的交互逻辑
+ /// 仅用于 InitializeComponent,其余逻辑放在 ViewModel 中。
+ ///
+ public partial class OEMInventoryTransactionView : UserControl
+ {
+ public OEMInventoryTransactionView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/Views/OEMOutboundView.xaml b/FATrace.WPLApp/Views/OEMOutboundView.xaml
new file mode 100644
index 0000000..0956266
--- /dev/null
+++ b/FATrace.WPLApp/Views/OEMOutboundView.xaml
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FATrace.WPLApp/Views/OEMOutboundView.xaml.cs b/FATrace.WPLApp/Views/OEMOutboundView.xaml.cs
new file mode 100644
index 0000000..5bbaa62
--- /dev/null
+++ b/FATrace.WPLApp/Views/OEMOutboundView.xaml.cs
@@ -0,0 +1,15 @@
+using System.Windows.Controls;
+
+namespace FATrace.WPLApp.Views
+{
+ ///
+ /// OEMOutboundView.xaml 的交互逻辑
+ ///
+ public partial class OEMOutboundView : UserControl
+ {
+ public OEMOutboundView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/Views/OEMRawUsageInfoView.xaml b/FATrace.WPLApp/Views/OEMRawUsageInfoView.xaml
new file mode 100644
index 0000000..aa39b3e
--- /dev/null
+++ b/FATrace.WPLApp/Views/OEMRawUsageInfoView.xaml
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FATrace.WPLApp/Views/OEMRawUsageInfoView.xaml.cs b/FATrace.WPLApp/Views/OEMRawUsageInfoView.xaml.cs
new file mode 100644
index 0000000..92de33d
--- /dev/null
+++ b/FATrace.WPLApp/Views/OEMRawUsageInfoView.xaml.cs
@@ -0,0 +1,15 @@
+using System.Windows.Controls;
+
+namespace FATrace.WPLApp.Views
+{
+ ///
+ /// OEMRawUsageInfoView.xaml 的交互逻辑
+ ///
+ public partial class OEMRawUsageInfoView : UserControl
+ {
+ public OEMRawUsageInfoView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/FATrace.WPLApp/Views/RawProInputView.xaml b/FATrace.WPLApp/Views/RawProInputView.xaml
index 5ee506c..42afe73 100644
--- a/FATrace.WPLApp/Views/RawProInputView.xaml
+++ b/FATrace.WPLApp/Views/RawProInputView.xaml
@@ -2,170 +2,307 @@
x:Class="FATrace.WPLApp.Views.RawProInputView"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
+ d:DesignHeight="720"
+ d:DesignWidth="1280"
prism:ViewModelLocator.AutoWireViewModel="True"
- mc:Ignorable="d"
- d:DesignWidth="1280" d:DesignHeight="720">
+ mc:Ignorable="d">
-
+
+
-
-
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
-
-
-
+
+
+
+
+
-
+
diff --git a/FATrace.WPLApp/Views/RawProUseView.xaml b/FATrace.WPLApp/Views/RawProUseView.xaml
index f91f9f0..b61ee66 100644
--- a/FATrace.WPLApp/Views/RawProUseView.xaml
+++ b/FATrace.WPLApp/Views/RawProUseView.xaml
@@ -15,14 +15,22 @@
+
+
+
+ Command="{Binding SearchCommand}">
+
+
+
+
+
-
-
+ Command="{Binding ClearCommand}">
+
+
+
+
+
+
+
+ ItemsSource="{Binding Items}"
+ RowHeight="34">
-
-
+
diff --git a/FATrace.WPLApp/Views/ThemedDialogWindow.xaml b/FATrace.WPLApp/Views/ThemedDialogWindow.xaml
new file mode 100644
index 0000000..e181fc1
--- /dev/null
+++ b/FATrace.WPLApp/Views/ThemedDialogWindow.xaml
@@ -0,0 +1,19 @@
+
+
+
diff --git a/FATrace.WPLApp/Views/ThemedDialogWindow.xaml.cs b/FATrace.WPLApp/Views/ThemedDialogWindow.xaml.cs
new file mode 100644
index 0000000..a2ff2e5
--- /dev/null
+++ b/FATrace.WPLApp/Views/ThemedDialogWindow.xaml.cs
@@ -0,0 +1,24 @@
+using Prism.Services.Dialogs;
+using Syncfusion.Windows.Shared;
+
+namespace FATrace.WPLApp.Views
+{
+ ///
+ /// Prism 弹窗承载窗口(使用 Syncfusion ChromelessWindow 统一主题样式)
+ ///
+ public partial class ThemedDialogWindow : ChromelessWindow, IDialogWindow
+ {
+ ///
+ /// 构造
+ ///
+ public ThemedDialogWindow()
+ {
+ InitializeComponent();
+ }
+
+ ///
+ /// 弹窗返回结果(由 Prism DialogService 写入)
+ ///
+ public IDialogResult Result { get; set; }
+ }
+}
diff --git a/FATrace.WPLApp/Views/UserManageView.xaml b/FATrace.WPLApp/Views/UserManageView.xaml
new file mode 100644
index 0000000..dd2f4e4
--- /dev/null
+++ b/FATrace.WPLApp/Views/UserManageView.xaml
@@ -0,0 +1,227 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FATrace.WPLApp/Views/UserManageView.xaml.cs b/FATrace.WPLApp/Views/UserManageView.xaml.cs
new file mode 100644
index 0000000..a13df3f
--- /dev/null
+++ b/FATrace.WPLApp/Views/UserManageView.xaml.cs
@@ -0,0 +1,15 @@
+using System.Windows.Controls;
+
+namespace FATrace.WPLApp.Views
+{
+ ///
+ /// UserManageView.xaml 的交互逻辑
+ ///
+ public partial class UserManageView : UserControl
+ {
+ public UserManageView()
+ {
+ InitializeComponent();
+ }
+ }
+}