@@ -17,6 +17,8 @@ using System.Windows.Media.Imaging;
using System.Windows.Navigation ;
using System.Windows.Shapes ;
using System.Windows.Threading ;
using System.Linq ;
using System.Threading ;
using 头 罩 视 野 .Services.Data ;
using static 头 罩 视 野 . TestDataStore ;
@@ -78,40 +80,302 @@ namespace 头罩视野.Views
}
////#endregion
////#region 3. 保存为Excel
//private void btnSave_Click(object sender, RoutedEventArgs e)
//{
// try
// {
// // 1. 构建表格内容
// StringBuilder sb = new StringBuilder();
// sb.AppendLine("编号,日期,时间,左目视野面积,右目视野面积,双目视野面积,空白视野面积,下方视野,视野保存率");
// foreach (var item in TestDataStore.Records)
// {
// sb.AppendLine($"{item.Id},{item.Date},{item.Time},{item.LeftEyeArea},{item.RightEyeArea},{item.BinocularArea},{item.LowerVision},{item.VisionRetentionRate}");
// }
// // 2. 弹出保存框,让用户自己选位置
// Microsoft.Win32.SaveFileDialog saveFileDialog = new Microsoft.Win32.SaveFileDialog();
// saveFileDialog.Filter = "CSV 文件 (*.csv)|*.csv|所有文件 (*.*)|*.*";
// saveFileDialog.FileName = $"测试记录_{DateTime.Now:yyyyMMddHHmmss}";
// if (saveFileDialog.ShowDialog() == true)
// {
// // 3. 保存文件
// File.WriteAllText(saveFileDialog.FileName, sb.ToString(), Encoding.UTF8);
// MessageBox.Show("保存成功!\n文件路径: " + saveFileDialog.FileName, "成功");
// }
// }
// catch (Exception ex)
// {
// MessageBox.Show("保存失败:" + ex.Message);
// }
//}
//#endregion
//#region 3. 保存为Excel
private List < TestDataStore . TestRecord > GenerateMockRecords ( )
{
var mockList = new List < TestDataStore . TestRecord > ( ) ;
Random rand = new Random ( ) ;
for ( int i = 1 ; i < = 5 ; i + + )
{
double left = rand . Next ( 6000 , 8500 ) ;
double right = rand . Next ( 6000 , 8500 ) ;
double binocular = ( left + right ) / 2 * 0.85 ;
double lower = rand . Next ( 30 , 50 ) ;
double totalArea = left + right - binocular ; // 总视野面积
double preservation = totalArea / 12000 * 100 ; // 视野保存率(%)
double totalPreserve = preservation * 0.95 ; // 总视野保存率(示例)
mockList . Add ( new TestDataStore . TestRecord
{
Id = i ,
Date = DateTime . Now . AddDays ( - i ) . ToString ( "yyyy-MM-dd" ) ,
Time = DateTime . Now . AddHours ( - i ) . ToString ( "HH:mm:ss" ) ,
LeftEyeArea = left ,
RightEyeArea = right ,
BinocularArea = binocular ,
LowerVision = lower ,
VisionRetentionRate = preservation ,
totalVisionArea = totalArea , // ✅ 必须赋值
GetVisionRetentionRate = totalPreserve // ✅ 必须赋值
} ) ;
}
return mockList ;
}
private void btnSave_Click ( object sender , RoutedEventArgs e )
{
//// 判断是否有真实数据
//bool hasRealData = TestDataStore.Records != null && TestDataStore.Records.Count > 0;
//List<TestDataStore.TestRecord> exportRecords;
//if (!hasRealData)
//{
// exportRecords = GenerateMockRecords();
// TestDataStore.Records.AddRange(exportRecords);
//}
//else
//{
// exportRecords = TestDataStore.Records.ToList();
//}
try
{
// 1. 构建表格内容
StringBuilder sb = new StringBuilder ( ) ;
sb . AppendLine ( "编号,日期,时间,左目视野面积,右目视野面积,双目视野面积,空白视野面积,下方视野,视野保存率" ) ;
ExcelPackage . LicenseContext = OfficeOpenXml . LicenseContext . NonCommercial ;
foreach ( var item in TestDataStore . Records )
var saveDialog = new Microsoft . Win32 . SaveFileDialog
{
sb . AppendLine ( $"{item.Id},{item.Date},{item.Time},{item.LeftEyeArea},{item.RightEyeArea},{item.BinocularArea},{item.LowerVision},{item.VisionRetentionRate}" ) ;
}
Filter = "Excel 文件 (*.xlsx)|*.xlsx" ,
FileName = $"视野测试报告_{DateTime.Now:yyyyMMddHHmmss}.xlsx"
} ;
if ( saveDialog . ShowDialog ( ) ! = true ) return ;
// 2. 弹出保存框,让用户自己选位置
Microsoft . Win32 . SaveFileDialog saveFileDialog = new Microsoft . Win32 . SaveFileDialog ( ) ;
saveFileDialog . Filter = "CSV 文件 (*.csv)|*.csv|所有文件 (*.*)|*.*" ;
saveFileDialog . FileName = $"测试记录_{DateTime.Now:yyyyMMddHHmmss}" ;
if ( saveFileDialog . ShowDialog ( ) = = true )
using ( var package = new OfficeOpenXml . ExcelPackage ( ) )
{
// 3. 保存文件
File . WriteAllText ( saveFileDialog . FileName , sb . ToString ( ) , Encoding . UTF8 ) ;
MessageBox . Show ( "保存成功!\n文件路径: " + saveFileDialog . FileName , "成功" ) ;
// ========== 工作表1: 报告摘要 ==========
var sheet1 = package . Workbook . Worksheets . Add ( "报告摘要" ) ;
// 标题
sheet1 . Cells [ "A1" ] . Value = "头罩视野测试报告" ;
sheet1 . Cells [ "A1" ] . Style . Font . Size = 22 ;
sheet1 . Cells [ "A1" ] . Style . Font . Bold = true ;
sheet1 . Cells [ "A1" ] . Style . HorizontalAlignment = OfficeOpenXml . Style . ExcelHorizontalAlignment . Center ;
sheet1 . Cells [ "A1:G1" ] . Merge = true ;
// 报告编号和日期
sheet1 . Cells [ "A3" ] . Value = "报告编号:" ;
sheet1 . Cells [ "B3" ] . Value = $"REP-{DateTime.Now:yyyyMMddHHmmss}" ;
sheet1 . Cells [ "D3" ] . Value = "试验日期:" ;
sheet1 . Cells [ "E3" ] . Value = DateTime . Now . ToString ( "yyyy年MM月dd日" ) ;
sheet1 . Cells [ "A3:E3" ] . Style . Font . Size = 12 ;
// 测试参数
sheet1 . Cells [ "A5" ] . Value = "测试参数" ;
sheet1 . Cells [ "A5" ] . Style . Font . Bold = true ;
sheet1 . Cells [ "A6" ] . Value = "设备型号" ;
sheet1 . Cells [ "B6" ] . Value = "GT-2024" ;
sheet1 . Cells [ "A7" ] . Value = "分辨角度" ;
sheet1 . Cells [ "B7" ] . Value = "5° / 10° / 15°( 按实际) " ;
sheet1 . Cells [ "A8" ] . Value = "测试模式" ;
sheet1 . Cells [ "B8" ] . Value = "左眼/右眼/双目" ;
// 结果表格标题
int rowStart = 10 ;
string [ ] headers = { "编号" , "日期" , "时间" , "左目视野(cm²)" , "右目视野(cm²)" ,
"双目视野(cm²)" , "下方视野(°)" , "视野保存率(%)" ,
"总视野面积(cm²)" , "总视野保存率(%)" } ;
for ( int i = 0 ; i < headers . Length ; i + + )
sheet1 . Cells [ rowStart , i + 1 ] . Value = headers [ i ] ;
sheet1 . Cells [ rowStart , 1 , rowStart , headers . Length ] . Style . Font . Bold = true ;
sheet1 . Cells [ rowStart , 1 , rowStart , headers . Length ] . Style . Fill . PatternType = OfficeOpenXml . Style . ExcelFillStyle . Solid ;
sheet1 . Cells [ rowStart , 1 , rowStart , headers . Length ] . Style . Fill . BackgroundColor . SetColor ( System . Drawing . Color . LightGray ) ;
// 填充数据
int row = rowStart + 1 ;
foreach ( var item in TestDataStore . Records )
{
sheet1 . Cells [ row , 1 ] . Value = item . Id ;
sheet1 . Cells [ row , 2 ] . Value = item . Date ;
sheet1 . Cells [ row , 3 ] . Value = item . Time ;
sheet1 . Cells [ row , 4 ] . Value = item . LeftEyeArea ;
sheet1 . Cells [ row , 5 ] . Value = item . RightEyeArea ;
sheet1 . Cells [ row , 6 ] . Value = item . BinocularArea ;
sheet1 . Cells [ row , 7 ] . Value = item . LowerVision ;
sheet1 . Cells [ row , 8 ] . Value = item . VisionRetentionRate ;
sheet1 . Cells [ row , 9 ] . Value = item . totalVisionArea ;
sheet1 . Cells [ row , 10 ] . Value = item . GetVisionRetentionRate ;
row + + ;
}
sheet1 . Cells [ 1 , 1 , row - 1 , headers . Length ] . AutoFitColumns ( ) ;
// ========== 插入柱状图( 展示最近5次左右眼面积对比) ==========
var records = TestDataStore . Records . TakeLast ( 5 ) . ToList ( ) ;
if ( records . Count > 0 )
{
int chartStartRow = row + 3 ;
sheet1 . Cells [ chartStartRow , 1 ] . Value = "最近5次测试视野面积对比图" ;
sheet1 . Cells [ chartStartRow , 1 ] . Style . Font . Bold = true ;
int dataRowStart = chartStartRow + 1 ;
sheet1 . Cells [ dataRowStart , 1 ] . Value = "序号" ;
sheet1 . Cells [ dataRowStart , 2 ] . Value = "左眼面积" ;
sheet1 . Cells [ dataRowStart , 3 ] . Value = "右眼面积" ;
for ( int i = 0 ; i < records . Count ; i + + )
{
sheet1 . Cells [ dataRowStart + 1 + i , 1 ] . Value = i + 1 ;
sheet1 . Cells [ dataRowStart + 1 + i , 2 ] . Value = records [ i ] . LeftEyeArea ;
sheet1 . Cells [ dataRowStart + 1 + i , 3 ] . Value = records [ i ] . RightEyeArea ;
}
var chart = sheet1 . Drawings . AddChart ( "AreaChart" , OfficeOpenXml . Drawing . Chart . eChartType . ColumnClustered ) ;
chart . SetPosition ( chartStartRow , 0 , 0 , 0 ) ;
chart . SetSize ( 600 , 400 ) ;
chart . Title . Text = "左右眼视野面积对比" ;
chart . Legend . Position = OfficeOpenXml . Drawing . Chart . eLegendPosition . Bottom ;
var series1 = chart . Series . Add ( sheet1 . Cells [ dataRowStart + 1 , 2 , dataRowStart + records . Count , 2 ] ,
sheet1 . Cells [ dataRowStart + 1 , 1 , dataRowStart + records . Count , 1 ] ) ;
series1 . Header = "左眼面积" ;
var series2 = chart . Series . Add ( sheet1 . Cells [ dataRowStart + 1 , 3 , dataRowStart + records . Count , 3 ] ,
sheet1 . Cells [ dataRowStart + 1 , 1 , dataRowStart + records . Count , 1 ] ) ;
series2 . Header = "右眼面积" ;
}
//// ========== 新增:三个饼图(左眼、右眼、双目)基于最新一次测试数据 ==========
//var latestRecord = TestDataStore.Records.LastOrDefault();
//if (latestRecord != null)
//{
// // 标准总面积(与 GetArea 中定义一致)
// double standardTotalArea = 2 * Math.PI * 330 * 330; // ≈ 684,000, 可根据实际调整
// // 如果您的 GetArea 中有公开的标准面积字段, 建议使用: GetArea._standardTotalArea
// // 这里为了兼容,直接计算
// // 饼图数据区域起始行(放在柱状图右侧,可根据实际微调)
// int pieStartRow = row + 3;
// int pieStartColLeft = 8; // 左眼饼图起始列 H
// int pieStartColRight = 12; // 右眼饼图起始列 L
// int pieStartColBin = 16; // 双目饼图起始列 P
// // 左眼饼图数据
// double leftArea = latestRecord.LeftEyeArea;
// double leftRemain = standardTotalArea - leftArea;
// if (leftRemain < 0) leftRemain = 0;
// sheet1.Cells[pieStartRow, pieStartColLeft].Value = "左眼视野";
// sheet1.Cells[pieStartRow + 1, pieStartColLeft].Value = leftArea;
// sheet1.Cells[pieStartRow + 2, pieStartColLeft].Value = "剩余区域";
// sheet1.Cells[pieStartRow + 3, pieStartColLeft].Value = leftRemain;
// var pieLeft = sheet1.Drawings.AddChart("PieLeft", OfficeOpenXml.Drawing.Chart.eChartType.Pie);
// pieLeft.SetPosition(pieStartRow - 1, 0, pieStartColLeft - 2, 0);
// pieLeft.SetSize(300, 200);
// pieLeft.Title.Text = "左眼视野占比";
// pieLeft.Legend.Position = OfficeOpenXml.Drawing.Chart.eLegendPosition.Bottom;
// var seriesLeft = pieLeft.Series.Add(sheet1.Cells[pieStartRow + 1, pieStartColLeft, pieStartRow + 3, pieStartColLeft],
// sheet1.Cells[pieStartRow, pieStartColLeft, pieStartRow + 2, pieStartColLeft]);
// seriesLeft.Header = "面积 (cm²)";
// //这里需要加入百分比显示, EPPlus 的饼图默认不显示百分比,需要手动设置数据标签
// // 右眼饼图数据
// double rightArea = latestRecord.RightEyeArea;
// double rightRemain = standardTotalArea - rightArea;
// if (rightRemain < 0) rightRemain = 0;
// sheet1.Cells[pieStartRow, pieStartColRight].Value = "右眼视野";
// sheet1.Cells[pieStartRow + 1, pieStartColRight].Value = rightArea;
// sheet1.Cells[pieStartRow + 2, pieStartColRight].Value = "剩余区域";
// sheet1.Cells[pieStartRow + 3, pieStartColRight].Value = rightRemain;
// var pieRight = sheet1.Drawings.AddChart("PieRight", OfficeOpenXml.Drawing.Chart.eChartType.Pie);
// pieRight.SetPosition(pieStartRow - 1, 0, pieStartColRight - 2, 0);
// pieRight.SetSize(300, 200);
// pieRight.Title.Text = "右眼视野占比";
// pieRight.Legend.Position = OfficeOpenXml.Drawing.Chart.eLegendPosition.Bottom;
// var seriesRight = pieRight.Series.Add(sheet1.Cells[pieStartRow + 1, pieStartColRight, pieStartRow + 3, pieStartColRight],
// sheet1.Cells[pieStartRow, pieStartColRight, pieStartRow + 2, pieStartColRight]);
// // 双目饼图数据
// double binArea = latestRecord.BinocularArea;
// double binRemain = standardTotalArea - binArea;
// if (binRemain < 0) binRemain = 0;
// sheet1.Cells[pieStartRow, pieStartColBin].Value = "双目视野";
// sheet1.Cells[pieStartRow + 1, pieStartColBin].Value = binArea;
// sheet1.Cells[pieStartRow + 2, pieStartColBin].Value = "剩余区域";
// sheet1.Cells[pieStartRow + 3, pieStartColBin].Value = binRemain;
// var pieBin = sheet1.Drawings.AddChart("PieBin", OfficeOpenXml.Drawing.Chart.eChartType.Pie);
// pieBin.SetPosition(pieStartRow - 1, 0, pieStartColBin - 2, 0);
// pieBin.SetSize(300, 200);
// pieBin.Title.Text = "双目视野占比";
// pieBin.Legend.Position = OfficeOpenXml.Drawing.Chart.eLegendPosition.Bottom;
// var seriesBin = pieBin.Series.Add(sheet1.Cells[pieStartRow + 1, pieStartColBin, pieStartRow + 3, pieStartColBin],
// sheet1.Cells[pieStartRow, pieStartColBin, pieStartRow + 2, pieStartColBin]);
//}
// 添加备注说明(位置下移避免覆盖饼图)
int noteRow = row + 30 ; // 原先是 row+20, 现在下移避免与饼图重叠
sheet1 . Cells [ noteRow , 1 ] . Value = "备注:" ;
sheet1 . Cells [ noteRow , 2 ] . Value = "1. 视野保存率公式依据 GB 2890-2022 计算;" ;
sheet1 . Cells [ noteRow + 1 , 2 ] . Value = "2. 空白视野面积为标准头模未戴面罩时的理论值;" ;
sheet1 . Cells [ noteRow + 2 , 2 ] . Value = "3. 测试过程中若出现异常,请参考原始记录。" ;
sheet1 . Cells [ noteRow , 1 , noteRow + 2 , 4 ] . Style . Font . Size = 10 ;
sheet1 . Cells [ noteRow , 1 , noteRow + 2 , 4 ] . Style . Font . Italic = true ;
// ========== 工作表2: 原始数据明细 ==========
var sheet2 = package . Workbook . Worksheets . Add ( "原始数据" ) ;
for ( int i = 0 ; i < headers . Length ; i + + )
sheet2 . Cells [ 1 , i + 1 ] . Value = headers [ i ] ;
sheet2 . Cells [ 1 , 1 , 1 , headers . Length ] . Style . Font . Bold = true ;
row = 2 ;
foreach ( var item in TestDataStore . Records )
{
sheet2 . Cells [ row , 1 ] . Value = item . Id ;
sheet2 . Cells [ row , 2 ] . Value = item . Date ;
sheet2 . Cells [ row , 3 ] . Value = item . Time ;
sheet2 . Cells [ row , 4 ] . Value = item . LeftEyeArea ;
sheet2 . Cells [ row , 5 ] . Value = item . RightEyeArea ;
sheet2 . Cells [ row , 6 ] . Value = item . BinocularArea ;
sheet2 . Cells [ row , 7 ] . Value = item . LowerVision ;
sheet2 . Cells [ row , 8 ] . Value = item . VisionRetentionRate ;
sheet2 . Cells [ row , 9 ] . Value = item . totalVisionArea ;
sheet2 . Cells [ row , 10 ] . Value = item . GetVisionRetentionRate ;
row + + ;
}
sheet2 . Cells [ 1 , 1 , row - 1 , headers . Length ] . AutoFitColumns ( ) ;
// 保存文件
package . SaveAs ( new FileInfo ( saveDialog . FileName ) ) ;
MessageBox . Show ( $"报告已成功生成!\n路径: {saveDialog.FileName}" , "导出成功" , MessageBoxButton . OK , MessageBoxImage . Information ) ;
}
}
catch ( Exception ex )
{
MessageBox . Show ( "保存失败:" + ex . Message ) ;
MessageBox . Show ( $"生成报告失败:{ex.Message}" , "错误" , MessageBoxButton . OK , MessageBoxImage . Error ) ;
}
}
//#endregion
private void Page_Loaded ( object sender , RoutedEventArgs e )
{