RecordPage.xaml.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. using Microsoft.WindowsAPICodePack.Dialogs;
  2. using OfficeOpenXml;
  3. using OfficeOpenXml.Style;
  4. using Panuon.WPF.UI;
  5. using SWRIS.Core;
  6. using SWRIS.Dtos;
  7. using SWRIS.Extensions;
  8. using SWRIS.Models.ViewModel;
  9. using SWRIS.Repository;
  10. using System;
  11. using System.Collections.Generic;
  12. using System.Collections.ObjectModel;
  13. using System.Drawing;
  14. using System.IO;
  15. using System.Linq;
  16. using System.Threading.Tasks;
  17. using System.Windows;
  18. using System.Windows.Controls;
  19. using System.Windows.Input;
  20. namespace SWRIS.Pages
  21. {
  22. /// <summary>
  23. /// RecordPage.xaml 的交互逻辑
  24. /// </summary>
  25. public partial class RecordPage : Page
  26. {
  27. private readonly IRecordRepository _recordRepository;
  28. public RecordViewModel ViewModel { get; set; } = new RecordViewModel();
  29. public RecordPage()
  30. {
  31. _recordRepository = new RecordRepository();
  32. InitializeComponent();
  33. BindTable();
  34. DataContext = this;
  35. }
  36. // 在非 UI 线程上使用 Graphics 测量内容并设置列宽
  37. private void AutoFitColumnsByGraphics(ExcelWorksheet ws, int totalColumns)
  38. {
  39. if (ws?.Dimension == null)
  40. return;
  41. int rows = ws.Dimension.End.Row;
  42. // 使用 Bitmap/Graphics 在当前线程上进行测量(后台线程上也可安全使用)
  43. using (var bmp = new Bitmap(1, 1))
  44. using (var g = Graphics.FromImage(bmp))
  45. {
  46. // 选择一个常见的字体,EPPlus 默认字体可能不同,可根据需要调整
  47. using (var measureFont = new Font("Arial", 10))
  48. {
  49. for (int col = 1; col <= totalColumns; col++)
  50. {
  51. double maxPx = 0;
  52. for (int row = 1; row <= rows; row++)
  53. {
  54. var txt = ws.Cells[row, col].Text ?? string.Empty;
  55. if (string.IsNullOrEmpty(txt))
  56. continue;
  57. var sz = g.MeasureString(txt, measureFont);
  58. if (sz.Width > maxPx) maxPx = sz.Width;
  59. }
  60. // 像素宽度转 Excel 列宽的近似转换
  61. double excelWidth = PixelToExcelColumnWidth((int)Math.Ceiling(maxPx)) + 1.5; // 适度内边距
  62. if (excelWidth < 1) excelWidth = 1;
  63. ws.Column(col).Width = excelWidth;
  64. }
  65. }
  66. }
  67. }
  68. // 简单像素->Excel列宽近似转换
  69. private double PixelToExcelColumnWidth(int px)
  70. {
  71. // 近似:Excel 列宽 = (像素 - 5) / 7 (基于常见字体度量),结果可根据需要调整
  72. if (px <= 0) return 8;
  73. return Math.Round((px - 5) / 7.0, 2);
  74. }
  75. void BindTable()
  76. {
  77. var records = _recordRepository.GetRecords(ViewModel.SearchInput.RopeNumber,
  78. ViewModel.SearchInput.StartTime,
  79. ViewModel.SearchInput.EndTime,
  80. ViewModel.SearchInput.RiskLevel,
  81. ViewModel.SearchInput.Limit,
  82. ViewModel.SearchInput.Offset,
  83. true);
  84. ViewModel.IsAllSelected = false;
  85. ViewModel.Records = new ObservableCollection<RecordDto>(records);
  86. }
  87. private void Search_Click(object sender, RoutedEventArgs e)
  88. {
  89. BindTable();
  90. }
  91. private void Report_Click(object sender, RoutedEventArgs e)
  92. {
  93. if (sender is Button button && button.Tag is RecordDto record)
  94. {
  95. int reportedCount = Report(new List<RecordDto> { record });
  96. if (reportedCount > 0)
  97. {
  98. NoticeBox.Show("报告生成成功", "通知", MessageBoxIcon.Success, true, 2000);
  99. }
  100. else if (reportedCount == 0)
  101. {
  102. NoticeBox.Show("报告生成失败", "通知", MessageBoxIcon.Error, true, 5000);
  103. }
  104. }
  105. }
  106. private int Report(List<RecordDto> records)
  107. {
  108. string reportPath = SelectExportFolder();
  109. if (string.IsNullOrEmpty(reportPath))
  110. {
  111. return -1;
  112. }
  113. return ExportMultipleReports(records, reportPath);
  114. }
  115. private int Export(List<RecordDto> records)
  116. {
  117. // 保留兼容性:同步导出入口仍然可用(内部调用异步路径)
  118. string reportPath = SelectExportFolder(initialDirectory: "钢丝绳检测数据");
  119. if (string.IsNullOrEmpty(reportPath))
  120. {
  121. return -1;
  122. }
  123. return ExportMultipleRecords(records, reportPath);
  124. }
  125. private void Export_Click(object sender, RoutedEventArgs e)
  126. {
  127. if (sender is Button button && button.Tag is RecordDto record)
  128. {
  129. int exportedCount = Export(new List<RecordDto> { record });
  130. if (exportedCount > 0)
  131. {
  132. NoticeBox.Show("记录导出成功", "通知", MessageBoxIcon.Success, true, 2000);
  133. }
  134. else if (exportedCount == 0)
  135. {
  136. NoticeBox.Show("记录导出失败", "通知", MessageBoxIcon.Error, true, 5000);
  137. }
  138. }
  139. }
  140. private void RecordDataGrid_MouseDoubleClick(object sender, MouseButtonEventArgs e)
  141. {
  142. if (e.ChangedButton == MouseButton.Left && sender is DataGrid dataGrid && dataGrid.SelectedItem != null)
  143. {
  144. var selectedItem = dataGrid.SelectedItem as RecordDto; // 获取当前选中行的数据
  145. (Application.Current.MainWindow as MainWindow).NavigatePage(new Uri($"Pages/DamagesPage.xaml?id=" + selectedItem.Id, uriKind: UriKind.Relative));
  146. }
  147. }
  148. private void Delete_Click(object sender, RoutedEventArgs e)
  149. {
  150. if (sender is Button button)
  151. {
  152. int id = (int)button.Tag;
  153. var result = MessageBoxX.Show(Application.Current.MainWindow,
  154. "此操作不可恢复,是否确认删除记录?",
  155. "警告",
  156. MessageBoxButton.YesNo,
  157. MessageBoxIcon.Warning,
  158. DefaultButton.YesOK);
  159. if (result == MessageBoxResult.Yes)
  160. {
  161. var effect = _recordRepository.DeleteRecord(id);
  162. if (effect)
  163. {
  164. var deletedRecord = ViewModel.Records.FirstOrDefault(x => x.Id == id);
  165. if (deletedRecord != null)
  166. {
  167. ViewModel.Records.Remove(deletedRecord);
  168. DatFileHandler.DeleteDatFile(deletedRecord.DataFilePath);
  169. }
  170. NoticeBox.Show("记录删除成功", "通知", MessageBoxIcon.Success, true, 5000);
  171. }
  172. else
  173. {
  174. NoticeBox.Show("记录删除失败", "通知", MessageBoxIcon.Error, true, 5000);
  175. }
  176. }
  177. }
  178. }
  179. private int Delete(int[] recordIds)
  180. {
  181. var result = MessageBoxX.Show(Application.Current.MainWindow,
  182. "此操作不可恢复,是否确认删除记录?",
  183. "警告",
  184. MessageBoxButton.YesNo,
  185. MessageBoxIcon.Warning,
  186. DefaultButton.YesOK);
  187. if (result == MessageBoxResult.Yes)
  188. {
  189. return _recordRepository.DeleteRecords(recordIds);
  190. }
  191. return 0;
  192. }
  193. private void Batch_Report(object sender, RoutedEventArgs e)
  194. {
  195. var records = ViewModel.Records.Where(c => c.IsSelected).ToList();
  196. if (records.Any())
  197. {
  198. int reportCount = Report(records);
  199. if (reportCount > 0)
  200. {
  201. Application.Current.Dispatcher.Invoke(() =>
  202. {
  203. NoticeBox.Show($"检测报告文件{(reportCount > 1 ? $"({reportCount})条" : "")}已生成成功", "通知", MessageBoxIcon.Success, true, 5000);
  204. });
  205. }
  206. }
  207. else
  208. {
  209. MessageBoxX.Show(Application.Current.MainWindow, "请选择要生成报告的检测记录", "通知", MessageBoxButton.OK,
  210. MessageBoxIcon.Info, DefaultButton.YesOK);
  211. }
  212. }
  213. private void Batch_Delete(object sender, RoutedEventArgs e)
  214. {
  215. var records = ViewModel.Records.Where(c => c.IsSelected).ToList();
  216. if (records.Any())
  217. {
  218. int[] recordIds = records.Select(x => (int)x.Id).ToArray();
  219. int deletedCount = Delete(recordIds);
  220. if (deletedCount > 0)
  221. {
  222. foreach (var record in records)
  223. {
  224. ViewModel.Records.Remove(record);
  225. DatFileHandler.DeleteDatFile(record.DataFilePath);
  226. }
  227. Application.Current.Dispatcher.Invoke(() =>
  228. {
  229. NoticeBox.Show($"检测记录{(recordIds.Length > 1 ? $"({recordIds.Length}条)" : "")}已成功删除", "通知", MessageBoxIcon.Success, true, 5000);
  230. });
  231. }
  232. }
  233. else
  234. {
  235. MessageBoxX.Show(Application.Current.MainWindow, "请选择要删除的检测记录", "提醒", MessageBoxButton.OK,
  236. MessageBoxIcon.Info, DefaultButton.YesOK);
  237. }
  238. }
  239. private async void Batch_Export(object sender, RoutedEventArgs e)
  240. {
  241. var records = ViewModel.Records.Where(c => c.IsSelected).ToList();
  242. if (!records.Any())
  243. {
  244. MessageBoxX.Show(Application.Current.MainWindow, "请选择要导出的检测记录", "通知", MessageBoxButton.OK,
  245. MessageBoxIcon.Info, DefaultButton.YesOK);
  246. return;
  247. }
  248. string reportPath = SelectExportFolder(initialDirectory: "钢丝绳检测数据");
  249. if (string.IsNullOrEmpty(reportPath))
  250. return;
  251. // 可在此禁用导出按钮或显示进度指示器
  252. int exportCount = 0;
  253. try
  254. {
  255. exportCount = await Task.Run(() => ExportMultipleRecords(records, reportPath));
  256. if (exportCount > 0)
  257. {
  258. Application.Current.Dispatcher.Invoke(() =>
  259. {
  260. NoticeBox.Show($"检测数据文件{(exportCount > 1 ? $"({exportCount})条" : "")}已成功导出", "通知", MessageBoxIcon.Success, true, 5000);
  261. });
  262. }
  263. }
  264. catch (Exception ex)
  265. {
  266. Application.Current.Dispatcher.Invoke(() =>
  267. {
  268. MessageBoxX.Show(Application.Current.MainWindow, $"导出失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxIcon.Error, DefaultButton.YesOK);
  269. });
  270. }
  271. finally
  272. {
  273. // 恢复按钮/进度
  274. }
  275. }
  276. private string SelectExportFolder(string title = "选择文件夹", string initialDirectory = "钢丝绳检测报告")
  277. {
  278. using (var dialog = new CommonOpenFileDialog())
  279. {
  280. dialog.Title = title;
  281. dialog.IsFolderPicker = true;
  282. dialog.EnsurePathExists = true;
  283. dialog.InitialDirectory = GetDefaultExportDirectory(initialDirectory);
  284. if (dialog.ShowDialog() == CommonFileDialogResult.Ok)
  285. {
  286. return dialog.FileName;
  287. }
  288. }
  289. return null;
  290. }
  291. private int ExportMultipleReports(List<RecordDto> records, string folderPath)
  292. {
  293. int savedCount = 0;
  294. string craneName = App.Config.Crane;
  295. // 按钢丝绳编号分组处理
  296. var groupedRecords = records.OrderBy(r => r.RopeNumber).GroupBy(r => r.RopeNumber);
  297. foreach (var group in groupedRecords)
  298. {
  299. var equipment = App.Config.Equipments
  300. .FirstOrDefault(c => c.RopeNumber == group.Key);
  301. foreach (var record in group)
  302. {
  303. string fileName = GenerateReportFileName(record);
  304. string filePath = Path.Combine(folderPath, fileName);
  305. var export = new ReportPdfExporter(craneName, record, equipment);
  306. if (export.SavePdf(filePath))
  307. {
  308. savedCount++;
  309. _recordRepository.AddRecordExportCount(record.Id.Value);
  310. }
  311. }
  312. }
  313. return savedCount;
  314. }
  315. private int ExportMultipleRecords(List<RecordDto> records, string folderPath)
  316. {
  317. int exportedCount = 0;
  318. foreach (var record in records)
  319. {
  320. if (record == null || string.IsNullOrEmpty(record.DataFilePath))
  321. continue;
  322. var damageData = DatFileHandler.ReadDatFile(record.DataFilePath);
  323. if ((damageData?.Length ?? 0) <= 0)
  324. continue;
  325. var parsedData = ParseSensorData(damageData, record.SensorCount, record.StartPoint, record.EndPoint);
  326. if (parsedData.Positions == null || parsedData.Damages == null)
  327. continue;
  328. string fileName = GenerateExportFileName(record);
  329. string filePath = Path.Combine(folderPath, fileName);
  330. try
  331. {
  332. using (var package = new ExcelPackage())
  333. {
  334. var ws = package.Workbook.Worksheets.Add("检测数据");
  335. // 设置表头
  336. ws.Cells[1, 1].Value = "位置(m)";
  337. for (int i = 0; i < record.SensorCount; i++)
  338. {
  339. ws.Cells[1, i + 2].Value = $"传感器{i + 1}";
  340. }
  341. ws.Cells[1, record.SensorCount + 2].Value = "均值";
  342. // 设置表头样式
  343. using (var headerRange = ws.Cells[1, 1, 1, record.SensorCount + 2])
  344. {
  345. headerRange.Style.Font.Bold = true;
  346. headerRange.Style.Fill.PatternType = ExcelFillStyle.Solid;
  347. headerRange.Style.Fill.BackgroundColor.SetColor(Color.LightGray);
  348. }
  349. // 填充数据行
  350. int sampleCount = parsedData.Positions.Length;
  351. for (int row = 0; row < sampleCount; row++)
  352. {
  353. int excelRow = row + 2;
  354. ws.Cells[excelRow, 1].Value = Math.Round(parsedData.Positions[row], 3);
  355. double sum = 0;
  356. for (int col = 0; col < record.SensorCount; col++)
  357. {
  358. ushort value = parsedData.Damages[col, row];
  359. ws.Cells[excelRow, col + 2].Value = value;
  360. sum += value;
  361. }
  362. ws.Cells[excelRow, record.SensorCount + 2].Value = Math.Round(sum / record.SensorCount, 1);
  363. }
  364. // 自动调整列宽:在后台线程上使用 Graphics.MeasureString 测量并手动设置列宽,避免调用可能依赖 UI 的 AutoFit
  365. AutoFitColumnsByGraphics(ws, record.SensorCount + 2);
  366. package.SaveAs(new FileInfo(filePath));
  367. }
  368. exportedCount++;
  369. _recordRepository.AddRecordExportCount(record.Id.Value);
  370. }
  371. catch (Exception ex)
  372. {
  373. LogHelper.Error($"导出记录 {record.Id} 到Excel失败: {ex.Message}", ex);
  374. }
  375. }
  376. return exportedCount;
  377. }
  378. public (double[] Positions, ushort[,] Damages) ParseSensorData(byte[] data, int sensorCount, double startPosition, double endPosition)
  379. {
  380. if (data.Length % sensorCount != 0)
  381. {
  382. LogHelper.Error($"数据长度不匹配。数据长度 {data.Length} 字节不能被传感器数量 {sensorCount} 整除。");
  383. return (null, null);
  384. }
  385. int sampleCount = data.Length / sensorCount;
  386. double samplingStep = (endPosition - startPosition) / (sampleCount - 1);
  387. var positions = new double[sampleCount];
  388. var damages = new ushort[sensorCount, sampleCount];
  389. for (int sampleIndex = 0; sampleIndex < sampleCount; sampleIndex++)
  390. {
  391. positions[sampleIndex] = startPosition;
  392. for (int sensorIndex = 0; sensorIndex < sensorCount; sensorIndex++)
  393. {
  394. int dataIndex = (sampleIndex * sensorCount) + sensorIndex;
  395. damages[sensorIndex, sampleIndex] = data[dataIndex];
  396. }
  397. startPosition += samplingStep;
  398. }
  399. return (positions, damages);
  400. }
  401. private string GenerateExportFileName(RecordDto record)
  402. {
  403. string ropeName = CleanFileName(record.RopeName);
  404. string time = record.EndTime.ToString("yyyyMMdd_HHmmss");
  405. return $"{ropeName}_{time}_检测数据.xlsx";
  406. }
  407. private string GetDefaultExportDirectory(string folderName = "钢丝绳检测报告")
  408. {
  409. string docsPath = Path.Combine(
  410. Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
  411. folderName,
  412. DateTime.Now.ToString("yyyy-MM"));
  413. // 确保目录存在
  414. try
  415. {
  416. Directory.CreateDirectory(docsPath);
  417. return docsPath;
  418. }
  419. catch
  420. {
  421. // 失败时返回桌面
  422. return Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
  423. }
  424. }
  425. private string GenerateReportFileName(RecordDto record)
  426. {
  427. // 清理文件名中的非法字符
  428. string ropeName = CleanFileName(record.RopeName);
  429. string time = record.EndTime.ToString("yyyyMMdd_HHmmss");
  430. return $"{ropeName}_{time}_检测报告.pdf";
  431. }
  432. private string CleanFileName(string fileName)
  433. {
  434. if (string.IsNullOrEmpty(fileName))
  435. return "未知";
  436. // 移除非法字符
  437. char[] invalidChars = Path.GetInvalidFileNameChars();
  438. foreach (char c in invalidChars)
  439. {
  440. fileName = fileName.Replace(c, '_');
  441. }
  442. // 限制长度
  443. if (fileName.Length > 50)
  444. {
  445. fileName = fileName.Substring(0, 50);
  446. }
  447. return fileName.Trim();
  448. }
  449. }
  450. }