EasyAdminBlazor 第十篇:数据导入导出——批量处理不再麻烦
前面九篇我们讲了快速搭建、目录结构、CRUD、权限、多租户等。今天这篇非常实用——数据导入导出。
做过后台系统的都知道,导入导出是刚需。用户总有几千条数据要批量录入,总有报表要导出整理。如果每个模块都自己写一遍导入导出逻辑,那简直是重复造轮子的噩梦。
EasyAdminBlazor 提供了一套开箱即用的导入导出机制,有两种玩法:
| 模式 | 适用场景 | 代码量 |
|---|---|---|
| 通用导入导出 | 简单实体,直接映射 Excel 列 | 零代码 |
| 自定义导入导出 | 复杂业务,需要特殊处理(去重、关联、动态列) | 几十行 |
下面我们一个一个看。
一、通用导入导出:一行代码都不用写
启用方式
在 AdminTable 组件上,加两个属性:
<AdminTable TItem="Product" TKey="long"
ShowImportButton="true"
ShowExportButton="true">
</AdminTable>
搞定。 页面上会出现「导入」和「导出」按钮。
导出效果
点击「导出」,自动下载一个 Product.xlsx 文件,包含当前表格的所有数据,列名就是实体属性的中文名称。
导入效果
点击「导入」,弹出导入对话框:
- 下载模板 — 点击「下载模板」,系统自动生成一个空 Excel,列结构跟表格一致
- 上传文件 — 把填好的 Excel 拖拽或选择上传(最大 5MB)
- 自动处理 — 系统用 MiniExcel 自动把 Excel 行映射为实体对象,调用
InsertOrUpdate批量写入数据库 - 完成 — 导入成功,表格自动刷新
整个过程,你不需要写一行代码。
原理是什么?
// AdminTable 内部处理流程(简化)
// 1. MiniExcel 将 Excel 行直接映射为 List<TItem>
var rows = memoryStream.Query<TItem>().ToList();
// 2. 如果有 OnBeforeImportAsync,触发导入前事件
if (OnBeforeImportAsync.HasDelegate) { /* 用户自定义预处理 */ }
// 3. 默认:批量插入或更新
// 如果定义了 OnImportAsync,则调用自定义逻辑
// 否则自动 InsertOrUpdate
affectedRows = OnImportAsync == null
? await _repo.Orm.InsertOrUpdate<TItem>()
.SetSource(rows)
.ExecuteAffrowsAsync()
: await OnImportAsync(rows);
// 4. 如果有 OnFinishImportAsync,触发导入后事件
if (OnFinishImportAsync.HasDelegate) { /* 用户自定义后处理 */ }
看到没?默认行为就是全自动的——Excel → 实体 → 数据库,一条龙。
二、自定义导入:处理复杂业务场景
通用导入虽然方便,但真实业务往往没这么简单。比如:
- Excel 里是中文列名,不能直接映射实体属性
- 导入时要按业务键去重(相同的记录不重复插入)
- 需要自动创建关联数据(比如 Excel 中的厂商名称,要自动匹配或创建厂商记录)
- 不同分类的列结构不一样(电子元件的属性列是动态的)
这时候就需要自定义导入了。EasyAdminBlazor 提供了两种自定义方式:
| 回调 | 用途 |
|---|---|
OnImportAsync |
自定义保存逻辑,但列映射仍然是实体属性 |
OnImportFromDictionaryAsync |
完全自定义,Excel 按字典方式读取(中文列名) |
最强大的是 OnImportFromDictionaryAsync——它把 Excel 的每一行以 IDictionary<string, object> 的方式交给你处理,列名就是 Excel 表头,你想怎么解析就怎么解析。
实战:电子元件产品导入
下面以一个真实的电子元件管理项目为例,看看自定义导入怎么实现。
第一步:启用自定义导入
<AdminTable @ref="product"
TItem="TpProduct" TKey="long"
ShowImportButton="@(selectedCategory != null)"
ShowExportButton="@(selectedCategory != null)"
OnImportFromDictionaryAsync="OnImportFromDictionaryAsync"
OnExportAllDataAsync="OnExportAllDataAsync">
注意:这里的 ShowImportButton 绑定了一个条件——先选分类,才能导入,因为不同分类的 Excel 列结构不同。
第二步:实现导入逻辑
private async Task OnImportFromDictionaryAsync(AdminImportDictionaryEventArgs e)
{
// 1. 预加载已有数据(用于判断更新还是插入)
var allProducts = (await repo.Select
.Where(x => x.CategoryId == selectedCategory.Id)
.ToListAsync(x => new {
Key = x.Model + x.ManufacturerId,
Product = x
}))
.ToDictionary(x => x.Key, x => x.Product);
// 2. 按(型号+厂商)去重
foreach (var item in e.Items.DistinctBy(x => new {
型号 = x["型号"]?.ToString()?.Trim() ?? "",
厂商 = x["厂商"]?.ToString()?.Trim() ?? ""
}))
{
// 3. 自动匹配或创建厂商
var mfrName = item["厂商"]?.ToString();
if (!manufacturers.ContainsKey(mfrName))
{
var mfr = await repoMfr.InsertAsync(new TpManufacturer {
CnName = mfrName,
EnName = mfrName,
Enable = false // 新厂商默认为未启用
});
manufacturers.Add(mfrName, mfr.Id);
}
// 4. 构建产品实体
var product = new TpProduct {
CategoryId = selectedCategory.Id,
ManufacturerId = manufacturers[mfrName],
Model = item["型号"]?.ToString()?.Trim(),
RoHS = ParseRoHS(item["RoHS"]?.ToString()),
StockQuantity = int.TryParse(
item["库存"]?.ToString()?.Trim(), out var qty) ? qty : 0,
Attributes = new List<TpProductAttribute>()
};
// 5. 判断插入还是更新
var key = product.Model + product.ManufacturerId;
if (allProducts.ContainsKey(key))
{
product.Id = allProducts[key].Id; // 更新已有记录
product.Content = allProducts[key].Content; // 保留原内容
}
// 6. 处理动态属性列
foreach (var attr in selectedCategory.Attributes)
{
product.Attributes.Add(new TpProductAttribute {
AttributeId = attr.Id,
Value = item[attr.Name]?.ToString()?.Trim()
});
}
// 7. 保存
await repo.InsertOrUpdateAsync(product);
}
}
这段代码做了很多智能处理:
| 处理 | 说明 |
|---|---|
| 去重 | 按「型号+厂商」去重,Excel 中重复的行不会重复导入 |
| 自动建厂商 | 如果 Excel 中的厂商在数据库不存在,自动创建(标记为未启用,人工审核) |
| 更新 vs 插入 | 已存在的产品更新,不存在的插入 |
| 保留旧值 | 更新时保留原有的富文本内容等字段 |
| 动态属性 | 根据分类的自定义属性定义,动态解析 Excel 中的属性列 |
| RoHS 智能解析 | 支持 0/FALSE/N/YES/TRUE 等多种格式 |
三、自定义导出:动态列结构
导出也一样,通用导出是零配置的,但如果你需要动态列,可以用自定义导出。
在同一个电子元件项目中,不同分类的产品有不同的属性列:
分类A(电阻):型号、厂商、封装、阻值、功率、温度系数
分类B(电容):型号、厂商、封装、容值、电压、材质
导出的 Excel 列结构需要根据当前选中的分类动态变化:
private async Task OnExportAllDataAsync(AdminExportEventArgs<TpProduct> e)
{
// 1. 构建基础列
var columns = new List<DynamicExcelColumn>
{
new("Model") { Name = "型号", Width = 15 },
new("Manufacturer") { Name = "厂商", Width = 15 },
new("Description") { Name = "描述", Width = 15 },
new("StockQuantity"){ Name = "库存", Width = 15 },
};
// 2. 根据分类配置动态添加列
if (selectedCategory.ShowPackage)
columns.Insert(2, new DynamicExcelColumn("Package")
{ Name = "封装", Width = 15 });
if (selectedCategory.ShowProductSn)
columns.Add(new DynamicExcelColumn("ProductSn")
{ Name = "产品编码", Width = 15 });
// 3. 添加分类自定义属性列
foreach (var attr in selectedCategory.Attributes.OrderBy(x => x.Sort))
columns.Add(new DynamicExcelColumn(attr.Name)
{ Name = attr.Name, Width = 15 });
// 4. 配置自动筛选
var config = new OpenXmlConfiguration {
AutoFilter = true,
DynamicColumns = columns.ToArray()
};
// 5. 导出(如果是模板,数据为空)
await product.DynamicExport(
e.IsTemplate ? new() : BuildExportData(e.Items),
config,
$"{selectedCategory.Name}.xlsx");
}
效果:选择「电阻分类」导出 → Excel 列是「型号、厂商、封装、阻值…」
选择「电容分类」导出 → Excel 列是「型号、厂商、封装、容值…」
一套代码,动态适配所有分类。
四、完整数据流
┌─────────────────────┐
│ AdminTable │
└──────────┬──────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 导入 │ │ 导出 │ │ 模板 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
MiniExcel 解析 MiniExcel 写入 MiniExcel 写入
│ │ │
┌─────┴──────┐ │ │
▼ ▼ │ │
字典模式 实体模式 │ │
(自定义) (默认) │ │
│ │ │ │
▼ ▼ ▼ ▼
自定义处理 InsertOrUpdate 下载 .xlsx 下载空模板
(去重/关联) 自动完成
五、功能对比总览
| 特性 | 通用模式 | 自定义模式 |
|---|---|---|
| 启用方式 | ShowImportButton="true" |
绑定事件回调 |
| 代码量 | 0 行 | 几十行 |
| Excel 列名 | 实体属性名(英文) | 任意(中文也可以) |
| 去重处理 | ✅ 数据库唯一建去重 | ✅ 按业务键去重 |
| 关联数据自动创建 | ❌ 不支持 | ✅ 自动建厂商等 |
| 动态列结构 | ❌ 固定列 | ✅ 随分类变化 |
| 导入前/后事件 | ✅ OnBefore/OnFinish | ✅ 内置在回调中 |
| 断点续传 | ❌ | ❌ |
| 文件大小限制 | 5MB | 5MB |
| Excel 引擎 | MiniExcel | MiniExcel |
六、什么时候用哪种?
| 场景 | 推荐模式 |
|---|---|
| 字典数据导入(数据字典、参数配置) | 通用导入,零代码 |
| 基础数据导入(用户、角色) | 通用导入,零代码 |
| 产品/商品导入(需去重+关联) | 自定义导入,用 OnImportFromDictionaryAsync |
| 需要中文列名的 Excel | 自定义导入,用字典方式 |
| 不同分类有不同列结构 | 自定义导出,用 DynamicExcelColumn |
| 简单的数据导出 | 通用导出,零代码 |
七、总结
EasyAdminBlazor 的导入导出设计遵循一个原则:简单场景零代码,复杂场景不限制。
| 能力 | 说明 |
|---|---|
| 通用导入 | ShowImportButton="true",自动映射、自动保存 |
| 通用导出 | ShowExportButton="true",一键下载 Excel |
| 模板下载 | 自动生成含列结构的空 Excel |
| 自定义导入 | OnImportFromDictionaryAsync,字典方式处理复杂业务 |
| 自定义导出 | OnExportAllDataAsync,动态列、自定义列名 |
| 导入事件 | OnBeforeImportAsync / OnFinishImportAsync 精细化控制 |
| 文件限制 | 最大 5MB,.xlsx 格式 |
| 操作日志 | 导入导出自动记录操作日志 |
核心依赖:MiniExcel — 轻量级 Excel 读写库,无需安装 Office,性能优异。
下一篇预告
《EasyAdminBlazor 第十一篇:定时任务可视化调度——FreeScheduler 集成实战》
🔗 文档:https://easyadmim.wang-zhan.com.cn/doc
🔗 源码:https://gitee.com/gudufy/EasyAdminBlazor
EasyAdminBlazor —— 通用导入零代码,自定义导入不限制,批量处理不再麻烦
更多推荐



所有评论(0)