441 lines
16 KiB
C#
441 lines
16 KiB
C#
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
|
||
{
|
||
_logService?.Info($"[Excel] 开始生成 Excel,共 {records.Count} 条记录");
|
||
|
||
// 确保输出目录存在
|
||
var outputDir = Path.GetDirectoryName(outputPath);
|
||
if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir))
|
||
{
|
||
Directory.CreateDirectory(outputDir);
|
||
_logService?.Info($"[Excel] 创建输出目录: {outputDir}");
|
||
}
|
||
|
||
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);
|
||
_logService?.Info($"[Excel] 准备保存文件: {fileInfo.FullName}");
|
||
|
||
await package.SaveAsAsync(fileInfo, cancellationToken);
|
||
|
||
// 验证文件
|
||
if (fileInfo.Exists)
|
||
{
|
||
fileInfo.Refresh();
|
||
_logService?.Info($"[Excel] 文件保存成功: {fileInfo.FullName}, 大小: {fileInfo.Length} 字节");
|
||
}
|
||
else
|
||
{
|
||
_logService?.Error($"[Excel] 文件保存后不存在: {fileInfo.FullName}");
|
||
}
|
||
|
||
_logService?.Info($"[Excel] Excel 导出成功: {outputPath}, 共 {records.Count} 条记录");
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
_logService?.Info("[Excel] Excel 导出已取消");
|
||
throw;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService?.Error($"[Excel] 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
|
||
};
|
||
}
|
||
}
|
||
}
|