前面九篇我们讲了快速搭建、目录结构、CRUD、权限、多租户等。今天这篇非常实用——数据导入导出

做过后台系统的都知道,导入导出是刚需。用户总有几千条数据要批量录入,总有报表要导出整理。如果每个模块都自己写一遍导入导出逻辑,那简直是重复造轮子的噩梦。

EasyAdminBlazor 提供了一套开箱即用的导入导出机制,有两种玩法:

模式 适用场景 代码量
通用导入导出 简单实体,直接映射 Excel 列 零代码
自定义导入导出 复杂业务,需要特殊处理(去重、关联、动态列) 几十行

下面我们一个一个看。


一、通用导入导出:一行代码都不用写

启用方式

AdminTable 组件上,加两个属性:

<AdminTable TItem="Product" TKey="long"
            ShowImportButton="true"
            ShowExportButton="true">
</AdminTable>

搞定。 页面上会出现「导入」和「导出」按钮。

导出效果

点击「导出」,自动下载一个 Product.xlsx 文件,包含当前表格的所有数据,列名就是实体属性的中文名称。

导入效果

点击「导入」,弹出导入对话框:

  1. 下载模板 — 点击「下载模板」,系统自动生成一个空 Excel,列结构跟表格一致
  2. 上传文件 — 把填好的 Excel 拖拽或选择上传(最大 5MB)
  3. 自动处理 — 系统用 MiniExcel 自动把 Excel 行映射为实体对象,调用 InsertOrUpdate 批量写入数据库
  4. 完成 — 导入成功,表格自动刷新

整个过程,你不需要写一行代码

原理是什么?

// 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 —— 通用导入零代码,自定义导入不限制,批量处理不再麻烦

Logo

一站式 AI 云服务平台

更多推荐