diff --git a/FATrace.WPLApp/App.xaml.cs b/FATrace.WPLApp/App.xaml.cs index 691b665..1160e20 100644 --- a/FATrace.WPLApp/App.xaml.cs +++ b/FATrace.WPLApp/App.xaml.cs @@ -115,6 +115,7 @@ namespace FATrace.WPLApp var fsql = Container.Resolve(); var sysRun = Container.Resolve(); var DataServices = Container.Resolve(); + var CsvServices = Container.Resolve(); LogService.Info("Background services initialized"); } catch (Exception ex) @@ -197,8 +198,9 @@ namespace FATrace.WPLApp //containerRegistry.RegisterSingleton(); //containerRegistry.RegisterSingleton(); //containerRegistry.RegisterSingleton(); - + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); containerRegistry.RegisterSingleton(); diff --git a/FATrace.WPLApp/ModelDto/RawProUserCsvDto.cs b/FATrace.WPLApp/ModelDto/RawProUserCsvDto.cs new file mode 100644 index 0000000..627ab4f --- /dev/null +++ b/FATrace.WPLApp/ModelDto/RawProUserCsvDto.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FATrace.WPLApp.ModelDto +{ + /// + /// 原料生产使用信息 CSV DTO + /// + public class RawProUserCsvDto + { + /// + /// 原料编号 + /// + public string? RawCode { get; set; } + + /// + /// 原料名称 + /// + public string? RawName { get; set; } + + /// + /// 内袋二维码 + /// + public string? InBagCode { get; set; } + + /// + /// 外箱二维码 + /// + public string? BoxCode { get; set; } + + /// + /// 批号 + /// + public string? Batch { get; set; } + + /// + /// 保质期 年 + /// + public double ShelfLife { get; set; } + + /// + /// 称重重量 g 克 + /// + public double Weight { get; set; } + + /// + /// 配料日期 当天日期 + /// 年,月,日 + /// + public string? DeliveryDate { get; set; } + + /// + /// 剩余重量 g 克 + /// 剩余重量 = 当前产品的入库总重量-当前称重的称量重量 + /// + public double RemainWeight { get; set; } + + /// + /// 入库总重量 + /// 当前的入库的总重量 + /// + public double StockWeight { get; set; } + + /// + /// 称重时间 + /// + public DateTime WeightTime { get; set; } + + /// + /// 操作者 + /// + public string? OpUser { get; set; } + + /// + /// 确认者 + /// + public string? CheckUser { get; set; } + + /// + /// 出库时间 + /// 外箱扫码出库时间 + /// + public DateTime OutTime { get; set; } + + } +} diff --git a/FATrace.WPLApp/Services/CsvServices.cs b/FATrace.WPLApp/Services/CsvServices.cs index f8ae3f2..a6bbdf7 100644 --- a/FATrace.WPLApp/Services/CsvServices.cs +++ b/FATrace.WPLApp/Services/CsvServices.cs @@ -1,20 +1,135 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using System.Globalization; +using System.IO; +using CsvHelper; +using CsvHelper.Configuration; +using FATrace.WPLApp.ModelDto; +using Prism.Mvvm; + namespace FATrace.WPLApp.Services { /// /// CSV服务 ///导出CSV文件 /// - public class CsvServices + public class CsvServices:BindableBase { - public CsvServices() + public ILogService LogService { get; } + public IFreeSql FreeSql { get; } + + public CsvServices(ILogService logService, IFreeSql freeSql) { - + LogService = logService; + FreeSql = freeSql; + } + + /// + /// 导出单条记录为 CSV 文件,文件名即记录的 InBagCode(自动追加 .csv 扩展名)。 + /// + /// 要导出的数据项,类型为 RawProUserCsvDto + /// 导出目录,不存在将自动创建 + /// 若目标文件已存在,是否允许覆盖 + /// 导出后的完整文件路径 + /// item 或 outputDirectory 为 null + /// InBagCode 为空或仅空白 + /// 文件已存在且不允许覆盖 + public string ExportSingle(RawProUserCsvDto item, string outputDirectory, bool overwrite = false) + { + if (item is null) throw new ArgumentNullException(nameof(item)); + if (outputDirectory is null) throw new ArgumentNullException(nameof(outputDirectory)); + + var inBagCode = (item.InBagCode ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(inBagCode)) + throw new ArgumentException("InBagCode 不能为空", nameof(item.InBagCode)); + + // 目录确保存在 + Directory.CreateDirectory(outputDirectory); + + var safeName = SanitizeFileName(inBagCode); + var filePath = Path.Combine(outputDirectory, safeName + ".csv"); + + if (File.Exists(filePath) && !overwrite) + { + throw new IOException($"文件已存在且不允许覆盖: {filePath}"); + } + + // 使用 UTF8 BOM,便于 Excel 正确识别中文 + using var writer = new StreamWriter(filePath, false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)); + using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture); + + // 固定列顺序映射,并设置日期格式 + csv.Context.RegisterClassMap(); + + csv.WriteHeader(); + csv.NextRecord(); + csv.WriteRecord(item); + csv.NextRecord(); + + return filePath; + } + + /// + /// 批量导出:把每一条记录分别导出为以各自 InBagCode 命名的 CSV 文件。 + /// + /// 数据集合 + /// 导出目录,不存在将自动创建 + /// 若目标文件已存在,是否允许覆盖 + /// 成功导出的文件完整路径集合 + public IEnumerable ExportMany(IEnumerable items, string outputDirectory, bool overwrite = false) + { + if (items is null) throw new ArgumentNullException(nameof(items)); + + var results = new List(); + foreach (var item in items) + { + // 逐条导出,沿用相同规则 + var path = ExportSingle(item, outputDirectory, overwrite); + results.Add(path); + } + return results; + } + + /// + /// 清理文件名中的非法字符。 + /// + private static string SanitizeFileName(string name) + { + var invalid = Path.GetInvalidFileNameChars(); + var sb = new StringBuilder(name.Length); + foreach (var ch in name) + { + sb.Append(invalid.Contains(ch) ? '_' : ch); + } + return sb.ToString(); + } + + /// + /// RawProUserCsvDto 的 CSV 列映射,固定输出顺序并设置日期格式。 + /// + private sealed class RawProUserCsvDtoMap : ClassMap + { + public RawProUserCsvDtoMap() + { + Map(x => x.RawCode).Index(0).Name(nameof(RawProUserCsvDto.RawCode)); + Map(x => x.RawName).Index(1).Name(nameof(RawProUserCsvDto.RawName)); + Map(x => x.InBagCode).Index(2).Name(nameof(RawProUserCsvDto.InBagCode)); + Map(x => x.BoxCode).Index(3).Name(nameof(RawProUserCsvDto.BoxCode)); + Map(x => x.Batch).Index(4).Name(nameof(RawProUserCsvDto.Batch)); + Map(x => x.ShelfLife).Index(5).Name(nameof(RawProUserCsvDto.ShelfLife)); + Map(x => x.Weight).Index(6).Name(nameof(RawProUserCsvDto.Weight)); + Map(x => x.DeliveryDate).Index(7).Name(nameof(RawProUserCsvDto.DeliveryDate)); + Map(x => x.RemainWeight).Index(8).Name(nameof(RawProUserCsvDto.RemainWeight)); + Map(x => x.StockWeight).Index(9).Name(nameof(RawProUserCsvDto.StockWeight)); + Map(x => x.WeightTime).Index(10).Name(nameof(RawProUserCsvDto.WeightTime)).TypeConverterOption.Format("yyyy-MM-dd HH:mm:ss"); + Map(x => x.OpUser).Index(11).Name(nameof(RawProUserCsvDto.OpUser)); + Map(x => x.CheckUser).Index(12).Name(nameof(RawProUserCsvDto.CheckUser)); + Map(x => x.OutTime).Index(13).Name(nameof(RawProUserCsvDto.OutTime)).TypeConverterOption.Format("yyyy-MM-dd HH:mm:ss"); + } } } } diff --git a/FATrace.WPLApp/Views/RawProUseView.xaml b/FATrace.WPLApp/Views/RawProUseView.xaml index ad558ca..f91f9f0 100644 --- a/FATrace.WPLApp/Views/RawProUseView.xaml +++ b/FATrace.WPLApp/Views/RawProUseView.xaml @@ -139,6 +139,10 @@ Width="100" Command="{Binding ExportCommand}" Content="导出Excel" /> +