feat(Table): support export as Pdf (#2550)

* doc: 更新示例文档

* feat: 增加 Pdf 导出方法

* chore: 增加 Csv 相关资源文件

* refactor: 完善 IHtml2Pdf 接口服务

* refactor: 重构 TableExport 导出服务

* feat: 合并导出服务

* feat: 增加 IHtml2Pdf 服务

* feat: 增加 Csv 图标参数

* feat: 增加 ExportCsvDropdownItemText 参数

* feat: 内置导出 Csv 按钮参数

* feat: 移除 Pdf/Excel 服务统一使用 TableExport 服务

* refator: 调整注入服务

* refactor: 更改图标参数名称

* refactor: 增加图标宽度

* refactor: 精简代码

* doc: 更新导出按钮示例

* doc: 重构 Html2Pdf 示例

* feat: 增加 ITableExportPdf 服务

* feat: 增加 PdfSteamAsync 方法

* doc: 重构代码

* feat: 实现项目 Pdf 导出服务

* refactor: 增加 PdfStreamAsync 方法

* feat: 增加 ExportPdfAsync 方法

* chore: 更新图标

* test: 更新单元测试

* chore: 更新字典

* chore: bump version 8.0.6-beta01

* chore: bump version 8.0.2-beta01

* chore: bump version 8.0.1-beta01

* chore: bump version 8.0.2-beta01

* chore: 更新依赖组件包到最新

* refactor: 重构代码消除警告信息

* test: 更新单元测试

* refactor: 精简代码

* test: 更新单元测试
This commit is contained in:
Argo Zhang 2023-12-11 14:00:29 +08:00 committed by GitHub
parent 498d5f92ff
commit 9b2d9d467a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 515 additions and 233 deletions

View File

@ -55,3 +55,4 @@ Splittings
Foos
Localizer
onchange
Render

View File

@ -230,6 +230,7 @@
"ExportToastTitle": "Daten exportieren",
"ExportToastContent": "Daten exportieren {0}, automatisches Schließen in {1} Sekunden",
"ExportToastInProgressContent": "Daten exportieren, automatisches Schließen in {0} Sekunden",
"ExportCsvDropdownItemText": "MS-Csv",
"ExportExcelDropdownItemText": "MS-Excel",
"ExportPdfDropdownItemText": "Pdf",
"PageInfoText": "{0} - {1} Total {2}",

View File

@ -230,6 +230,7 @@
"ExportToastTitle": "Exportar datos",
"ExportToastContent": "Exportar datos {0}, cierre automático en {0} segundos",
"ExportToastInProgressContent": "Exportar datos, cierre automático en {0} segundos",
"ExportCsvDropdownItemText": "MS-Csv",
"ExportExcelDropdownItemText": "MS-Excel",
"ExportPdfDropdownItemText": "Pdf",
"PageInfoText": "{0} - {1} Total {2}",

View File

@ -230,6 +230,7 @@
"ExportToastTitle": "Exportar dados",
"ExportToastContent": "Exportar dados {0}, Fechamento automático em {0} segundos",
"ExportToastInProgressContent": "Exportar dados, Fechamento automático em {0} segundos",
"ExportCsvDropdownItemText": "MS-Csv",
"ExportExcelDropdownItemText": "MS-Excel",
"ExportPdfDropdownItemText": "Pdf",
"PageInfoText": "{0} - {1} Total {2}",

View File

@ -1,4 +1,4 @@
{
{
"BootstrapBlazor.Components.AutoComplete": {
"NoDataTip": "ไม่มีข้อมูลที่ตรงกัน",
"PlaceHolder": "กรุณากรอก"
@ -225,6 +225,7 @@
"ExportToastTitle": "ส่งออกข้อมูล",
"ExportToastContent": "ส่งออกข้อมูล {0}{1} ปิดอัตโนมัติหลังจากวินาที",
"ExportToastInProgressContent": "กำลังส่งออกข้อมูล โปรดทราบภายหลัง, {0} ปิดอัตโนมัติหลังจากวินาที",
"ExportCsvDropdownItemText": "ไมโครซอฟท์ Csv รูปแบบ",
"ExportExcelDropdownItemText": "ไมโครซอฟท์ Excel รูปแบบ",
"ExportPdfDropdownItemText": "Pdf รูปแบบ",
"PageInfoText": "{0} - {1} ทั้งหมด {2} บทความ",

View File

@ -239,6 +239,7 @@
"ExportToastTitle": "匯出資料",
"ExportToastContent": "匯出資料 {0}{1} 秒後自動關閉",
"ExportToastInProgressContent": "正在匯出資料,請稍後, {0} 秒後自動關閉",
"ExportCsvDropdownItemText": "微軟 Csv 格式",
"ExportExcelDropdownItemText": "微軟 Excel 格式",
"ExportPdfDropdownItemText": "Pdf 格式"
},

View File

@ -42,12 +42,12 @@
<PackageReference Include="BootstrapBlazor.FileViewer" Version="8.0.0" />
<PackageReference Include="BootstrapBlazor.FontAwesome" Version="8.0.1" />
<PackageReference Include="BootstrapBlazor.Gantt" Version="8.0.0" />
<PackageReference Include="BootstrapBlazor.Html2Pdf" Version="8.0.1" />
<PackageReference Include="BootstrapBlazor.Html2Pdf" Version="8.0.2-beta01" />
<PackageReference Include="BootstrapBlazor.ImageCropper" Version="*" />
<PackageReference Include="BootstrapBlazor.Live2DDisplay" Version="8.0.0" />
<PackageReference Include="BootstrapBlazor.Markdown" Version="8.0.0" />
<PackageReference Include="BootstrapBlazor.MaterialDesign" Version="8.0.1" />
<PackageReference Include="BootstrapBlazor.MaterialDesign.Extensions" Version="8.0.1" />
<PackageReference Include="BootstrapBlazor.MaterialDesign.Extensions" Version="8.0.2-beta01" />
<PackageReference Include="BootstrapBlazor.Middleware" Version="8.0.0" />
<PackageReference Include="BootstrapBlazor.MindMap" Version="8.0.0" />
<PackageReference Include="BootstrapBlazor.MouseFollower" Version="8.0.0" />
@ -56,7 +56,7 @@
<PackageReference Include="BootstrapBlazor.SignaturePad" Version="8.0.0" />
<PackageReference Include="BootstrapBlazor.Splitting" Version="8.0.0" />
<PackageReference Include="BootstrapBlazor.SummerNote" Version="8.0.1" />
<PackageReference Include="BootstrapBlazor.TableExport" Version="8.0.0" />
<PackageReference Include="BootstrapBlazor.TableExport" Version="8.0.1-beta01" />
<PackageReference Include="BootstrapBlazor.Topology" Version="8.0.1" />
<PackageReference Include="BootstrapBlazor.VideoPlayer" Version="8.0.1" />
<PackageReference Include="BootstrapBlazor.WebAPI" Version="8.0.0" />

View File

@ -19,10 +19,6 @@ public partial class Html2Pdfs
[NotNull]
private DownloadService? DownloadService { get; set; }
[Inject]
[NotNull]
private IComponentHtmlRenderer? HtmlRenderService { get; set; }
[Inject]
[NotNull]
private IWebHostEnvironment? WebHostEnvironment { get; set; }
@ -63,7 +59,7 @@ public partial class Html2Pdfs
// 拼接导出文件网址
var url = $"{NavigationManager.BaseUri}{fileName}";
var data = await PdfService.ExportDataAsync(url);
var data = await PdfService.PdfDataAsync(url);
using var stream = new MemoryStream(data);
await DownloadService.DownloadFromStreamAsync("table.pdf", stream);
await ToastService.Success("Pdf Export", "Export pdf element success.");

View File

@ -10,7 +10,7 @@
<PackageTips Name="BootstrapBlazor.TableExport" />
<p><b>@Localizer["TablesExportTips"]</b></p>
<p class="mt-3"><b>@Localizer["TablesExportTips"]</b></p>
<Pre>public void ConfigureServices(IServiceCollection services)
{
@ -38,6 +38,24 @@
</Table>
</DemoBlock>
<DemoBlock Title="@Localizer["TablesExportShowExportCsvButtonTitle"]"
Introduction="@Localizer["TablesExportShowExportCsvButtonIntro"]"
Name="ShowCsvExportButton">
<Table TItem="Foo"
IsPagination="true" PageItemsSource="@PageItemsSource"
IsStriped="true" IsBordered="true" IsMultipleSelect="true"
ShowToolbar="true" ShowDefaultButtons="false"
ShowExportButton="true" ShowExportCsvButton="true" ShowExportPdfButton="true"
OnQueryAsync="@OnQueryAsync">
<TableColumns>
<TableColumn @bind-Field="@context.DateTime" Width="180" />
<TableColumn @bind-Field="@context.Name" Width="100" />
<TableColumn @bind-Field="@context.Address" />
<TableColumn @bind-Field="@context.Count" />
</TableColumns>
</Table>
</DemoBlock>
<DemoBlock Title="@Localizer["TablesExportOnExportAsyncTitle"]"
Introduction="@Localizer["TablesExportOnExportAsyncIntro"]"
Name="OnExportAsync">

View File

@ -25,6 +25,7 @@ public partial class TablesExport
/// </summary>
[NotNull]
private List<Foo>? Items { get; set; }
private static IEnumerable<int> PageItemsSource => new int[] { 4, 10, 20 };
/// <summary>
@ -53,7 +54,7 @@ public partial class TablesExport
// 自定义导出方法
// 通过 context 参数可以自己查询数据进行导出操作
// 本例使用 context 传递来的 Rows/Columns 自定义文件名为 Test.xlsx
var ret = await Exporter.ExportAsync(context.Rows, context.Columns, "Test.xlsx");
var ret = await TableExport.ExportExcelAsync(context.Rows, context.Columns, "Test.xlsx");
// 返回 true 时自动弹出提示框
return ret;
@ -61,7 +62,7 @@ public partial class TablesExport
[Inject]
[NotNull]
private ITableExcelExport? Exporter { get; set; }
private ITableExport? TableExport { get; set; }
[Inject]
[NotNull]
@ -124,7 +125,7 @@ public partial class TablesExport
// 自定义导出模板导出当前页面数据为 Excel 方法
// 使用 BootstrapBlazor 内置服务 ITableExcelExport 实例方法 ExportAsync 进行导出操作
// 导出数据使用 context 传递来的 Rows/Columns 即为当前页数据
var ret = await Exporter.ExportAsync(context.Rows, context.Columns, $"Test_{DateTime.Now:yyyyMMddHHmmss}.xlsx");
var ret = await TableExport.ExportExcelAsync(context.Rows, context.Columns, $"Test_{DateTime.Now:yyyyMMddHHmmss}.xlsx");
// 返回 true 时自动弹出提示框
await ShowToast(ret);
@ -145,7 +146,7 @@ public partial class TablesExport
var data = Items.Where(filter.GetFilterFunc<Foo>());
// 导出符合条件的所有数据 data
var ret = await Exporter.ExportAsync(data, context.Columns, $"Test_{DateTime.Now:yyyyMMddHHmmss}.xlsx");
var ret = await TableExport.ExportExcelAsync(data, context.Columns, $"Test_{DateTime.Now:yyyyMMddHHmmss}.xlsx");
// 返回 true 时自动弹出提示框
await ShowToast(ret);
@ -156,7 +157,7 @@ public partial class TablesExport
// 自定义导出模板导出当前页面数据为 Csv 方法
// 使用 BootstrapBlazor 内置服务 ITableExcelExport 实例方法 ExportCsvAsync 进行导出操作
// 导出数据使用 context 传递来的 Rows/Columns 即为当前页数据
var ret = await Exporter.ExportCsvAsync(context.Rows, context.Columns, $"Test_{DateTime.Now:yyyyMMddHHmmss}.csv");
var ret = await TableExport.ExportCsvAsync(context.Rows, context.Columns, $"Test_{DateTime.Now:yyyyMMddHHmmss}.csv");
// 返回 true 时自动弹出提示框
await ShowToast(ret);

View File

@ -143,6 +143,7 @@ internal static class ServicesExtensions
// 增加 Table Excel 导出服务
services.AddBootstrapBlazorTableExcelExport();
services.AddTransient<ITableExportPdf, TableExportPdfService>();
return services;
}

View File

@ -5136,6 +5136,8 @@
"TablesExportNote2": "Add Table Excel export service",
"TablesExportShowExportButtonTitle": "Table export function",
"TablesExportShowExportButtonIntro": "Whether to display the export button by setting the <code>ShowExportButton</code> property, the default is <code>false</code>",
"TablesExportShowExportCsvButtonTitle": "Export Csv/Pdf Button",
"TablesExportShowExportCsvButtonIntro": "Show/hide <code>Csv/Pdf</code> button by set <code>ShowExportCsvButton=\"true\"</code> <code>ShowExportPdfButton=\"true\"</code>",
"TablesExportOnExportAsyncTitle": "Custom export method",
"TablesExportOnExportAsyncIntro": "You can customize the export method by setting the <code>OnExportAsync</code> callback delegate method. If you don't set it, the built-in export function of the component will be used.",
"TablesExportButtonDropdownTemplateTitle": "Custom export dropdown button",

View File

@ -5136,6 +5136,8 @@
"TablesExportNote2": "增加 Table Excel 导出服务",
"TablesExportShowExportButtonTitle": "表格导出功能",
"TablesExportShowExportButtonIntro": "通过设置 <code>ShowExportButton=\"true\"</code> 属性是否显示导出按钮,默认为<code>false</code>",
"TablesExportShowExportCsvButtonTitle": "导出 Csv/Pdf",
"TablesExportShowExportCsvButtonIntro": "通过设置 <code>ShowExportCsvButton=\"true\"</code> <code>ShowExportPdfButton=\"true\"</code> 控制 <code>Csv/Pdf</code> 导出按钮",
"TablesExportOnExportAsyncTitle": "自定义导出方法",
"TablesExportOnExportAsyncIntro": "通过设置 <code>OnExportAsync</code> 回调委托方法可自定义导出方法,不设置将使用组件内置导出函数",
"TablesExportButtonDropdownTemplateTitle": "自定义导出下拉框按钮",

View File

@ -7,17 +7,12 @@ namespace BootstrapBlazor.Server.Services;
/// <summary>
/// 演示网站示例数据注入服务实现类
/// </summary>
internal class TableDemoDataService<TModel> : DataServiceBase<TModel> where TModel : class, new()
class TableDemoDataService<TModel>(IStringLocalizer<TModel> localizer) : DataServiceBase<TModel> where TModel : class, new()
{
[NotNull]
private List<TModel>? Items { get; set; }
private IStringLocalizer<TModel> Localizer { get; set; }
public TableDemoDataService(IStringLocalizer<TModel> localizer)
{
Localizer = localizer;
}
private IStringLocalizer<TModel> Localizer { get; } = localizer;
/// <summary>
/// 查询操作方法

View File

@ -0,0 +1,46 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
namespace BootstrapBlazor.Server.Services;
class TableExportPdfService(IWebHostEnvironment webHostEnvironment, NavigationManager navigationManager, IHtml2Pdf html2Pdf) : ITableExportPdf
{
private readonly IWebHostEnvironment _webHostEnvironment = webHostEnvironment;
private readonly NavigationManager _navigationManager = navigationManager;
private readonly IHtml2Pdf _html2Pdf = html2Pdf;
public async Task<byte[]> PdfDataAsync(string content)
{
var url = await GenerateHtmlAsync(content);
// 生成 Pdf
return await _html2Pdf.PdfDataAsync(url);
}
public async Task<Stream> PdfStreamAsync(string content)
{
var url = await GenerateHtmlAsync(content);
// 生成 Pdf
return await _html2Pdf.PdfStreamAsync(url);
}
private async Task<string> GenerateHtmlAsync(string content)
{
// 通过 template 模板文件生成网页文件
var templateFileName = Path.Combine(_webHostEnvironment.WebRootPath, "pdf/template.htm");
var template = await File.ReadAllTextAsync(templateFileName);
// 生成静态 html 文件
var htmlFileName = $"pdf/{Guid.NewGuid()}.html";
var filePath = Path.Combine(_webHostEnvironment.WebRootPath, htmlFileName);
using var writer = File.CreateText(filePath);
await writer.WriteLineAsync(string.Format(template, content));
await writer.FlushAsync();
writer.Close();
// 拼接导出文件网址
return $"{_navigationManager.BaseUri}{htmlFileName}";
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<Version>8.0.5</Version>
<Version>8.0.6-beta01</Version>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">

View File

@ -125,6 +125,13 @@
}
else
{
if (ShowExportCsvButton)
{
<div class="dropdown-item" @onclick="@ExportCsvAsync">
<i class="@CsvExportIcon"></i>
<span>@ExportCsvDropdownItemText</span>
</div>
}
if (ShowExportExcelButton)
{
<div class="dropdown-item" @onclick="@ExportExcelAsync">

View File

@ -414,6 +414,7 @@ public partial class Table<TItem>
ExportToastTitle ??= Localizer[nameof(ExportToastTitle)];
ExportToastContent ??= Localizer[nameof(ExportToastContent)];
ExportToastInProgressContent ??= Localizer[nameof(ExportToastInProgressContent)];
ExportCsvDropdownItemText ??= Localizer[nameof(ExportCsvDropdownItemText)];
ExportExcelDropdownItemText ??= Localizer[nameof(ExportExcelDropdownItemText)];
ExportPdfDropdownItemText ??= Localizer[nameof(ExportPdfDropdownItemText)];
CopyColumnTooltipText ??= Localizer[nameof(CopyColumnTooltipText)];

View File

@ -63,6 +63,12 @@ public partial class Table<TItem>
[Parameter]
public bool ShowExportExcelButton { get; set; } = true;
/// <summary>
/// 获得/设置 是否显示 Csv 导出按钮 默认为 false 显示
/// </summary>
[Parameter]
public bool ShowExportCsvButton { get; set; }
/// <summary>
/// 获得/设置 是否显示 Pdf 导出按钮 默认为 false 显示
/// </summary>
@ -75,6 +81,12 @@ public partial class Table<TItem>
[Parameter]
public string? ExportButtonIcon { get; set; }
/// <summary>
/// 获得/设置 内置导出 Csv 按钮图标
/// </summary>
[Parameter]
public string? CsvExportIcon { get; set; }
/// <summary>
/// 获得/设置 内置导出 Excel 按钮图标
/// </summary>
@ -117,6 +129,12 @@ public partial class Table<TItem>
[Parameter]
public RenderFragment<ITableExportContext<TItem>>? ExportButtonDropdownTemplate { get; set; }
/// <summary>
/// 获得/设置 内置导出微软 Csv 按钮文本 默认 null 读取资源文件
/// </summary>
[Parameter]
public string? ExportCsvDropdownItemText { get; set; }
/// <summary>
/// 获得/设置 内置导出微软 Excel 按钮文本 默认 null 读取资源文件
/// </summary>
@ -374,16 +392,12 @@ public partial class Table<TItem>
[Inject]
[NotNull]
private ITableExcelExport? ExcelExport { get; set; }
[Inject]
[NotNull]
private ITablePdfExport? PdfExport { get; set; }
private ITableExport? TableExport { get; set; }
/// <summary>
/// 获得/设置 各列是否显示状态集合
/// </summary>
private List<ColumnVisibleItem> VisibleColumns { get; } = new();
private List<ColumnVisibleItem> VisibleColumns { get; } = [];
/// <summary>
/// 获得当前可见列集合
@ -1021,15 +1035,19 @@ public partial class Table<TItem>
private Task ExportAsync() => ExecuteExportAsync(() => OnExportAsync != null
? OnExportAsync(new TableExportDataContext<TItem>(TableExportType.Unknown, Rows, GetVisibleColumns(), BuildQueryPageOptions()))
: ExcelExport.ExportAsync(Rows, GetVisibleColumns()));
: TableExport.ExportAsync(Rows, GetVisibleColumns()));
private Task ExportCsvAsync() => ExecuteExportAsync(() => OnExportAsync != null
? OnExportAsync(new TableExportDataContext<TItem>(TableExportType.Pdf, Rows, GetVisibleColumns(), BuildQueryPageOptions()))
: TableExport.ExportCsvAsync(Rows, GetVisibleColumns()));
private Task ExportPdfAsync() => ExecuteExportAsync(() => OnExportAsync != null
? OnExportAsync(new TableExportDataContext<TItem>(TableExportType.Pdf, Rows, GetVisibleColumns(), BuildQueryPageOptions()))
: PdfExport.ExportAsync(Rows, GetVisibleColumns()));
: TableExport.ExportPdfAsync(Rows, GetVisibleColumns()));
private Task ExportExcelAsync() => ExecuteExportAsync(() => OnExportAsync != null
? OnExportAsync(new TableExportDataContext<TItem>(TableExportType.Excel, Rows, GetVisibleColumns(), BuildQueryPageOptions()))
: ExcelExport.ExportAsync(Rows, GetVisibleColumns()));
: TableExport.ExportExcelAsync(Rows, GetVisibleColumns()));
/// <summary>
/// 获取当前 Table 选中的所有行数据

View File

@ -130,9 +130,9 @@ public partial class Table<TItem> : ITable, IModelEqualityComparer<TItem> where
private static string? GetColspan(int colspan) => colspan > 1 ? colspan.ToString() : null;
private bool IsShowFooter => ShowFooter && (Rows.Any() || !IsHideFooterWhenNoData);
private bool IsShowFooter => ShowFooter && (Rows.Count > 0 || !IsHideFooterWhenNoData);
private int PageStartIndex => Rows.Any() ? (PageIndex - 1) * PageItems + 1 : 0;
private int PageStartIndex => Rows.Count > 0 ? (PageIndex - 1) * PageItems + 1 : 0;
private string? PageInfoLabelString => Localizer[nameof(PageInfoText), PageStartIndex, (PageIndex - 1) * PageItems + Rows.Count, TotalCount];
@ -187,7 +187,7 @@ public partial class Table<TItem> : ITable, IModelEqualityComparer<TItem> where
/// <summary>
/// 明细行集合用于数据懒加载
/// </summary>
protected List<TItem> ExpandRows { get; } = new List<TItem>();
protected List<TItem> ExpandRows { get; } = [];
/// <summary>
/// 获得/设置 组件工作模式为 Excel 模式 默认 false
@ -350,7 +350,7 @@ public partial class Table<TItem> : ITable, IModelEqualityComparer<TItem> where
/// <summary>
/// 明细行集合用于数据懒加载
/// </summary>
protected List<TItem> DetailRows { get; } = new List<TItem>();
protected List<TItem> DetailRows { get; } = [];
/// <summary>
/// 获得 表头集合
@ -695,8 +695,9 @@ public partial class Table<TItem> : ITable, IModelEqualityComparer<TItem> where
RefreshButtonIcon ??= IconTheme.GetIconByKey(ComponentIcons.TableRefreshButtonIcon);
CardViewButtonIcon ??= IconTheme.GetIconByKey(ComponentIcons.TableCardViewButtonIcon);
ColumnListButtonIcon ??= IconTheme.GetIconByKey(ComponentIcons.TableColumnListButtonIcon);
ExcelExportIcon ??= IconTheme.GetIconByKey(ComponentIcons.TableExcelExportIcon);
PdfExportIcon ??= IconTheme.GetIconByKey(ComponentIcons.TablePdfExportIcon);
CsvExportIcon ??= IconTheme.GetIconByKey(ComponentIcons.TableExportCsvIcon);
ExcelExportIcon ??= IconTheme.GetIconByKey(ComponentIcons.TableExportExcelIcon);
PdfExportIcon ??= IconTheme.GetIconByKey(ComponentIcons.TableExportPdfIcon);
SearchButtonIcon ??= IconTheme.GetIconByKey(ComponentIcons.TableSearchButtonIcon);
ResetSearchButtonIcon ??= IconTheme.GetIconByKey(ComponentIcons.TableResetSearchButtonIcon);
CloseButtonIcon ??= IconTheme.GetIconByKey(ComponentIcons.TableCloseButtonIcon);
@ -845,6 +846,8 @@ public partial class Table<TItem> : ITable, IModelEqualityComparer<TItem> where
private string? GetTableName(bool hasHeader) => hasHeader ? ClientTableName : null;
private readonly JsonSerializerOptions _serializerOption = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
private async Task<IEnumerable<ColumnWidth>> ReloadColumnWidth()
{
IEnumerable<ColumnWidth>? ret = null;
@ -858,10 +861,7 @@ public partial class Table<TItem> : ITable, IModelEqualityComparer<TItem> where
var doc = JsonDocument.Parse(jsonData);
if (doc.RootElement.TryGetProperty("cols", out var element))
{
ret = element.Deserialize<IEnumerable<ColumnWidth>>(new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
ret = element.Deserialize<IEnumerable<ColumnWidth>>(_serializerOption);
}
if (doc.RootElement.TryGetProperty("table", out var tableEl) && tableEl.TryGetInt32(out var tableWidth))
{
@ -1106,7 +1106,7 @@ public partial class Table<TItem> : ITable, IModelEqualityComparer<TItem> where
var onValueChanged = Utility.GetOnValueChangedInvoke<IDynamicObject>(col.PropertyType);
if (DynamicContext.OnValueChanged != null)
{
var parameters = col.ComponentParameters?.ToList() ?? new List<KeyValuePair<string, object>>();
var parameters = col.ComponentParameters?.ToList() ?? [];
parameters.Add(new(nameof(ValidateBase<string>.OnValueChanged), onValueChanged.Invoke(d, col, (model, column, val) => DynamicContext.OnValueChanged(model, column, val))));
col.ComponentParameters = parameters;
}
@ -1134,7 +1134,7 @@ public partial class Table<TItem> : ITable, IModelEqualityComparer<TItem> where
/// <summary>
/// 获得 过滤集合
/// </summary>
public Dictionary<string, IFilterAction> Filters { get; } = new();
public Dictionary<string, IFilterAction> Filters { get; } = [];
#endregion
private async ValueTask<ItemsProviderResult<TItem>> LoadItems(ItemsProviderRequest request)
@ -1165,7 +1165,7 @@ public partial class Table<TItem> : ITable, IModelEqualityComparer<TItem> where
.AddClass("is-dbcell", trigger)
.Build();
private bool IsShowEmpty => ShowEmpty && !Rows.Any();
private bool IsShowEmpty => ShowEmpty && Rows.Count == 0;
private int GetColumnCount()
{
@ -1224,7 +1224,7 @@ public partial class Table<TItem> : ITable, IModelEqualityComparer<TItem> where
/// 返回 true 时按钮禁用
/// </summary>
/// <returns></returns>
private bool GetDeleteButtonStatus() => ShowAddForm || AddInCell || !SelectedRows.Any();
private bool GetDeleteButtonStatus() => ShowAddForm || AddInCell || SelectedRows.Count == 0;
private async Task InvokeItemsChanged()
{

View File

@ -555,14 +555,19 @@ public enum ComponentIcons
TableColumnListButtonIcon,
/// <summary>
/// Table 组件 ExcelExportIcon 属性图标
/// Table 组件 ExportExcelIcon 属性图标
/// </summary>
TableExcelExportIcon,
TableExportExcelIcon,
/// <summary>
/// Table 组件 PdfExportIcon 属性图标
/// Table 组件 ExportCsvIcon 属性图标
/// </summary>
TablePdfExportIcon,
TableExportCsvIcon,
/// <summary>
/// Table 组件 ExportPdfIcon 属性图标
/// </summary>
TableExportPdfIcon,
/// <summary>
/// Table 组件 SearchButtonIcon 属性图标

View File

@ -37,9 +37,6 @@ public static class BootstrapBlazorServiceCollectionExtensions
services.TryAddSingleton<IZipArchiveService, DefaultZipArchiveService>();
services.TryAddSingleton(typeof(IDispatchService<>), typeof(DefaultDispatchService<>));
services.TryAddTransient<ITableExcelExport, DefaultExcelExport>();
services.TryAddTransient<ITablePdfExport, DefaultPdfExport>();
services.TryAddScoped(typeof(IDataService<>), typeof(NullDataService<>));
services.TryAddScoped<IIPLocatorProvider, DefaultIPLocatorProvider>();
services.TryAddScoped<IReconnectorProvider, ReconnectorProvider>();
@ -63,6 +60,9 @@ public static class BootstrapBlazorServiceCollectionExtensions
services.AddScoped<NotificationService>();
services.AddScoped<EyeDropperService>();
services.TryAddTransient<ITableExport, DefaultTableExport>();
services.TryAddTransient<ITableExportPdf, DefaultTableExportPdf>();
services.ConfigureBootstrapBlazorOption(configureOptions);
services.ConfigureIPLocatorOption();

View File

@ -225,6 +225,7 @@
"ExportToastTitle": "Export",
"ExportToastContent": "Export data {0}, auto close after {1}s",
"ExportToastInProgressContent": "Exporting data, please wait a moment, auto close after {0}s",
"ExportCsvDropdownItemText": "MS-Csv",
"ExportExcelDropdownItemText": "MS-Excel",
"ExportPdfDropdownItemText": "Pdf",
"PageInfoText": "{0} - {1} Total {2}",

View File

@ -225,6 +225,7 @@
"ExportToastTitle": "导出数据",
"ExportToastContent": "导出数据 {0}{1} 秒后自动关闭",
"ExportToastInProgressContent": "正在导出数据,请稍后, {0} 秒后自动关闭",
"ExportCsvDropdownItemText": "微软 Csv 格式",
"ExportExcelDropdownItemText": "微软 Excel 格式",
"ExportPdfDropdownItemText": "Pdf 格式",
"PageInfoText": "{0} - {1} 共 {2} 条",

View File

@ -165,8 +165,9 @@ public class IconThemeOptions
{ ComponentIcons.TableRefreshButtonIcon, "fa-solid fa-arrows-rotate" },
{ ComponentIcons.TableCardViewButtonIcon, "fa-solid fa-bars" },
{ ComponentIcons.TableColumnListButtonIcon, "fa-solid fa-table-list" },
{ ComponentIcons.TableExcelExportIcon, "fa-regular fa-file-excel" },
{ ComponentIcons.TablePdfExportIcon, "fa-regular fa-file-pdf" },
{ ComponentIcons.TableExportCsvIcon, "fa-solid fa-fw fa-file-csv" },
{ ComponentIcons.TableExportExcelIcon, "fa-solid fa-fw fa-file-excel" },
{ ComponentIcons.TableExportPdfIcon, "fa-solid fa-fw fa-file-pdf" },
{ ComponentIcons.TableSearchButtonIcon, "fa-solid fa-magnifying-glass" },
{ ComponentIcons.TableResetSearchButtonIcon, "fa-regular fa-trash-can" },
{ ComponentIcons.TableCloseButtonIcon, "fa-solid fa-xmark" },

View File

@ -9,9 +9,9 @@ namespace BootstrapBlazor.Components;
class ComponentHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory) : IComponentHtmlRenderer
{
private IServiceProvider ServiceProvider { get; set; } = serviceProvider;
private readonly IServiceProvider _serviceProvider = serviceProvider;
private ILoggerFactory LoggerFactory { get; set; } = loggerFactory;
private readonly ILoggerFactory _loggerFactory = loggerFactory;
/// <summary>
/// <inheritdoc/>
@ -21,7 +21,7 @@ class ComponentHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory log
/// <returns></returns>
public async Task<string> RenderAsync<TComponent>(IDictionary<string, object?>? parameters = null) where TComponent : IComponent
{
using var htmlRenderer = new HtmlRenderer(ServiceProvider, LoggerFactory);
using var htmlRenderer = new HtmlRenderer(_serviceProvider, _loggerFactory);
var html = await htmlRenderer.Dispatcher.InvokeAsync(async () =>
{
parameters ??= new Dictionary<string, object?>();
@ -40,7 +40,7 @@ class ComponentHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory log
/// <returns></returns>
public async Task<string> RenderAsync(Type componentType, IDictionary<string, object?>? parameters = null)
{
using var htmlRenderer = new HtmlRenderer(ServiceProvider, LoggerFactory);
using var htmlRenderer = new HtmlRenderer(_serviceProvider, _loggerFactory);
var html = await htmlRenderer.Dispatcher.InvokeAsync(async () =>
{
parameters ??= new Dictionary<string, object?>();

View File

@ -4,18 +4,9 @@
namespace BootstrapBlazor.Components;
internal class DefaultIconTheme : IIconTheme
class DefaultIconTheme(IOptions<IconThemeOptions> options) : IIconTheme
{
private IOptions<IconThemeOptions> Options { get; set; }
/// <summary>
///
/// </summary>
/// <param name="options"></param>
public DefaultIconTheme(IOptions<IconThemeOptions> options)
{
Options = options;
}
private readonly IOptions<IconThemeOptions> _options = options;
/// <summary>
/// <inheritdoc/>
@ -23,9 +14,9 @@ internal class DefaultIconTheme : IIconTheme
/// <returns></returns>
public Dictionary<ComponentIcons, string> GetIcons()
{
if (!Options.Value.Icons.TryGetValue(Options.Value.ThemeKey, out var icons))
if (!_options.Value.Icons.TryGetValue(_options.Value.ThemeKey, out var icons))
{
icons = new Dictionary<ComponentIcons, string>();
icons = [];
}
return icons;
}

View File

@ -1,21 +0,0 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
namespace BootstrapBlazor.Components;
class DefaultPdfExport : ITablePdfExport
{
/// <summary>
/// <inheritdoc/>
/// </summary>
/// <typeparam name="TItem"></typeparam>
/// <param name="items"></param>
/// <param name="cols"></param>
/// <param name="fileName"></param>
/// <returns></returns>
public Task<bool> ExportAsync<TItem>(IEnumerable<TItem> items, IEnumerable<ITableColumn>? cols = null, string? fileName = null) where TItem : class
{
return Task.FromResult(false);
}
}

View File

@ -4,7 +4,7 @@
namespace BootstrapBlazor.Components;
class DefaultExcelExport : ITableExcelExport
class DefaultTableExport : ITableExport
{
/// <summary>
/// <inheritdoc/>
@ -15,6 +15,15 @@ class DefaultExcelExport : ITableExcelExport
return Task.FromResult(false);
}
/// <summary>
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
public Task<bool> ExportExcelAsync<TItem>(IEnumerable<TItem> items, IEnumerable<ITableColumn>? cols = null, string? fileName = null)
{
return Task.FromResult(false);
}
/// <summary>
/// <inheritdoc/>
/// </summary>
@ -27,4 +36,17 @@ class DefaultExcelExport : ITableExcelExport
{
return Task.FromResult(false);
}
/// <summary>
/// <inheritdoc/>
/// </summary>
/// <typeparam name="TItem"></typeparam>
/// <param name="items"></param>
/// <param name="cols"></param>
/// <param name="fileName"></param>
/// <returns></returns>
public Task<bool> ExportPdfAsync<TItem>(IEnumerable<TItem> items, IEnumerable<ITableColumn>? cols, string? fileName = null)
{
return Task.FromResult(false);
}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
namespace BootstrapBlazor.Components;
[ExcludeFromCodeCoverage]
class DefaultTableExportPdf : ITableExportPdf
{
/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public Task<byte[]> PdfDataAsync(string content) => Task.FromResult(Array.Empty<byte>());
/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public Task<Stream> PdfStreamAsync(string content) => Task.FromResult(Stream.Null);
}

View File

@ -2,8 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
using PuppeteerSharp;
namespace BootstrapBlazor.Components;
/// <summary>
@ -15,13 +13,11 @@ public interface IHtml2Pdf
/// Export method
/// </summary>
/// <param name="url">url</param>
/// <param name="options">the instance of PdfOptions</param>
Task<byte[]> ExportDataAsync(string url, PdfOptions? options = null);
Task<byte[]> PdfDataAsync(string url);
/// <summary>
/// 导出流
/// Export method
/// </summary>
/// <param name="url">url</param>
/// <param name="options">the instance of PdfOptions</param>
Task<Stream> ExportStreamAsync(string url, PdfOptions? options = null);
Task<Stream> PdfStreamAsync(string url);
}

View File

@ -7,6 +7,7 @@ namespace BootstrapBlazor.Components;
/// <summary>
/// Table 组件 Excel 导出接口
/// </summary>
[Obsolete("已过期,请使用 ITableExport 代替 Please use ITableExport instead")]
public interface ITableExcelExport
{
/// <summary>
@ -24,4 +25,12 @@ public interface ITableExcelExport
/// <param name="cols">当前可见列数据集合 默认 null 导出全部列</param>
/// <param name="fileName">文件名 默认 null ExportData_{DateTime.Now:yyyyMMddHHmmss}.xlsx</param>
Task<bool> ExportCsvAsync<TModel>(IEnumerable<TModel> items, IEnumerable<ITableColumn>? cols, string? fileName = null);
/// <summary>
/// 导出 Pdf 方法
/// </summary>
/// <param name="items">导出数据集合</param>
/// <param name="cols">当前可见列数据集合 默认 null 导出全部列</param>
/// <param name="fileName">文件名 默认 null ExportData_{DateTime.Now:yyyyMMddHHmmss}.xlsx</param>
Task<bool> ExportPdfAsync<TModel>(IEnumerable<TModel> items, IEnumerable<ITableColumn>? cols, string? fileName = null);
}

View File

@ -0,0 +1,43 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
namespace BootstrapBlazor.Components;
/// <summary>
/// Table 组件 Excel 导出接口
/// </summary>
public interface ITableExport
{
/// <summary>
/// 导出 方法
/// </summary>
/// <param name="items">导出数据集合</param>
/// <param name="cols">当前可见列数据集合 默认 null 导出全部列</param>
/// <param name="fileName">文件名 默认 null ExportData_{DateTime.Now:yyyyMMddHHmmss}.xlsx</param>
Task<bool> ExportAsync<TModel>(IEnumerable<TModel> items, IEnumerable<ITableColumn>? cols, string? fileName = null);
/// <summary>
/// 导出 Excel 方法
/// </summary>
/// <param name="items">导出数据集合</param>
/// <param name="cols">当前可见列数据集合 默认 null 导出全部列</param>
/// <param name="fileName">文件名 默认 null ExportData_{DateTime.Now:yyyyMMddHHmmss}.xlsx</param>
Task<bool> ExportExcelAsync<TModel>(IEnumerable<TModel> items, IEnumerable<ITableColumn>? cols, string? fileName = null);
/// <summary>
/// 导出 Csv 方法
/// </summary>
/// <param name="items">导出数据集合</param>
/// <param name="cols">当前可见列数据集合 默认 null 导出全部列</param>
/// <param name="fileName">文件名 默认 null ExportData_{DateTime.Now:yyyyMMddHHmmss}.xlsx</param>
Task<bool> ExportCsvAsync<TModel>(IEnumerable<TModel> items, IEnumerable<ITableColumn>? cols, string? fileName = null);
/// <summary>
/// 导出 Pdf 方法
/// </summary>
/// <param name="items">导出数据集合</param>
/// <param name="cols">当前可见列数据集合 默认 null 导出全部列</param>
/// <param name="fileName">文件名 默认 null ExportData_{DateTime.Now:yyyyMMddHHmmss}.xlsx</param>
Task<bool> ExportPdfAsync<TModel>(IEnumerable<TModel> items, IEnumerable<ITableColumn>? cols, string? fileName = null);
}

View File

@ -0,0 +1,23 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
namespace BootstrapBlazor.Components;
/// <summary>
/// Table 组件 Pdf 导出接口
/// </summary>
public interface ITableExportPdf
{
/// <summary>
/// 导出 Pdf 数据
/// </summary>
/// <returns></returns>
Task<byte[]> PdfDataAsync(string content);
/// <summary>
/// 导出 Pdf 流
/// </summary>
/// <returns></returns>
Task<Stream> PdfStreamAsync(string content);
}

View File

@ -7,6 +7,7 @@ namespace BootstrapBlazor.Components;
/// <summary>
/// Table 组件 Pdf 导出接口
/// </summary>
[Obsolete("已过期,统一使用 ITableExport 接口")]
public interface ITablePdfExport
{
/// <summary>

View File

@ -16,7 +16,7 @@ public class CssBuilder
/// Call Build() to return the completed CSS Classes as a string.
/// </summary>
/// <param name="value"></param>
public static CssBuilder Default(string? value = null) => new CssBuilder(value);
public static CssBuilder Default(string? value = null) => new(value);
/// <summary>
/// Creates a CssBuilder used to define conditional CSS classes used in a component.
@ -25,7 +25,7 @@ public class CssBuilder
/// <param name="value"></param>
protected CssBuilder(string? value)
{
stringBuffer = new List<string>();
stringBuffer = [];
AddClass(value);
}
@ -124,5 +124,5 @@ public class CssBuilder
/// Finalize the completed CSS Classes as a string.
/// </summary>
/// <returns>string</returns>
public string? Build() => stringBuffer.Any() ? string.Join(" ", stringBuffer) : null;
public string? Build() => stringBuffer.Count > 0 ? string.Join(" ", stringBuffer) : null;
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<Version>8.0.1</Version>
<Version>8.0.2-beta01</Version>
</PropertyGroup>
<PropertyGroup>
@ -10,6 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BootstrapBlazor" Version="8.0.6-beta01" />
<PackageReference Include="PuppeteerSharp" Version="13.0.2" />
</ItemGroup>

View File

@ -7,35 +7,39 @@ using PuppeteerSharp;
namespace BootstrapBlazor.Components;
/// <summary>
/// 构造函数
/// 默认 Html to Pdf 实现
/// </summary>
class DefaultPdfService : IHtml2Pdf
{
/// <summary>
/// <inheritdoc/>
/// </summary>
public async Task<byte[]> ExportDataAsync(string url, PdfOptions? options = null)
public async Task<byte[]> PdfDataAsync(string url)
{
using var browserFetcher = new BrowserFetcher();
await browserFetcher.DownloadAsync();
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions() { Headless = true });
await using var page = await browser.NewPageAsync();
await page.GoToAsync(url);
var content = await page.GetContentAsync();
return await page.PdfDataAsync(options ?? new PdfOptions());
return await page.PdfDataAsync();
}
/// <summary>
/// <inheritdoc/>
/// </summary>
public async Task<Stream> ExportStreamAsync(string url, PdfOptions? options = null)
public async Task<Stream> PdfStreamAsync(string url)
{
using var browserFetcher = new BrowserFetcher();
await browserFetcher.DownloadAsync();
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions() { Headless = true });
await using var page = await browser.NewPageAsync();
await page.GoToAsync(url);
var content = await page.GetContentAsync();
return await page.PdfStreamAsync(options ?? new PdfOptions());
return await page.PdfStreamAsync();
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<Version>8.0.1</Version>
<Version>8.0.2-beta01</Version>
</PropertyGroup>
<PropertyGroup>
@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BootstrapBlazor" Version="8.0.5" />
<PackageReference Include="BootstrapBlazor" Version="8.0.6-beta01" />
</ItemGroup>
</Project>

View File

@ -142,8 +142,9 @@ internal class DefaultIcon
{ ComponentIcons.TableRefreshButtonIcon, "mdi mdi-refresh" },
{ ComponentIcons.TableCardViewButtonIcon, "mdi mdi-menu" },
{ ComponentIcons.TableColumnListButtonIcon, "mdi mdi-format-list-bulleted" },
{ ComponentIcons.TableExcelExportIcon, "mdi mdi-file-excel" },
{ ComponentIcons.TablePdfExportIcon, "mdi mdi-file-pdf-box" },
{ ComponentIcons.TableExportCsvIcon, "mdi mdi-file-table-box" },
{ ComponentIcons.TableExportExcelIcon, "mdi mdi-file-excel-box" },
{ ComponentIcons.TableExportPdfIcon, "mdi mdi-file-pdf-box" },
{ ComponentIcons.TableSearchButtonIcon, "mdi mdi-magnify" },
{ ComponentIcons.TableResetSearchButtonIcon, "mdi mdi-trash-can-outline" },
{ ComponentIcons.TableCloseButtonIcon, "mdi mdi-close" },

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<Version>8.0.0</Version>
<Version>8.0.1-beta01</Version>
</PropertyGroup>
<PropertyGroup>
@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BootstrapBlazor" Version="8.0.0" />
<PackageReference Include="BootstrapBlazor" Version="8.0.6-beta01" />
<PackageReference Include="MiniExcel" Version="1.31.2" />
</ItemGroup>

View File

@ -0,0 +1,172 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MiniExcelLibs;
using System.Text;
namespace BootstrapBlazor.Components;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="serviceProvider"></param>
class DefaultTableExport(IServiceProvider serviceProvider) : ITableExport
{
private IServiceProvider ServiceProvider { get; set; } = serviceProvider;
/// <summary>
/// 导出 方法
/// </summary>
/// <param name="items">导出数据集合</param>
/// <param name="cols">导出列集合 默认 null 全部导出</param>
/// <param name="fileName">导出后下载文件名</param>
/// <returns></returns>
public Task<bool> ExportAsync<TModel>(IEnumerable<TModel> items, IEnumerable<ITableColumn>? cols = null, string? fileName = null) => InternalExportAsync(items, cols, ExcelType.XLSX, fileName);
/// <summary>
/// 导出 Excel 方法
/// </summary>
/// <param name="items">导出数据集合</param>
/// <param name="cols">导出列集合 默认 null 全部导出</param>
/// <param name="fileName">导出后下载文件名</param>
/// <returns></returns>
public Task<bool> ExportExcelAsync<TModel>(IEnumerable<TModel> items, IEnumerable<ITableColumn>? cols = null, string? fileName = null) => InternalExportAsync(items, cols, ExcelType.XLSX, fileName);
/// <summary>
/// <inheritdoc/>
/// </summary>
/// <typeparam name="TModel"></typeparam>
/// <param name="items"></param>
/// <param name="cols"></param>
/// <param name="fileName"></param>
/// <returns></returns>
public Task<bool> ExportCsvAsync<TModel>(IEnumerable<TModel> items, IEnumerable<ITableColumn>? cols, string? fileName = null) => InternalExportAsync(items, cols, ExcelType.CSV, fileName);
private async Task<bool> InternalExportAsync<TModel>(IEnumerable<TModel> items, IEnumerable<ITableColumn>? cols, ExcelType excelType, string? fileName = null)
{
var value = new List<Dictionary<string, object?>>();
cols ??= Utility.GetTableColumns<TModel>();
foreach (var item in items)
{
if (item != null)
{
var row = new Dictionary<string, object?>();
foreach (var pi in cols)
{
var val = await FormatValue(pi, Utility.GetPropertyValue(item, pi.GetFieldName()));
row.Add(pi.GetDisplayName(), val);
}
value.Add(row);
}
}
using var stream = new MemoryStream();
await MiniExcel.SaveAsAsync(stream, value, excelType: excelType);
fileName ??= $"ExportData_{DateTime.Now:yyyyMMddHHmmss}.{GetExtension()}";
stream.Position = 0;
var downloadService = ServiceProvider.GetRequiredService<DownloadService>();
await downloadService.DownloadFromStreamAsync(fileName, stream);
return true;
string GetExtension() => excelType == ExcelType.XLSX ? "xlsx" : "csv";
}
private static async Task<object?> FormatValue(ITableColumn col, object? value)
{
var ret = value;
if (col.Lookup != null)
{
ret = col.Lookup.FirstOrDefault(i => i.Value.Equals(value?.ToString(), col.LookupStringComparison))?.Text;
}
if (col.Formatter != null)
{
// 格式化回调委托
ret = await col.Formatter(value);
}
else if (!string.IsNullOrEmpty(col.FormatString))
{
// 格式化字符串
ret = Utility.Format(value, col.FormatString);
}
else if (col.PropertyType.IsEnum())
{
ret = col.PropertyType.ToDescriptionString(value?.ToString());
}
else if (value is IEnumerable<object> v)
{
ret = string.Join(",", v);
}
return ret;
}
/// <summary>
/// <inheritdoc/>
/// </summary>
/// <typeparam name="TModel"></typeparam>
/// <param name="items"></param>
/// <param name="cols"></param>
/// <param name="fileName"></param>
/// <returns></returns>
public async Task<bool> ExportPdfAsync<TModel>(IEnumerable<TModel> items, IEnumerable<ITableColumn>? cols, string? fileName = null)
{
var ret = false;
var logger = ServiceProvider.GetRequiredService<ILogger<DefaultTableExport>>();
try
{
// 生成表格
var html = await GenerateTableHtmlAsync(items, cols);
// 得到 Pdf 文件数据
var pdfService = ServiceProvider.GetRequiredService<ITableExportPdf>();
var stream = await pdfService.PdfStreamAsync(html);
// 下载 Pdf 文件
var downloadService = ServiceProvider.GetRequiredService<DownloadService>();
fileName ??= $"ExportData_{DateTime.Now:yyyyMMddHHmmss}.pdf";
await downloadService.DownloadFromStreamAsync(fileName, stream);
ret = true;
}
catch (Exception ex)
{
logger.LogError(ex, "ExportPdfAsync execute failed");
}
return ret;
}
private static async Task<string> GenerateTableHtmlAsync<TModel>(IEnumerable<TModel> items, IEnumerable<ITableColumn>? cols)
{
var builder = new StringBuilder();
cols ??= Utility.GetTableColumns<TModel>();
builder.AppendLine("<table class=\"table table-bordered table-striped\">");
// 生成表头
builder.AppendLine("<thead><tr>");
foreach (var pi in cols)
{
builder.AppendLine($"<th><div class=\"table-cell\">{pi.GetDisplayName()}</div></th>");
}
builder.AppendLine("</tr></thead>");
builder.AppendLine("<tbody>");
foreach (var item in items)
{
if (item != null)
{
builder.AppendLine("<tr>");
foreach (var pi in cols)
{
var val = await FormatValue(pi, Utility.GetPropertyValue(item, pi.GetFieldName()));
builder.AppendLine($"<td><div class=\"table-cell\">{val}</div></td>");
}
builder.AppendLine("</tr>");
}
}
builder.AppendLine("</tbody>");
builder.AppendLine("</table>");
return builder.ToString();
}
}

View File

@ -1,90 +0,0 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
using Microsoft.Extensions.DependencyInjection;
using MiniExcelLibs;
namespace BootstrapBlazor.Components;
class ExcelExport : ITableExcelExport
{
private IServiceProvider ServiceProvider { get; set; }
/// <summary>
/// 构造函数
/// </summary>
/// <param name="serviceProvider"></param>
public ExcelExport(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
}
/// <summary>
/// 导出 Excel 方法
/// </summary>
/// <param name="items">导出数据集合</param>
/// <param name="cols">导出列集合 默认 null 全部导出</param>
/// <param name="fileName">导出后下载文件名</param>
/// <returns></returns>
public Task<bool> ExportAsync<TModel>(IEnumerable<TModel> items, IEnumerable<ITableColumn>? cols = null, string? fileName = null) => InternalExportAsync(items, cols, ExcelType.XLSX, fileName);
public Task<bool> ExportCsvAsync<TModel>(IEnumerable<TModel> items, IEnumerable<ITableColumn>? cols, string? fileName = null) => InternalExportAsync(items, cols, ExcelType.CSV, fileName);
private async Task<bool> InternalExportAsync<TModel>(IEnumerable<TModel> items, IEnumerable<ITableColumn>? cols, ExcelType excelType, string? fileName = null)
{
var value = new List<Dictionary<string, object?>>();
cols ??= Utility.GetTableColumns<TModel>();
foreach (var item in items)
{
if (item != null)
{
var row = new Dictionary<string, object?>();
foreach (var pi in cols)
{
var val = await FormatValue(pi, Utility.GetPropertyValue(item, pi.GetFieldName()));
row.Add(pi.GetDisplayName(), val);
}
value.Add(row);
}
}
using var stream = new MemoryStream();
await MiniExcel.SaveAsAsync(stream, value, excelType: excelType);
fileName ??= $"ExportData_{DateTime.Now:yyyyMMddHHmmss}.{GetExtension()}";
stream.Position = 0;
var downloadService = ServiceProvider.GetRequiredService<DownloadService>();
await downloadService.DownloadFromStreamAsync(fileName, stream);
return true;
string GetExtension() => excelType == ExcelType.XLSX ? "xlsx" : "csv";
}
private static async Task<object?> FormatValue(ITableColumn col, object? value)
{
var ret = value;
if (col.Lookup != null)
{
ret = col.Lookup.FirstOrDefault(i => i.Value.Equals(value?.ToString(), col.LookupStringComparison))?.Text;
}
if (col.Formatter != null)
{
// 格式化回调委托
ret = await col.Formatter(value);
}
else if (!string.IsNullOrEmpty(col.FormatString))
{
// 格式化字符串
ret = Utility.Format(value, col.FormatString);
}
else if (col.PropertyType.IsEnum())
{
ret = col.PropertyType.ToDescriptionString(value?.ToString());
}
else if (value is IEnumerable<object> v)
{
ret = string.Join(",", v);
}
return ret;
}
}

View File

@ -7,7 +7,7 @@ namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// BootstrapBlazor 服务扩展类
/// </summary>
public static class TableExcelExportServiceCollectionExtensions
public static class TableExportServiceCollectionExtensions
{
/// <summary>
/// 增加 BootstrapBlazor 服务
@ -16,7 +16,7 @@ public static class TableExcelExportServiceCollectionExtensions
/// <returns></returns>
public static IServiceCollection AddBootstrapBlazorTableExcelExport(this IServiceCollection services)
{
services.AddTransient<ITableExcelExport, ExcelExport>();
services.AddTransient<ITableExport, DefaultTableExport>();
return services;
}
}

View File

@ -752,7 +752,7 @@ public class TableTest : TableTestBase
}
[Fact]
public void ShowExportButton_Ok()
public async Task ShowExportButton_Ok()
{
var localizer = Context.Services.GetRequiredService<IStringLocalizer<Foo>>();
var cut = Context.RenderComponent<BootstrapBlazorRoot>(pb =>
@ -789,15 +789,22 @@ public class TableTest : TableTestBase
// Excel 导出图标监测
// Pdf 导出图标监测
table.DoesNotContain("fa-regular fa-file-excel");
table.DoesNotContain("fa-regular fa-file-pdf");
table.DoesNotContain("fa-solid fa-fw fa-file-excel");
table.DoesNotContain("fa-solid fa-fw fa-file-pdf");
table.SetParametersAndRender(pb =>
{
pb.Add(a => a.ShowExportCsvButton, true);
pb.Add(a => a.ShowExportExcelButton, true);
pb.Add(a => a.ShowExportPdfButton, true);
});
table.Contains("fa-regular fa-file-excel");
table.Contains("fa-regular fa-file-pdf");
table.Contains("fa-solid fa-fw fa-file-csv");
table.Contains("fa-solid fa-fw fa-file-excel");
table.Contains("fa-solid fa-fw fa-file-pdf");
// 导出 csv
var button = table.Find(".fa-file-csv");
await table.InvokeAsync(() => button.Click());
}
[Fact]
@ -807,7 +814,7 @@ public class TableTest : TableTestBase
ITableExportDataContext<Foo>? exportContext = null;
bool exported = false;
var localizer = Context.Services.GetRequiredService<IStringLocalizer<Foo>>();
var export = Context.Services.GetRequiredService<ITableExcelExport>();
var export = Context.Services.GetRequiredService<ITableExport>();
var cut = Context.RenderComponent<BootstrapBlazorRoot>(pb =>
{
pb.AddChildContent<Table<Foo>>(pb =>
@ -6158,6 +6165,7 @@ public class TableTest : TableTestBase
pb.Add(a => a.OnQueryAsync, OnQueryAsync(localizer));
pb.Add(a => a.ShowToolbar, true);
pb.Add(a => a.ShowExportButton, true);
pb.Add(a => a.ShowExportCsvButton, true);
pb.Add(a => a.ShowExportPdfButton, true);
pb.Add(a => a.TableColumns, foo => builder =>
{
@ -6169,33 +6177,29 @@ public class TableTest : TableTestBase
});
});
cut.InvokeAsync(() =>
var buttons = cut.FindAll(".dropdown-menu-end .dropdown-item");
foreach (var button in buttons)
{
var button = cut.Find(".dropdown-menu-end .dropdown-item");
button.Click();
});
cut.InvokeAsync(() =>
{
var buttons = cut.FindAll(".dropdown-menu-end .dropdown-item");
buttons[buttons.Count - 1].Click();
});
cut.InvokeAsync(() =>
{
button.Click();
});
}
var table = cut.FindComponent<Table<Foo>>();
table.SetParametersAndRender(pb =>
{
pb.Add(a => a.OnExportAsync, _ => Task.FromResult(true));
});
cut.InvokeAsync(() =>
buttons = cut.FindAll(".dropdown-menu-end .dropdown-item");
foreach (var button in buttons)
{
var button = cut.Find(".dropdown-menu-end .dropdown-item");
button.Click();
});
cut.InvokeAsync(() =>
{
var buttons = cut.FindAll(".dropdown-menu-end .dropdown-item");
buttons[buttons.Count - 1].Click();
});
cut.InvokeAsync(() =>
{
button.Click();
});
}
}
[Fact]