メインコンテンツまでスキップ

報表 Pattern


Pattern 1:標準報表模板

場景

公司所有報表的統一版面:公司頁首 + Logo、資料表格、頁尾頁碼。

實作

public class StandardReport : IDocument
{
private readonly string _title;
private readonly Action<TableDescriptor> _tableBuilder;

public StandardReport(string title, Action<TableDescriptor> tableBuilder)
{
_title = title;
_tableBuilder = tableBuilder;
}

public DocumentMetadata GetMetadata() => DocumentMetadata.Default;

public void Compose(IDocumentContainer container)
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.MarginHorizontal(1.5f, Unit.Centimetre);
page.MarginVertical(2, Unit.Centimetre);
page.DefaultTextStyle(s =>
s.FontFamily("Microsoft JhengHei").FontSize(9));

page.Header().ComposeHeader(_title);
page.Content().ComposeContent(_tableBuilder);
page.Footer().ComposeFooter();
});
}
}

// Header 擴充方法
static class ReportComponents
{
public static void ComposeHeader(this IContainer container, string title)
{
container.Row(row =>
{
row.RelativeItem().Column(col =>
{
col.Item().Text("GatherTech").FontSize(12).Bold()
.FontColor(Colors.Blue.Darken2);
col.Item().Text(title).FontSize(16).Bold();
col.Item().Text($"產出日期:{DateTime.Now:yyyy/MM/dd HH:mm}")
.FontSize(8).FontColor(Colors.Grey.Medium);
});
row.ConstantItem(60).AlignRight()
.Image("assets/company-logo.png");
});
container.PaddingBottom(10)
.BorderBottom(2).BorderColor(Colors.Blue.Darken2);
}

public static void ComposeContent(
this IContainer container, Action<TableDescriptor> tableBuilder)
{
container.PaddingTop(10).Table(tableBuilder);
}

public static void ComposeFooter(this IContainer container)
{
container.BorderTop(1).BorderColor(Colors.Grey.Lighten1)
.PaddingTop(5).Row(row =>
{
row.RelativeItem().Text("GatherTech 機密文件")
.FontSize(7).FontColor(Colors.Grey.Medium);
row.RelativeItem().AlignRight().Text(text =>
{
text.DefaultTextStyle(s => s.FontSize(8));
text.Span("第 ");
text.CurrentPageNumber();
text.Span(" / ");
text.TotalPages();
text.Span(" 頁");
});
});
}
}

使用方式:

var report = new StandardReport("設備參數報表", table =>
{
table.ColumnsDefinition(c =>
{
c.RelativeColumn(3);
c.RelativeColumn(2);
c.RelativeColumn(1);
});

table.Header(h =>
{
h.Cell().HeaderCell("參數名稱");
h.Cell().HeaderCell("數值");
h.Cell().HeaderCell("單位");
});

foreach (var p in parameters)
{
table.Cell().DataCell(p.Name);
table.Cell().DataCell(p.Value.ToString("F2"));
table.Cell().DataCell(p.Unit);
}
});

report.GeneratePdf("設備參數報表.pdf");

Pattern 2:動態表格(從 List<T> 自動產生)

泛用表格產生器

public static void GenerateTableFromData<T>(
IContainer container,
IEnumerable<T> data,
params (string Header, Func<T, string> Getter, float Width)[] columns)
{
container.Table(table =>
{
table.ColumnsDefinition(def =>
{
foreach (var col in columns)
def.RelativeColumn(col.Width);
});

// 表頭
table.Header(header =>
{
foreach (var col in columns)
{
header.Cell()
.Background(Colors.Grey.Darken2)
.Padding(5)
.Text(col.Header)
.FontColor(Colors.White).Bold().FontSize(9);
}
});

// 資料列(交替背景色)
int rowIndex = 0;
foreach (var item in data)
{
var bgColor = rowIndex % 2 == 0
? Colors.White
: Colors.Grey.Lighten4;

foreach (var col in columns)
{
table.Cell()
.Background(bgColor)
.BorderBottom(1).BorderColor(Colors.Grey.Lighten2)
.Padding(4)
.Text(col.Getter(item)).FontSize(9);
}
rowIndex++;
}
});
}

使用方式:

page.Content().Column(col =>
{
col.Item().Text("感測器讀數").FontSize(14).Bold();
col.Item().PaddingTop(10);

GenerateTableFromData(col.Item(), sensorReadings,
("感測器 ID", r => r.SensorId, 2),
("數值", r => r.Value.ToString("F2"), 1.5f),
("單位", r => r.Unit, 1),
("時間", r => r.Timestamp.ToString("HH:mm:ss"), 1.5f),
("狀態", r => r.IsNormal ? "正常" : "異常", 1));
});

Pattern 3:多語系報表

場景

同一份報表需要支援繁體中文、英文、日文。

實作

public class LocalizedReport
{
private readonly CultureInfo _culture;

public LocalizedReport(CultureInfo culture)
{
_culture = culture;
}

private string GetFontFamily() => _culture.Name switch
{
"ja-JP" => "Noto Sans CJK JP",
"zh-TW" => "Microsoft JhengHei",
"zh-CN" => "Microsoft YaHei",
_ => "Arial"
};

private bool IsRtl() => _culture.TextInfo.IsRightToLeft;

public void Compose(IDocumentContainer container, ReportData data)
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.DefaultTextStyle(s =>
s.FontFamily(GetFontFamily()).FontSize(10));

if (IsRtl())
page.ContentFromRightToLeft();

page.Header().Text(GetLocalizedString("ReportTitle"))
.FontSize(16).Bold();

page.Content().Column(col =>
{
col.Item().Text(
$"{GetLocalizedString("Date")}: " +
$"{DateTime.Now.ToString("d", _culture)}");
// ... 內容
});
});
}
}

Pattern 4:批次產出

場景

一次產生多台設備的個別報表。

實作

public static async Task GenerateBatchReportsAsync(
IEnumerable<Device> devices,
string outputDirectory)
{
// 平行產出,但限制同時數量避免記憶體暴漲
var semaphore = new SemaphoreSlim(4); // 最多同時 4 個

var tasks = devices.Select(async device =>
{
await semaphore.WaitAsync();
try
{
var fileName = $"{device.Id}_{DateTime.Now:yyyyMMdd}.pdf";
var filePath = Path.Combine(outputDirectory, fileName);

Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.DefaultTextStyle(s =>
s.FontFamily("Microsoft JhengHei").FontSize(9));

page.Header().Text($"設備報表 - {device.Name}")
.FontSize(16).Bold();

page.Content().Table(table =>
{
// ... 該設備的資料
});
});
})
.GeneratePdf(filePath);

Logger.Info($"已產出: {fileName}");
}
finally
{
semaphore.Release();
}
});

await Task.WhenAll(tasks);
}

Pattern 5:QA 測試報告(含 Pass/Fail 標記與截圖)

public void ComposeTestReport(
IDocumentContainer container,
TestReport report)
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.DefaultTextStyle(s =>
s.FontFamily("Microsoft JhengHei").FontSize(9));

page.Header().ComposeHeader($"QA 測試報告 - {report.DeviceName}");

page.Content().Column(col =>
{
// 摘要
col.Item().Row(row =>
{
row.RelativeItem().Text($"測試日期:{report.TestDate:yyyy/MM/dd}");
row.RelativeItem().Text($"總項目:{report.Items.Count}");
row.RelativeItem().Text(text =>
{
text.Span("結果:");
var allPass = report.Items.All(i => i.Passed);
text.Span(allPass ? "PASS" : "FAIL")
.Bold()
.FontColor(allPass ? Colors.Green.Darken1 : Colors.Red.Darken1);
});
});

col.Item().PaddingVertical(10);

// 測試項目表格
col.Item().Table(table =>
{
table.ColumnsDefinition(c =>
{
c.ConstantColumn(30); // 編號
c.RelativeColumn(3); // 項目
c.RelativeColumn(2); // 期望值
c.RelativeColumn(2); // 實際值
c.ConstantColumn(50); // 結果
});

table.Header(h =>
{
h.Cell().HeaderCell("#");
h.Cell().HeaderCell("測試項目");
h.Cell().HeaderCell("期望值");
h.Cell().HeaderCell("實際值");
h.Cell().HeaderCell("結果");
});

int idx = 1;
foreach (var item in report.Items)
{
table.Cell().DataCell(idx.ToString());
table.Cell().DataCell(item.Name);
table.Cell().DataCell(item.Expected);
table.Cell().DataCell(item.Actual);
table.Cell()
.Background(item.Passed
? Colors.Green.Lighten4
: Colors.Red.Lighten4)
.Padding(4)
.AlignCenter()
.Text(item.Passed ? "PASS" : "FAIL")
.Bold()
.FontColor(item.Passed
? Colors.Green.Darken2
: Colors.Red.Darken2);
idx++;
}
});

// 失敗項目截圖
foreach (var item in report.Items.Where(i => !i.Passed && i.Screenshot != null))
{
col.Item().PaddingTop(10);
col.Item().Text($"失敗截圖:{item.Name}").Bold();
col.Item().Image(item.Screenshot).FitWidth();
}
});

page.Footer().ComposeFooter();
});
}

效能注意事項

項目建議
大量報表SemaphoreSlim 限制平行數量(建議 4-8)
圖片多壓縮圖片後再嵌入,避免 PDF 檔案過大
記憶體每份 Document.Create 完成後記憶體即釋放
預覽開發用 ShowInPreviewer(),正式用 GeneratePdf()
字體字體只需註冊一次,不要在迴圈中重複註冊

延伸閱讀