WorkCamera/client/WorkCameraExport/Services/ExcelService.cs
2026-01-05 23:58:56 +08:00

418 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using OfficeOpenXml;
using OfficeOpenXml.Drawing;
using OfficeOpenXml.Style;
using WorkCameraExport.Models;
using WorkCameraExport.Services.Interfaces;
namespace WorkCameraExport.Services
{
/// <summary>
/// Excel 服务 - 负责生成 Excel 文件
/// </summary>
public class ExcelService
{
private readonly ILogService? _logService;
// Excel 图片配置
private const int ImageWidth = 100;
private const int ImageHeight = 60;
private const int ImageSpacing = 5;
static ExcelService()
{
// 设置 EPPlus 许可证(非商业用途)
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
}
public ExcelService(ILogService? logService = null)
{
_logService = logService;
}
/// <summary>
/// 导出工作记录到 Excel
/// </summary>
public async Task ExportWorkRecordsToExcelAsync(
List<WorkRecordExportItem> records,
string outputPath,
CancellationToken cancellationToken = default)
{
try
{
using var package = new ExcelPackage();
var worksheet = package.Workbook.Worksheets.Add("工作记录");
// 设置表头
SetWorkRecordHeaders(worksheet);
// 填充数据
var row = 2;
foreach (var record in records)
{
cancellationToken.ThrowIfCancellationRequested();
await FillWorkRecordRowAsync(worksheet, row, record, cancellationToken);
row++;
}
// 自动调整列宽(除了图片列)
AutoFitColumns(worksheet, excludeColumn: 3);
// 保存文件
var fileInfo = new FileInfo(outputPath);
await package.SaveAsAsync(fileInfo, cancellationToken);
_logService?.Info($"Excel 导出成功: {outputPath}, 共 {records.Count} 条记录");
}
catch (OperationCanceledException)
{
_logService?.Info("Excel 导出已取消");
throw;
}
catch (Exception ex)
{
_logService?.Error($"Excel 导出失败: {ex.Message}", ex);
throw;
}
}
/// <summary>
/// 导出月报表到 Excel
/// </summary>
public async Task ExportMonthlyReportToExcelAsync(
List<MonthlyReportDto> data,
string outputPath,
CancellationToken cancellationToken = default)
{
try
{
using var package = new ExcelPackage();
var worksheet = package.Workbook.Worksheets.Add("月报表");
// 设置表头
worksheet.Cells[1, 1].Value = "序号";
worksheet.Cells[1, 2].Value = "时间";
worksheet.Cells[1, 3].Value = "部门名称";
worksheet.Cells[1, 4].Value = "人员名称";
worksheet.Cells[1, 5].Value = "工作天数";
// 设置表头样式
using (var range = worksheet.Cells[1, 1, 1, 5])
{
range.Style.Font.Bold = true;
range.Style.Fill.PatternType = ExcelFillStyle.Solid;
range.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.LightGray);
range.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
}
// 填充数据
for (int i = 0; i < data.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var row = i + 2;
var item = data[i];
worksheet.Cells[row, 1].Value = i + 1;
worksheet.Cells[row, 2].Value = item.YearMonth;
worksheet.Cells[row, 3].Value = item.DeptName;
worksheet.Cells[row, 4].Value = item.WorkerName;
worksheet.Cells[row, 5].Value = item.WorkDays;
}
// 自动调整列宽
worksheet.Cells.AutoFitColumns();
// 保存文件
var fileInfo = new FileInfo(outputPath);
await package.SaveAsAsync(fileInfo, cancellationToken);
_logService?.Info($"月报表导出成功: {outputPath}, 共 {data.Count} 条记录");
}
catch (OperationCanceledException)
{
_logService?.Info("月报表导出已取消");
throw;
}
catch (Exception ex)
{
_logService?.Error($"月报表导出失败: {ex.Message}", ex);
throw;
}
}
/// <summary>
/// 设置工作记录表头
/// </summary>
private void SetWorkRecordHeaders(ExcelWorksheet worksheet)
{
var headers = new[]
{
"序号", "部门名称", "图片", "地址信息", "工作内容",
"施工人员", "状态", "拍照时间", "创建时间", "更新时间"
};
for (int i = 0; i < headers.Length; i++)
{
worksheet.Cells[1, i + 1].Value = headers[i];
}
// 设置表头样式
using (var range = worksheet.Cells[1, 1, 1, headers.Length])
{
range.Style.Font.Bold = true;
range.Style.Fill.PatternType = ExcelFillStyle.Solid;
range.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.LightGray);
range.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
}
// 设置图片列宽度(根据最大图片数计算)
worksheet.Column(3).Width = 50; // 默认宽度,会根据实际图片数调整
}
/// <summary>
/// 填充工作记录行数据
/// </summary>
private async Task FillWorkRecordRowAsync(
ExcelWorksheet worksheet,
int row,
WorkRecordExportItem record,
CancellationToken cancellationToken)
{
worksheet.Cells[row, 1].Value = row - 1; // 序号
worksheet.Cells[row, 2].Value = record.DeptName;
// 第3列是图片稍后处理
worksheet.Cells[row, 4].Value = record.Address;
worksheet.Cells[row, 5].Value = record.Content;
worksheet.Cells[row, 6].Value = string.Join("、", record.Workers);
worksheet.Cells[row, 7].Value = record.StatusName;
worksheet.Cells[row, 8].Value = record.RecordTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "";
worksheet.Cells[row, 9].Value = record.CreateTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "";
worksheet.Cells[row, 10].Value = record.UpdateTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "";
// 设置行高以容纳图片
worksheet.Row(row).Height = ImageHeight + 10;
// 嵌入图片
if (record.ImagePaths != null && record.ImagePaths.Count > 0)
{
await EmbedImagesAsync(worksheet, row, 3, record.ImagePaths, cancellationToken);
}
}
/// <summary>
/// 嵌入图片到单元格(水平排列)
/// </summary>
private async Task EmbedImagesAsync(
ExcelWorksheet worksheet,
int row,
int column,
List<string> imagePaths,
CancellationToken cancellationToken)
{
var validPaths = imagePaths.Where(p => !string.IsNullOrEmpty(p) && File.Exists(p)).ToList();
if (validPaths.Count == 0) return;
// 计算所需的列宽
var totalWidth = validPaths.Count * ImageWidth + (validPaths.Count - 1) * ImageSpacing + 10;
var currentWidth = worksheet.Column(column).Width * 7; // 大约转换为像素
if (totalWidth > currentWidth)
{
worksheet.Column(column).Width = totalWidth / 7.0 + 5;
}
var xOffset = 5; // 起始偏移
for (int i = 0; i < validPaths.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var imagePath = validPaths[i];
var imageData = await File.ReadAllBytesAsync(imagePath, cancellationToken);
using var stream = new MemoryStream(imageData);
var picture = worksheet.Drawings.AddPicture(
$"img_{row}_{column}_{i}",
stream);
// 设置图片位置和大小
picture.SetPosition(row - 1, 5, column - 1, xOffset);
picture.SetSize(ImageWidth, ImageHeight);
xOffset += ImageWidth + ImageSpacing;
}
catch (Exception ex)
{
_logService?.Warn($"嵌入图片失败: {validPaths[i]}, {ex.Message}");
}
}
}
/// <summary>
/// 自动调整列宽
/// </summary>
private void AutoFitColumns(ExcelWorksheet worksheet, int excludeColumn = -1)
{
for (int col = 1; col <= worksheet.Dimension?.End.Column; col++)
{
if (col != excludeColumn)
{
worksheet.Column(col).AutoFit();
}
}
}
/// <summary>
/// 生成 Excel 文件(兼容旧版 MainForm 调用)
/// </summary>
/// <param name="outputPath">输出文件路径</param>
/// <param name="records">工作记录列表</param>
/// <param name="downloadedImages">已下载的图片字典URL -> 图片数据)</param>
/// <param name="progressCallback">进度回调(当前行, 总行数)</param>
public void GenerateExcel(
string outputPath,
List<WorkRecordExportDto> records,
Dictionary<string, byte[]> downloadedImages,
Action<int, int>? progressCallback = null)
{
try
{
using var package = new ExcelPackage();
var worksheet = package.Workbook.Worksheets.Add("工作记录");
// 设置表头
SetWorkRecordHeaders(worksheet);
// 填充数据
for (int i = 0; i < records.Count; i++)
{
var row = i + 2;
var record = records[i];
worksheet.Cells[row, 1].Value = i + 1; // 序号
worksheet.Cells[row, 2].Value = record.DeptName;
// 第3列是图片
worksheet.Cells[row, 4].Value = record.Address;
worksheet.Cells[row, 5].Value = record.Content;
worksheet.Cells[row, 6].Value = string.Join("、", record.WorkerNames);
worksheet.Cells[row, 7].Value = record.StatusName;
worksheet.Cells[row, 8].Value = record.RecordTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "";
worksheet.Cells[row, 9].Value = record.CreateTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "";
worksheet.Cells[row, 10].Value = record.UpdateTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "";
// 设置行高以容纳图片
worksheet.Row(row).Height = ImageHeight + 10;
// 嵌入图片
if (record.Images != null && record.Images.Count > 0)
{
EmbedImagesFromBytes(worksheet, row, 3, record.Images, downloadedImages);
}
progressCallback?.Invoke(i + 1, records.Count);
}
// 自动调整列宽(除了图片列)
AutoFitColumns(worksheet, excludeColumn: 3);
// 保存文件
var fileInfo = new FileInfo(outputPath);
package.SaveAs(fileInfo);
_logService?.Info($"Excel 导出成功: {outputPath}, 共 {records.Count} 条记录");
}
catch (Exception ex)
{
_logService?.Error($"Excel 导出失败: {ex.Message}", ex);
throw;
}
}
/// <summary>
/// 从字节数组嵌入图片到单元格(水平排列)
/// </summary>
private void EmbedImagesFromBytes(
ExcelWorksheet worksheet,
int row,
int column,
List<string> imageUrls,
Dictionary<string, byte[]> downloadedImages)
{
var validImages = imageUrls
.Where(url => !string.IsNullOrEmpty(url) && downloadedImages.ContainsKey(url))
.Select(url => downloadedImages[url])
.Where(data => data != null && data.Length > 0)
.ToList();
if (validImages.Count == 0) return;
// 计算所需的列宽
var totalWidth = validImages.Count * ImageWidth + (validImages.Count - 1) * ImageSpacing + 10;
var currentWidth = worksheet.Column(column).Width * 7;
if (totalWidth > currentWidth)
{
worksheet.Column(column).Width = totalWidth / 7.0 + 5;
}
var xOffset = 5;
for (int i = 0; i < validImages.Count; i++)
{
try
{
using var stream = new MemoryStream(validImages[i]);
var picture = worksheet.Drawings.AddPicture(
$"img_{row}_{column}_{i}",
stream);
picture.SetPosition(row - 1, 5, column - 1, xOffset);
picture.SetSize(ImageWidth, ImageHeight);
xOffset += ImageWidth + ImageSpacing;
}
catch (Exception ex)
{
_logService?.Warn($"嵌入图片失败: {ex.Message}");
}
}
}
}
/// <summary>
/// 工作记录导出项 - 包含本地图片路径
/// </summary>
public class WorkRecordExportItem
{
public int Id { get; set; }
public string DeptName { get; set; } = "";
public DateTime? RecordTime { get; set; }
public string Address { get; set; } = "";
public string Content { get; set; } = "";
public string StatusName { get; set; } = "";
public List<string> Workers { get; set; } = new(); // 工作人员名称列表
public List<string> ImagePaths { get; set; } = new(); // 本地图片路径
public DateTime? CreateTime { get; set; }
public DateTime? UpdateTime { get; set; }
/// <summary>
/// 从 WorkRecordExportDto 创建
/// </summary>
public static WorkRecordExportItem FromDto(WorkRecordExportDto dto)
{
return new WorkRecordExportItem
{
Id = dto.Id,
DeptName = dto.DeptName,
RecordTime = dto.RecordTime,
Address = dto.Address,
Content = dto.Content,
StatusName = dto.StatusName,
Workers = dto.WorkerNames, // 使用 WorkerNames 获取字符串列表
CreateTime = dto.CreateTime,
UpdateTime = dto.UpdateTime
};
}
}
}