using Microsoft.WindowsAPICodePack.Dialogs; using OfficeOpenXml; using OfficeOpenXml.Style; using Panuon.WPF.UI; using SWRIS.Core; using SWRIS.Dtos; using SWRIS.Extensions; using SWRIS.Models.ViewModel; using SWRIS.Repository; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Drawing; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace SWRIS.Pages { /// /// RecordPage.xaml 的交互逻辑 /// public partial class RecordPage : Page { private readonly IRecordRepository _recordRepository; public RecordViewModel ViewModel { get; set; } = new RecordViewModel(); public RecordPage() { _recordRepository = new RecordRepository(); InitializeComponent(); BindTable(); DataContext = this; } // 在非 UI 线程上使用 Graphics 测量内容并设置列宽 private void AutoFitColumnsByGraphics(ExcelWorksheet ws, int totalColumns) { if (ws?.Dimension == null) return; int rows = ws.Dimension.End.Row; // 使用 Bitmap/Graphics 在当前线程上进行测量(后台线程上也可安全使用) using (var bmp = new Bitmap(1, 1)) using (var g = Graphics.FromImage(bmp)) { // 选择一个常见的字体,EPPlus 默认字体可能不同,可根据需要调整 using (var measureFont = new Font("Arial", 10)) { for (int col = 1; col <= totalColumns; col++) { double maxPx = 0; for (int row = 1; row <= rows; row++) { var txt = ws.Cells[row, col].Text ?? string.Empty; if (string.IsNullOrEmpty(txt)) continue; var sz = g.MeasureString(txt, measureFont); if (sz.Width > maxPx) maxPx = sz.Width; } // 像素宽度转 Excel 列宽的近似转换 double excelWidth = PixelToExcelColumnWidth((int)Math.Ceiling(maxPx)) + 1.5; // 适度内边距 if (excelWidth < 1) excelWidth = 1; ws.Column(col).Width = excelWidth; } } } } // 简单像素->Excel列宽近似转换 private double PixelToExcelColumnWidth(int px) { // 近似:Excel 列宽 = (像素 - 5) / 7 (基于常见字体度量),结果可根据需要调整 if (px <= 0) return 8; return Math.Round((px - 5) / 7.0, 2); } void BindTable() { var records = _recordRepository.GetRecords(ViewModel.SearchInput.RopeNumber, ViewModel.SearchInput.StartTime, ViewModel.SearchInput.EndTime, ViewModel.SearchInput.RiskLevel, ViewModel.SearchInput.Limit, ViewModel.SearchInput.Offset, true); ViewModel.IsAllSelected = false; ViewModel.Records = new ObservableCollection(records); } private void Search_Click(object sender, RoutedEventArgs e) { BindTable(); } private void Report_Click(object sender, RoutedEventArgs e) { if (sender is Button button && button.Tag is RecordDto record) { int reportedCount = Report(new List { record }); if (reportedCount > 0) { NoticeBox.Show("报告生成成功", "通知", MessageBoxIcon.Success, true, 2000); } else if (reportedCount == 0) { NoticeBox.Show("报告生成失败", "通知", MessageBoxIcon.Error, true, 5000); } } } private int Report(List records) { string reportPath = SelectExportFolder(); if (string.IsNullOrEmpty(reportPath)) { return -1; } return ExportMultipleReports(records, reportPath); } private int Export(List records) { // 保留兼容性:同步导出入口仍然可用(内部调用异步路径) string reportPath = SelectExportFolder(initialDirectory: "钢丝绳检测数据"); if (string.IsNullOrEmpty(reportPath)) { return -1; } return ExportMultipleRecords(records, reportPath); } private void Export_Click(object sender, RoutedEventArgs e) { if (sender is Button button && button.Tag is RecordDto record) { int exportedCount = Export(new List { record }); if (exportedCount > 0) { NoticeBox.Show("记录导出成功", "通知", MessageBoxIcon.Success, true, 2000); } else if (exportedCount == 0) { NoticeBox.Show("记录导出失败", "通知", MessageBoxIcon.Error, true, 5000); } } } private void RecordDataGrid_MouseDoubleClick(object sender, MouseButtonEventArgs e) { if (e.ChangedButton == MouseButton.Left && sender is DataGrid dataGrid && dataGrid.SelectedItem != null) { var selectedItem = dataGrid.SelectedItem as RecordDto; // 获取当前选中行的数据 (Application.Current.MainWindow as MainWindow).NavigatePage(new Uri($"Pages/DamagesPage.xaml?id=" + selectedItem.Id, uriKind: UriKind.Relative)); } } private void Delete_Click(object sender, RoutedEventArgs e) { if (sender is Button button) { int id = (int)button.Tag; var result = MessageBoxX.Show(Application.Current.MainWindow, "此操作不可恢复,是否确认删除记录?", "警告", MessageBoxButton.YesNo, MessageBoxIcon.Warning, DefaultButton.YesOK); if (result == MessageBoxResult.Yes) { var effect = _recordRepository.DeleteRecord(id); if (effect) { var deletedRecord = ViewModel.Records.FirstOrDefault(x => x.Id == id); if (deletedRecord != null) { ViewModel.Records.Remove(deletedRecord); DatFileHandler.DeleteDatFile(deletedRecord.DataFilePath); } NoticeBox.Show("记录删除成功", "通知", MessageBoxIcon.Success, true, 5000); } else { NoticeBox.Show("记录删除失败", "通知", MessageBoxIcon.Error, true, 5000); } } } } private int Delete(int[] recordIds) { var result = MessageBoxX.Show(Application.Current.MainWindow, "此操作不可恢复,是否确认删除记录?", "警告", MessageBoxButton.YesNo, MessageBoxIcon.Warning, DefaultButton.YesOK); if (result == MessageBoxResult.Yes) { return _recordRepository.DeleteRecords(recordIds); } return 0; } private void Batch_Report(object sender, RoutedEventArgs e) { var records = ViewModel.Records.Where(c => c.IsSelected).ToList(); if (records.Any()) { int reportCount = Report(records); if (reportCount > 0) { Application.Current.Dispatcher.Invoke(() => { NoticeBox.Show($"检测报告文件{(reportCount > 1 ? $"({reportCount})条" : "")}已生成成功", "通知", MessageBoxIcon.Success, true, 5000); }); } } else { MessageBoxX.Show(Application.Current.MainWindow, "请选择要生成报告的检测记录", "通知", MessageBoxButton.OK, MessageBoxIcon.Info, DefaultButton.YesOK); } } private void Batch_Delete(object sender, RoutedEventArgs e) { var records = ViewModel.Records.Where(c => c.IsSelected).ToList(); if (records.Any()) { int[] recordIds = records.Select(x => (int)x.Id).ToArray(); int deletedCount = Delete(recordIds); if (deletedCount > 0) { foreach (var record in records) { ViewModel.Records.Remove(record); DatFileHandler.DeleteDatFile(record.DataFilePath); } Application.Current.Dispatcher.Invoke(() => { NoticeBox.Show($"检测记录{(recordIds.Length > 1 ? $"({recordIds.Length}条)" : "")}已成功删除", "通知", MessageBoxIcon.Success, true, 5000); }); } } else { MessageBoxX.Show(Application.Current.MainWindow, "请选择要删除的检测记录", "提醒", MessageBoxButton.OK, MessageBoxIcon.Info, DefaultButton.YesOK); } } private async void Batch_Export(object sender, RoutedEventArgs e) { var records = ViewModel.Records.Where(c => c.IsSelected).ToList(); if (!records.Any()) { MessageBoxX.Show(Application.Current.MainWindow, "请选择要导出的检测记录", "通知", MessageBoxButton.OK, MessageBoxIcon.Info, DefaultButton.YesOK); return; } string reportPath = SelectExportFolder(initialDirectory: "钢丝绳检测数据"); if (string.IsNullOrEmpty(reportPath)) return; // 可在此禁用导出按钮或显示进度指示器 int exportCount = 0; try { exportCount = await Task.Run(() => ExportMultipleRecords(records, reportPath)); if (exportCount > 0) { Application.Current.Dispatcher.Invoke(() => { NoticeBox.Show($"检测数据文件{(exportCount > 1 ? $"({exportCount})条" : "")}已成功导出", "通知", MessageBoxIcon.Success, true, 5000); }); } } catch (Exception ex) { Application.Current.Dispatcher.Invoke(() => { MessageBoxX.Show(Application.Current.MainWindow, $"导出失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxIcon.Error, DefaultButton.YesOK); }); } finally { // 恢复按钮/进度 } } private string SelectExportFolder(string title = "选择文件夹", string initialDirectory = "钢丝绳检测报告") { using (var dialog = new CommonOpenFileDialog()) { dialog.Title = title; dialog.IsFolderPicker = true; dialog.EnsurePathExists = true; dialog.InitialDirectory = GetDefaultExportDirectory(initialDirectory); if (dialog.ShowDialog() == CommonFileDialogResult.Ok) { return dialog.FileName; } } return null; } private int ExportMultipleReports(List records, string folderPath) { int savedCount = 0; string craneName = App.Config.Crane; // 按钢丝绳编号分组处理 var groupedRecords = records.OrderBy(r => r.RopeNumber).GroupBy(r => r.RopeNumber); foreach (var group in groupedRecords) { var equipment = App.Config.Equipments .FirstOrDefault(c => c.RopeNumber == group.Key); foreach (var record in group) { string fileName = GenerateReportFileName(record); string filePath = Path.Combine(folderPath, fileName); var export = new ReportPdfExporter(craneName, record, equipment); if (export.SavePdf(filePath)) { savedCount++; _recordRepository.AddRecordExportCount(record.Id.Value); } } } return savedCount; } private int ExportMultipleRecords(List records, string folderPath) { int exportedCount = 0; foreach (var record in records) { if (record == null || string.IsNullOrEmpty(record.DataFilePath)) continue; var damageData = DatFileHandler.ReadDatFile(record.DataFilePath); if ((damageData?.Length ?? 0) <= 0) continue; var parsedData = ParseSensorData(damageData, record.SensorCount, record.StartPoint, record.EndPoint); if (parsedData.Positions == null || parsedData.Damages == null) continue; string fileName = GenerateExportFileName(record); string filePath = Path.Combine(folderPath, fileName); try { using (var package = new ExcelPackage()) { var ws = package.Workbook.Worksheets.Add("检测数据"); // 设置表头 ws.Cells[1, 1].Value = "位置(m)"; for (int i = 0; i < record.SensorCount; i++) { ws.Cells[1, i + 2].Value = $"传感器{i + 1}"; } ws.Cells[1, record.SensorCount + 2].Value = "均值"; // 设置表头样式 using (var headerRange = ws.Cells[1, 1, 1, record.SensorCount + 2]) { headerRange.Style.Font.Bold = true; headerRange.Style.Fill.PatternType = ExcelFillStyle.Solid; headerRange.Style.Fill.BackgroundColor.SetColor(Color.LightGray); } // 填充数据行 int sampleCount = parsedData.Positions.Length; for (int row = 0; row < sampleCount; row++) { int excelRow = row + 2; ws.Cells[excelRow, 1].Value = Math.Round(parsedData.Positions[row], 3); double sum = 0; for (int col = 0; col < record.SensorCount; col++) { ushort value = parsedData.Damages[col, row]; ws.Cells[excelRow, col + 2].Value = value; sum += value; } ws.Cells[excelRow, record.SensorCount + 2].Value = Math.Round(sum / record.SensorCount, 1); } // 自动调整列宽:在后台线程上使用 Graphics.MeasureString 测量并手动设置列宽,避免调用可能依赖 UI 的 AutoFit AutoFitColumnsByGraphics(ws, record.SensorCount + 2); package.SaveAs(new FileInfo(filePath)); } exportedCount++; _recordRepository.AddRecordExportCount(record.Id.Value); } catch (Exception ex) { LogHelper.Error($"导出记录 {record.Id} 到Excel失败: {ex.Message}", ex); } } return exportedCount; } public (double[] Positions, ushort[,] Damages) ParseSensorData(byte[] data, int sensorCount, double startPosition, double endPosition) { if (data.Length % sensorCount != 0) { LogHelper.Error($"数据长度不匹配。数据长度 {data.Length} 字节不能被传感器数量 {sensorCount} 整除。"); return (null, null); } int sampleCount = data.Length / sensorCount; double samplingStep = (endPosition - startPosition) / (sampleCount - 1); var positions = new double[sampleCount]; var damages = new ushort[sensorCount, sampleCount]; for (int sampleIndex = 0; sampleIndex < sampleCount; sampleIndex++) { positions[sampleIndex] = startPosition; for (int sensorIndex = 0; sensorIndex < sensorCount; sensorIndex++) { int dataIndex = (sampleIndex * sensorCount) + sensorIndex; damages[sensorIndex, sampleIndex] = data[dataIndex]; } startPosition += samplingStep; } return (positions, damages); } private string GenerateExportFileName(RecordDto record) { string ropeName = CleanFileName(record.RopeName); string time = record.EndTime.ToString("yyyyMMdd_HHmmss"); return $"{ropeName}_{time}_检测数据.xlsx"; } private string GetDefaultExportDirectory(string folderName = "钢丝绳检测报告") { string docsPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), folderName, DateTime.Now.ToString("yyyy-MM")); // 确保目录存在 try { Directory.CreateDirectory(docsPath); return docsPath; } catch { // 失败时返回桌面 return Environment.GetFolderPath(Environment.SpecialFolder.Desktop); } } private string GenerateReportFileName(RecordDto record) { // 清理文件名中的非法字符 string ropeName = CleanFileName(record.RopeName); string time = record.EndTime.ToString("yyyyMMdd_HHmmss"); return $"{ropeName}_{time}_检测报告.pdf"; } private string CleanFileName(string fileName) { if (string.IsNullOrEmpty(fileName)) return "未知"; // 移除非法字符 char[] invalidChars = Path.GetInvalidFileNameChars(); foreach (char c in invalidChars) { fileName = fileName.Replace(c, '_'); } // 限制长度 if (fileName.Length > 50) { fileName = fileName.Substring(0, 50); } return fileName.Trim(); } } }